Expo에서 app.json 수정이 OTA 업데이트에 반영되지 않는 이유
Expo 앱의 app.json을 고친 뒤 저장소에 푸시했습니다. production 채널의 OTA 작업도 평소처럼 실행됐습니다. 배포 작업이 성공하면 변경 사항이 앱에 들어간 것처럼 느껴집니다. 이번에는 그렇지 않았습니다.
수정한 것은 iOS 권한 요청 문구였습니다. 사용하지 않는 카메라·마이크·Face ID 설명을 없애고, 실제로 사용하는 사진 보관함 설명을 구체적으로 바꿨습니다. OTA 작업은 끝났지만 App Store에 제출할 바이너리의 Info.plist는 이전 그대로였습니다.
app.json도 JSON이니 업데이트에 포함될 줄 알았습니다
처음에는 app.json이 JavaScript 프로젝트 안의 설정 파일이므로 OTA에 함께 실릴 거라고 생각했습니다. 화면 문구를 바꿀 때와 권한 문구를 바꿀 때 같은 커밋과 같은 배포 워크플로가 움직였기 때문입니다.
이 가설은 생성 결과를 확인하면서 깨졌습니다. Expo config plugin은 빌드 전에 app.json을 읽어 iOS의 Info.plist, Android의 manifest와 같은 네이티브 파일을 만듭니다. 권한 문구는 앱이 실행된 뒤 JavaScript가 읽어 보여 주는 텍스트가 아니라, 운영체제가 바이너리에서 읽는 네이티브 설정입니다.
즉 파일의 확장자가 JSON인지 아닌지는 기준이 아니었습니다. 그 설정이 최종적으로 JavaScript 번들에 들어가는지, 네이티브 바이너리에 구워지는지가 기준이었습니다.
Expo 앱에는 바뀌는 층과 고정된 층이 있습니다
Expo 공식 문서는 빌드를 두 층으로 설명합니다. 네이티브 코드는 앱 바이너리에 들어가며 OTA로 바꿀 수 없습니다. 그 위의 업데이트 층에는 호환되는 JavaScript 코드와 에셋을 배포할 수 있습니다.
제가 작업할 때는 다음처럼 구분합니다.
| 변경 내용 | OTA로 처리한 사례 | 새 빌드가 필요한 사례 |
|---|---|---|
| 화면 로직·문구·스타일 | JavaScript/TypeScript 수정 | |
| 네이티브 권한 설명 | Info.plist, Android manifest |
|
| 라이브러리 | 기존 네이티브 모듈을 사용하는 JS 수정 | 새 네이티브 모듈 설치·업그레이드 |
| 앱 설정 | 런타임에서 읽는 업데이트 데이터 | config plugin이 생성하는 설정 |
| 시스템 통합 | entitlement, URL scheme, 앱 아이콘 등 |
이 표의 목적은 “app.json은 전부 빌드”라고 외우는 것이 아닙니다. 같은 파일 안에서도 어떤 항목은 업데이트 메타데이터에 영향을 주고, 어떤 항목은 네이티브 설정을 생성합니다. 변경한 키가 어느 결과물로 가는지 확인해야 합니다.
runtimeVersion은 안전선이지 변환기가 아닙니다
EAS Update는 runtimeVersion으로 업데이트와 설치된 바이너리의 호환성을 판단합니다. 새 OTA가 기존 바이너리에 없는 네이티브 API를 호출하면 앱이 실행될 수 없기 때문입니다.
{
"expo": {
"runtimeVersion": {
"policy": "appVersion"
}
}
}여기서도 오해할 수 있습니다. runtime version이 맞는다고 해서 OTA가 네이티브 설정을 바꿔 주지는 않습니다. 이것은 “이 업데이트를 이 바이너리가 실행해도 되는가”를 구분하는 호환성 표지입니다. 네이티브 코드나 설정을 바꿨다면 그 내용을 담은 새 빌드가 먼저 있어야 합니다.
Expo는 네이티브 변경을 자주 하는 프로젝트에서 fingerprint 정책도 안내합니다. 네이티브 런타임에 영향을 줄 수 있는 프로젝트 변경으로부터 값을 계산해 호환되지 않는 업데이트가 기존 빌드에 전달될 가능성을 줄이는 방식입니다. 어떤 정책을 쓰든 새 바이너리가 필요한 변경 자체를 OTA로 바꾸는 것은 아닙니다.
저는 생성 파일을 확인하고 빌드를 교체했습니다
이번 수정은 아래 순서로 처리했습니다.
app.json의 플러그인 옵션에서 사용하지 않는 권한 문구를 제거했습니다.expo prebuild로 iOS 프로젝트를 다시 생성했습니다.- 생성된
Info.plist에 실제 사용하는 사진 보관함 문구만 남았는지 확인했습니다. - iOS production 빌드를 새로 만들면서 빌드 번호를 올렸습니다.
- TestFlight 처리가 끝난 뒤 App Store Connect에서 새 빌드를 선택해 심사에 다시 제출하도록 구분했습니다.
네이티브 생성 폴더가 Git에서 제외되어 있으면 저장소 상태가 clean으로 보여도 이상하지 않습니다. clean은 생성 파일이 바뀌지 않았다는 뜻이 아니라 추적 대상 파일에 변경이 없다는 뜻입니다. 그래서 prebuild 결과를 눈으로 확인하는 단계가 필요했습니다.
당시 앱 버전은 아직 출시되지 않았습니다. 사용자에게 보이는 버전은 유지하고 내부 빌드 번호만 올렸습니다. 심사에서 반려된 바이너리를 같은 버전의 새 빌드로 교체한 사례입니다. 이미 출시된 버전의 업데이트라면 App Store Connect의 버전 상태에 맞춰 별도로 판단해야 합니다.
EAS Submit 다음에도 버튼 하나가 남았습니다
eas build --auto-submit이나 eas submit의 “submit”은 공개 심사 제출과 같은 말처럼 보입니다. iOS 기본 동작은 바이너리를 App Store Connect에 업로드하고 TestFlight에서 사용할 수 있게 만드는 단계입니다. Expo 문서에도 공개 출시를 위해서는 App Store Connect에 로그인해 해당 빌드를 App Review로 제출해야 한다고 적혀 있습니다.
자동화 상태를 볼 때 저는 이제 세 단계를 따로 봅니다.
- OTA 배포가 성공했는가
- 새 네이티브 바이너리가 빌드되고 TestFlight에 올라갔는가
- App Store Connect에서 그 빌드를 버전에 연결해 심사 제출했는가
세 화면이 모두 초록색일 필요는 없습니다. JavaScript만 고쳤다면 첫 번째로 끝날 수 있습니다. 권한 문구를 고쳤다면 첫 번째의 성공 여부와 관계없이 두 번째와 세 번째가 필요했습니다.
다음에는 커밋 전에 변경된 파일 이름보다 변경 결과를 먼저 볼 생각입니다. Info.plist, manifest, entitlement, 네이티브 모듈, config plugin 출력 중 하나가 달라지면 새 빌드 쪽에 놓습니다. 화면 로직과 스타일처럼 기존 런타임 안에서 실행되는 변경만 OTA 후보로 둡니다.
참고한 공식 문서
함께 읽기
- Expo SDK 56 앱을 App Store와 Google Play에 배포하기Expo 앱을 배포한다고 하면 보통 eas build 명령 하나를 떠올립니다. 실제 운영 배포는 빌드, 앱 서명, 스토어 업로드, 베타 테스트, 심사, 공개 출시, OTA 업데이트가 서로 다른 단계입니다. 이 구분을 놓치면 CI가 성공했는데 스토어에는 앱이 없거나, JavaScript 수정이라고 생각해 OTA를 발행했는…
- 쓰지 않는 iOS 권한 문구 때문에 App Store 자동 검사에서 반려되는 이유앱스토어 심사라고 하면 사람이 아이폰을 들고 앱을 하나씩 눌러 보는 장면부터 떠올리기 쉽습니다. 저도 그랬습니다. 그런데 iOS 앱을 제출한 뒤 받은 반려 메시지에는 automated analysis라는 표현이 적혀 있었습니다. 사람 심사관의 판단을 기다리기도 전에 업로드한 바이너리의 권한 문구가 자동 분석에서 걸린 경…
- React Native와 Expo에서 OTA 업데이트를 이해하는 방법OTA(Over-The-Air) 업데이트는 앱 스토어 심사를 거치지 않고 설치된 앱의 JavaScript 코드와 에셋을 갱신하는 방식입니다. React Native와 Expo 환경에서는 빠른 버그 수정과 작은 기능 개선에 매우 유용합니다.
- Expo 앱은 만들었는데 출시가 안 됩니다: `eas build`보다 먼저 준비할 것들Expo를 사용하면 네이티브 빌드 과정이 크게 단순해집니다. 하지만 앱스토어 출시까지 명령어 한두 줄로 끝나는 것은 아닙니다. 실제 자동 배포를 만들려면 앱 등록, 서명 자격 증명, 스토어 API 권한, CI/CD 승인 절차가 먼저 준비되어야 합니다.
- 같은 Expo 앱인데 배포 방식은 달랐습니다: iOS와 Android 심사 제출 비교기앞선 글에서는 이미 TestFlight에 올라간 iOS 빌드를 AI가 App Store 심사까지 제출한 과정을 다뤘습니다.