React Native 의 Hot updater

eeensu·5일 전

React Native

목록 보기
34/35

Hot Updater란?

React Native 앱을 위한 셀프 호스팅 OTA(Over-The-Air) 업데이트 솔루션이다. 앱스토어 심사 없이 JS 로직과 에셋(이미지, 폰트 등)을 실시간으로 유저에게 배포할 수 있다. 과거에는 Microsoft App Center의 한 기능이었던 'CodePush'가 업계 표준이었으나, 2024년 종료가 공지되면서 RN 진영에 대안이 필요해졌고 그 대표주자로 등장한 것이 hot-updater다.

특징

  • 비용 절감 (자체 호스팅) : 별도 SaaS 구독 없이 회사가 이미 쓰는 AWS S3, Firebase, Supabase, Cloudflare R2+D1 등에 번들을 올려 직접 서비스한다. 사실상 스토리지 비용만 든다.
  • 플러그인 시스템 : Build(Metro / Re.Pack / Expo), Storage, Database 세 축이 플러그인으로 분리되어 있어 환경에 맞춰 자유롭게 조합 가능.
  • 최신 아키텍처 지원 : New Architecture(Fabric) 호환, 구형 아키텍처도 지원.
  • 크래시 자동 롤백 : 새 번들이 크래시를 유발하면 직전 안정 번들로 자동 롤백
    (HotUpdater.wrap이 이를 담당).
  • 심사 우회 : 네이티브 코드(Java/Kotlin/Swift/ObjC) 변경 없는 JS/에셋 수정은 스토어 심사 없이 즉시 배포 가능. 단, 네이티브 변경이 동반되면 반드시 스토어 심사 필요.

동작 흐름 (hot-updater deploy 실행 시)

  1. 번들링 : RN 코드를 단일 JS 번들로 묶음
  2. 업로드 : 설정한 스토리지(S3/R2/Supabase 등)에 번들 + 에셋 업로드
  3. 메타데이터 갱신 : DB에 새 버전(bundleId, 타깃 앱 버전, forceUpdate 여부 등) 기록
  4. 클라이언트 반영 : 앱 실행 시 SDK가 서버에 신버전 존재 여부를 묻고,
    있으면 백그라운드 다운로드. 다음 실행 때 새 번들 적용 (Silent Update).

세팅

1. Supabase 사전 준비 — Project Settings → API에서 확인

  • Supabase Project URL
  • Supabase Anon Key

2. 설치 및 초기화

# CLI와 RN 모듈 설치
npm install -D hot-updater
npm install @hot-updater/react-native

# 초기화
npx hot-updater init

프롬프트 질문:

  • Build Plugin : Bare 선택 (RN CLI 환경)
  • Provider : Supabase 선택
  • 이후 Supabase URL / Anon Key 입력

완료되면 루트에 .env.hotupdaterhot-updater.config.ts가 생성되고,
Supabase에는 필요한 Storage 버킷과 메타데이터 테이블이 자동 세팅된다.

3. Root App에 HotUpdater.wrap 적용 (필수)

⚠️ wrap은 선택이 아니라 필수다. wrap이 없으면 checkForUpdate, reload
모든 HotUpdater 메서드가 에러를 던지며, 크래시 자동 롤백 기능도 동작하지 않는다.

기본형 (자동 모드):

import { HotUpdater } from '@hot-updater/react-native';

function App() {
  return <YourRootComponent />;
}

export default HotUpdater.wrap({
  source: 'https://your-update-server-url',  // 또는 baseURL
  updateStrategy: 'appVersion', // 'appVersion' | 'fingerprint'
  updateMode: 'auto',
  reloadOnForceUpdate: true,    // force update 시 다운로드 후 자동 reload
  fallbackComponent: ({ progress, status, message }) => (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>{status === 'UPDATING' ? `Updating... ${Math.round(progress * 100)}%` 
                                    : 'Checking for Update...'}</Text>
    </View>
  ),
})(App);

동작:
1. 앱 진입 시 서버에 업데이트 확인
2. 업데이트 존재 시 새 번들 다운로드
3. force update면 다운로드 후 자동 reload, 그동안 fallbackComponent 표시
4. 일반 업데이트면 백그라운드 다운로드 후 다음 실행 시 적용

4. 네이티브 코드 설정

iOS(AppDelegate)와 Android(MainApplication)에서 jsBundleURL을
HotUpdater.bundleURL()로 바꿔준다. 이렇게 해야 앱 시작 시 로컬에 다운로드된
최신 번들이 있으면 그걸 로드하고, 없으면 기본 번들을 로드한다.

5. 배포

# 일반 배포
npx hot-updater deploy

# 인터랙티브 + 강제 업데이트
npx hot-updater deploy -i -f
  • -i (--interactive): 채널·플랫폼 등을 대화형으로 선택
  • -f (--force-update): 이번 배포를 force update로 표시

주의사항: Silent Update가 기본

hot-updater의 기본 동작은 Silent Update다.
1차 실행에서 백그라운드 다운로드 → 2차 실행에서 적용. UX를 해치지 않기 위한 업계 표준 방식이다.

치명적 버그 픽스 등 즉시 적용이 필요하면 두 가지 방법이 있다:

방법 A — wrap의 force update 활용 (권장)
배포할 때 -f 플래그로 force update로 마킹하고, wrap의 reloadOnForceUpdate: true를 켜둔다.
서버가 forceUpdate를 내려주면 wrap이 자동으로 다운로드 → 즉시 reload까지 처리하며,
그동안 fallbackComponent가 화면을 가린다.

방법 B — 수동(manual) 모드
유저에게 알림을 띄우는 등 커스텀 UX가 필요할 때:

import React, { useEffect } from 'react';
import { Alert, View, Text } from 'react-native';
import { HotUpdater } from '@hot-updater/react-native';

function App() {
  useEffect(() => {
    (async () => {
      try {
        const updateInfo = await HotUpdater.checkForUpdate({
          updateStrategy: 'appVersion',
        });
        if (!updateInfo) return;

        Alert.alert(
          '업데이트 안내',
          '새로운 버전이 있습니다. 지금 업데이트할까요?',
          [{
            text: '확인',
            onPress: async () => {
              // 1) 번들 다운로드 (이게 빠지면 reload해도 적용될 게 없음)
              await updateInfo.updateBundle();
              // 2) 강제 새로고침
              await HotUpdater.reload();
            },
          }],
          { cancelable: false },
        );
      } catch (e) {
        console.log('업데이트 확인 실패:', e);
      }
    })();
  }, []);

  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>현재 앱 화면 (버전 1.0.0)</Text>
    </View>
  );
}

// wrap은 manual 모드에서도 필수다.
export default HotUpdater.wrap({
  source: 'https://your-update-server-url',
  updateStrategy: 'appVersion',
  updateMode: 'manual',
})(App);

핵심 포인트: checkForUpdate()확인만, 실제 다운로드는
updateInfo.updateBundle()을 호출해야 일어난다. 그 후 HotUpdater.reload()로 즉시 반영.

profile
안녕하세요! 프론트엔드 개발자입니다! (2024/03 ~)

0개의 댓글