AppCenter 없이 React Native CodePush 배포하기

AppCenter 없이 React Native CodePush 배포하기

Tech

들어가며

안녕하세요, 숨고 Mobile App Engineer Floyd입니다.

CodePush 배포를 운영 중인 팀이라면 AppCenter에 대해서, 특히 2025년 3월 서비스 종료가 예고된 것을 잘 알고 계실것입니다. 이 글을 통해 숨고 Mobile Chapter가 AppCenter의 느린 인프라에 의해 CodePush 업데이트 확인 및 번들 다운로드가 느린 문제를 해결한 방법을 말씀드리고자 합니다. 그리고 CodePush 라이브러리와의 호환성을 유지하면서 별도의 API 서버 없이도 AppCenter 종료에 대응할 수 있었던 방법을 소개하려고 합니다.

진행 배경

문제 정의

숨고 앱은 구동 시점에 새 업데이트 여부를 확인하고, 필요한 경우 업데이트를 완료해야만 서비스에 진입하도록 구현되어 있습니다. 이는 유저 경험 향상에 필수적인 변경 점이나 비즈니스에 결정적인 영향을 주는 변경 점을 배포하는 경우, 유저의 여정에 미치는 부정적인 영향을 최소화하고, 변경점을 확실히 적용할 수 있는 시점이 서비스의 홈 화면 진입 전이라고 판단했기 때문입니다.

24년 1분기 숨고 Mobile Chapter의 액션 아이템 중 하나는 유저가 앱을 실행한 뒤 서비스에 빠르게 진입하도록 개선하는 것이었습니다. Chapter에서 단독으로 진행할 수 있는 기술 영역 프로젝트이면서, 동시에 유저 경험을 향상하여 비즈니스에 이바지할 수 있는 목표였기 때문입니다.

진행에 참여한 개발자들은 앱 시작 과정의 병목을 분석했고 몇 가지 원인을 찾을 수 있었습니다. 이후 개선할 만한 문제들을 정의했고, 그중 가장 큰 비중을 차지한 문제는 CodePush 업데이트 확인 동작이었습니다.

개선 방안 도출

당시 숨고 앱은 CodePush 라이브러리의 일반적인 사용 방법을 따라, AppCenter의 API 서버에 업데이트를 확인했습니다. Firebase Performance를 통해 확인한 API 통신 소요 시간은 백분위 90% 기준으로 1.5초가 걸렸고, 50% 기준으로도 1초에 근접한 수준이었습니다. 데이터가 1초 근처에 주로 분포된 것을 보고 우리 사용자들이 높은 확률로 느린 응답을 받고 있음을 알 수 있었습니다.

외부 인프라에 원인이 있었기 때문에 이를 해결하기 위해서는 CodePush 대신 EAS Update로 마이그레이션하여 가까운 리전에 서버를 구축해 인프라를 교체할 수 있을 것으로로 생각했습니다. 하지만 검토해보니 EAS 서버 구축과 앱 라이브러리 마이그레이션이 필요 이상의 복잡한 작업이라는 생각이 강하게 들었습니다.

더 단순한 방법에 대해 고민해 본 결과 CodePush API 응답을 앱센터보다 더 빠른 인프라에 복제해 놓고, 이 정보를 느린 인프라 대신 활용하는 방법을 생각해 냈습니다. 이것이 가능하다면 서버를 구축하고 트래픽을 관리하는 반영구적인 유지보수 비용을 제거하고, 앱 구현의 변경도 최소화할 수 있을 것이라고고 생각하였습니다.

숨고 앱의 배포 전략과 버전 정책을 고려해 봤을 때 API 응답을 캐싱하며 운영하는 것이 어렵지 않다고 생각되어 이 방향으로 리서치를 시작했습니다.

문제 해결 1단계 - AppCenter 응답 캐싱

업데이트 확인 동작 리서치

CodePush 라이브러리의 코드를 살펴보고 HTTP 요청을 검사하여 어떤 정보를 업데이트 확인에 사용하는지 파악했습니다. 응답 데이터도 살펴본 결과 CodePush 번들을 다운로드 할 URL이 함께 내려오는 것을 확인했습니다. 그리고 이를 손쉽게 단순히 치환하는 것만으로도 업데이트 수행 시 번들 다운로드 소요 시간이 획기적으로 줄어드는 것을 확인했습니다.

계속해서 업데이트 확인 동작을 변경하기 위해 react-native-code-push 라이브러리 코드 일부를 파악했습니다. 라이브러리의 의존성인 code-push 패키지로부터 queryUpdateWithCurrentPackage 함수를 가져와 업데이트 정보를 조회함을 알 수 있었는데, HTTP URL을 수정하려면 code-push 패키지를 수정해야 했습니다. 가능하면 code-push 패키지를 배제하여 의존성을 줄이고 유지보수성을 향상하고자 했는데, 다행히 이 함수의 동작이 단순해서 react-native-code-push 라이브러리 실행시 업데이트를 확인할 새로운 함수를 전달받아 실행하도록 수정할 수 있었습니다. 그리고 모든 변경은 patch-package를 사용하여 repository를 fork 하는 방법보다 적은 리소스를 들여 빠르게 제품에 적용해 보았습니다.

업데이트 확인 동작 변경

런타임에 설정할 수 있는 updateChecker 옵션을 추가하여, 함수를 전달함으로써 앞서 언급한 queryUpdateWithCurrentPackage 함수를 대체하도록 했습니다.

updateChecker 함수는 기존 함수와 마찬가지로 업데이트 확인에 필요한 정보를 인자로 전달받아 기존과 동일한 시점에 호출됩니다. 또한 이 함수는 기존 함수와 동일한 정보를 리턴하도록 구현해야 합니다. 입출력 정보들은 code-push 라이브러리에 각각 UpdateCheckRequest, RemotePackage 인터페이스로 정의된 것을 가져와 호환성을 유지할 수 있게 수정했습니다.

숨고 앱의 경우 updateChecker 함수의 구현에서 AWS S3에 미리 캐싱한 업데이트 정보를 CDN을 통해 획득하고, 앱 런타임에만 알 수 있는 정보를 활용해 캐싱된 정보의 부정확한 면을 보완하도록 구현했습니다. 예를 들어, 조회된 업데이트의 적용을 필수적인 업데이트로 처리해야 하는지 여부를 현재 실행중인 앱 번들의 버전 정보를 활용해 판단합니다.

캐싱된 정보에 부정확한 면이 있는 이유는, API 응답을 캐싱할 때 모든 앱 버전을 고려하지 않기를 선택했기 때문입니다. 기존에는 AppCenter API 서버에 앱의 런타임 정보를 보내면 서버가 일련의 로직을 수행하여 정확한 응답을 제공했습니다. 반면에 저희는 관리의 용이성을 위해 런타임 정보의 수를 줄여 네이티브 앱 버전만 사용하기로 했습니다.

캐싱 자동화

한편, 앱 런타임에 캐싱 된 정보를 조회하려면 정확히 정보를 캐싱하기 위한 자동화도 필요합니다. 도입 초기에 몇 번은 수동으로 캐싱을 수행했는데, 개인이 일일이 실행하기엔 복잡하고 실수 여지도 있었습니다.

팀 내에서 사용하던 기존 CodePush 배포 자동화 워크플로우를 그대로 사용하면서 후반에 단계를 추가했습니다. CodePush 배포가 완료되면 AppCenter API 서버에 단순한 HTTP 요청을 수행하고, 방금 배포한 CodePush 업데이트 정보를 얻습니다. API 응답은 JSON 파일로 저장하여 AWS S3에 업로드합니다. 또한 코드푸시 번들도 다운로드하여 S3에 업로드합니다. 앱 런타임에 라이브러리는 CloudFront CDN을 통해 JSON 파일을 조회하고, 업데이트가 있으면 번들 파일 다운로드도 CDN에 요청하게 됩니다.

예를 들어 iOS 네이티브 앱 버전 1.0.0을 대상으로 1.1.0 CodePush 배포를 수행했다면 자동화된 프로세스는 다음과 같습니다.

  1. 기존 배포 워크플로우에 의해 AppCenter에 CodePush 배포가 완료됨
  2. AppCenter API 서버에 적절한 deployment_key, app_version 값 1.0.0을 넣어 HTTP 통신으로 업데이트 정보를 요청
  3. 1.1.0 업데이트에 대한 정보를 얻어 생성한 JSON 파일과 번들 파일을 빠른 인프라에 업로드
  4. 숨고 앱은 AppCenter 대신 빠른 인프라에서 업데이트 정보와 번들을 획득

구현된 메커니즘이 일반적인 캐싱 개념과 동일하므로 이를 ‘코드푸시 캐싱’이라고 이름붙였습니다.

대체 이미지

개선 효과 측정

효과는 굉장히 유의미했습니다. 백분위 90% 기준, 업데이트 체크에 걸리는 시간은 1.5초에서 0.3초 수준으로 줄어들어 더 이상 앱 구동의 병목이 되지 않을 정도로 개선되었습니다. 번들 다운로드 소요 시간은 백분위 90% 기준 15초에서 3초로 감소했습니다. (평균 7초 → 0.9초)

앱 구동 시 유저들이 서비스를 이용하기 전 필수로 업데이트를 적용해야 할 때에도 거의 기다림 없이 업데이트를 완료하고 앱 서비스를 이용할 수 있게 되었습니다.

(백분위 90% 기준)기존캐싱 후
업데이트 체크 소요시간1.5초0.3초
번들 다운로드 소요시간15초3초

오픈 소스

한 달 정도 프로덕션 운영을 하면서 속도 향상 효과와 안정성을 검증하여 확신이 생겼고, React Native 생태계의 많은 팀들에게 공유하고자 오픈소스 프로젝트를 시작했습니다. react-native-code-push를 fork 하여 ‘업데이트 체크 함수를 교체할 수 있음’을 주요 기능으로 하여 @bravemobile/react-native-code-push를 배포한 뒤 숨고 앱에서도 patch-package 대신 해당 라이브러리를 사용하기 시작했습니다.

해당 프로젝트는 Github에서 확인하실 수 있습니다.

대체 이미지
@bravemobile/react-native-code-push 라이브러리 발췌

단점과 예상치 못한 변수

배포 자동화를 충분히 마련해 두었기 때문에 배포 시 큰 불편함은 없었지만, 아무래도 AppCenter에 배포한 뒤 이를 다른 저장소로 옮기는 방식은 비효율적이었습니다. 또한 배포된 업데이트를 필수 업데이트로 전환하는 등의 정보 변경이 필요한 경우 그때마다 캐시를 갱신해야 하는 불편함도 있었습니다. AppCenter를 우리 배포 과정에서 제거할 수 있다면 배포 과정이 더욱 단순해지고, 휴먼 에러의 여지도 줄어들 것입니다.

게다가 AppCenter는 25년 3월 종료를 예고한 바 있고, Mobile Chapter에서는 당시 CodePush가 별도의 클라우드 서비스로 분리 운영을 지속할 것으로 예측했지만 이와 달리 다른 대안은 발표하지 않았습니다.

이 두 가지 문제를 고려해 본 결과 AppCenter 대한 의존을 완전히 제거하기로 했습니다.

대체 이미지
AppCenter 2025년 3월 31일 종료

문제 해결 2단계 - AppCenter와 작별하기

CodePush 번들 생성

앞서 설명해 드린 캐싱 구현을 기반으로, AppCenter 의존성 없이 CodePush 번들과 업데이트 정보를 직접 생성하고 업로드하는 방법을 어렵지 않게 떠올릴 수 있었습니다.

AppCenter 배포는 appcenter-cli를 사용해 CodePush 번들을 생성하고 업로드하여 AppCenter 인프라에 등록함으로써 이뤄집니다. AppCenter는 앞으로 사라질 운명이므로 이번엔 appcenter-cli 저장소를 fork 하는 대신에 코드를 발췌하여 팀 내부에서 사용할 배포 스크립트를 구현했습니다. 그 결과 CodePush 라이브러리와 완전한 호환을 보장할 수 있었습니다.

appcenter-cli는 CodePush를 비롯하여 상당히 많은 기능을 내장하고 있으나, 우리의 관심사는 React Native용 CodePush 번들을 생성하는 기능뿐입니다. 코드를 탐색하며 CodePush 번들 생성에 어떤 과정이 필요한지 파악하고, 필요한 코드들을 발췌했습니다. 여기에는 React Native 번들 생성 명령, Hermes 컴파일 명령, Asset과 함께 zip 압축 파일 생성 동작, 번들 동일성 검증에 사용할 해시값을 산출하는 동작이 포함됩니다. 코드를 발췌하고 엮어서 만든 Node 스크립트로 기존 appcenter-cli 배포 명령어를 대체했습니다.

업데이트 정보 생성

업데이트 확인시 AppCenter API 서버에 의존하지 않으려면 캐싱 구현에서 JSON 파일로 저장했던 API 응답을 직접 생성해야 합니다.

이를 위해서 기존 API 서버의 응답을 구성하는 각 속성의 역할과 값의 의미를 파악했습니다. 이 과정은 react-native-code-push 라이브러리를 주로 살펴봐야 했으며, 정확한 용도를 파악하기 위해 데이터 흐름을 따라 네이티브 코드도 살펴볼 필요가 있었습니다.

주목할 속성은 업데이트 활성화 여부를 나타내는 enabled, 필수 업데이트 여부를 나타내는 mandatory, 번들 해시값을 담는 package_hash, 업데이트 대상 앱 버전을 나타내는 target_binary_range가 있으며, 다운로드 할 CodePush 번들의 URL은 download_url 속성에 담깁니다. 배포 스크립트 실행 시 인자에 전달한 정보와 방금 생성한 CodePush 번들 정보를 사용하여 JSON 파일을 생성하고 S3에 업로드하도록 구현했습니다.

enabled 속성은 배포 시점엔 항상 true 값을 갖지만, 배포한 업데이트를 비활성화할 때 false로 변경할 수 있습니다. mandatory 속성 역시 배포 이후 변경할 수 있는 정보입니다. target_binary_range는 이름과 달리 실제 동작 시 특정 버전이 기입됩니다. 숨고 앱의 경우 바이너리 버전 하나마다 JSON 파일 하나를 생성하여 관리하기 때문에 각 JSON 파일 생성시 한 번 기입하면 변경할 필요가 없었습니다.

필수 업데이트 판단 및 롤백 기능

@bravemobile/react-native-code-push 라이브러리를 사용하면 업데이트 정보 조회뿐만 아니라 조회한 결과에 추가적인 정보를 더해 필요한 동작을 수행할 수 있는 이점이 있습니다. 저희는 업데이트 정보 JSON에 배포 내역 정보를 추가하고, JS 실행 시점의 앱 버전 정보를 이용하여 최신 업데이트가 무엇인지 판단할 수 있도록 구현했습니다. 스토어 배포 시 JSON 파일을 생성하면서 첫 내역을 기록하며, CodePush 배포마다 내역에 정보를 추가합니다.

AppCenter 서버 대신 라이브러리 런타임에 배포 내역을 탐색하여 업데이트 여부와 적용 방식을 결정합니다. 물론 비활성화한 업데이트는 무시되며, 적용하려는 업데이트의 강제 여부도 AppCenter와 동일하게 판단할 수 있도록 구현했습니다. 예를 들어, 최신 업데이트가 필수가 아니더라도 현재 실행 중인 버전과 최신 버전 사이에 필수 업데이트가 끼어있다면 최신 업데이트를 필수 업데이트로 적용합니다. 여기에 더해 이전 버전으로의 빠른 롤백이 가능한데, 특히 업데이트가 모두 비활성화되면 앱 구동 시 네이티브 앱 코드로 즉시 복원됩니다.

배포 정보 변경

배포한 업데이트를 비활성화하여 롤백하거나, 강제 업데이트로 전환하는 기능은 JSON 파일을 변경함으로써 동작합니다. JSON 파일을 조회하고 갱신하는 Node 스크립트를 작성했고, Github Actions를 수동 실행하여 모든 챕터원들이 사용할 수 있습니다. 개발자마다 AWS 인증 정보를 발급할 필요가 없어 보안상 안전하며, 간단한 GUI를 구현할 수 있어서 사용하기에 편리하다는 장점이 있습니다.

대체 이미지
Github Actions를 수동 실행하여 배포 정보 변경

개선 효과

캐싱 단계가 제거됨으로써 챕터원들의 배포 경험이 향상되었습니다.

구체적으로는,

AppCenter 배포 후 개발자가 캐시를 생성하거나 갱신하던 단계를 제거해 휴먼에러가 발생하지 않는 환경을 마련하였습니다.

단 한 번의 배포 명령으로 쉽게 배포하고, 배포 후 치명적인 문제가 발견되면 즉시 롤백할 수 있게 됐습니다.

AppCenter 장애로부터 자유로워졌습니다.

이러한 효과는 이즈음부터 숨고 팀에서 적용한 스몰 릴리즈 배포 전략을 이행하며 하루에도 여러 차례 씩 월 30회가량의 배포를 실행하게 되어 효과를 크게 체감할 수 있었습니다.

스몰 릴리즈는 XP(eXtreme Programming)의 Agile 실천 방법의 하나로, 작은 단위로 자주 배포하여 사용자로부터 빠르게 피드백을 받고 유연하게 방향을 바꾸면서 더 높은 가치를 제공하려는 방법입니다.

대체 이미지

이후 방향성

API 서버 없이 단순한 인프라로 코드푸시 배포를 운영할 수 있는 @bravemobile/react-native-code-push 라이브러리는 Mobile Chapter에서 지속적으로 발전시키고자 노력하고 있습니다.

지금 배포되어 있는 버전은 높은 자유도를 활용하여 앞서 설명해 드린 방식뿐만 아니라 다양한 방법으로 활용하실 수 있습니다. 하지만 높은 수준의 자유도는 라이브러리를 도입하는 개발 조직 입장에서 어떻게 라이브러리를 효율적으로 활용할 수 있을지 판단하기 어렵게 만듭니다. 그리고 CodePush 라이브러리 내부 동작을 자세히 알아야 한다는 러닝 커브가 존재합니다.

또한 코드푸시 번들 생성 등 운영에 필수적인 도구가 제공되지 않아 라이브러리 활용하기기에 어려우실 것으로 생각합니다. 따라서 필요한 도구를 구현하고 있으며, 자유도를 조금 낮추는 대신 쉽게 도입할 수 있도록 개선하는 작업을 진행하고 있습니다.

개선하는 버전에서는 AppCenter 종료에 대응하여 업데이트 설치 이벤트, 롤백 이벤트 등을 AppCenter에 수집하기 위해 존재했던 코드를 우선 제거하였습니다. 이후에는 사용처에서 원하는 수단으로 이벤트를 추적할 수 있도록 지원할 예정입니다.

React Native의 New Architecture 지원 문제도 머지않아 해결해야 할 과제로 남아있습니다. 관심 있으신 분들의 응원을 부탁드리며, 오픈 소스 기여도 환영합니다.

읽어주셔서 감사합니다.

대체 이미지
  • #appcenter
  • #codepush
  • #mobileappengineer
  • #reactnative
Floyd Kim

Floyd Kim

Mobile App Engineer

연결을 통해 가치를 만드는 숨고팀과
함께할 당신을 기다립니다

채용중인 공고 보기