홈으로 돌아가기

OTA 업데이트 Capacitor: 아키텍처와 GitHub

이 기사는 GitHub Releases와 클린 아키텍처를 사용한 Capacitor 애플리케이션의 OTA 업데이트 시스템 리팩토링을 설명합니다. NATIVE/PATCH 전략, Capgo 어댑터, Zustand 스토어 및 다운로드 진행 상황 처리가 고려되었습니다.

Capacitor 앱을 위한 클린 OTA 아키텍처
Advertisement 728x90

Capacitor OTA 업데이트 아키텍처: 전략과 GitHub 릴리스 활용

Capacitor는 Google Play나 App Store의 심사 없이 모바일 앱의 JavaScript 번들을 업데이트할 수 있게 해줍니다. 네이티브 셸은 그대로 유지하면서, 제품 코드는 ZIP 아카이브를 통해 교체됩니다. 이 시스템은 GitHub 릴리스를 버전 소스로 사용하며, 다운로드를 위해 @capgo/capacitor-updater를 활용합니다. 실패 시 자동 롤백은 앱 실행 후 10초 이내에 notifyAppReady()를 호출함으로써 보장됩니다.

법적 제약과 한계

Google Play는 앱의 주요 목적이 변경되지 않는 한 해석형 코드(JavaScript) 업데이트를 허용합니다. Apple은 기능이 유지되고 보안 메커니즘이 우회되지 않는 한 JS 번들 다운로드를 허용합니다. 명확한 기준은 다음과 같습니다: android/ 또는 ios/ 디렉토리의 변경은 스토어를 통한 배포가 필요합니다.

설치 방법:

Google AdInline article slot
npm install @capgo/capacitor-updater
npx cap sync

capacitor.config.ts에서 자동 업데이트 비활성화:

const config: CapacitorConfig = {
  plugins: {
    CapacitorUpdater: {
      autoUpdate: false,
    },
  },
};

번들 빌드:

npm run build:dist
npx @capgo/cli bundle zip io.your.app --name app-bundle.zip

새로운 아키텍처: FSD와 포트 & 어댑터

코드는 src/features/app-update/(비즈니스 로직)와 src/shared/api/(어댑터)로 분리됩니다. 기능은 인터페이스를 통해 작동하며, Capgo나 GitHub에 의존하지 않습니다. Zustand 스토어가 상태를 관리하고, UI 컴포넌트는 독립적으로 구성됩니다.

Google AdInline article slot

구조:

  • features/app-update/model/types.ts — 상태(IDLE, CHECKING, AVAILABLE)와 전략(NONE, NATIVE, PATCH)
  • features/app-update/model/contracts.ts — 의존성 인터페이스
  • shared/api/app-updater/capgo-updater-runtime.ts — 플러그인 래퍼
  • shared/api/github/github-runtime.ts — GitHub API 요청

Semver 기반 업데이트 전략

업데이트 유형 결정:

const isReleaseHasNativeUpdate = (current: SemVer, remote: SemVer): boolean =>
  remote.major > current.major || remote.minor > current.minor;

const isReleaseHasPatchUpdate = (current: SemVer, remote: SemVer): boolean =>
  remote.major === current.major &&
  remote.minor === current.minor &&
  remote.patch > current.patch;
  • 1.1.0 vs 1.0.2NATIVE(스토어)
  • 1.0.3 vs 1.0.2PATCH(OTA)

단일 소스로서의 GitHub 릴리스

GitHub API는 태그, 변경 로그, app-bundle.zip 링크를 반환합니다. 유스케이스는 버전을 파싱하고 에셋을 선택합니다:

Google AdInline article slot
export const checkForAvailableUpdate = async (deps: AppUpdateDependencies): Promise<CheckResult> => {
  const releaseResult = await deps.githubRuntime.getLatestReleaseInfo();
  // ...
  const bundleAsset = assets.find(a => a.name === 'app-bundle.zip');
  // ...
};

GitHub 어댑터:

export const createGithubRuntime = (): GithubRuntime => ({
  getLatestReleaseInfo: async () => {
    const response = await fetch(config.APP_RELEASE_URL);
    const data: GithubLatestReleaseResponse = await response.json();
    return { status: true, data };
  },
});

Capgo 어댑터와 이벤트 처리

인터페이스는 비즈니스 로직을 플러그인으로부터 분리합니다:

export const createCapgoUpdaterRuntime = (): CapgoUpdaterRuntime => ({
  downloadBundle: (url, version) => CapacitorUpdater.download({ url, version }),
  notifyAppReady: () => CapacitorUpdater.notifyAppReady(),
  addDownloadListener: (listener) => CapacitorUpdater.addListener('download', listener),
});

Zustand 스토어는 진행 상황을 구독합니다:

startDownloadUpdate: async () => {
  set({ status: APP_UPDATE_STATUSES.DOWNLOADING, downloadProgress: 0 });
  await capgoRuntime.addDownloadListener(({ percent }) => {
    set({ downloadProgress: percent });
  });
  const bundle = await capgoRuntime.downloadBundle(bundleDownloadUrl, remoteVersion);
},

적용: CapacitorUpdater.set(bundle) + App.minimizeApp()으로 부드러운 재시작.

초기화와 UI

main.tsx에서 렌더링 후:

await initializeUpdater();

initializeUpdater()는 의존성을 생성하고, notifyAppReady()를 호출하며, 업데이트를 확인합니다. AppUpdateInfo 컴포넌트는 상태(AVAILABLE, DOWNLOADING, DOWNLOADED)에 따라 조건부로 표시됩니다.

핵심 포인트:

  • OTA는 패치 버전에만 적용; 메이저/마이너 업데이트는 스토어를 통해 진행
  • notifyAppReady()를 10초 이내에 호출하여 롤백 방지
  • GitHub 릴리스는 버전 관리, CDN, API를 한곳에 통합
  • 포트/어댑터 아키텍처로 테스트와 공급자 교체 간소화
  • 네이티브 플러그인 이벤트를 통한 다운로드 진행률 표시

— Editorial Team

Advertisement 728x90

다음 읽기