Mocco에서 OTA 업데이트 호스팅하기

Mocco는 React Native 앱의 OTA(over-the-air) 업데이트를 직접 제공할 수 있습니다. 앱은 기본 expo-updates 모듈을 그대로 쓰고(네이티브 React Native 앱도 설치할 수 있습니다), 이 모듈이 Mocco를 가리키게 합니다. CI가 모든 업데이트에 서명하며, 서명 키는 CI 밖으로 나가지 않습니다. Mocco는 저장하기 전에 서명을 검증하고 서명된 바이트를 그대로 제공하므로, 기기는 CI가 서명한 것만 실행합니다.

1. 앱 연결하기

프로젝트의 Overview 페이지에서 플랫폼을 React Native로 하여 앱을 추가합니다. 그런 다음 OTA updates(Hosted by Mocco 탭)를 열고 Host OTA updates를 선택합니다. Mocco가 앱의 매니페스트 URL과 app.json에 넣을 expo.updates 블록을 보여 줍니다.

OTA updates의 Hosted by Mocco: 매니페스트 URL과 app.json 블록, 활성 서명 인증서

앱 저장소에서 다음을 실행합니다.

npx mocco ota init --manifest-url <the manifest URL>

이 명령은 서명 키 쌍과 인증서(keys/private-key.pem은 git에서 제외되고, certs/certificate.pem)를 만들고 app.json에 updates 블록을 기록합니다. 이 설정으로 새 바이너리를 빌드해 배포하세요. 인증서가 바이너리에 포함됩니다.

app.json에 runtimeVersion이 없다면 설정하세요. runtimeVersion은 JS 번들과 바이너리를 짝짓는 값입니다. 기기는 런타임 버전이 자기 것과 일치하는 업데이트만 불러오므로, 네이티브 코드가 실행할 수 없는 바이너리에 번들이 들어가는 일을 이 값이 막아 줍니다.

"runtimeVersion": { "policy": "fingerprint" }

fingerprint는 네이티브 의존성과 설정을 해시하므로, 네이티브 쪽이 바뀌면 값도 저절로 바뀝니다. 아무도 따로 기억할 필요가 없습니다. mocco ota publish는 프로젝트의 Expo CLI로 플랫폼별 값을 계산하므로, iOS와 Android는 해시가 달라 별도의 릴리스가 됩니다. 문자열 값이나 appVersion 정책도 쓸 수 있으며, 이 경우 두 플랫폼이 하나의 릴리스로 묶입니다.

2. 인증서 등록하기

certs/certificate.pem을 Signing certificates에 붙여 넣습니다(소유자와 관리자). Mocco는 활성 인증서로 서명이 검증되지 않는 업데이트를 모두 거부합니다. 각 인증서에는 이 인증서에 의존하는 런타임 버전이 표시됩니다. 인증서를 폐기하면 그 인증서가 포함된 바이너리의 업데이트가 중단되므로, 해당 바이너리가 모두 사라진 뒤에 폐기하세요.

keys/private-key.pem은 CI 시크릿 MOCCO_OTA_SIGNING_KEY로 저장하고 백업해 두세요. 인증서가 바이너리에 포함되어 있어서 키를 바꾸려면 새 스토어 빌드가 필요합니다. 키를 잃으면 새 빌드가 나갈 때까지 OTA를 쓸 수 없습니다.

키는 OTA 앱 하나보다 오래 쓰입니다. 나중에 같은 앱이 다른 Mocco 앱을 가리키게 하려면(두 번째 환경, 자체 호스팅으로 이전, 로컬 체험 뒤 실제 연결 등) --keep-key를 붙여 init을 다시 실행하세요. updates 블록만 다시 쓰고 키와 인증서는 건드리지 않으므로, 이미 배포된 바이너리가 계속 검증됩니다. 새 앱에도 같은 인증서를 등록하세요.

3. 채널 만들기

빌드는 expo-channel-name 헤더의 채널에서 업데이트를 읽습니다(init은 production으로 설정하며, 빌드 프로필마다 바꿀 수 있습니다). Channels에서 staging과 production을 만드세요. production을 보호하면 모든 변경에 승인이 필요합니다. 일시 중지(pause)와 롤백은 승인을 기다리지 않습니다.

4. CI에서 배포하기

GitHub Actions를 쓴다면 Trusted publishing에서 저장소(숫자 저장소 ID), ref, 필요하면 워크플로를 신뢰 대상으로 등록하고, 승격할 수 있는 채널을 고르세요. 그러면 job에 Mocco 시크릿이 필요 없습니다.

permissions:
  id-token: write
  contents: read
steps:
  - uses: actions/checkout@v4
  - uses: fi-workers/mocco/actions/ota-publish@main
    with:
      channel: staging
    env:
      MOCCO_OTA_SIGNING_KEY: ${{ secrets.MOCCO_OTA_SIGNING_KEY }}

다른 CI에서는 API keys에서 ota:write 권한의 secret API 키를 만들고 MOCCO_API_KEY=… npx mocco ota publish --channel staging을 실행하세요. publish는 expo export를 실행하고, Mocco에 아직 없는 파일만 업로드하고, 매니페스트에 서명하고(롤백도 미리 서명합니다), Mocco가 업로드를 검증할 때까지 기다린 뒤 승격합니다. 앱이 다음 안전한 시점에 적용해야 하는 업데이트에는 --mandatory를 붙이세요.

앱에서

expo-updates는 실행할 때 스스로 업데이트를 확인합니다. 도입 현황 수치, 크래시 폴백 알림, 필수 업데이트가 필요하면 @mocco/react-native를 추가하세요.

import { MoccoOta, useMoccoUpdate } from '@mocco/react-native/ota';

export default function App() {
  const update = useMoccoUpdate(); // { status, isMandatory, applyNow }
  return (
    <>
      <MoccoOta appId="<your OTA app id>" clientId={installId} />
      {update.status === 'ready' && !update.isMandatory ? <Button title="Restart to update" onPress={update.applyNow} /> : null}
      {/* …your app… */}
    </>
  );
}

<MoccoOta>는 긴급 실행(업데이트가 크래시해서 내장 번들이 대신 실행된 경우)을 포함해 모든 실행을 보고합니다. useMoccoUpdate()는 사용 가능한 업데이트를 백그라운드에서 내려받습니다. 필수 업데이트(--mandatory)는 앱이 다음에 포그라운드로 돌아올 때 적용됩니다. clientId는 설치마다 고정된 아무 ID(예: expo-application에서 가져온 값)이면 되고, Mocco는 그 해시만 저장합니다. mocco ota init이 app.json을 쓰는 대신 config plugin을 쓸 수도 있습니다: "plugins": [["@mocco/react-native", { "manifestUrl": "…", "channel": "production" }]].

5. 롤아웃과 롤백

릴리스를 일부 기기에 승격합니다(--rollout 10, 또는 Promote 옆의 퍼센트 입력란). 채널 페이지에는 플랫폼별로 무엇을 제공하고 있는지, 몇 대의 기기가 확인했는지, 몇 대가 후보 릴리스를 실행하는지, 모든 변경의 이력이 표시됩니다.

30% 롤아웃과 그 컨트롤, 채널 이력이 표시된 채널 페이지

  • Set share는 대상 기기를 늘리고, Complete는 릴리스를 모든 사용자에게 제공합니다.

  • Pause는 새로운 도입을 멈춥니다. 이미 업데이트를 받은 기기는 그대로 유지합니다.

  • Roll back은 이전 릴리스를 다시 제공합니다. CI가 미리 다시 서명해 둔 것이라, 문제 있는 릴리스를 실행하던 기기는 다음 확인 때 이전 릴리스를 받습니다. 롤아웃 중에는 이것이 롤아웃을 중단하는 방법입니다.

  • Roll back to embedded는 기기를 바이너리에 포함된 번들로 되돌립니다.

보호된 채널에서는 승격과 롤아웃 변경이 승인 요청을 만듭니다. 승인자는 릴리스, 크기, 커밋, 기기가 내려받을 파일을 확인합니다. 요청이 요청자의 키를 쓰는 CI에서 온 경우에도 요청자는 자기 요청을 승인할 수 없습니다.

각 릴리스 페이지에는 서명된 업데이트(미리 서명된 롤백 포함), 제공되는 위치, 최근 2주간의 도입 현황, 승인 내역이 나열됩니다. 업데이트 뒤에 많은 기기가 내장 번들로 폴백하면(긴급 실행), Mocco는 OTA 이벤트를 구독하는 채널에 알립니다.

staging에서 30%로 제공 중인 릴리스 페이지: 서명된 업데이트와 승인 내역