👋🏻 Visual Studio App Center is scheduled for retirement
이 포스팅을 시작하기 전 알아두어야 할 점은 Microsoft는 Visual Studio App Center에 대한 지원을 2025년 3월 31일부로 중단할 것을 발표했다는 것이다. CodePush는 Visual Studio App Center에서 제공하는 서비스이므로, 기존에 Codepush를 사용하고 있었다면 새로운 환경을 모색해야 하는 상황이다.

🤔 근데 이 아티클은 왜 작성한거지?
CodePush는 Visual Studio App Center의 일부로 통합되어 운영되었지만 Microsoft는 App Center 종료 이후에도 CodePush는 별도의 대안으로 계속 마이그레이션 가이드를 발표했다. 이에 따라 CodePush 사용자는 App Center CLI를 통해 기존 배포 데이터를 백업하고 새로운 환경으로 이전할 수도 있고, AWS S3, Firebase Hosting 등과 같은 클라우드 스토리지를 활용하거나 자체 서버를 구축해 CodePush를 계속 사용할 수 있게 되었다.
앱 개발을 하면서, 심사를 거치지 않는 빠르게 업데이트가 필요한 경우 CodePush를 유용하게 사용해왔고, 앞으로도 자주 사용하게 될 것이라고 예상되기 때문에 App Center가 완전히 종료되기 전 새로운 환경을 마련할 필요가 있었다. 이런 상황에서 내가 생각한 대안은 아래의 두 가지였다.
- Expo에서 제공하는 EAS Update 서비스로 마이그레이션하는 방법
- MS 마이그레이션 가이드를 참고하여 자체 서버를 구축하는 방법

- Expo Application Services의 일부로, React Native 앱에서 JavaScript 코드와 리소스를 앱 스토어 재배포 없이 업데이트 할 수 있게 해주는 OTA 업데이트 솔루션이다.
- Expo 환경에 최적화 되어 있긴 하지만 Expo SDK를 사용하지 않는 Bare Workflow(순수 React Native 환경)에서도 의존성 및 설정 추가를 통해 사용할 수 있다.
- Expo CDN을 통해 빠르고 안정적인 업데이트를 제공하고, 채널을 통해 다양한 환경에서의 업데이트를 지원한다.
🤨 사실 충분히 안정적이고 괜찮은 대안이지만, 아래와 같은 이유로 고민 없이 바로 도입하는 것은 망설여졌다.
- 유료 서비스이다. 한 달에 30회의 빌드를 업데이트할 수 있는 무료 버전이 존재하고, 이후에는 유료 서비스를 구독하거나 추가 사용량 만큼 요금을 지불하는 방식이다.
- Expo 생태계에 대한 의존성이 추가된다. 현재 구현된 앱이 순수 React Native 환경인데, OTA을 위해 Expo 관련 라이브러리와 설정을 추가해야 한다.
🤔 직접 환경을 구축해볼까?
- 이러한 이유로 MS사에서 공개한 마이그레이션 가이드를 기반으로 직접 회사에서 사용할 수 있는 로컬 OTA 환경을 구축하는 것에 대해 고민해보게 되었다.
- 이미 코드 베이스를 public으로 공개해둔 상태여서 서버 환경만 구축해도 되긴 하지만 조금 더 본질적으로 CodePush의 동작 원리를 이해하면 기존 CodePush의 문제점을 개선해볼 수 있겠다는 생각이 들었다.
- CodePush API 응답 지연으로 인해 사용자가 CodePush 적용 버전을 받아보기 까지 1-2초의 지연이 발생하는 경우가 존재한다.
- 이로 인해 이미 앱을 사용 중이던 사용자의 화면이 초기 화면으로 돌아가 '앱이 튕긴다', '앱이 깜빡인다'는 등의 피드백을 받는 등 사용자 경험에 실제로 부정적인 영향을 주었다.
- 이러한 이유로 App Center는 곧 서비스를 종료하지만, Code Push 기능이 어떻게 동작하는지에 대해 더 자세히 알아보게 되었다.
✨ CodePush란?

CodePush란 MicroSoft App Center에서 제공하는 클라우드 서비스로, React Native 개발자가 앱의 JavaScript 코드와 이미지 등의 asset을 무선(OTA, Over-The-Air)으로 사용자 기기에 직접 배포할 수 있게 한다.
장점
- 앱 스토어의 심사를 통한 재배포 없이도 변경 사항을 즉시 사용자에게 전달할 수 있다.
- React Native 앱에 CodePush 클라이언트 SDK를 추가하고 App Center CLI를 통해 간편하게 업데이트 할 수 있다.
- 실제로 회사에서도
codepush-{os명}
만 입력하면 간단하게 JS 번들 파일을 App Center에 업로드할 수 있게 구현되어 있었다.
- 롤백 및 롤아웃 지원
- 배포된 업데이트에 문제가 발생할 경우 자동으로 이전 안정 버전으로 롤백하여 사용자 경험을 보호한다.
- 크래시가 발생하지 않더라도 문제가 있는 버전, 수정이 필요한 버전이라고 판단될 경우 수동으로 해당 버전을 롤백할 수도 있다.
- 전체 사용자가 아닌 n%의 사용자에 대해서만 해당 번들을 노출시키는 롤아웃 설정도 가능하다.
한계
- 네이티브 코드를 변경할 수 없다. Code Push는 JavaScript단 업데이트에만 적용되며, 네이티브 코드(C, C++, Java, Objective-C 등)의 변경은 지원하지 않는다. 따라서 의존성이 변경되거나 권한 관련 설정을 추가한 경우 등에는 앱 심사를 거쳐 재배포해야 한다.
💫 CodePush의 동작 원리
🏗️ React Native 아키텍쳐

이미지 출처
- React Native로 만들어진 앱에서 무엇을 렌더링할지 결정하는 영역은 JavaScript로 이루어진 React Native의 영역이다.
- React Native 앱에서 JS 번들과 앱의 메인 스레드 코드는 분리되어 있다.
- JS 번들 파일을 네트워크를 통해 전달받을 수 있다면, 실시간으로 파일 업데이트가 가능하다.
- 새로운 업데이트 사항이 플랫폼 네이티브 코드를 변경하지 않는다면 React Native의 렌더링 시스템인 Fabric에게 새로운 React Element를 그리도록 명령하는 React 코드만 작성하면 된다.
Fabric
- React에서 생성된 React Element Tree를 기반으로 Shadow Tree라는 구조를 만들어 네이티브 화면에 최적화된 방식으로 렌더링한다.
- 새로운 업데이트에서 JavaScript 코드만 변경된 경우 Fabric에게 새로 생성된 React Element Tree를 전달하면 된다. Fabric은 새로 받은 JavaScript 코드를 바탕으로 새로 화면을 그린다.
- 이 과정은 웹 페이지에서 DOM을 새로고침하는 것처럼 변경된 내용을 즉시 반영할 수 있게 한다.
- JavaScript는 컴파일 없이 바로 실행할 수 있는 언어여서 이미 React Native 앱에 내장되어 있는 JavaScript 런타임이 실행할 JS 번들을 새로 받아오기만 하면 업데이트된 UI와 로직이 즉시 반영된다.
- JavaScript는 인터프리터 방식으로 동작하여 코드가 작성된 순간 바로 실행될 수 있다.
- 반면 네이티브 환경을 구성하는 C와 Java는 컴파일 언어로, 반드시 컴파일 환경을 거쳐야 해서 코드 변경 사항이 즉각적으로 반영되지 않는다.
- CodePush는 새로 받아온 JS 번들을 실시간으로 서버에서 다운로드 해 기존 번들과 교체하는 OTA 업데이트 과정을 거친다.
🧐 정리하자면..
- CodePush는 서버에서 최신 JavaScript 번들을 다운로드한 뒤 기존 번들과 교체한다. 이후 Fabric Renderer는 새로운 JS 코드를 읽어 화면을 다시 그린다.
- 사용자가 앱을 재시작하거나 특정 시점에
codePush.sync()
메서드가 호출되면 새로운 React Element Tree가 생성되고 Fabric이 이를 네이티브 UI로 변환한다.
1. CLI로 업데이트 번들 업로드
1️⃣ 번들 생성 및 메타데이터 수집
- 개발자가 CLI에서 명령어를 실행하면 React Native 앱의 JavaScript 코드와 이미지 등은 Metro Bundler를 통해 하나의 번들 파일로 만들어진다.
- Metro Bundler: React Native에서 기본적으로 사용하는 JavaScript 번들러. 여러 개로 나뉜 JavaScript 파일과 종속성을 하나의 파일로 결합하여 앱에서 실행 가능한 단일 JavaScript 번들을 생성한다.
- 이때 앱 ID, 배포 키, 앱 버전, 업데이트 설명, 필수 적용 여부(Mandatory) 등 업데이트에 필요한 정보를 함께 수집한다.
2️⃣ HTTP 요청을 통한 업로드
- CLI 내부에서 Node.js 모듈을 사용해 CodePush 서버의 REST API 엔드포인트로 POST 요청을 보낸다.
- 번들 파일의 내용과 해시, 앱 버전 등 검증 정보를 HTTP 요청의 본문에 포함하여 전송한다.
2. 서버 측 업데이트 처리 (코드)
1️⃣ 요청 수신 및 검증
- CodePush 서버는 Node.js 프레임워크를 사용해 클라이언트(현재는 App Center)의 업로드 요청을 수신하고 검증하는 과정을 거친다.
- 전달된 배포 키, 앱 버전, 해시 값 등을 검사해 해당 번들이 유효한 업데이트인지 무결성을 체크한다.
- 또한 문제가 발생할 경우 롤백에 대비해 이전 업데이트 정보도 함께 보관한다.
2️⃣ 번들 파일 저장 및 메타데이터 갱신
- 번들 파일 검증이 완료되면 새 업데이트 번들 파일을 로컬 파일 시스템이나 Azure Blob Storage와 같은 외부 스토리지에 저장한다.
- Azure Blob Storage: 클라우드를 위한 Microsoft의 개체 스토리지 솔루션. 대량의 비정형 데이터를 저장하는 데 최적화되어 있다.
- 데이터베이스를 갱신하여 해당 배포 버전(Staging, Production)의 현재 릴리즈 정보를 새 업데이트로 교체한다.
3️⃣ 업데이트 활성화 및 자동 롤백
- 클라이언트가 업데이트를 체크할 때 최신 릴리즈 정보를 응답할 수 있도록 배포 정보를 업데이트 한다.
- 만약 앱 충돌 등의 이유로 업데이트가 실패한 경우 이전의 안정된 버전으로 복구할 수 있도록 롤백 정보를 유지한다.
3. 클라이언트 측 업데이트 적용
1️⃣ 업데이트 확인
-
앱 내의 CodePush 클라이언트 SDK는 앱 시작 또는 백그라운드에서 상태 변환 시 checkForUpdate()
메서드를 호출해 서버에서 업데이트 정보를 가져온다.
codePush.checkForUpdate().then((update) => {
if (update) {
codePush.sync();
}
});
- CodePush 클라이언트 SDK는 해당 메서드가 실행되면 앱이 실행 중일 때 CodePush 서버에 업데이트 정보를 요청한다.
const queryUpdateWithCurrentPackage = (serverUrl, packageInfo) => {
return fetch(`${serverUrl}/updateCheck`, {
method: "POST",
body: JSON.stringify(packageInfo),
}).then((response) => response.json());
};
- 클라이언트는 앱의 현재 패키지 정보(
package_hash
, target_binary_range
)를 서버로 전송하고, 서버는 이에 맞는 업데이트 정보를 반환한다.
- 서버는 클라이언트가 보낸 정보와 저장된 최신 릴리즈를 비교해 새 업데이트가 있을 경우 업데이트 데이터(다운로드 URL, 해시, 버전 등)를 반환한다.
- 자체 프로세스를 구축하면서 네트워크 병목을 줄이기 위해서 이 JSON 응답 데이터를 캐싱하거나 S3와 같은 CDN을 활용해 다운로드 속도를 개선해보면 좋을 것 같다. (사용자가 느끼던 앱 충돌도 이 방식으로 개선해볼 수 있을 것으로 예상된다.)
-
업데이트가 필요한 경우 클라이언트는 sync()
메서드를 통해 번들을 다운로드한다.
codePush.sync({
updateDialog: true,
installMode: codePush.InstallMode.IMMEDIATE,
});
DownloadProgress
이벤트로 다운로드 진행률을 추적할 수 있다.
2️⃣ 업데이트 번들 다운로드 및 설치
- 클라이언트는 제공받은 다운로드 URL을 통해 새 번들을 다운로드한다.
- 다운로드 후 파일의 해시 등을 검증해 무결성을 검사하고, 앱의 Document 디렉터리와 같은 로컬 파일 시스템에 저장한다.
- iOS의
NSDocumentDirectory
, Android의 getFilesDir()
- 이후 앱 실행 시 기본 번들(
main.jsbundle
) 대신 이 저장된 번들을 로드한다.
- iOS:
RCTCodePush.m
에서 loadBundle
메서드로 번들을 로드한다.
- Android:
CodePush.java
에서 React Native의 ReactInstanceManager
를 사용해 JS 번들 경로를 변경한다.
iOS에서 JS 번들 경로 파악하기
AppDelegate.m
파일의 sourceURLForBridge
메서드
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge {
#ifdef DEBUG
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
#else
return [CodePush bundleURL];
#endif
}
- 앱 초기화 시점에
RCTBridge
가 사용할 JS 번들의 경로를 설정하고, Code Push가 관리하는 최신 번들을 로드하도록 설정한다.
- 앱 실행 시 호출되어 JS 번들의 URL을 반환한다.
- 디버그 모드에서는 로컬 개발 서버의 번들(
jsBundleURLForBundleRoot
), 릴리즈 모드에서는 CodePush가 다운로드한 최신 번들의 경로(CodePush bundleURL
)를 반환한다.
- 새로운 JS 번들을 로드하기 위해서는 앱을 재시작해야 한다.
Android에서 JS 번들 경로 파악하기
MainApplication.java
의 getJSBundleFile()
메서드
@Override
protected String getJSBundleFile() {
return CodePush.getJSBundleFile();
}
- 앱 실행 시 React Native의 JS 번들을 로드할 경로를 반환한다.
- 앱이 실행될 때, CodePush가 관리하는 최신 JS 번들의 경로를 반환한다.
- 기본적으로
index.android.bundle
파일을 로드하지만, CodePush를 사용하는 경우 업데이트된 번들의 경로를 반환하도록 설정된다.
- 첫 실행 시에는 앱에 내장된 기본 JS 번들(
assets/index.android.bundle
)을 로드한다.
- CodePush가 업데이트된 번들을 다운로드한 경우 디바이스의 로컬 저장소에 저장된 최신 번들의 경로를 반환한다.
새로운 JS 번들을 다운로드하고 기존 번들과 교체하기
이는 각 OS에 관계 없이 CodePush 라이브러리에 내부에서 자동으로 처리된다. 호출된 매서드는 React Native Bridge가 사용한다.
RCTCodePush.m
파일의 replaceBundleAtPath
메서드
+ (void)replaceBundleAtPath:(NSString *)newBundlePath {
}
- 업데이트 처리 과정에서 CodePush 내부적으로 호출되어 새로운 JS 번들을 저장하고 교체한다.
- CodePush가 다운로드한 새로운 JS 번들을 로컬 저장소에 저장하고, 이를 현재 실행 중인 앱에 반영한다.
- 새로운 번들이 다운로드 되면 기존 번들을 대체하며, 다음 앱 실행 시 이 새로운 번들이 로드되게 한다.
- 내부 업데이트 프로세스에서 호출되는 메서드로, 자동 호출되기 때문에 개발자가 직접 호출할 필요는 없다.
3️⃣ 업데이트 승인 또는 롤백
- 새 업데이트가 정상적으로 실행되면 코드 내에서
notifyAppReady()
(또는 notifyApplicationReady()
) 메서드를 호출해 업데이트 성공을 서버에 알린다.
- 업데이트 후 앱이 충돌을 일으키거나 앞에서 언급한
notifyAppReady()
가 호출되지 않으면 CodePush 클라이언트는 자동으로 이전 안정 버전으로 롤백한다.
- 클라이언트 SDK 내부 및 네이티브 모듈에서 관리되는 로직이다.
- 사용자는 항상 안전하게 작동하는 앱 버전을 보게 된다.
4️⃣ 앱 재시작 관리
- 업데이트 적용을 완료하기 위해 CodePush는 앱을 재시작하여 새로운 JS 번들을 적용한다.
- 업데이트는 기본적으로 다음 앱 재시작 시 적용되고,
installMode
설정 값에 따라 재시작 시점이 달라지므로, 사용자 경험을 고려하여 적절한 시점을 선택하는 것이 중요하다.
즉시 적용
installMode: codePush.InstallMode.IMMEDIATE
- 다운로드 완료 후 바로 업데이트를 적용하며, 앱이 즉시 재시작된다.
- 사용자 경험에 영향을 줄 수 있지만, 빠르게 변경 사항을 반영할 수 있다.
다음 실행 시 적용
installMode: codePush.InstallMode.ON_NEXT_RESTART
- 다운로드 된 업데이트는 다음 앱 실행 시 로드된다.
- 사용자 경험을 방해하지 않으면서 안정적으로 업데이트를 적용할 수 있다.
백그라운드 복귀 시 적용
installMode: codePush.InstallMode.ON_NEXT_RESUME
- 앱이 백그라운드에 있다가 다시 활성화될 때 업데이트가 적용된다.
minimumBackgroundDuration
속성을 사용하여 백그라운드 상태 유지 시간을 설정할 수 있다(default: 0s).
백그라운드 전환 시 적용
installMode: codePush.InstallMode.ON_NEXT_SUSPEND
- 앱이 백그라운드로 전환된 뒤 일정 시간이 지나면 자동으로 재시작하여 업데이트를 반영한다.
- 기본적으로 5초의 지연 시간이 있으며, 이는 사용자 활동이 없는 동안 업데이트를 반영하기 위한 설정이다.
iOS: RCTRestart.m
파일의 RCTTriggerReloadCommandListeners
- React Native의 브릿지(Bridge)를 통해 앱을 강제로 재시작한다.
- React Native의 JS 번들 리로드 기능을 활용한다.
- 앱이 실행 중인 상태에서 새로운 JavaScript 번들을 로드하고, 화면을 초기화한다.
- 네이티브 코드 자체는 종료되지 않고, JS 레이어만 새로고침된다.
Android: CodePush.java
파일의 getCurrentActivity().recreate()
- Android의 네이티브 코드에서 React Native의 현재 액티비티를 가져와 재생성한다.
getCurrentActivity()
를 통해 현재 실행 중인 액티비티를 참조한다.
recreate()
메서드를 호출해 해당 액티비티를 종료하고 다시 생성한다.
- 이 과정에서 새로운 JS 번들이 로드되고, 앱 화면이 초기화됩니다.
🧐 그래서, App Center를 구현할 수 있을까?
📦 기존 App Center의 역할

CodePush 업데이트를 위한 중앙 리포지토리 및 CDN
- 개발자가 배포한 JavaScript 번들, 이미지, CSS 등 리소스 파일을 저장 및 관리하는 역할을 했다.
- 앱은 실행 시 App Center 서버에 연결하여 현재 설치된 패키지와 서버에 저장된 최신 업데이트를 비교하고, 업데이트가 있다면 이를 다운로드 할 수 있었다.
- 개발자는 App Center를 통해 업데이트 상태를 확인하고, 특정 버전으로 롤백하거나 점진적 배포(Gradual Rollout)를 설정할 수 있었다.
- CDN(Content Delivery Network)을 통해 빠르고 안정적으로 번들을 다운로드할 수 있도록 지원했다.
- 앱이 실행될 때
checkForUpdate
요청을 통해 업데이트 정보를 가져오고, 필요한 경우 CDN에서 번들 데이터를 다운로드 할 수 있었다.
- App Center는 Azure Blob Storage와 통합되어 번들을 안전하게 저장하고 관리했다.
CLI 기반 배포 도구
UI 기반 관리 도구

- App Center는 웹 UI를 통해 CodePush 업데이트를 시각적으로 관리할 수 있는 인터페이스를 제공했다.
- CLI 없이도 웹 대시보드에서 업데이트 상태를 모니터링하고, 필요한 경우 롤백이나 추가 설정을 할 수 있었다.
버전 및 디바이스 범위 관리
- 배포 시 특정 앱 버전 또는 플랫폼(Android/iOS)에만 적용되도록 설정할 수 있는 옵션을 제공했다.
- 이를 통해 앱 스토어에서 승인된 특정 바이너리 버전에만 업데이트가 적용되도록 하여, 잘못된 업데이트가 호환되지 않는 앱 버전에 적용되는 문제를 방지했다.
💡 대안 구축 방안
CodePush 업데이트를 위한 중앙 리포지토리 및 CDN
- Microsoft는 Azure Blob Storage나 AWS S3와 같은 클라우드 스토리지를 통해 중앙 리포지토리를 구축할 것을 권장한다.
- 이를 통해 번들 파일과 업데이트 메타데이터를 저장하고, CDN과 연결하여 배포할 수 있다.
- 자체적으로 업데이트 정보가 담긴 JSON 파일을 생성하고, 이를 클라우드 스토리지에 업데이트한다면 해당 기능을 대체할 수 있을 것으로 보인다.
- 기존 App Center에서 Azure CDN을 사용했던 것처럼 AWS CloudFront, Azure CDN 등과 같은 CDN 서비스를 사용하고, 클라이언트 측에서는 CDN URL을 통해 번들을 다운로드하도록 구성할 수 있다.
CLI 기반 배포 도구
- 기존 Microsoft에서
appcenter-cli
를 통해 React Native 번들을 생성하고 이를 압축한 뒤 스토리지에 업로드하는 과정을 구현해야 한다.
- 퍼블릭으로 공개 되어 있는 리포지토리에서 기존 CLI 관련 코드를 fork 한 뒤 자체 배포 스크립트를 구현해야 할 것으로 보인다.
- Hermes 바이트코드 생성, 번들 압축, 해시 계산 등은 기존에 사용하고 있던
react-native-code-push
라이브러리 및 스크립트를 통해 구현 가능하다.
UI 기반 관리 도구
- 단순 웹 대시보드 기능이므로 간단한 시각화 웹 애플리케이션을 제작하거나, UI를 제작하는 대신 자동화 명령어를 생성하는 방식을 고려 중이다.
버전 및 디바이스 범위 관리
- App Center는 특정 앱 버전(
target_binary_range
)이나 디바이스에만 업데이트를 적용할 수 있는 기능을 제공했다.
- Microsoft는 JSON 파일에
target_binary_range
필드를 추가해 특정 앱 버전에만 업데이트를 적용하는 방식을 권장한다. // 예시
{
"update": {
"version": "1.0.1",
"mandatory": true,
"target_binary_range": ">=1.0.0 <2.0.0",
"download_url": "https://cdn.example.com/update.zip"
}
}
App Center 서비스 종료가 마무리 되기 전 위의 방안을 실제로 적용해보고, (성공 여부에 관계 없이😢) 구현 결과를 포스팅 해 볼 예정이다.
🔎 References
RN 공부중인데 작성해주신 글 너무 잘 읽었습니다.
-궁금한 점들이 있어서 이메일 드렸는데 시간 되실 때 답변 부탁드려도 될까요,,,?