빌드 우회 셋 정리: 어디까지가 내가 고칠 수 있는 문제인가
앱 하나를 두 스토어에 올리는 동안 빌드가 여러 번 깨졌고, 그때마다 우회를 하나씩 넣었습니다. 다 끝나고 저장소를 보니 임시방편이 셋 쌓여 있었습니다.
그 자리에서는 각각 옳은 판단이었습니다. 문제는 남겨 두면 몇 달 뒤에 "이게 왜 여기 있지"가 된다는 것입니다. 그래서 하나씩 뿌리를 파봤는데, 셋 중 제가 고칠 수 있는 건 하나뿐이었습니다.
쌓인 것 셋
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 를 올리러 갔는데, 그러려면 운영체제부터 올려야 했습니다. 몇십 기가를 받아야 하는 일이라 저녁으로 미뤘습니다.
하필 유일하게 손댈 수 있는 항목이 제일 오래 걸리는 일이었습니다. 나머지 둘은 고치고 싶어도 못 고치고, 고칠 수 있는 하나는 시간이 없어서 못 고치는 상태로 오늘이 끝났습니다.
함께 읽기
- EAS 배포 구조: Build·Submit·Update 는 왜 따로인가앱 하나를 두 스토어에 올리면서 EAS 를 처음 제대로 썼습니다. 그 전까지는 "배포 명령을 돌리면 스토어에 올라간다" 정도로만 알고 있었는데, 실제로 해보니 그 한 문장 안에 서로 다른 세 가지 일이 섞여 있었습니다. 그걸 구분하고 나서야 어디서 막혔는지가 보였습니다.
- Metro 포트 충돌로 배운 것: RN이 OTA 업데이트를 할 수 있는 이유새로 만든 앱을 아이폰에 설치했습니다. 빌드는 성공했고 설치도 됐는데, 앱을 열자 빨간 에러 화면이 떴습니다. 그런데 에러가 가리키는 파일이 이상했습니다.
- Expo 앱 스토어 배포 준비 체크리스트: 순서가 중요한 이유새 Expo 앱 하나를 두 스토어에 올릴 준비를 했습니다. 필요한 항목 자체는 검색하면 다 나옵니다. 그런데 정작 저를 막은 건 항목이 아니라 순서였습니다. 앞 단계가 끝나야 뒤 단계의 값이 생기는 구간이 몇 군데 있고, 그걸 모르고 덤비면 "왜 이 필드가 비어 있지"에서 멈춥니다.
- Expo로 개발할 때 자주 쓰는 실행 명령어 모음Expo 앱을 개발하다 보면 명령어보다 “지금 다시 빌드해야 하나?”가 더 헷갈립니다. 화면 코드만 바꿨는데 Gradle 빌드를 다시 돌리기도 하고, 반대로 스플래시 이미지를 바꾼 뒤 Metro만 재시작해서 왜 그대로인지 한참 보기도 합니다.
- 여러 고객 앱을 운영할 때 계정과 서명 키를 분리하는 방법React Native와 Expo로 여러 고객 앱을 개발·배포한다면 계정, 비용, 서명 키, 스토어 권한을 어떻게 나눌지 초기에 정해야 합니다. 이 구조를 잘못 잡으면 나중에 소유권 이전, 업데이트, 보안 관리가 복잡해집니다.