개발에 AtoZ까지

CodePush가 끝났습니다 — React Native OTA 업데이트를 EAS Update로 옮기기 본문

프론트엔드

CodePush가 끝났습니다 — React Native OTA 업데이트를 EAS Update로 옮기기

AtoZ 개발자 2026. 7. 19. 13:25
반응형

😀 개요

안녕하세요 😀

React Native 앱을 운영해 보셨다면 이런 경험이 있으실 겁니다. 오타 하나, 버튼 색 하나 고치자고 스토어 심사를 다시 기다리는 게 아까워서, CodePush로 JavaScript만 무선(OTA, 심사 없이 앱에 바로 반영)으로 밀어 넣던 방식 말입니다.

그런데 그 CodePush가 이제 없습니다. Microsoft의 App Center가 2025년 3월 31일 은퇴하면서 호스팅형 CodePush도 함께 종료됐고, 관련 저장소도 읽기 전용으로 아카이브됐습니다. 새로 OTA를 도입하려는 분도, 쓰던 걸 옮겨야 하는 분도 같은 질문 앞에 섭니다. "그럼 이제 뭘로 하지?"

결론부터 말하면, Expo가 공식적으로 권장하는 대체재는 EAS Update입니다. 다만 갈아타기 전에 꼭 짚어야 할 게 있습니다. OTA로 바꿀 수 있는 것과 없는 것의 경계, 그리고 우리 팀에 EAS Update가 맞는지 아니면 자체 호스팅이 맞는지입니다.

이 글은 Expo vs React Native CLI에서 시작해 development build로 네이티브 라이브러리 붙이기까지 이어 온 흐름의 다음 편입니다. 앱을 빌드했으면, 그다음은 배포와 업데이트니까요.

🎯 오늘 끝내는 목표

이 글을 다 읽으면 아래를 할 수 있습니다.

  • CodePush가 지금 어떤 상태인지 한 문장으로 설명할 수 있습니다.
  • OTA로 바꿀 수 있는 변경과, 반드시 새 빌드가 필요한 변경을 구분할 수 있습니다.
  • 우리 팀이 EAS Update로 갈지, 자체 호스팅이나 다른 도구를 볼지 판단 기준이 생깁니다.
  • EAS Update의 설정 흐름(명령·설정키) 을 처음부터 압니다.

기능 하나를 소개하는 게 아니라, "코드를 고친 뒤 사용자 손에 전달하는 경로" 를 다시 세우는 것이 이 글의 목적입니다.

🧭 근거부터 짚고 갑니다

미리 말씀드리면, 여기 나오는 명령·설정·가격은 제가 실제 상용 앱의 OTA 파이프라인을 운영해 본 결과가 아니라 Expo·Microsoft·Apple·Google의 공식 문서에서 확인해 정리한 것입니다.

버전과 수치는 확인일(2026년 7월 19일, Expo SDK 57 · React Native 0.86 기준) 입니다. Expo의 가격과 무료 한도, 그리고 각 라이브러리 버전은 자주 바뀌므로, 실제 작업 전에는 본인 시점의 공식 문서로 다시 확인하시는 편이 안전합니다.

🪦 CodePush는 정말 끝났나

먼저 현재 상태부터 정리하겠습니다. 여기가 흔들리면 뒤 결정이 다 흔들리기 때문입니다.

용어부터. OTA(Over-the-Air) 업데이트는 쉽게 말하면 앱스토어 심사를 거치지 않고 JavaScript·이미지 같은 부분만 서버에서 앱으로 바로 내려보내는 업데이트입니다. React Native 진영에서 이 방식을 대표하던 도구가 Microsoft의 CodePush였습니다.

공식 문서로 확인한 현재 상태는 이렇습니다.

  • App Center는 2025년 3월 31일 은퇴했습니다. 이후로는 로그인도, API 호출도 되지 않습니다.
  • 호스팅형 CodePush도 같은 날 완전히 종료됐고, 신규 앱 온보딩이 불가능합니다.
  • CodePush의 두 저장소(클라이언트 SDK와 서버)는 2025년 5월 20일 읽기 전용으로 아카이브됐습니다. Microsoft는 더 이상 유지보수·지원·기여를 받지 않고, "커뮤니티 포크용"으로만 공개해 두었습니다.
  • App Center 기능 중 지원이 연장된 것은 Analytics & Diagnostics뿐이고(2027년 3월까지), CodePush는 그 연장 대상이 아닙니다.

즉 "관리형 CodePush"라는 선택지는 이제 존재하지 않습니다. Microsoft가 은퇴 페이지에서 안내하는 유일한 CodePush 경로도 자체 호스팅용 오픈소스 서버인데, 그 서버 저장소마저 위처럼 아카이브 상태입니다.

그림 1. GitHub의 microsoft/react-native-code-push 저장소. 상단에 "2025년 5월 20일 소유자가 아카이브함, 읽기 전용" 배너와 "Public archive" 배지가 붙어 있고, 마지막 릴리스는 v9.0.1(2024-12-19)에서 멈춰 있습니다.
출처: github.com/microsoft/react-native-code-push, 확인일 2026-07-19.

🚦 OTA로 되는 것과 안 되는 것

갈아탈 도구를 고르기 전에, 사실 더 중요한 건 이겁니다. OTA로 애초에 바꿀 수 없는 게 있습니다. 여기를 오해하면 어떤 도구를 골라도 사고가 납니다.

Expo 공식 문서 기준으로, OTA 업데이트로 교체할 수 있는 건 JavaScript 번들, 스타일, 이미지 같은 asset 등 "네이티브가 아닌 부분"뿐입니다.

반대로 아래는 OTA로 안 되고, 새 바이너리를 빌드해서 스토어에 다시 제출해야 합니다.

  • 네이티브 코드나 네이티브 의존성(라이브러리) 변경
  • 카메라·위치 같은 앱 권한 변경
  • Expo SDK 버전 업그레이드
  • 그 밖에 새 앱 바이너리 버전이 필요한 모든 것

그림 2. JavaScript·asset 같은 비네이티브 변경만 OTA로 나가고, 네이티브·권한·SDK 변경은 새 빌드가 필요합니다.
runtimeVersion이 둘 사이의 호환성 게이트 역할을 합니다.

공식 문서도 같은 기준을 표로 제시합니다. "When to use EAS Update" 항목을 보면 어떤 변경이 OTA로 되고 어떤 변경이 새 빌드를 요구하는지 한눈에 정리돼 있습니다.

그림 3. Expo 공식 "EAS Update 소개" 문서의 When to use 표. JavaScript 버그 수정·문구/번역·UI 변경·롤아웃·CI 게시는 OTA로 가능(✓), 네이티브 코드·앱 권한·Expo SDK 버전·새 바이너리는 불가(✗)로 명시합니다. 출처: docs.expo.dev/eas-update/introduction, 확인일 2026-07-19.

이 경계를 지켜 주는 장치가 runtimeVersion 입니다. 쉽게 말하면 "이 빌드의 네이티브 코드와 호환되는 업데이트만 실행하라"는 표식입니다. 공식 문서 표현으로, 업데이트는 빌드의 네이티브 코드와 호환돼야 그 빌드에서 실행됩니다.

그래서 네이티브 코드가 바뀔 때마다, 업데이트를 게시하기 전에 새 빌드가 필요합니다. 앞서 development build 글에서 "네이티브 라이브러리를 추가하면 다시 빌드해야 한다"고 했던 그 원리가, 배포 단계에서도 그대로 이어집니다.

여기서 실수하기 쉬운 지점이 있습니다. 네이티브를 바꿨는데 runtimeVersion을 올리지 않으면, 버전이 "거짓으로 일치"해서 호환되지 않는 업데이트가 로드될 수 있습니다. 이때 expo-updates가 오류를 감지해 롤백을 시도하긴 하지만, 이건 최선의 노력일 뿐 항상 보장되지는 않습니다. 안전은 결국 "네이티브를 바꾸면 반드시 runtimeVersion을 올린다"는 규칙에서 나옵니다.

🛰️ EAS Update는 어떻게 동작하나

이제 대체재를 봅니다. EAS Update는 쉽게 말하면 Expo가 운영하는 OTA 업데이트 서비스이고, 앱 쪽에서는 expo-updates 라이브러리가 그 역할을 맡습니다.

기본 동작은 이렇습니다. 앱이 실행될 때 새 업데이트가 있는지 확인하고, 더 최신이면 내려받아 다음 실행 때(또는 설정에 따라) 반영합니다. 없으면 캐시된 최신 업데이트나 앱에 내장된 버전으로 동작합니다.

자동 확인 동작은 app.jsonupdates 설정으로 조정하고, 원하는 시점에 직접 확인하고 싶으면 공식 expo-updates API를 씁니다(공식 문서 기준).

import * as Updates from 'expo-updates';

async function checkForUpdate() {
  const result = await Updates.checkForUpdateAsync();
  if (result.isAvailable) {
    await Updates.fetchUpdateAsync();
    await Updates.reloadAsync(); // 새 업데이트로 앱을 다시 시작
  }
}

배포를 이해하려면 두 단어만 잡으면 됩니다.

  • 채널(channel) — 쉽게 말하면 "이 빌드가 구독하는 업데이트 줄"입니다. 빌드 시점에 확정되어 앱의 네이티브 코드 안에 박힙니다. 보통 production, preview처럼 씁니다.
  • 브랜치(branch) — 업데이트들이 순서대로 쌓이는 목록입니다. git 브랜치에 커밋이 쌓이는 것과 비슷합니다. 브랜치의 가장 최근 업데이트가 활성 업데이트가 됩니다.

채널은 기본적으로 같은 이름의 브랜치에 연결됩니다. 필요하면 채널이 가리키는 브랜치를 바꿀 수 있어서(eas channel:edit production --branch version-2.0), "staging에서 검증한 업데이트를 production으로 승격" 같은 패턴이 가능합니다.

🛠️ 설정 흐름: 설치부터 첫 게시까지

큰 흐름을 먼저 보겠습니다.

 

1단계 — eas-cli 준비

먼저 EAS 명령줄 도구를 설치하고 로그인합니다.

npm install --global eas-cli
eas login

2단계 — (필요 시) Expo 모듈 설치

프로젝트에 expo 패키지가 아직 없는 순정 React Native 앱이라면, EAS Update를 붙이기 전에 Expo 모듈을 먼저 설치합니다. 이미 Expo로 만든 프로젝트라면 이 단계는 건너뜁니다.

npx install-expo-modules@latest

참고로 예전 안내에 있던 "npx expo install expo-updates
따로 실행" 단계는 현재 공식 "Get started" 가이드에는 없습니다.
아래 configure가 필요한 설정을 대신 잡아 줍니다.

3단계 — 프로젝트 설정 (eas update:configure)

핵심 명령은 이 하나입니다.

eas update:configure

이 명령이 자동으로 아래를 처리해 줍니다.

  • app.jsonruntimeVersion, updates.url 을 쓰고 extra.eas.projectId 를 추가합니다.
  • 순정/네이티브 프로젝트(비 CNG)라면 AndroidManifest.xmlExpo.plist의 관련 값도 함께 편집합니다.
  • EAS Build를 쓴다면 eas.jsonpreview·production 프로파일에 channel을 자동으로 설정합니다. 채널을 프로파일마다 손으로 넣을 필요가 없다는 뜻입니다.

runtimeVersion의 기본값은 아래처럼 앱 버전을 따르는 정책입니다.

{
  "expo": {
    "runtimeVersion": { "policy": "appVersion" }
  }
}

정책은 목적에 따라 고릅니다(공식 문서 기준).

  • appVersion — 앱의 version 값과 동일. 기본이자 가장 무난합니다.
  • nativeVersion버전(빌드번호) 조합으로 더 촘촘하게 나눕니다.
  • fingerprint@expo/fingerprint가 네이티브 구성을 해시로 계산해 자동으로 버전을 매깁니다. 네이티브에 영향 주는 변경이 생기면 버전이 자동으로 바뀌어, 비호환 업데이트가 나갈 가능성을 크게 줄여 줍니다. 대신 빌드가 더 자주 필요해집니다.

4단계 — 빌드 (채널·runtimeVersion 심기)

채널과 runtimeVersion이 박힌 빌드를 만듭니다. 이 빌드가 나중에 어떤 업데이트를 받을지가 여기서 확정됩니다.

eas build --profile production --platform all

5단계 — 업데이트 게시 (eas update)

이제 JavaScript를 고친 뒤 OTA로 내보냅니다.

eas update --channel production --message "결제 화면 문구 수정" --environment production

게시할 때 알아 두면 좋은 세 가지입니다.

  • --channel(채널이 가리키는 브랜치에 게시) 또는 --branch(브랜치 직접 지정) 중 하나로 게시합니다. --channel로 게시하면 채널이 가리키는 브랜치(기본은 같은 이름의 브랜치)에 올라가고, 그 브랜치가 아직 없으면 자동으로 생성됩니다.
  • --environment 플래그는 Expo SDK 55 이상에서 필수입니다. (54 이하는 생략 시 로컬 .env로 폴백합니다.)
  • 단계적 배포(rollout)가 진행 중인 채널에는 --channel을 쓸 수 없습니다. 어느 브랜치인지 확정할 수 없기 때문이며, 이때는 --branch로 게시합니다.

브랜치·채널 관리는 eas branch:*, eas channel:* 명령으로 하고, 필요하면 eas update:republish(과거 업데이트 재게시)나 eas update:roll-back-to-embedded(내장 버전으로 롤백)도 있습니다.

🔁 CodePush를 쓰던 앱이라면

위 5단계는 새로 붙이는 경우이고, 이미 CodePush로 배포하던 앱은 정리할 게 더 있습니다. Expo 공식 마이그레이션 가이드가 짚는 핵심은 세 가지입니다.

  • 기존 react-native-code-push 의존성과 네이티브 연결을 제거합니다. 두 업데이트 시스템이 동시에 돌면 서로 업데이트를 덮어써 충돌하기 때문입니다.
  • codePush()로 앱을 감싸던 코드와 수동 sync() 호출을 걷어냅니다. 그 역할은 expo-updates가 대신합니다(기본은 실행 시 자동 확인).
  • 개념은 아래처럼 매핑됩니다.
CodePush EAS Update
Deployment (Staging / Production) Channel (preview / production) + Branch
Deployment key로 빌드가 구독 빌드에 박힌 channel로 구독
네이티브 호환성 개념이 약함 runtimeVersion으로 명시적 호환성 관리

가장 큰 차이는 마지막 줄입니다. CodePush에서 느슨했던 "이 업데이트가 이 빌드와 맞나"를 EAS Update는 runtimeVersion으로 분명하게 관리합니다. 그래서 마이그레이션의 절반은 도구를 바꾸는 일이 아니라, "네이티브가 바뀌면 새 빌드"라는 습관을 세우는 일입니다. 구체적인 전환 순서는 아래 참고 자료의 Expo "Migrate from CodePush" 문서를 그대로 따르시면 됩니다.

🍏 앱 심사 규정은 괜찮을까

"OTA로 앱을 고치는 게 스토어 정책상 괜찮나?"는 자주 나오는 걱정입니다. 결론은 범위를 지키면 허용입니다.

Apple — App Store 심사 지침 2.5.2는 앱이 자기 완결적이어야 하고 "앱의 기능을 새로 도입하거나 바꾸는 코드를 내려받아 실행"하는 것을 제한합니다. 다만 이건 절대 금지가 아니라 기능이 바뀌는지를 보는 기준입니다. 해석형(interpreted) 코드에 대한 예외 덕분에, JavaScript만 내려보내 이미 심사된 앱을 고치는 것은 허용됩니다. 대신 지켜야 할 선이 있습니다.

  • 앱스토어에 제출해 심사받은 주 목적(primary purpose)을 OTA로 바꾸지 않을 것
  • 심사 때 숨겨 둔 기능을 나중에 OTA로 "잠금 해제"하지 않을 것(지침 2.3.1, 숨은 기능 금지)

Google Play — 기기·네트워크 악용 정책은 "Google Play 외의 방법으로 앱을 수정·교체·업데이트하거나 실행 코드를 내려받는 것"을 원칙적으로 금지합니다. 하지만 인터프리터에서 도는 코드(웹뷰의 JavaScript 등)에는 이 제한이 적용되지 않는다는 예외가 있어, React Native의 JS 번들 OTA는 여기에 해당합니다. 단, OTA로 내려보내는 JS도 Play 정책을 지켜야 하며 심사 우회 용도로 쓰면 안 됩니다.

정리하면 이렇습니다. 심사된 앱의 범위 안에서 버그 수정·문구/번역 변경·UI 다듬기 같은 JS/asset 변경은 OTA로 나가도 됩니다. 반려는 "OTA를 써서"가 아니라 "OTA로 심사된 앱의 본질을 바꿔서" 생깁니다.

🧩 EAS Update가 맞는 팀, 아닌 팀

이제 결정입니다. 먼저 모든 선택지에 공통으로 걸리는 조건부터 짚겠습니다. New Architecture(React Native의 새 네이티브 구조)는 RN 0.76부터 기본값이었고, 0.82부터는 레거시 비활성화 설정(newArchEnabled=false 등)이 무시되어 사실상 강제됩니다(레거시 API 제거는 0.83부터 시작). 그래서 어떤 OTA 도구를 고르든 New Architecture 지원 여부가 협상 불가 조건입니다.

그 전제 위에서, 성향별로 이렇게 갈립니다.

이런 팀이라면 선택 New Arch 상태
관리형 편의, 운영 부담 최소화, Expo 생태계 EAS Update O Expo 공식·활발
CodePush API를 그대로 유지하고 싶음(호스팅형) Revopush O (RN ≥0.76) 활성, 클라이언트 fork
완전 통제 + 현대 OSS, 클라우드 인프라 운영 가능 hot-updater O 활성 자체호스팅
최소·DIY, git 저장소나 작은 서버로 충분 react-native-ota-hot-update O (v2+) 활성 경량

반대로 지금 피해야 할 선택도 분명합니다.

  • 순정 microsoft/react-native-code-push 단독 — 2025년 5월 아카이브됐고 New Architecture를 지원하지 않습니다. New Arch가 강제되는 RN 0.82 이상에서는 설치 자체가 막힙니다. 게다가 백엔드도 이미 종료됐습니다.
  • Ionic Appflow — 신규 고객 판매가 중단됐고(기존 고객 접근은 2027년까지), 애초에 Live Updates는 Capacitor/Cordova의 웹 asset을 배포하는 것이라 React Native 번들 OTA와는 대상이 다릅니다.

롤백이 걱정이라면, EAS Update도 이전 업데이트를 다시 올리는 eas update:republish와 앱에 내장된 버전으로 되돌리는 eas update:roll-back-to-embedded를 제공해 CodePush의 롤백을 대체합니다. 단계적 배포(rollout)와 롤백의 실전 운용은 다음 글에서 따로 다루겠습니다.

가격도 결정에 큰 변수입니다. 아래는 MAU(월 활성 사용자, 그달에 업데이트를 한 번이라도 받은 고유 기기 수) 기준 공개 가격입니다(expo.dev/pricing, 확인일 2026-07-19).

플랜 월요금 포함 MAU 초과 과금
Free $0 1,000 없음(초과 시 서빙 중단)
Starter $19 3,000 MAU당 $0.005 · 대역폭 $0.10/GiB
Production $199 50,000 MAU당 $0.005 · 대역폭 $0.10/GiB

무료 1,000 MAU는 하드 캡이라 넘으면 업데이트 서빙이 멈춥니다. 사용자 규모가 커지면 유료 플랜이나 자체 호스팅을 검토해야 합니다.

per-MAU 과금을 피하고 싶다면, expo-updates는 Expo Updates 프로토콜을 구현한 임의의 서버와도 동작합니다. Expo가 참조 서버를 공개하지만 "데모"일 뿐 프로덕션용은 아니라고 명시하므로, 프로덕션에서는 expo-open-ota 같은 커뮤니티 구현으로 인프라 비용만 부담하는 방식이 있습니다(대신 직접 운영).

✅ 옮기기 전 체크리스트

도구를 정하고 붙이기 전에 이 순서대로 짚으면 대부분의 사고를 줄일 수 있습니다. 필요하면 복사해서 쓰세요.

[현재 상태 파악]
- [ ] 지금 CodePush를 쓰는가? → 아카이브 상태 확인, 마이그레이션 시 CodePush 제거(중복 업데이트 충돌 방지)
- [ ] 우리 RN 버전은? 0.82 이상이면 New Architecture 전제 → 대안의 New Arch 지원 필수 확인

[경계 확인]
- [ ] 이번 변경이 JS/asset만인가(OTA 가능) vs 네이티브·권한·SDK 변경인가(새 빌드 필요)
- [ ] 네이티브를 바꿨다면 runtimeVersion을 올리고 새 빌드를 했는가

[EAS Update 설정]
- [ ] npm install --global eas-cli && eas login
- [ ] (순정 RN이면) npx install-expo-modules@latest
- [ ] eas update:configure  (app.json runtimeVersion/updates.url + eas.json channel 자동)
- [ ] eas build --profile production
- [ ] eas update --channel production --message "..." --environment production

[운영 판단]
- [ ] 예상 MAU가 무료 1,000을 넘는가 → 유료 플랜 or 자체 호스팅 검토
- [ ] 앱스토어 범위 준수: 심사된 주 목적/숨은 기능 규정 위반 없는가

🫡 마무리

정리하면, "CodePush가 끝났다"는 건 OTA를 포기하라는 뜻이 아니라 경로를 다시 고르라는 신호입니다. Expo 생태계에 있다면 EAS Update가 가장 매끄럽고, 완전한 통제나 비용 통제가 필요하면 hot-updater 같은 자체 호스팅이 답이 됩니다.

핵심은 두 가지입니다. OTA로 되는 것(JS·asset)과 안 되는 것(네이티브·권한·SDK)의 경계를 지키는 것, 그리고 네이티브가 바뀌면 runtimeVersion을 올리고 새 빌드를 한다는 것입니다. 이 두 규칙만 지키면 도구는 상황에 맞게 고르면 됩니다.

이 글은 Expo vs React Native CLI로 스택을 정하고, development build로 네이티브 라이브러리를 붙인 다음의 "배포" 편이었습니다. 아직 그 앞 단계에서 고민 중이라면 두 글을 먼저 보셔도 좋습니다.

다음 글에서는 EAS Update에서 단계적 배포(rollout)와 롤백을 실제로 어떻게 다루는지, 사고가 났을 때 되돌리는 흐름을 이어서 정리해 보겠습니다.

읽어 주셔서 감사합니다 🙇

참고 자료

대안 OTA 프로젝트

반응형
Comments