빌드 우회 셋 정리: 어디까지가 내가 고칠 수 있는 문제인가
앱 하나를 두 스토어에 올리는 동안 빌드가 여러 번 깨졌고, 그때마다 우회를 하나씩 넣었습니다. 다 끝나고 저장소를 보니 임시방편이 셋 쌓여 있었습니다.
그 자리에서는 각각 옳은 판단이었습니다. 문제는 남겨 두면 몇 달 뒤에 "이게 왜 여기 있지"가 된다는 것입니다. 그래서 하나씩 뿌리를 파봤는데, 셋 중 제가 고칠 수 있는 건 하나뿐이었습니다.
쌓인 것 셋
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 를 올리러 갔는데, 그러려면 운영체제부터 올려야 했습니다. 몇십 기가를 받아야 하는 일이라 저녁으로 미뤘습니다.
하필 유일하게 손댈 수 있는 항목이 제일 오래 걸리는 일이었습니다. 나머지 둘은 고치고 싶어도 못 고치고, 고칠 수 있는 하나는 시간이 없어서 못 고치는 상태로 오늘이 끝났습니다.
우리 서비스에 적용할 계획이 있으신가요?
모바일 앱 개발 살펴보기 →함께 읽기
- React Native와 Expo에서 OTA 업데이트를 이해하는 방법OTA(Over-The-Air) 업데이트는 앱 스토어 심사를 거치지 않고 설치된 앱의 JavaScript 코드와 에셋을 갱신하는 방식입니다. React Native와 Expo 환경에서는 빠른 버그 수정과 작은 기능 개선에 매우 유용합니다.
- Watchman은 React Native 개발에서 왜 필요할까React Native나 Expo로 개발하다 보면 코드를 고쳤는데 앱에 바로 반영되지 않는 경우가 있습니다. 이때 원인 중 하나가 파일 변경 감시 도구인 Watchman 문제일 수 있습니다.
- 여러 고객 앱을 운영할 때 계정과 서명 키를 분리하는 방법React Native와 Expo로 여러 고객 앱을 개발·배포한다면 계정, 비용, 서명 키, 스토어 권한을 어떻게 나눌지 초기에 정해야 합니다. 이 구조를 잘못 잡으면 나중에 소유권 이전, 업데이트, 보안 관리가 복잡해집니다.
- Expo SDK 56 앱을 App Store와 Google Play에 배포하기Expo 앱을 배포한다고 하면 보통 eas build 명령 하나를 떠올립니다. 실제 운영 배포는 빌드, 앱 서명, 스토어 업로드, 베타 테스트, 심사, 공개 출시, OTA 업데이트가 서로 다른 단계입니다. 이 구분을 놓치면 CI가 성공했는데 스토어에는 앱이 없거나, JavaScript 수정이라고 생각해 OTA를 발행했는…
- Expo로 개발할 때 자주 쓰는 실행 명령어 모음Expo 앱을 개발하다 보면 명령어보다 “지금 다시 빌드해야 하나?”가 더 헷갈립니다. 화면 코드만 바꿨는데 Gradle 빌드를 다시 돌리기도 하고, 반대로 스플래시 이미지를 바꾼 뒤 Metro만 재시작해서 왜 그대로인지 한참 보기도 합니다.