# 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 블록, 활성 서명 인증서](https://fi-workers-mocco-storage.s3.ap-northeast-2.amazonaws.com/pub/w/93e11fe8-0494-4e3d-b1cb-fe4dffd3ee7e/p/d64785b8-9a23-4fa1-a81d-550af30e203a/helpcenter/1bde2b62-9eba-4835-a4fa-fea911c31a1a/hosting-overview.png)

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

```bash
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 번들과 바이너리를 짝짓는 값입니다. 기기는 런타임 버전이 자기 것과 일치하는 업데이트만 불러오므로, 네이티브 코드가 실행할 수 없는 바이너리에 번들이 들어가는 일을 이 값이 막아 줍니다.

```json
"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 시크릿이 필요 없습니다.

```yaml
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`를 추가하세요.

```tsx
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% 롤아웃과 그 컨트롤, 채널 이력이 표시된 채널 페이지](https://fi-workers-mocco-storage.s3.ap-northeast-2.amazonaws.com/pub/w/93e11fe8-0494-4e3d-b1cb-fe4dffd3ee7e/p/d64785b8-9a23-4fa1-a81d-550af30e203a/helpcenter/6f78b389-716e-40cc-a410-5f846780b947/hosting-channel.png)

- **Set share**는 대상 기기를 늘리고, **Complete**는 릴리스를 모든 사용자에게 제공합니다.
- **Pause**는 새로운 도입을 멈춥니다. 이미 업데이트를 받은 기기는 그대로 유지합니다.
- **Roll back**은 이전 릴리스를 다시 제공합니다. CI가 미리 다시 서명해 둔 것이라, 문제 있는 릴리스를 실행하던 기기는 다음 확인 때 이전 릴리스를 받습니다. 롤아웃 중에는 이것이 롤아웃을 중단하는 방법입니다.
- **Roll back to embedded**는 기기를 바이너리에 포함된 번들로 되돌립니다.

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

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

![staging에서 30%로 제공 중인 릴리스 페이지: 서명된 업데이트와 승인 내역](https://fi-workers-mocco-storage.s3.ap-northeast-2.amazonaws.com/pub/w/93e11fe8-0494-4e3d-b1cb-fe4dffd3ee7e/p/d64785b8-9a23-4fa1-a81d-550af30e203a/helpcenter/e622118c-3eff-4f44-9c64-934fd04ad84f/hosting-release.png)
