Published on

Flutter iOS 빌드 실패? CocoaPods 충돌 해결법

Authors

Flutter로 iOS 빌드를 하다 보면 어느 날 갑자기 pod install 단계에서 멈추거나, Xcode 빌드는 되는데 아카이브에서만 실패하는 식의 문제가 터집니다. 특히 CocoaPods 충돌은 증상이 다양하지만, 원인과 해결 루틴은 꽤 정형화되어 있습니다.

이 글은 “일단 다 지우고 다시 깔자”가 아니라, 왜 충돌이 생겼는지를 기준으로 진단하고, 재발을 줄이는 방식으로 정리합니다. (문제 해결 접근법은 원인 체크리스트로 좁혀가는 게 핵심인데, 비슷한 방식의 트러블슈팅 글로는 GitHub Actions GITHUB_TOKEN 403 권한오류 해결도 참고가 됩니다.)

1) CocoaPods 충돌이란 무엇이 충돌하는가

Flutter iOS는 기본적으로 ios/Podfile을 통해 네이티브 의존성을 CocoaPods로 설치합니다. 충돌의 실체는 보통 아래 중 하나입니다.

  • Pod 버전 제약 충돌: A 플러그인은 Firebase/CoreOnly를 특정 버전 이상 요구, B 플러그인은 특정 버전 이하 요구
  • Podspec 소스 충돌: 같은 Pod를 서로 다른 소스(예: trunk vs git)에서 가져오려 함
  • iOS Deployment Target 불일치: 일부 Pod가 최소 iOS 13을 요구하는데 프로젝트는 iOS 12로 설정
  • 정적/동적 프레임워크 링크 방식 충돌: use_frameworks!/use_modular_headers! 조합 이슈
  • Xcode/Swift toolchain 변화: Xcode 업데이트 후 Pods.xcodeproj 설정이 호환되지 않음

증상 메시지는 다양하지만, 로그에서 키워드를 잡으면 분류가 됩니다.

2) 가장 흔한 에러 로그 패턴과 의미

패턴 A: 버전 충돌

대표적으로 아래 형태가 나옵니다.

  • CocoaPods could not find compatible versions for pod ...
  • Podfile.lock에 고정된 버전과 Podfile/플러그인이 요구하는 버전이 다름

이 경우 핵심은 잠금 파일(Podfile.lock)과 Flutter 플러그인들이 생성하는 요구사항이 어긋난 상태를 정리하는 것입니다.

패턴 B: 최소 iOS 버전 미달

  • Specs satisfying the ... were found, but they required a higher minimum deployment target

이 경우는 iOS 타깃을 올리면 대부분 해결됩니다. 단, 올릴 때는 Podfile과 Xcode 프로젝트 설정 둘 다 맞춰야 합니다.

패턴 C: 모듈/헤더 관련

  • Include of non-modular header inside framework module
  • Module not found

대개 use_frameworks!/use_modular_headers! 또는 CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES 같은 설정이 얽혀 있습니다.

3) 10분 안에 끝내는 표준 복구 루틴(안전한 순서)

아래는 “대부분의 충돌”을 안전하게 정리하는 순서입니다. 중요한 점은 순서를 지키는 것입니다.

3-1) Xcode 종료, DerivedData 정리

rm -rf ~/Library/Developer/Xcode/DerivedData

DerivedData가 오래된 모듈 캐시를 들고 있으면, Pods를 정상 재설치해도 Module not found 같은 오류가 계속 날 수 있습니다.

3-2) Flutter 빌드 캐시 정리

flutter clean
flutter pub get

3-3) iOS Pods 완전 초기화 후 재설치

ios 폴더에서 실행합니다.

cd ios
rm -rf Pods
rm -f Podfile.lock
pod deintegrate
pod repo update
pod install --verbose
  • Podfile.lock을 지우는 이유: 잠금 버전이 충돌의 원인이 되는 경우가 많음
  • pod repo update: 로컬 spec repo가 오래되어 최신 Pod 버전을 못 찾는 경우 방지

3-4) 워크스페이스로 열기 확인

CocoaPods를 쓰면 Xcode는 반드시 .xcworkspace로 열어야 합니다.

  • 열어야 하는 파일: Runner.xcworkspace
  • 열면 안 되는 파일: Runner.xcodeproj

이 실수는 “로컬에서는 되는데 CI에서만 실패” 같은 형태로도 나타납니다.

4) iOS Deployment Target 충돌 해결(가장 많이 터짐)

Flutter 플러그인들이 점점 최신 iOS를 요구하면서, 기존 프로젝트의 iOS 타깃이 낮으면 충돌이 납니다.

4-1) Podfile에서 플랫폼 버전 올리기

ios/Podfile 상단을 확인합니다.

platform :ios, '13.0'

실무에서는 iOS 12를 유지해야 하는 요구가 없다면, 최소 iOS 13 이상으로 올리는 편이 충돌이 적습니다.

4-2) Xcode 프로젝트 설정도 함께 맞추기

Xcode에서 Runner 타깃의 iOS Deployment Target도 동일하게 맞춰야 합니다.

  • Runner(앱 타깃)
  • RunnerTests(테스트 타깃)

둘 중 하나만 낮아도 빌드가 꼬일 수 있습니다.

5) use_frameworks!/use_modular_headers! 충돌 다루기

Flutter 플러그인 조합에 따라 동적 프레임워크가 필요하거나, 반대로 정적 링크가 안정적인 경우가 있습니다.

5-1) 원칙: 먼저 기본값을 유지

가능하면 Podfile에 불필요한 설정을 추가하지 않는 게 좋습니다. 하지만 특정 플러그인(특히 Swift 기반, 또는 일부 Firebase 조합)에서 요구하는 경우가 있습니다.

5-2) use_frameworks!가 필요한 경우

아래처럼 명시합니다.

use_frameworks!

target 'Runner' do
  flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
end

다만 use_frameworks!를 켜면 다른 Pod에서 헤더/모듈 이슈가 생길 수 있어, 켠 뒤에는 반드시 전체 재설치를 권장합니다.

5-3) 모듈 헤더 문제가 날 때의 완화 옵션

특정 Pod에서 non-modular header 에러가 날 때는 아래처럼 post_install에서 설정을 주기도 합니다.

post_install do |installer|
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      config.build_settings['CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES'] = 'YES'
    end
  end
end

이 설정은 “근본 해결”이라기보다 “막힌 빌드를 뚫는” 성격이므로, 가능하면 원인 Pod 조합(혹은 use_frameworks! 사용 여부)을 재검토하는 게 좋습니다.

6) Podfile.lock이 문제를 재발시키는 구조 이해

팀 개발/CI에서는 Podfile.lock이 커밋되어 있을 때가 많습니다. 이때 로컬에서 플러그인을 업데이트하면 pubspec.lockPodfile.lock의 기대가 어긋나면서 충돌이 발생합니다.

권장 전략은 둘 중 하나입니다.

  • 전략 1: 잠금 파일을 엄격히 유지: 플러그인 업데이트 시 iOS 담당자가 pod install까지 완료하고 Podfile.lock 변경을 함께 커밋
  • 전략 2: iOS 잠금을 느슨하게: 상황에 따라 Podfile.lock을 재생성(단, 재현성은 낮아짐)

CI 안정성을 최우선으로 보면 전략 1이 일반적입니다.

7) Apple Silicon(M1/M2)에서만 터지는 CocoaPods/ffi 이슈

특히 오래된 Ruby/CocoaPods 환경에서 M 시리즈 맥은 ffi 네이티브 확장 문제로 pod install이 깨지기도 합니다.

이때는 “Pods 충돌”이라기보다 “도구체인 문제”에 가깝습니다.

7-1) CocoaPods 버전 확인

pod --version
ruby --version

7-2) Bundler로 CocoaPods 고정(권장)

프로젝트에 Gemfile을 두고 CocoaPods 버전을 고정하면 팀/CI 편차가 크게 줄어듭니다.

ios/Gemfile 예시:

source 'https://rubygems.org'

gem 'cocoapods', '1.15.2'

설치/실행:

cd ios
bundle install
bundle exec pod install

이 방식은 “내 컴퓨터에서는 되는데” 류의 문제를 줄이는 데 특히 효과적입니다. (문제 원인을 좁혀나가는 운영 관점은 OpenAI Responses API 400 invalid_request_error 해결처럼 로그 기반으로 분류하는 접근과 유사합니다.)

8) Xcode 업데이트 후 아카이브만 실패하는 케이스

디버그 빌드는 되는데 아카이브에서만 실패하는 경우는 아래를 의심합니다.

  • Build Settings에서 EXCLUDED_ARCHS가 릴리즈 구성에만 다르게 설정됨
  • Swift Compiler 설정 차이
  • Other Linker Flags 충돌

8-1) 릴리즈 구성에서만 다른 설정 찾기

Xcode에서 Runner 타깃의 Build Settings를 열고, Debug/Release를 번갈아 보며 차이를 확인하세요.

특히 아래 항목을 먼저 봅니다.

  • Excluded Architectures
  • Other Linker Flags
  • Enable Bitcode(요즘은 대부분 NO, 오래된 설정이 남아있으면 문제)

9) 그래도 안 되면: 충돌 Pod를 특정하는 방법

pod install --verbose 로그에서 충돌 Pod 이름이 나오면, 그 Pod가 어떤 Flutter 플러그인에서 유입되는지를 찾는 게 핵심입니다.

9-1) Pod 의존성 트리 확인

cd ios
pod deps

9-2) Flutter 플러그인과 매핑

ios/Podfile에는 Flutter가 생성한 플러그인 Pod가 들어오며, 실제 매핑은 ios/.symlinks/plugins 경로 아래에서 확인 가능합니다.

ls ios/.symlinks/plugins

문제 Pod가 특정 플러그인에서만 들어온다면,

  • 플러그인 버전 업/다운
  • 대체 플러그인 검토
  • 해당 플러그인의 iOS 최소 버전 요구 확인

같은 방식으로 해결 방향이 정해집니다.

10) 재발 방지 체크리스트(팀/CI 기준)

  • Runner.xcworkspace로만 빌드하도록 문서화
  • ios/Gemfile로 CocoaPods 버전 고정 후 bundle exec pod install 표준화
  • Flutter/플러그인 업데이트 시, iOS 타깃 버전과 Podfile.lock 변경을 함께 관리
  • CI에서 캐시를 쓰더라도, Pods 관련 캐시는 키를 엄격히(Flutter 버전, Podfile.lock 해시 등) 잡기
  • Xcode 업데이트 시점에 한 번은 DerivedData/Pods 완전 초기화 루틴 수행

마무리

Flutter iOS 빌드 실패는 겉으로는 “CocoaPods 충돌” 한 줄로 보이지만, 실제로는 버전 잠금(Podfile.lock), iOS 최소 타깃, 프레임워크 링크 방식, 도구체인(Ruby/CocoaPods/Xcode) 중 어디가 흔들렸는지에 따라 해결법이 달라집니다.

위의 표준 복구 루틴으로 환경을 깨끗하게 만든 다음, 로그 패턴에 따라 iOS 타깃 조정 또는 Podfile 설정을 최소한으로 적용하면 대부분의 케이스는 재현 가능하게 해결됩니다. 만약 특정 에러 로그(예: CocoaPods could not find compatible versions...)를 공유할 수 있다면, 충돌 Pod를 기준으로 더 정확한 처방(플러그인 버전 조합, Podfile 수정 방향)까지 좁혀서 정리할 수 있습니다.