앱을 출시하고 나서 가장 답답했던 게 이거다. 오타 하나, 색깔 한 줄 고치려고 해도 앱스토어 심사를 다시 받아야 한다. 길면 며칠씩 걸린다. 급한 버그는 더 미칠 노릇이다.
그런데 RN 앱은 결국 JS 번들이 본체다. 네이티브 코드를 안 건드리는 수정이라면, 번들만 새로 내려주면 된다. 이게 OTA(Over-The-Air) 업데이트고, Expo에서는 EAS Update가 그 역할을 한다.
세팅
기존 Expo 프로젝트라면 명령어 한 번이면 된다.
eas update:configure
이러면 app.json에 updates.url이 자동으로 들어가고, runtimeVersion 정책이 잡힌다. 여기서 runtimeVersion이 핵심이다. 네이티브 코드의 "버전"을 가리키는 값인데, 이게 같아야만 OTA 번들이 호환된다고 판단한다.
{
"expo": {
"runtimeVersion": { "policy": "appVersion" },
"updates": { "url": "https://u.expo.dev/your-project-id" }
}
}
배포
빌드는 채널(channel)에 연결되고, 업데이트는 브랜치(branch)로 올라간다. 그냥 git 브랜치명에 맞춰 쏘면 된다.
eas update --branch production --message "결제 버튼 오타 수정"
20~30초면 끝난다. 이미 설치된 앱들은 다음 실행 때 새 번들을 받아간다. 심사 없음. 즉시 반영.
언제 안 되는지가 더 중요하다
처음에 제일 헷갈렸던 부분. OTA는 JS와 에셋만 갈아끼운다. 다음 경우는 절대 OTA로 안 된다. 새 빌드를 올려야 한다.
- 새 네이티브 라이브러리 추가 (
expo-camera등 설치) app.json의 네이티브 설정 변경 (권한, 아이콘, 스플래시)- Expo SDK 버전 업그레이드
이걸 모르고 네이티브 의존성을 추가한 채 eas update만 쏘면, 구버전 앱은 업데이트를 무시한다. runtimeVersion이 안 맞으니 호환되는 빌드만 받는 안전장치가 작동한 거다. 헷갈렸지만 사실 이게 앱이 깨지는 걸 막아주는 거였다.
업데이트 적용 시점 다루기
기본값은 "다음 콜드 스타트 때 적용"이다. 사용자가 앱을 껐다 켜야 보인다는 뜻. 더 빨리 적용하고 싶으면 코드에서 직접 제어한다.
import * as Updates from 'expo-updates';
async function checkUpdate() {
const update = await Updates.checkForUpdateAsync();
if (update.isAvailable) {
await Updates.fetchUpdateAsync();
await Updates.reloadAsync(); // 즉시 재시작
}
}
다만 앱 켜자마자 강제 리로드하면 사용자 입장에선 화면이 한 번 깜빡인다. 나는 "다음 실행 때 조용히 반영"을 기본으로 두고, 핫픽스가 급할 때만 강제 리로드를 쓰는 쪽으로 정리했다.
정리하면 EAS Update는 만능이 아니라 "JS 레벨 수정용 지름길"이다. 네이티브를 안 건드리는 버그·문구·로직 수정은 OTA로 즉시, 네이티브가 얽히면 정식 빌드로 — 이 경계만 명확히 두면 출시 후 운영이 훨씬 가벼워진다.