문제 현상: 런칭 직후 발생하는 SIGTRAP 크래시
iOS 26 시뮬레이터에서는 아무런 경고 없이 정상 동작하던 Flutter 앱이, Xcode 27(iOS 27 SDK)로 빌드하여 iOS 27 시뮬레이터에서 실행하면 스플래시 화면이 뜨자마자 비정상 종료(Crash)되는 현상을 겪었습니다.
실제 크래시 리포트(~/Library/Logs/DiagnosticReports/Runner-*.ips)에 찍힌 내용은 다음과 같습니다.
Exception Type: EXC_BREAKPOINT (SIGTRAP)
Thread 0 Crashed:
0 UIKitCore ___UIApplicationEvaluateRuntimeIssueForNoSceneLifecycleAdoption_block_invoke
1 libdispatch.dylib _dispatch_client_callout
2 libdispatch.dylib _dispatch_once_callout
3 UIKitCore -[UIApplication workspace:didCreateScene:withTransitionContext:completion:]
4 UIKitCore -[UIApplicationSceneClientAgent scene:didInitializeWithEvent:completion:]
5 FrontBoardServices __95-[FBSScene _callOutQueue_didCreateWithTransitionContext:alternativeCreationCallout:completion:]_block_invoke
주요 에러 심볼은 UIApplicationEvaluateRuntimeIssueForNoSceneLifecycleAdoption이며, SIGTRAP 트랩 신호와 함께 앱의 메인 스레드가 즉시 중단됩니다. 앱 코드에 도달하기도 전에, 시스템이 씬을 만드는 단계에서 멈춘다는 점이 특징입니다.
원인 분석: UIScene 라이프사이클의 강제 적용
이 크래시는 버그가 아니라, Apple의 라이프사이클 정책 변경에 따른 의도된 차단입니다.
1. 레거시 단일 윈도우 구조의 종말
과거의 앱은 UIApplicationDelegate 하나만으로 앱의 수명주기와 UIWindow를 모두 관리했습니다. iOS 13에서 멀티 윈도우를 위해 UIScene과 UISceneDelegate가 도입되었지만, 하위 호환을 위해 UIApplicationDelegate만 쓰는 구조도 계속 실행을 허용해 왔습니다.
2. "iOS 27 SDK로 빌드했는가"가 조건입니다
Apple은 TN3187에서 다음과 같이 예고했습니다.
In the next major release following iOS 26, UIScene lifecycle will be required when building with the latest SDK; otherwise, your app won't launch.
여기서 중요한 조건은 기기의 iOS 버전이 아니라, 앱을 어떤 SDK로 빌드했는가입니다. 직접 확인한 결과는 다음과 같습니다.
| 같은 Xcode 27 빌드 | 수정 전 | 수정 후 |
|---|---|---|
| iOS 27 시뮬레이터 | 즉시 종료 | 정상 실행 |
| iOS 26.4 시뮬레이터 | 정상 실행 | 정상 실행 |
즉, 이미 Xcode 26으로 빌드해 스토어에 올라가 있는 버전은 iOS 27 기기에서도 영향을 받지 않을 가능성이 높습니다. 문제는 Xcode가 27로 업데이트된 뒤 새로 빌드한 바이너리입니다. Xcode가 자동 업데이트되었다면 본인도 모르게 iOS 27 SDK로 아카이브했을 수 있으니, 아카이브의 DTSDKName이 iphoneos27.0인지 확인해 보세요.
해결 방법: Flutter 앱 마이그레이션 (실제 적용)
1) Flutter 3.38 이상으로 업그레이드
Flutter에서 UIScene을 지원하는 FlutterSceneDelegate와 FlutterImplicitEngineDelegate는 Flutter 3.38부터 제공됩니다. 그 이전 버전에서는 이 방법을 쓸 수 없습니다. 다른 프로젝트에 영향을 주지 않도록 fvm으로 해당 프로젝트만 3.41.9에 고정했습니다.
fvm use 3.41.9
fvm flutter clean
fvm flutter pub get
버전을 바꾼 뒤에는 flutter clean을 꼭 실행하세요. 이전 빌드 산출물이 남아 있으면 아키텍처나 헤더 관련 빌드 오류가 납니다.
2) Info.plist에 UIApplicationSceneManifest 추가
씬 델리게이트로 Flutter가 제공하는 FlutterSceneDelegate를 지정합니다. 스토리보드(Main)로 FlutterViewController를 띄우는 기본 템플릿이라면 UISceneStoryboardFile도 함께 넣어야 합니다.
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneDelegateClassName</key>
<string>FlutterSceneDelegate</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
3) AppDelegate.swift 정리: 채널과 플러그인 등록 위치 옮기기
자동으로 해결되지 않는 부분이 여기입니다. 기존 템플릿에서 흔히 쓰던 아래 코드는 UIScene 구조에서 더 이상 성립하지 않습니다.
// didFinishLaunching 시점에는 아직 FlutterViewController가 없습니다.
let controller = window?.rootViewController as! FlutterViewController
씬 방식에서는 앱 시작 시점에 Flutter 화면이 아직 만들어지지 않았기 때문입니다. 그래서 FlutterImplicitEngineDelegate를 채택하고, 엔진이 준비되었다는 콜백에서 플러그인 등록과 MethodChannel 생성을 하도록 옮겨야 합니다.
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// SDK 초기화 등 화면과 무관한 셋업만 남깁니다.
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
// 스토리보드의 FlutterViewController가 만든 엔진이 준비되면 호출됩니다.
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
let messenger = engineBridge.applicationRegistrar.messenger()
let channel = FlutterMethodChannel(name: "com.example.app/config", binaryMessenger: messenger)
channel.setMethodCallHandler { call, result in
// 기존 핸들러 그대로 유지
}
}
}
GeneratedPluginRegistrant.register(with: self)를 didFinishLaunching에서 호출하던 코드도 이 콜백 안으로 옮깁니다.
4) 크래시는 사라졌는데 스플래시에서 멈춘다면: 플러그인 업데이트
1~3단계를 적용하자 크래시는 사라졌지만, 네이티브 스플래시 화면에서 Flutter 첫 화면으로 넘어가지 않고 멈추는 문제가 이어서 나타났습니다. 로그상으로는 각종 SDK 초기화가 모두 성공한 상태였습니다.
원인은 UIScene을 지원하지 않는 구버전 플러그인이었습니다. 다음 플러그인들을 올려서 해결했습니다.
firebase_core^4.14,firebase_messaging16.6.0 (iOS SDK 업데이트를 위해pod update Firebase필요)- firebase_messaging README의 "iOS apps using UIScene" 안내대로, 알림 센터 델리게이트를 직접
self로 지정하던 코드는 지우고didFinishLaunching에서super호출 전에FLTFirebaseMessagingPlugin.configureNotificationCenterDelegate()를 호출하도록 바꿨습니다.
- firebase_messaging README의 "iOS apps using UIScene" 안내대로, 알림 센터 델리게이트를 직접
google_sign_in7.x (google_sign_in_ios6.3.0 변경 기록에 "Adds UIScene compatibility"가 명시되어 있으며, 이 버전은 7.x에서만 사용됩니다. 7.x는 Dart API가 바뀌어 로그인 코드도 함께 수정해야 합니다.)
플러그인을 올린 뒤에도 flutter clean을 하지 않으면 옛 헤더가 남아 빌드 오류가 날 수 있습니다. 사용하는 플러그인마다 변경 기록에서 "UIScene" 지원 여부를 확인해 보시길 권합니다.
참고: 네이티브 UIKit 앱이라면
Flutter가 아닌 순수 UIKit 앱이라면 원리는 같습니다. UIWindowSceneDelegate를 구현한 SceneDelegate를 만들고 AppDelegate에서 하던 윈도우 생성 코드를 scene(_:willConnectTo:options:)로 옮긴 뒤, Info.plist의 UISceneDelegateClassName에 $(PRODUCT_MODULE_NAME).SceneDelegate를 지정하면 됩니다. 스토리보드를 쓴다면 UISceneStoryboardFile도 잊지 마세요.
React Native·Expo 등 다른 크로스 플랫폼 프레임워크는 직접 검증하지 않았으므로, 각 프레임워크의 공식 문서와 변경 기록에서 UIScene 지원 버전을 확인하시길 바랍니다.
요약
- 원인: iOS 27 SDK(Xcode 27)로 빌드한 앱은 UIScene 라이프사이클이 필수이며, 이를 채택하지 않으면
UIApplicationEvaluateRuntimeIssueForNoSceneLifecycleAdoption에서 SIGTRAP으로 종료됩니다. 기기 버전이 아니라 빌드 SDK가 조건입니다. - Flutter 해결 순서:
- Flutter 3.38 이상으로 업그레이드 (fvm 등으로 3.41.9 등 고정)
- Info.plist에
FlutterSceneDelegate씬 매니페스트 등록 rootViewController as! FlutterViewController제거,FlutterImplicitEngineDelegate의didInitializeImplicitFlutterEngine에서 플러그인·채널 등록- 스플래시에서 멈추면 UIScene을 지원하는 버전으로 플러그인 업데이트 (
firebase_messaging,google_sign_in등)
- 확인: 같은 Xcode 27 빌드로 iOS 26과 iOS 27 시뮬레이터 모두에서 실행해 비교해 보세요.