<--- 아직 작성 중! 추후 내용 더 추가 예정입니다 --->
feconf25에 참석해 세션을 들은 후 작성하는 후기 글입니다.
한때 우리 회사도 hot updater를 썼었는데
이 라이브러리를 만든 분이 직접 강연을 한다니 궁금해져서 후딱 들으러 감
2025.08.23. (토) FECONF25
세션2(12:50~13:20): 플러그인 시스템 기반의 유연한 React Native CodePush 대안
연사자: Toss RN Framework팀 강선규 개발자, 관련 링크드인
Intro
OTA(Over-theAir) 업데이터란?
정의
사용자가 앱스토어 심사를 하지 않고도 실시간으로 애플리케이션의 코드와 자원을 원격으로 업데이트하는 기술
주요 장점
- 업데이트를 위한 다운로드/설치 과정이 없어 사용자 경험을 개선할 수 있음
- 버그 수정과 기능 출시 주기를 단축해 빠르게 배포할 수 있음
- 앱스토러 심사를 생략할 수 있음
기존 솔루션: React Native CodePush
- Microsoft에서 개발한 ReactNative/Cordova 앱을 위한 라이브러리
- JavaScript 코드와 자원을 앱스토어 업데이트 없이 사용자에게 직접 배포함 (앱스토어 심사 X)
- Microsoft App Center 기반 솔루션
- 롤백 및 점진적 배포 가능
App Center의 구조적 문제점
CLI 종속적 워크 플로우
- Metro 번들러에 특화된 구조
- Expo, Re.Pack 등 다양한 RN 프레임워크 환경의 유연한 적용이 어려움
App Center 종속
인프라가 Microsoft App Center에 완전히 종속됨
자체 인프라로 마이그레이션 하거나 확장하기 어려움
CDN 부재
글로벌 CDN이 없음
특정 국가나 지역에서 업데이트 다운로드 속도가 현저히 느려지는 성능 저하 발생
-> Microsoft App Center, 2025년 3월 31일부터 모든 App Center 서비스 완전 종료 발표
발표자는 이미 종료 발표 전부터 이 구조적 문제점을 타파하고자 코드푸시 대안을 개발함
레딧에서 수요조사하고 계획만 하던 중에 종료 발표가 동기부여가 되어 열심히 개발을 시작하게 됨
CodePush가 종료된 이후 10개가 넘는 SaaS가 생기고 여전히 경쟁 중이라고 함
Hot Updater
개발 시작 전 요구사항 파악 및 기존 CodePush 분석
개발을 위한 요구사항
1. 다양한 인프라 활용
- AWS, Firebase, Supabase, Cloudflare 등 다양한 환경 지원 필요
- 각 기업/팀이 선호하는 인프라를 자유롭게 제약 없이 사용할 수 있도록 함
2. 다양한 프레임워크
- 기존 CodePush는 Metro에 특화되었음. 따라서 Re.Pack 등 다양한 번들러에 유연하게 대응하고자 함.
- Bare, Expo, Rock 등 RN 프레임워크를 모두 지원하고자 함
3. 플러그인 시스템
- 모듈화 구조: 필요한 기능만 선택적 활용 가능 (리소스 최적화)
- 확장성: 새로운 프레임워크나 인프라가 등장해서 플러그인만 추가하면 지원 가능
- 유지보수 용이성: 코어 기능/확장 기능을 분리해 유연한 아키텍처 구성, 개발 및 업데이트 효율화
기존 CodePush 분석
1. CodePush가 어디에서 JS 번들을 읽는가
- iOS(AppDelegate.mm) / Android(MainApplication.kt)에서 앱이 실행될 때 JS 번들을 어디서 가져올지 정의
- CodePush.bundleURL()을 호출해서 번들을 가져옴
- 빌드된 앱은 CodePush에서 내려받은 JS 번들을 실행함
2. 번들 경로 확인(PoC)
- iOS 디버깅 로그에 CodePush bundleURL을 찍어보면, 실제 기기 내부 파일 경로가 표시됨
/var/containers/.../main.jsbundle
- CodePush가 앱 안에 새로운 번들을 내려받아 저장하고 실행함을 확인함
3. CodePush 동작 원리 (추론)
1) 앱 실행 시 CodePush 업데이트 서버에 연결 - 새로운 업데이트(번들)가 있는지 확인
2) 새로운 번들이 있다면 다운로드
3) 기기 파일 시스템에 저장
4) CodePush.bundleURL이 저장된 번들의 파일 경로를 반환
5) RN 런타임이 해당 JS 번들을 실행
-> 이를 바탕으로 앱을 다시 빌드/배포하지 않고도 JS 번들을 교체해 앱을 업데이트함
플러그인 시스템 기반 번들 관리
OTA 구현을 위한 플러그인 구조
1. Build Plugin
- 다양한 RN 환경 (Bare Rn, Expo, RNFF 등)과 번들러(Metro, Re.Pack 등)에 맞춰 JS 번들 생성
- 어떤 프레임워크/번들러를 쓰든 플러그인을 통해 빌드 추상화
2. Storage Plugin
- 빌드된 번들을 저장하고 제공하는 저장소
- Supabase, Firebase Storage, Cloudflare R2, AWS S3
3. Database Plugin
- 번들의 메타데이터(버전, 배포 상태, 배포 기록 등)를 관리
- Supabase, Firbase, Cloudflare D1
- 지금 어떤 번들이 최신인지 기록하는 곳
Config 시스템
플러그인 구조를 실제로 적용하려면, 각각의 빌드/스토리지/DB 플러그인을 조합해 실행하는 Config 시스템이 필요함
// hot-updater.config.ts
export default defineConfig({
build: framework(),
storage: storage(),
database: database(),
});
CLI에서 hot-update deploy 실행하면, 이 Config를 기준으로 아래와 같은 흐름이 자동 실행 됨
빌드 - 저장소 업로드 - DB 메타데이터 갱신
플러그인 구조로 번들 관리 파이프라인을 모듈화해 원하는 인프라 조합을 쉽게 선택할 수 있음
핵심 구성 요소
1. 빌드 플러그인
- 번들을 어떤 RN 프레임워크/환경에서 빌드할지 선택
import { bare } from "@hot-updater/bare";
import { defineConfig } from "hot-updater";
export default defineConfig({
build: bare(),
});
Bare React Native를 쓴다면 build: bare()
Expo를 쓴다면 build: expo()
Rock을 쓴다면 build: rock()
= 어떤 환경으로 build할지 코드 한 줄만 바꿔주면 됨
2. 스토리지 플러그인
- 빌드된 번들을 업로드하고 제공할 저장소 선택
- 스토리지는 빌드 결과물(JS 번들, 소스맵, 자산 등)을 올리고 내려주기만 담당함
- 어디에 파일을 둘 것인가(원본 저장소)+앱이 어떻게 내려받게 할 것인가(download url)을 규격화함
예시에서는 supabase storage를 쓰거나 cloudflare r2를 쓰는 경우를 보여주고 있음
상위 로직은 그대로이고 storage 필드 내부 내용만 교체하면 된다는 것을 알 수 있음
export interface SupabaseStorageConfig {
supabaseUrl: string;
supabaseAnonKey: string;
bucketName: string;
}
supabase에 연결하기 위한 기본 정보
- supabaseUrl: Supabase 프로젝트 url
- supabaseAnonKey: 인증용 키
- bucketName: 번들을 올릴 버킷 이름
export const supabaseStorage =
(config: SupabaseStorageConfig, hooks?: StoragePluginHooks)
=> (_: BasePluginArgs): StoragePlugin => {
supabaseStorage라는 함수를 호출하면, supabase에 연결된 StoragePlugin 객체를 리턴
- defineConfig에서 storage: supabaseStorage({...}) 식으로 쓸 수 있게 만들어 둠
const bucket = supabase.storage.from(config.bucketName);
supabase SDK를 사용해 특정 버킷을 가리킴
이후 이 bucket 객체로 파일 업로드/다운로드/삭제 등을 수행함
return {
name: "supabaseStorage",
async upload(key: string, filePath: string) {
const upload = await bucket.upload(Key, Body);
// ...
},
};
실제 플러그인 객체
- name: 플러그인 이름
- upload: 파일 업로드 메서드 구현
- key: 스토리지 내 경로(예: ios/1.0.0/main.jsbundle)
- filePath: 실제 로컬 파일 경로
- 내부적으로 bucket.upload()를 호출해 Supabase Storage에 파일을 저장함
Supabase Storage는 클라우드 하드디스크
Supabase에 연결해서 하드디스크에 파일을 넣고 뺄 수있는 어댑터 역할
-> 스토리지에 파일을 업로드/삭제/다운로드를 추상화된 인터페이스로 호출할 수 있음
-> Supabase 말고 Cloudflare R2나 AWS S3로 갈아끼울 수 있고, 수정할 코드도 많지 않음
3. 데이터베이스 플러그인
- 스토리지에는 실제 js 번들 파일이 들어감
- 데이터베이스에는 번들의 정보(어떤 버전, 어떤 플랫폼용, 언제 배포됐는지, 어떤 파일 경로)가 저장됨
- 앱이 서버에 최신 번들을 요청하면, Db를 조회해 올바른 번들의 download Url을 알려줌
export interface DatabasePlugin {
getBundleById: (bundleId: string) => Promise<Bundle | null>;
updateBundle: (targetBundleId: string, newBundle: Partial<Bundle>) => Promise<void>;
appendBundle: (insertBundle: Bundle) => Promise<void>;
commitBundle: () => Promise<void>;
}
getBundleById: 특정 번들 id로 db에서 정보 조회
updateBundle: 기존 번들의 메타데이터 업데이트
appendBundle: 새로운 번들 정보 추가
commitBundle: 최종적으로 배포 확정 처리
파일이 업로드 된 경로와 번들의 활성 상태 여부를 db에서 관리
export const supabaseDatabase = (config) => (_): DatabasePlugin => {
return {
name: "supabaseDatabase",
async getBundleById(bundleId) {
return supabase.from("bundles").select("*").eq("id", bundleId).single();
},
};
};
supabase 구현 예시
- supabase의 bundles 테이블에서 id가 같은 행을 찾아 반환하는 코드
데이터베이스는 번들의 정체성을 추적함: 어떤 번들이 최신인지, 특정 클라이언트가 어떤 번들을 받아야 하는지
스토리지와 분리되어 있어서 저장소(s3, supabase, r2)를 바꿔도 db 구조만 유지하면 문제 없음
인터페이스가 표준화되어 있어 supabase -> cloudflare d1 같은 데이터베이스 교체도 쉽게 가능함
전반적으로 빌드 플러그인, 스토리지, 데이터베이스 모두 인터페이스가 표준화되어 있어
원하는 메소드를 적은 코드 변경으로 쉽게 바꿔 쓸 수 있다는 점이 특징인 듯 하다
hot-updater deploy 실행 과정
실행 흐름
1. hot-updater deploay 실행
-> cli 명령어를 입력해서 배포 프로 프로세스를 시작함
2. build plugin
-> 현재 RN 환경(Bare, Expo 등)에 맞춰 js 번들을 빌드함
3. storage plugin
-> 빌드된 번들을 supabase, cloudflare r2 같은 스토리지에 업로드함
4. database plugin
-> 업로드된 번들의 메타데이터(버전, 경로, 해시 등)를 db에 기록함
코드 흐름
const config = await loadConfig();
const [buildPlugin, storagePlugin, databasePlugin] = await Promise.all([
config.build({ cwd }),
config.storage({ cwd }),
config.database({ cwd }),
]);
// 1. 빌드 실행
buildPlugin?.build(...);
// 2. 스토리지 업로드
storagePlugin?.upload(...);
// 3. DB에 번들 정보 추가
databasePlugin?.appendBundle(...);
// 4. 최종 커밋 (배포 확정)
databasePlugin?.commitBundle();
loadConfig() -> hot-updater.config.ts 파일을 읽어서 어떤 플러그인을 쓸지 로드함
build -> upload -> appendBundle -> commitBundle 순서로 실행됨
<--- 아직 작성 중! 추후 내용 더 추가 예정입니다 --->
'개발 관련 컨퍼런스 참여' 카테고리의 다른 글
[FECONF25] 개발자를 위한 모션 그래픽 솔루션: Lottie의 기술 진화와 활용전략 (1) | 2025.08.31 |
---|---|
[FECONF25] 모노레포 절망편, 14개 레포로 부활하기까지 걸린 1년 (4) | 2025.08.30 |
토스 개발자 컨퍼런스 SLASH24 (7) | 2024.09.02 |