RSS듀오랩스

Expo SDK 56 iOS 빌드 오류와 Swift weak let 호환성

모바일 개발글: , Duolabs6분 읽기blogexpoiosswifttechnical-note

저는 견적킷을 iPhone에서 실행하려다 xcodebuild 코드 65를 만났습니다. 빌드 화면에는 Reanimated와 Worklets 컴파일 로그가 길게 지나갔고, 마지막에는 오류 15개가 있다는 메시지만 남았습니다.

같은 Mac에서 Bible Map은 정상적으로 빌드되고 있었습니다. 두 앱 모두 Expo SDK 56을 쓰고 있었기 때문에 먼저 패키지 버전 차이를 의심했습니다.

실제 설치 버전 비교

package.json의 버전 범위가 아니라 lock 파일에 기록된 버전을 비교했습니다.

패키지 견적킷 Bible Map
Expo 56.0.19 56.0.19
React Native 0.85.3 0.85.3
expo-modules-core 56.0.23 56.0.23
expo-modules-jsi 56.0.12 56.0.12
Worklets 0.8.3 0.8.3
Reanimated 4.3.1 4.3.3

Reanimated만 패치 버전이 달랐습니다. 하지만 오류가 난 expo-modules-jsi를 비롯한 핵심 패키지는 같았습니다. 이번 빌드 실패를 Reanimated 4.3.1과 4.3.3의 차이로 설명하기는 어려웠습니다.

이름이 비슷한 패키지가 많아 각각의 역할도 다시 구분해 봤습니다.

패키지 하는 일
Expo React Native 앱의 설정과 빌드 도구, 네이티브 기능 패키지를 제공하는 SDK
React Native JavaScript 코드와 iOS·Android 네이티브 UI를 연결하는 앱 실행 기반
expo-modules-core Expo 네이티브 모듈이 공통으로 사용하는 기반 구조
expo-modules-jsi Swift 네이티브 코드와 JavaScript 런타임을 연결하는 저수준 계층
Worklets UI 스레드 같은 별도 런타임에서 짧은 JavaScript 함수를 실행하는 엔진
Reanimated Worklets를 이용해 애니메이션을 UI 스레드에서 처리하는 라이브러리

오류가 난 곳은 expo-modules-jsi

전체 로그에서 처음 발생한 Swift 오류를 찾으니 expo-modules-jsi에 같은 메시지가 15번 나왔습니다.

'weak' must be a mutable variable, because it may change at runtime

Xcode는 여러 빌드 대상을 병렬로 컴파일합니다. 화면에 Worklets나 Reanimated가 마지막으로 보였더라도 실제 실패 지점은 다른 모듈일 수 있습니다. 함께 출력된 Script has ambiguous dependencies도 스크립트가 빌드마다 실행될 수 있다는 경고일 뿐, 코드 65를 만든 오류는 아니었습니다.

문제가 된 Swift 코드는 다음과 같은 형태였습니다.

weak let runtime: JavaScriptRuntime?

견적킷을 빌드한 환경은 Xcode 26.1.1과 Swift 6.2.1이었습니다. weak letSwift Evolution SE-0481에서 추가됐고 Swift 6.3부터 구현됐습니다. Swift 6.2에서는 weak 참조가 런타임에 nil로 바뀔 수 있다는 이유로 let을 허용하지 않습니다.

Expo SDK 56 공식 문서가 iOS 빌드에 Xcode 26.4 이상을 요구하는 이유가 여기에 있었습니다. 패키지는 같았지만 견적킷을 빌드하는 Swift 컴파일러가 SDK 56의 코드를 이해하지 못한 것입니다.

Bible Map의 patch-package

Bible Map에는 patch-packagepostinstall 설정이 들어 있었습니다. npm install이 끝날 때마다 expo-modules-jsi의 코드를 다음과 비슷하게 바꾸는 패치였습니다.

- weak let runtime: JavaScriptRuntime?
+ nonisolated(unsafe) weak var runtime: JavaScriptRuntime?

일부 Sendable 타입에는 @unchecked Sendable도 적용돼 있었습니다. 그래서 Bible Map은 같은 Xcode와 Swift 버전에서도 컴파일됐습니다. 두 앱의 결정적인 차이는 Reanimated의 패치 버전보다 이 Swift 호환 패치였습니다.

다만 nonisolated(unsafe)@unchecked Sendable은 컴파일러의 동시성 안전 검사를 우회합니다. 코드가 안전하다는 책임을 개발자가 직접 지는 방식이므로 장기적인 해결책으로 삼기에는 부담이 있습니다.

공식 해법은 Xcode 26.4 이상

Expo 유지보수자도 관련 이슈에서 SDK 56은 Xcode 26.4 이상과 Swift 6.3을 전제로 한다고 설명합니다. weak var로 바꾸는 패치는 권장하지 않으며, 빌드 환경을 올리는 것이 공식 해결책입니다.

따라서 견적킷은 Xcode를 26.4 이상으로 올리는 것이 우선입니다. 당장 개발을 이어가야 한다면 Bible Map의 패치를 임시로 적용할 수 있지만, Xcode를 올린 뒤에는 패치를 제거하고 깨끗한 상태에서 다시 빌드하는 편이 안전합니다.

이번 오류에서는 패키지 버전표보다 빌드 도구와 postinstall이 더 중요한 단서였습니다. 두 Expo 앱의 동작이 다르다면 lock 파일뿐 아니라 Xcode·Swift 버전, 적용된 패치와 전체 빌드 로그까지 함께 봐야 합니다.

마지막 수정:

공유하실 때는 출처(Duolabs)와 원문 주소를 표시해 주세요.