빌드 우회 셋 정리: 어디까지가 내가 고칠 수 있는 문제인가
앱 하나를 두 스토어에 올리는 동안 빌드가 여러 번 깨졌고, 그때마다 우회를 하나씩 넣었습니다. 다 끝나고 저장소를 보니 임시방편이 셋 쌓여 있었습니다.
그 자리에서는 각각 옳은 판단이었습니다. 문제는 남겨 두면 몇 달 뒤에 "이게 왜 여기 있지"가 된다는 것입니다. 그래서 하나씩 뿌리를 파봤는데, 셋 중 제가 고칠 수 있는 건 하나뿐이었습니다.
쌓인 것 셋
patch-package로 Expo 내부 Swift 코드 15군데를 옛 문법으로 되돌리기- 빌드 설정에
EXPO_USE_PRECOMPILED_MODULES=0넣기 overrides로@react-native/*네 개를 특정 버전에 묶기
첫째는 제 컴퓨터만의 문제였습니다
Expo SDK 56 의 내부 모듈이 weak let 이라는 문법을 씁니다. Swift 6.3 에서 들어온 것인데, 제 맥의 Xcode 는 26.1.1 이고 거기 딸린 Swift 는 6.2.1 이라 그 문법 자체가 없습니다. 그래서 로컬 빌드가 열다섯 개 에러로 깨졌습니다.
그런데 EAS 빌드 로그를 다시 읽다가 이 줄을 봤습니다.
sdk | iPhoneOS26.4.sdk빌드 서버는 처음부터 문제가 없었습니다. 저보다 새 Xcode 를 쓰고 있었으니까요.
즉 이 패치는 서버에서는 아무 쓸모가 없는데 제 컴퓨터가 낡아서 저장소에 들어간 코드입니다. 저장소는 팀이 공유하는 자리인데 거기에 개인 환경 사정이 커밋된 셈입니다.
근본 해결은 명확합니다. Xcode 를 올리고 패치를 지우면 됩니다.
둘째는 남의 문제였습니다
EAS 는 빌드를 빨리 하려고 일부 패키지를 소스에서 컴파일하지 않고 미리 만들어 둔 바이너리로 내려받습니다. 그런데 바이너리만 받으면 헤더 파일이 공개 경로에 깔리지 않습니다. 바이너리를 쓰는 쪽은 헤더가 필요 없으니 당연한 설계입니다.
문제는 그 패키지의 헤더를 다른 패키지가 참조하고 있었다는 것입니다. 한쪽은 바이너리로 오고 한쪽은 소스로 빌드되니 그 사이 연결이 끊어집니다.
이건 제가 고칠 수 있는 게 아닙니다. 미리컴파일을 끄는 것 말고는 방법이 없고, 대가는 빌드 시간입니다. 전부 소스에서 컴파일하게 되니까요.
셋째는 우회라기보다 위생이었습니다
한 패키지가 다른 패키지를 * 로 요구하고 있었습니다. 아무 버전이나 좋다는 뜻이라 최신이 끌려왔고, 그게 다른 곳에서 요구하는 버전과 충돌했습니다.
버전을 고정하는 건 사실 우회라기보다 정상적인 의존성 관리에 가깝습니다. 그대로 둬도 손해가 없어서, 셋 중 유일하게 걷어낼 이유가 없는 항목이었습니다.
둘째와 셋째의 뿌리가 같았습니다
여기서 한 가지 희망을 봤습니다. 두 문제 모두 특정 애니메이션 라이브러리와 그 짝이 의존성 트리에 있다는 데서 나옵니다. 그런데 저희 앱에는 애니메이션이 없습니다. 지도에 선을 긋고 점을 찍는 게 전부입니다.
그래서 빼면 둘이 한 번에 사라지지 않을까 싶어 확인해봤습니다. 라우터 패키지의 정의를 열어 보니 이렇게 돼 있었습니다.
"peerDependencies": { "react-native-reanimated": "*" },
"peerDependenciesMeta": { "react-native-reanimated": { "optional": true } }선택 사항입니다. 안 써도 된다는 뜻이라 잠깐 기뻤습니다.
그런데 같은 라우터가 서랍 메뉴 패키지를 진짜 의존성으로 끌어오고, 그 패키지가 애니메이션 라이브러리를 요구합니다. 저희는 서랍 메뉴를 한 번도 쓰지 않는데도 딸려옵니다.
라우터를 쓰는 한 뺄 수 없다는 결론이었습니다.
정리하면
| 우회 | 누구 문제인가 | 언제 걷어낼 수 있나 |
|---|---|---|
| Swift 패치 | 내 개발 환경 | Xcode 올리면 즉시 |
| 미리컴파일 끄기 | 업스트림 | 고쳐질 때까지 유지 |
| 버전 고정 | 업스트림의 느슨한 범위 | 걷어낼 이유 없음 |
이 표를 만들고 나서야 마음이 편해졌습니다. 셋을 뭉뚱그려 "임시방편이 쌓였다"고 볼 때는 다 빚처럼 느껴졌는데, 나눠 놓으니 갚아야 할 건 하나뿐이었습니다.
우회를 남길 때 같이 적어야 하는 것
전에는 우회를 넣을 때 왜 넣었는지만 적었습니다. 이번에 배운 건 그것보다 언제 빼도 되는지가 더 중요하다는 것입니다.
그래서 프로젝트 문서에 이렇게 적어 뒀습니다.
Xcode 를 26.2 이상으로 올리면 이 패치를 지워야 한다. 업스트림이 의도한 코드가 그쪽이다.
이유만 적혀 있으면 나중에 읽는 사람은 "그렇구나" 하고 그대로 둡니다. 조건이 적혀 있으면 그 조건이 충족됐는지 확인하게 됩니다.
오늘 못 한 것
셋 중 유일하게 제가 고칠 수 있는 것을 하려고 Xcode 를 올리러 갔는데, 그러려면 운영체제부터 올려야 했습니다. 몇십 기가를 받아야 하는 일이라 저녁으로 미뤘습니다.
하필 유일하게 손댈 수 있는 항목이 제일 오래 걸리는 일이었습니다. 나머지 둘은 고치고 싶어도 못 고치고, 고칠 수 있는 하나는 시간이 없어서 못 고치는 상태로 오늘이 끝났습니다.
함께 읽기
- Expo 앱 다국어: 사전의 키를 한국어 원문으로 둔 이유성경 지도 앱에 언어 다섯 개를 더했습니다. 영어, 일본어, 중국어 간체와 번체, 스페인어입니다. 화면이 하나뿐이고 서버도 없는 앱이라 붙이는 일 자체는 간단할 줄 알았는데, 번역문을 어디에 둘지에서 막혔습니다.
- EAS 배포 구조: Build·Submit·Update 는 왜 따로인가앱 하나를 두 스토어에 올리면서 EAS 를 처음 제대로 썼습니다. 그 전까지는 "배포 명령을 돌리면 스토어에 올라간다" 정도로만 알고 있었는데, 실제로 해보니 그 한 문장 안에 서로 다른 세 가지 일이 섞여 있었습니다. 그걸 구분하고 나서야 어디서 막혔는지가 보였습니다.
- Metro 포트 충돌로 배운 것: RN이 OTA 업데이트를 할 수 있는 이유새로 만든 앱을 아이폰에 설치했습니다. 빌드는 성공했고 설치도 됐는데, 앱을 열자 빨간 에러 화면이 떴습니다. 그런데 에러가 가리키는 파일이 이상했습니다.
- Expo config plugin이 반영되지 않는 이유: prebuild와 mod 순서생명시계 앱에 Face ID 잠금을 붙였습니다. expo-local-authentication 을 설치하고 app.json 에 플러그인과 권한 문구를 적었습니다.
- Android Studio 없이 실기기 빌드: 최소 환경 838MB와 각 구성요소의 역할Expo SDK 57 앱을 안드로이드 실기기에 올리려고 맥을 열었는데, 빌드에 필요한 것이 하나도 없었습니다. java -version은 "Unable to locate a Java Runtime"을 냈고 ANDROIDHOME은 비어 있었으며 adb는 명령어 자체가 없었습니다. Xcode만 들어 있는 기계였습니다.