Published on

Flutter iOS 빌드 에러 - Podfile·CocoaPods 충돌 해결

Authors

서론

Flutter로 iOS 빌드를 하다 보면, 어느 날 갑자기 pod install이 깨지거나 Xcode 빌드가 실패하면서 “Podfile이 문제다”, “CocoaPods가 꼬였다” 같은 메시지가 쏟아집니다. 특히 Flutter는 iOS 네이티브 프로젝트를 자동 생성/갱신(예: flutter pub get, flutter build ios)하는 흐름이 섞여 있어, Podfile과 Pods/Lockfile, Ruby/CocoaPods 버전, Xcode 프로젝트 설정이 조금만 어긋나도 충돌이 쉽게 납니다.

이 글은 “에러 메시지에 따라 무엇을 먼저 의심하고, 어떤 순서로 정리하면 가장 안전하게 복구되는지”를 원인-증상-해결 형태로 정리합니다. 운영 환경에서 장애를 디버깅할 때 체크리스트로 원인을 좁혀가는 방식이 효과적인데, 접근 자체는 Python uvloop 도입 후 Event loop is closed 해결 가이드처럼 “환경/캐시/버전/재현 순서”를 분해하는 것과 유사합니다.


Podfile·CocoaPods 충돌의 전형적인 증상

아래는 Flutter iOS에서 자주 만나는 에러 패턴입니다.

1) CocoaPods 버전/레포 상태 문제

  • CocoaPods could not find compatible versions for pod "..."
  • Specs satisfying the '...' dependency were found, but they required a higher minimum deployment target.
  • Unable to find a specification for ...

2) Podfile 설정과 Xcode 프로젝트 설정 불일치

  • The iOS deployment target 'IPHONEOS_DEPLOYMENT_TARGET' is set to ... but the range of supported deployment target versions is ...
  • use_frameworks! / use_modular_headers! 관련 충돌
  • Swift/Objective-C 혼합 프로젝트에서 module not found 또는 Framework not found

3) Pods 캐시/Lockfile 꼬임

  • Sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.
  • The sandbox is not in sync with the Podfile.lock가 반복

4) Apple Silicon / Ruby 환경 문제

  • M1/M2에서 ffi 빌드 실패, xcodebuild 단계에서 아키텍처 충돌
  • pod install은 되는데 Xcode Archive에서만 실패

가장 먼저 확인할 5가지(원인 분리용)

문제를 빨리 좁히려면, 무작정 rm -rf Pods부터 하기 전에 아래를 먼저 확인하세요.

  1. Flutter 채널/버전: flutter --version
  2. Xcode 버전: Xcode 업데이트 직후인지
  3. CocoaPods 버전: pod --version
  4. iOS Deployment Target: Podfile의 platform :ios, '...'와 Xcode 프로젝트의 Deployment Target
  5. Lockfile/Pods 상태: ios/Podfile.lock가 커밋되어 있는지(팀 작업 시 중요)

이 5개 중 하나라도 팀원/CI와 다르면, “내 컴퓨터에서만” 재현되는 충돌이 발생합니다. JWT 검증에서 kid 캐시가 어긋나면 특정 노드에서만 실패하는 것처럼, 로컬 iOS 빌드도 캐시/버전 불일치가 핵심 원인인 경우가 많습니다(관점 참고: JWT 검증 실패 - JWKS kid 불일치·캐시 7가지).


안전한 복구 순서(클린 → 재생성 → 재설치)

아래 순서는 “Pods/DerivedData/Flutter 캐시”를 단계적으로 정리하면서, 재현성을 최대화하는 루틴입니다.

1) Flutter/패키지 캐시 정리

flutter clean
rm -rf .dart_tool
flutter pub get

2) iOS Pods 관련 파일 정리

프로젝트 루트에서:

cd ios
rm -rf Pods
rm -f Podfile.lock
rm -rf ~/Library/Developer/Xcode/DerivedData
pod deintegrate
pod repo update
pod install
cd ..
  • pod deintegrate: Xcode 프로젝트에 주입된 CocoaPods 설정을 제거
  • Podfile.lock 제거: 의존성 버전을 새로 고정(단, 팀/CI 정책에 따라 유지가 맞을 수 있음)
  • DerivedData 삭제: Xcode 빌드 캐시로 인한 유령 에러 제거

3) Xcode에서 워크스페이스로 열기

CocoaPods를 쓰는 iOS 프로젝트는 반드시:

  • Runner.xcworkspace 열기
  • Runner.xcodeproj로 열면 Pods가 링크되지 않아 실패할 수 있음

Podfile에서 가장 자주 충돌 나는 지점 4가지와 해결

1) iOS Deployment Target 불일치

증상

  • 특정 Pod가 더 높은 iOS 최소 버전을 요구
  • Xcode 빌드 단계에서 IPHONEOS_DEPLOYMENT_TARGET 경고/에러

해결

Podfile의 플랫폼 버전을 올리고, Xcode 프로젝트 설정도 동일하게 맞춥니다.

# ios/Podfile
platform :ios, '13.0'

post_install do |installer|
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0'
    end
  end
end
  • Flutter 플러그인 중 일부는 iOS 12 이하를 사실상 지원하지 않는 경우가 늘고 있습니다.
  • “Podfile만 올리고 Xcode는 그대로”면, 아카이브 단계에서 다시 터지기도 합니다.

2) use_frameworks! / use_modular_headers! 충돌

증상

  • module not found / Swift pod cannot be integrated as static library 류 메시지
  • Firebase, gRPC, 일부 Swift 기반 Pod에서 충돌

원칙

  • 가능하면 Flutter 기본 템플릿 흐름을 유지
  • 특정 플러그인이 요구할 때만 옵션을 추가

예시: 동적 프레임워크가 필요할 때

# ios/Podfile
use_frameworks! :linkage => :static
use_modular_headers!

다만 이 설정은 모든 Pod에 영향을 주므로, 적용 후 새로 생긴 에러가 있다면 원인 Pod를 좁혀야 합니다. (예: 어떤 Pod는 modular headers에서만 깨짐)


3) “Sandbox is not in sync with Podfile.lock” 반복

증상

  • pod install 후에도 동일 에러
  • CI에서는 성공, 로컬만 실패(또는 반대)

원인

  • Pods/Manifest.lockPodfile.lock 불일치
  • Xcode workspace가 오래된 Pods 상태를 참조

해결 루틴

cd ios
pod deintegrate
rm -rf Pods
rm -f Podfile.lock
pod install --repo-update

팀 프로젝트라면 Podfile.lock커밋하고 고정하는 쪽이 일반적으로 재현성이 좋습니다. 다만 Flutter 플러그인 추가/업데이트 시에는 lock 갱신이 필요하므로, “갱신 PR”과 “기능 PR”을 분리하면 충돌이 줄어듭니다.


4) Apple Silicon(M1/M2)에서 gem/ffi 문제로 pod install 실패

증상

  • ffi 빌드 실패
  • gem install cocoapods에서 네이티브 확장 컴파일 오류

해결 방향

  • 시스템 Ruby 대신 rbenv/asdf로 Ruby 버전 고정
  • CocoaPods도 프로젝트/팀 단위로 버전 고정

예시(권장): Bundler로 CocoaPods 고정

cd ios
bundle init
# Gemfile에 cocoapods 버전 명시

ios/Gemfile 예시:

source 'https://rubygems.org'

gem 'cocoapods', '1.15.2'

설치/실행:

cd ios
bundle install
bundle exec pod install

이 방식은 “내 로컬에 깔린 pod 버전”에 의존하지 않게 만들어 CI/팀원 간 차이를 줄입니다.


Flutter iOS에서 자주 쓰는 Podfile 템플릿(충돌 최소화)

아래는 실무에서 무난하게 쓰이는 형태입니다. (프로젝트 상황에 맞게 최소한만 수정)

# ios/Podfile
platform :ios, '13.0'

ENV['COCOAPODS_DISABLE_STATS'] = 'true'

project 'Runner', {
  'Debug' => :debug,
  'Profile' => :release,
  'Release' => :release,
}

def flutter_root
  generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
  unless File.exist?(generated_xcode_build_settings_path)
    raise "Generated.xcconfig not found. Run 'flutter pub get' first."
  end

  File.foreach(generated_xcode_build_settings_path) do |line|
    matches = line.match(/FLUTTER_ROOT\=(.*)/)
    return matches[1].strip if matches
  end
  raise "FLUTTER_ROOT not found in Generated.xcconfig."
end

require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)

flutter_ios_podfile_setup

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

  # 필요 시만 사용
  # use_frameworks! :linkage => :static
  # use_modular_headers!
end

post_install do |installer|
  installer.pods_project.targets.each do |target|
    flutter_additional_ios_build_settings(target)

    target.build_configurations.each do |config|
      config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0'
    end
  end
end

핵심은 flutter_additional_ios_build_settings(target) 호출을 유지하면서, Deployment Target을 Pods까지 일괄 보정하는 것입니다.


Xcode 빌드 단계에서만 터질 때(Archive/Release 전용 이슈)

Debug에서는 되는데 Archive에서만 실패하는 경우는 대개 아래가 원인입니다.

  • Release에서만 활성화되는 빌드 설정(Dead code stripping, bitcode, swift optimization)
  • 서명/프로비저닝 이슈(이 글 범위 밖)
  • 특정 Pod가 Release에서만 스크립트를 실행하다 실패

점검 팁

  1. Xcode에서 Report navigator가장 첫 실패 지점을 봅니다.
  2. Run Script 단계가 실패면, 스크립트가 참조하는 경로(예: flutter 바이너리, .xcconfig)를 확인합니다.
  3. Pods-Runner 타겟의 Build Settings에서 IPHONEOS_DEPLOYMENT_TARGET이 Podfile과 동일한지 확인합니다.

CI/팀 개발에서 충돌을 줄이는 운영 팁

CocoaPods 충돌은 “개발자 1명은 되는데 다른 사람은 안 됨”으로 확산되기 쉽습니다. 아래를 팀 규칙으로 두면 재발이 크게 줄어듭니다.

  1. CocoaPods 버전 고정: Gemfile + bundle exec pod install
  2. Podfile.lock 정책 결정: 커밋/미커밋을 명확히(대부분 커밋 권장)
  3. Xcode 버전 통일: 최소한 메이저 버전은 맞추기
  4. 플러그인 업데이트 PR 분리: lockfile 변화가 큰 PR은 기능 PR과 분리

이런 “환경을 고정해 재현성을 확보”하는 방식은 인프라 디버깅에서도 동일하게 통합니다. 예를 들어 노드 상태가 간헐적으로 깨질 때도 버전/구성 드리프트를 먼저 의심하듯(EKS Node NotReady - CNI ENI 할당 실패 해결 가이드), iOS 빌드도 로컬 환경 드리프트가 1순위 원인입니다.


결론: 충돌을 “정리 절차”로 표준화하라

Flutter iOS 빌드에서 Podfile·CocoaPods 충돌은 대부분 아래 셋 중 하나로 귀결됩니다.

  • 버전 불일치(CocoaPods/Ruby/Xcode/플러그인)
  • Deployment Target 불일치(Podfile ↔ Xcode ↔ Pods)
  • 캐시/Lockfile 꼬임(Pods/DerivedData/Manifest.lock)

해결의 핵심은 “무엇이 꼬였는지”를 추측하기보다, **표준 복구 루틴(클린 → deintegrate → 재설치 → workspace 빌드)**을 팀에 고정하는 것입니다. 위 템플릿과 명령어를 그대로 체크리스트로 만들어 두면, 대부분의 iOS 빌드 에러는 10~15분 내에 안정적으로 복구됩니다.