Published on

Flutter iOS 빌드 실패? Podfile·CocoaPods 해결

Authors

Flutter로 iOS 빌드를 하다 보면 어느 날 갑자기 pod install 단계에서 멈추거나, Xcode 빌드가 Runner 타깃에서 실패하는 일이 자주 발생합니다. 특히 플러그인 업데이트, Xcode 버전 업, iOS 최소 버전 변경, Apple Silicon 환경 전환 같은 이벤트가 겹치면 Podfile 과 CocoaPods 설정이 꼬이기 쉽습니다.

이 글은 “일단 되는 조합”을 찾는 방식이 아니라, 실패 원인을 분류하고 재발을 줄이는 방향으로 정리합니다. 로컬 개발 환경뿐 아니라 CI에서도 안정적으로 통과시키는 것을 목표로 합니다.

문제 해결 흐름은 다른 장애 트러블슈팅과 동일합니다. 캐시/아티팩트 정리, 재현 범위 축소, 원인 레이어(Flutter 툴체인, CocoaPods, Xcode 프로젝트, 플러그인)별로 확인하는 방식이 가장 빠릅니다. CI에서 캐시가 오히려 문제를 키우는 패턴은 GitHub Actions 캐시 무효화로 빌드 느림 해결 글의 접근법과도 유사합니다.

1) 증상별로 먼저 분류하기

Flutter iOS 빌드 실패는 대략 아래 카테고리로 나뉩니다.

A. pod install 자체가 실패

  • CocoaPods could not find compatible versions for pod ...
  • Specs satisfying the ... dependency were found, but they required a higher minimum deployment target
  • CDN: trunk Repo update failed 또는 네트워크/SSL 관련

B. pod install 은 성공하지만 Xcode 빌드 실패

  • ld: library not found for -l...
  • Undefined symbols for architecture arm64
  • Module 'xxx' not found
  • non-modular header inside framework module

C. 아키텍처/환경(M1, 시뮬레이터)에서만 실패

  • 시뮬레이터에서만 arm64 관련 링크 에러
  • 인텔 맥에서는 되는데 Apple Silicon에서만 실패

이 분류가 중요한 이유는, A는 주로 Podfile/Pods 의존성 해결 단계, B는 Xcode 빌드 설정 및 링크 단계, C는 아키텍처/플랫폼 조건 분기 문제일 확률이 높기 때문입니다.

2) 가장 먼저 하는 “안전한 초기화” 절차

대부분의 Pod 관련 이슈는 캐시/락파일/Pods 디렉터리 불일치에서 시작합니다. 아래는 “과하게 지우는 것 같지만” 재현성을 확보하는 데 효과적입니다.

로컬에서 초기화

flutter clean
rm -rf ios/Pods ios/Podfile.lock
rm -rf ~/Library/Developer/Xcode/DerivedData

cd ios
pod repo update
pod install --verbose
cd ..

flutter pub get
flutter build ios --debug
  • ios/Podfile.lock의존성 해상 결과의 스냅샷입니다. 플러그인 버전이 바뀌었는데 락이 남아 있으면 충돌이 납니다.
  • DerivedData 는 Xcode 빌드 산출물이어서, 헤더/모듈 캐시가 꼬였을 때 결정타입니다.

CI에서 초기화(캐시를 쓴다면 더 중요)

CI는 Pods 나 CocoaPods 캐시를 저장해두는 경우가 많습니다. 캐시가 남아 있으면 “예전에 되던 조합”이 계속 재사용되어, 로컬과 CI 결과가 달라지거나 특정 커밋에서만 깨지는 현상이 생깁니다. 캐시 키에 Podfile.lockpubspec.lock 을 반드시 포함시키는 편이 안전합니다.

3) Podfile에서 가장 많이 깨지는 지점 6가지

아래는 Flutter iOS 프로젝트에서 Podfile 때문에 자주 터지는 포인트를 “수정 방향”과 함께 정리한 것입니다.

3-1. iOS 최소 버전(platform)이 낮아서 의존성 충돌

오류 예시:

  • required a higher minimum deployment target

해결은 대부분 platform :ios, '12.0' 또는 플러그인이 요구하는 버전으로 올리는 것입니다.

platform :ios, '12.0'

# Flutter 기본 템플릿 구조를 유지
flutter_ios_podfile_setup

target 'Runner' do
  use_frameworks!
  use_modular_headers!

  flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
end

주의할 점:

  • 최소 버전을 올리면, 지원 기기 범위가 바뀝니다. 제품 정책과 맞는지 먼저 확인하세요.

3-2. use_frameworks!use_modular_headers! 조합 문제

Flutter 플러그인 중 일부는 정적 라이브러리/모듈 헤더 처리에 민감합니다.

  • use_frameworks! 는 동적 프레임워크 방식으로 빌드하도록 유도합니다.
  • use_modular_headers! 는 헤더를 모듈 형태로 다루게 해서 non-modular header 류의 에러를 줄이는 데 도움이 됩니다.

하지만 모든 플러그인이 이 조합에서 잘 동작하는 것은 아닙니다. 의심되는 경우 아래처럼 특정 Pod만 예외 처리하는 전략을 씁니다.

target 'Runner' do
  use_frameworks!
  use_modular_headers!

  flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))

  post_install do |installer|
    installer.pods_project.targets.each do |t|
      if t.name == 'SomeLegacyPod'
        t.build_configurations.each do |config|
          config.build_settings['CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES'] = 'YES'
        end
      end
    end
  end
end

post_install 은 “최후의 수단”처럼 보이지만, 실제로는 iOS 의존성 생태계에서 꽤 표준적인 해결책입니다.

3-3. Apple Silicon(M1/M2) 시뮬레이터에서 arm64 링크 오류

증상:

  • Undefined symbols for architecture arm64
  • 특정 Pod가 시뮬레이터 arm64 를 지원하지 않음

해결책 중 하나는 시뮬레이터에서만 arm64를 제외하는 것입니다.

post_install do |installer|
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      # 시뮬레이터에서만 arm64 제외
      config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64'
    end
  end
end

주의:

  • 이 설정은 빌드를 통과시키는 데는 유효하지만, 장기적으로는 해당 Pod의 업데이트/대체가 필요할 수 있습니다.
  • 실제 디바이스(iphoneos)에는 적용되지 않으므로, 릴리즈 아카이브에는 영향이 제한적입니다.

3-4. pod install 은 되는데 Xcode에서 Module not found

이 경우는 보통 아래 중 하나입니다.

  • Pods 프로젝트가 갱신됐는데 Xcode가 오래된 워크스페이스/프로젝트를 바라봄
  • Runner.xcworkspace 가 아니라 Runner.xcodeproj 를 열고 빌드함
  • DerivedData 캐시 꼬임

해결 체크:

  • Xcode는 반드시 Runner.xcworkspace 로 열기
  • ios/Podsios/Podfile.lock 삭제 후 재설치
  • DerivedData 삭제

명령으로 강제 재설치하는 루틴을 팀 표준으로 만들면 재발이 줄어듭니다.

3-5. pod repo update 나 CDN 접근 실패(네트워크/SSL)

사내 네트워크, 프록시, 인증서, 혹은 일시적인 CDN 장애로 pod install 이 실패할 수 있습니다.

대응:

cd ios
pod install --repo-update --verbose
  • --repo-update 는 시간이 더 걸리지만, 스펙 인덱스가 꼬였을 때 효과적입니다.
  • CI에서 간헐 실패한다면 네트워크 레이어를 의심해야 합니다. “간헐적 실패”는 대개 환경 문제이며, 앱 코드와 무관할 수 있습니다.

3-6. Xcode/Flutter 업그레이드 후 IPHONEOS_DEPLOYMENT_TARGET 경고 폭발

플러그인들이 각자 다른 iOS 타깃을 갖고 있으면 경고가 쌓이고, 경우에 따라 빌드가 실패하기도 합니다. post_install 에서 일괄 정규화하는 방식이 흔합니다.

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

이렇게 하면 Pods 전체의 배포 타깃이 통일되어, “어떤 Pod는 11.0, 어떤 Pod는 12.0” 같은 혼선을 줄일 수 있습니다.

4) 플러그인/의존성 자체가 원인일 때의 접근

Podfile을 아무리 만져도 해결이 안 되면, 실제 원인은 플러그인 버전 충돌인 경우가 많습니다.

4-1. 어떤 플러그인이 iOS 빌드를 깨는지 빠르게 찾기

  1. 최근 변경된 플러그인부터 확인
  • pubspec.yaml 변경 이력
  • flutter pub outdated 결과
  1. iOS 쪽에서 어떤 Pod가 문제인지 확인
cd ios
pod install --verbose

로그에 “어떤 Pod를 어떤 버전으로 맞추려다 실패했는지”가 거의 항상 나옵니다.

4-2. Podfile.lockpubspec.lock 을 함께 관리

  • Flutter 패키지 버전이 바뀌면 iOS Pod 구성도 바뀝니다.
  • 팀 개발/CI에서 재현성을 확보하려면 두 락파일을 커밋하는 전략이 일반적입니다.

단, CocoaPods 생태계는 Xcode 버전, Ruby 버전, CocoaPods 버전 영향을 받기 때문에 “락파일만으로 100% 재현”이 보장되진 않습니다. 그래서 아래 5장의 환경 고정이 중요합니다.

5) CocoaPods/Ruby/Xcode 버전 고정(팀/CI 안정화)

로컬은 되는데 CI만 깨지는 대표 원인은 “도구 버전 불일치”입니다.

5-1. CocoaPods 버전 확인

pod --version
ruby --version
xcodebuild -version
flutter --version

이 네 가지를 빌드 로그에 남기면, 나중에 원인 추적이 쉬워집니다.

5-2. Bundler로 CocoaPods 버전 고정

ios/Gemfile 을 두고 CocoaPods 버전을 고정하면 팀/CI 간 편차가 크게 줄어듭니다.

# ios/Gemfile
source 'https://rubygems.org'

gem 'cocoapods', '1.14.3'

설치/실행:

cd ios
bundle install
bundle exec pod install

CI에서도 bundle exec 로 실행하도록 통일하는 것이 핵심입니다.

6) “정답 Podfile”을 만들기 전에 확인할 것

Podfile을 과도하게 커스터마이징하면, 다음 Flutter 업그레이드 때 더 크게 깨질 수 있습니다. 아래 원칙을 추천합니다.

  • Flutter 기본 템플릿 구조(flutter_ios_podfile_setup, flutter_install_all_ios_pods)는 최대한 유지
  • 필요한 수정은 post_install 에 모으기
  • 아키텍처 제외(EXCLUDED_ARCHS)는 임시 처방으로 취급하고, 플러그인/Pod 업데이트로 제거 목표 설정

트러블슈팅은 “한 번 고치고 끝”이 아니라 재발 방지까지 포함합니다. 운영 장애에서 재시도/폴백/서킷브레이커로 안정성을 확보하듯이, 빌드 파이프라인도 버전 고정과 캐시 전략으로 안정성을 확보해야 합니다. 비슷한 관점의 글로 OpenAI Responses API 500·503 대응 재시도 폴백 서킷브레이커도 참고할 만합니다.

7) 최종 체크리스트(실전용)

아래 순서대로 하면 대부분의 Flutter iOS Podfile/CocoaPods 이슈를 빠르게 정리할 수 있습니다.

  1. Xcode는 Runner.xcworkspace 로 열었는가
  2. flutter cleanios/Pods, ios/Podfile.lock, DerivedData 를 삭제했는가
  3. pod install --repo-update --verbose 로 실패 지점을 확인했는가
  4. 에러가 deployment target 이면 platform :ios, '12.0' 등으로 상향했는가
  5. Apple Silicon 시뮬레이터만 실패하면 EXCLUDED_ARCHS[sdk=iphonesimulator*] 를 적용해봤는가
  6. use_frameworks! / use_modular_headers! 조합이 플러그인과 충돌하지 않는가
  7. CocoaPods 버전을 Bundler로 고정했는가(bundle exec pod install)
  8. CI 캐시 키에 Podfile.lockpubspec.lock 이 포함되어 있는가

8) 결론

Flutter iOS 빌드 실패는 겉으로는 Podfile 문제처럼 보여도, 실제로는 “의존성 해상 결과(Podfile.lock)”, “도구 버전”, “아키텍처 조건”, “Xcode 캐시”가 얽힌 복합 문제인 경우가 많습니다.

가장 효과적인 전략은 다음 두 가지입니다.

  • 초기화 루틴을 표준화해서 재현성을 확보한다(삭제할 것, 다시 설치할 것, 열어야 할 워크스페이스까지)
  • 버전 고정(Bundler)과 캐시 키 설계로 팀/CI 편차를 줄인다

이 두 가지를 갖춰두면, 플러그인 업데이트나 Xcode 업그레이드 같은 이벤트가 와도 “원인 파악 가능한 실패”로 바뀌고, 해결 시간도 크게 줄어듭니다.