# Expo本番運用ガイド2026：Expo Router・CNG・EAS・OTA更新を実コードで解説

> Expo SDK 56（React Native 0.85 / React 19.2、New Architecture標準）を本番で使い倒すための実務ガイド。Expo Router v7のファイルベース・ルーティング、CNG（継続的ネイティブ生成）と config plugin、EAS Build/Update/Submit/Workflows、そして事故を防ぐOTA更新のruntimeVersion設計まで、実モノレポの動くコードで解説します。

- 公開日: 2026-06-24
- 著者: 友田 陽大
- タグ: React Native, Expo, TypeScript, EAS, CI/CD, モバイルアプリ, アーキテクチャ設計
- URL: https://tomodahinata.com/blog/expo-production-guide-router-eas-cng-ota

## 要点

- app.config.tsが唯一の真実。android/・ios/は成果物でgitにコミットせず、CNG＋config pluginで宣言的に管理する
- Expo Router v7はファイル構造＝画面遷移。全画面が自動でディープリンク対応になり、typedRoutesで遷移を型安全にできる
- EAS Build/Submitでmac不要の本番ビルドとストア提出。development / preview / productionの3プロファイルを使い分ける
- OTA（EAS Update）で送れるのはJSとアセットだけ。runtimeVersionのfingerprintポリシーでネイティブ変更の誤配信を構造的に防ぐ
- 秘密はバンドルに埋めない。EXPO_PUBLIC_接頭辞は平文で焼き込まれるため、機微情報はexpo-secure-storeに保存する

---

「Expoはプロトタイプ用で、本気のアプリでは結局 eject する」——これは**もう古い認識**です。2026年のExpoは、`react-native init` を置き換える公式推奨のフレームワークになりました。私自身、アマチュア野球向けの[リアルタイム試合記録アプリ](/case-studies/realtime-sports-scoring-app)（iOS / Android のモバイル + Web管理画面）を **Expo + Next.js + Supabase のモノレポ**で一人で作り、ストア配信から OTA 更新まで運用しています。

この記事は、その実運用で得た「**どの機能を・いつ・どう使うか**」を、Expo公式ドキュメント（[SDK 56](https://expo.dev/changelog/sdk-56) 時点）に忠実に、しかし公式より**判断軸を厚く**して解説するものです。コードはすべて、実際に手を動かせる粒度で載せます。

> 本記事の基準バージョン：**Expo SDK 56**（React Native 0.85 / React 19.2、Hermes v1 がデフォルトエンジン）。SDK 55 以降、**Legacy Architecture は廃止され New Architecture が常時有効**（無効化不可）です。Expo Router は **v7** に到達し、React Navigation から独立しました。

## 0. 全体像：Expoの「4つの柱」をどう組み合わせるか

Expoを本番で使うとは、実質この4つを設計することです。混同すると事故ります。

| 柱 | 役割 | 中心ツール | この記事の章 |
| --- | --- | --- | --- |
| **画面遷移** | ルーティング・ディープリンク | Expo Router v7 | §2 |
| **ネイティブ層** | iOS/Androidプロジェクト生成 | CNG / prebuild / config plugin | §3 |
| **ビルド・配信** | クラウドビルドとストア提出 | EAS Build / Submit | §4 |
| **無停止更新** | 審査を通さないJS更新 | EAS Update（OTA） | §5 |

順番に意味があります。**Router でアプリの形を決め、CNG でネイティブを宣言的に管理し、EAS でビルドし、Update で運用中に直す**。以下、この流れで進めます。

---

## 1. メンタルモデル：app.config.ts が唯一の真実

最初に頭を切り替えるべき点はここです。**`android/` と `ios/` ディレクトリは「成果物」であって「ソース」ではない**、という発想。

従来のReact Nativeは、`android/` `ios/` を一度作って手で育てます。Expoの**CNG（Continuous Native Generation / 継続的ネイティブ生成）** では、ネイティブプロジェクトを**設定から毎回生成し直す**。生成元は `app.config.ts` ひとつです。

```ts
// app.config.ts — アプリの「宣言的な唯一の真実」
import type { ExpoConfig } from 'expo/config';

export default (): ExpoConfig => ({
  name: 'Scorebook',
  slug: 'scorebook',
  scheme: 'scorebook', // ディープリンク・OAuthリダイレクトの基点
  version: '1.4.0',
  orientation: 'portrait',
  newArchEnabled: true, // SDK 56では既定。明示しておくと意図が伝わる
  ios: { bundleIdentifier: 'com.example.scorebook', supportsTablet: true },
  android: { package: 'com.example.scorebook', edgeToEdgeEnabled: true },
  plugins: [
    'expo-router',
    'expo-secure-store',
    ['expo-build-properties', { ios: { deploymentTarget: '15.1' } }],
  ],
  experiments: { typedRoutes: true }, // §2で使う型安全ルーティング
});
```

これがもたらす実利は、**SDKアップグレードが「他人が書いたネイティブコードの差分を読む作業」から「package.jsonのバージョンを上げて再生成するだけ」に変わる**こと。`npx expo prebuild --clean` で `android/` `ios/` を捨てて作り直せます。

> **判断軸：`android/`/`ios/` を git にコミットすべきか？**
> CNGを使うなら**コミットしない**（`.gitignore` に入れる）のが原則です。生成物をコミットすると「設定 vs 生成物」の二重管理が始まり、CNGの利点が消えます。手書きのネイティブ変更が必要になったら、§3の config plugin で**設定側に寄せる**のが正解です。

---

## 2. Expo Router v7：ファイルベース・ルーティングを正しく使う

Expo Router は **`app/` ディレクトリのファイル構造がそのまま画面遷移になる**仕組みです。Next.js App Router と同じメンタルモデルで、iOS / Android / Web に**同一のルーティング**が効きます。すべての画面が自動で**ディープリンク対応**になるのが本質的な価値です。

### 2-1. ファイル構造と命名規則（最初に覚える5つ）

```text
app/
├─ _layout.tsx          # ルートレイアウト（全画面の親）
├─ index.tsx            # "/" 画面
├─ (tabs)/              # () = URLに出ないグループ化フォルダ
│  ├─ _layout.tsx       # タブバーの定義
│  ├─ index.tsx         # "/" タブ（ホーム）
│  └─ games.tsx         # "/games" タブ
├─ games/
│  └─ [gameId].tsx      # "/games/:gameId" 動的ルート
├─ +not-found.tsx       # 404画面
└─ api/
   └─ health+api.ts     # "/api/health" APIルート（サーバー）
```

覚えるのはこの5記法だけです。

- `_layout.tsx` … その階層の**共通の枠**（ナビゲータ、プロバイダ）
- `[param].tsx` … **動的セグメント**（`[gameId]` → `useLocalSearchParams` で取得）
- `(group)/` … **URLに現れない**グループ化（タブ/認証状態別など）
- `+not-found.tsx` … マッチしなかった時のフォールバック
- `name+api.ts` … **APIルート**（Web/サーバー出力時のエンドポイント）

### 2-2. ルートレイアウト：プロバイダはここに一度だけ置く

`app/_layout.tsx` は全画面の親です。**認証・テーマ・データクライアントなどの Provider は、ここに一度だけ**置きます。各画面に書くのは典型的なアンチパターンです。

```tsx
// app/_layout.tsx
import { Stack } from 'expo-router';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useReactQueryDevTools } from '@dev-plugins/react-query';

const queryClient = new QueryClient();

export default function RootLayout() {
  useReactQueryDevTools(queryClient);
  return (
    <QueryClientProvider client={queryClient}>
      <Stack screenOptions={{ headerShown: false }}>
        {/* (tabs) グループはタブ自身がヘッダを持つので隠す */}
        <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
        <Stack.Screen
          name="games/[gameId]"
          options={{ presentation: 'modal', title: '試合詳細' }}
        />
      </Stack>
    </QueryClientProvider>
  );
}
```

### 2-3. タブとスタックの使い分け

判断はシンプルです。**横並びで行き来する＝Tabs、奥に潜る＝Stack**。実運用ではこの組み合わせ（タブの中で詳細にプッシュ）が大半です。

```tsx
// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import { Home, ListChecks } from 'lucide-react-native';

export default function TabLayout() {
  return (
    <Tabs screenOptions={{ tabBarActiveTintColor: '#2563eb' }}>
      <Tabs.Screen
        name="index"
        options={{
          title: 'ホーム',
          // a11y: スクリーンリーダ向けにアイコンの意味を言語化
          tabBarIcon: ({ color, size }) => <Home color={color} size={size} />,
          tabBarAccessibilityLabel: 'ホームタブ',
        }}
      />
      <Tabs.Screen
        name="games"
        options={{
          title: '試合',
          tabBarIcon: ({ color, size }) => <ListChecks color={color} size={size} />,
          tabBarAccessibilityLabel: '試合一覧タブ',
        }}
      />
    </Tabs>
  );
}
```

### 2-4. 動的ルートと型安全な遷移

`experiments.typedRoutes`（§1で有効化）を入れると、`Link` の `href` と `router.push` が**型チェックされます**。存在しないパスや必須paramの抜けがコンパイルで落ちる——本番アプリで地味に効く安全装置です。

```tsx
// app/games/[gameId].tsx
import { useLocalSearchParams, Link, router } from 'expo-router';
import { View, Text, Pressable } from 'react-native';

export default function GameScreen() {
  // 型引数で params の型を固定。境界で型を絞るのが鉄則
  const { gameId } = useLocalSearchParams<{ gameId: string }>();

  return (
    <View>
      <Text accessibilityRole="header">試合 #{gameId}</Text>

      {/* 宣言的遷移：typedRoutesにより存在しないパスは型エラー */}
      <Link href={`/games/${gameId}/score`} accessibilityRole="link">
        スコア入力へ
      </Link>

      {/* 命令的遷移：イベントハンドラ内ではこちら */}
      <Pressable
        accessibilityRole="button"
        accessibilityLabel="一覧に戻る"
        onPress={() => router.back()}
      >
        <Text>戻る</Text>
      </Pressable>
    </View>
  );
}
```

### 2-5. APIルート：軽い処理はアプリと同居させる

Expo Router は **`+api.ts` サフィックスでサーバーサイドのエンドポイント**を書けます（Web/サーバー出力時。EAS Hosting にそのままデプロイ可）。Webhook受け口や、**クライアントに秘密鍵を埋め込めない署名処理**を置くのに向きます。

```ts
// app/api/health+api.ts
export function GET(request: Request) {
  return Response.json({ ok: true, ts: Date.now() });
}

// app/api/notify+api.ts — クライアントに鍵を出さずサーバーで処理
export async function POST(request: Request) {
  const body = await request.json();
  // process.env.* はサーバー側でのみ参照可能（バンドルに埋まらない）
  const res = await fetch('https://api.example.com/push', {
    method: 'POST',
    headers: { Authorization: `Bearer ${process.env.PUSH_API_KEY}` },
    body: JSON.stringify(body),
  });
  return Response.json({ delivered: res.ok }, { status: res.ok ? 200 : 502 });
}
```

> **境界線の引き方**：重いビジネスロジック・RLS前提のデータアクセスは、私の場合 Supabase / 専用バックエンドに置きます。`+api.ts` は「アプリと密結合だが秘密を要する薄い処理」に限定するのがSRP的にきれいです。

---

## 3. CNG と config plugin：ネイティブ変更を「設定」で表現する

「このライブラリは `Info.plist` に権限文言を足せと言っている」——CNGの世界では、`ios/` を手で開きません。**config plugin で宣言的に注入**します。

### 3-1. まず公式プラグインで済ませる

権限文言やビルド設定の多くは、既存プラグインのパラメータで解決します。手書きする前に**これを探すのが先**です。

```ts
// app.config.ts（抜粋）
plugins: [
  [
    'expo-build-properties',
    {
      ios: { deploymentTarget: '15.1', useFrameworks: 'static' },
      android: { compileSdkVersion: 35, minSdkVersion: 24 },
    },
  ],
  [
    'expo-location',
    { locationWhenInUsePermission: '球場の位置から最寄りの試合を表示します。' },
  ],
],
```

### 3-2. 自作 config plugin（TypeScript・型安全）

公式にない調整は、自作プラグインで `AndroidManifest.xml` や `Info.plist` を**プログラムで**書き換えます。SDK 56 では**config pluginがTypeScriptで型安全に書ける**ようになりました。

```ts
// plugins/withClearTextDisabled.ts
// 目的：Androidの平文HTTP通信を本番で禁止する（セキュリティ既定の堅牢化）
import { type ConfigPlugin, withAndroidManifest } from 'expo/config-plugins';

const withClearTextDisabled: ConfigPlugin = (config) =>
  withAndroidManifest(config, (cfg) => {
    const app = cfg.modResults.manifest.application?.[0];
    if (app?.$) {
      app.$['android:usesCleartextTraffic'] = 'false';
    }
    return cfg;
  });

export default withClearTextDisabled;
```

```ts
// app.config.ts で読み込む
import withClearTextDisabled from './plugins/withClearTextDisabled';
// plugins 配列に: ...(他のplugin), './plugins/withClearTextDisabled.ts'
// もしくは関数を直接 import して plugins: [withClearTextDisabled]
```

CNGの嬉しさは「**孤児コードが生まれない**」点にもあります。パッケージをアンインストールしてプラグイン宣言を消すと、再生成時に対応するネイティブ変更も消える。「なぜか残った謎のネイティブ設定」が原理的に発生しません。

> **CNGを捨てて bare workflow にすべき時**：既存の巨大ネイティブアプリへ部分導入する（SDK 56の `expo-brownfield` が候補）、あるいはCNGで表現できない深いネイティブ実験を高速で回したい時。逆に言えば、**新規アプリでこれに当てはまることは稀**です。まずCNGで始めるべきです。

---

## 4. EAS Build / Submit：ローカルにXcodeが無くても本番ビルド

**EAS Build** はクラウドでiOS/Androidをビルド・署名します。Mac不要でiOSがビルドでき、署名証明書もEASに管理を任せられます。設定は `eas.json` の**ビルドプロファイル**に集約します。

```jsonc
// eas.json
{
  "cli": { "version": ">= 16.0.0", "appVersionSource": "remote" },
  "build": {
    // 開発：実機で動くdev client（Fast Refresh付き）
    "development": {
      "developmentClient": true,
      "distribution": "internal"
    },
    // 社内配布：ストアを通さずQA・関係者に配る
    "preview": {
      "distribution": "internal",
      "channel": "preview"          // ← §5のOTAチャンネルと対応
    },
    // 本番：ストア提出用。ビルド番号を自動インクリメント
    "production": {
      "channel": "production",
      "autoIncrement": true
    }
  },
  "submit": { "production": {} }
}
```

3プロファイルの使い分けが運用の肝です。

| プロファイル | 配布形態 | いつ使うか |
| --- | --- | --- |
| `development` | dev client（内部） | 日々の開発。実機でホットリロード |
| `preview` | internal | QA・クライアントレビュー。**TestFlight/ストア審査を待たずに**共有 |
| `production` | ストア | リリース本番 |

```bash
# 開発ビルド（一度入れれば、以降のJS変更はMetroから即反映）
eas build --profile development --platform ios

# 社内配布（QRやリンクでインストール）
eas build --profile preview --platform all

# 本番ビルド → そのままストア提出
eas build --profile production --platform all
eas submit --profile production --platform ios
```

**EAS Submit** は出来上がったバイナリを App Store Connect / Google Play へ CLI でアップロードします。証明書・API キーの取り回しが自動化され、「提出のためだけにブラウザを延々と触る」時間が消えます。

---

## 5. EAS Update（OTA）：ここを間違えると本番が壊れる

ここが**この記事で最も重要**な章です。**EAS Update は、ストア審査を通さずにJS・アセットの更新をユーザーへ配信**できます。タイポ修正やロジックのホットフィックスを、数分で全端末に届けられる——強力ですが、**「何をOTAで配れて、何は配れないか」を誤ると、起動不能のアプリを量産**します。

### 5-1. 黄金律：OTAで送れるのは「JSとアセットだけ」

```text
✅ OTAで配信できる（= JSバンドルとアセット）
   ・Reactコンポーネント / 画面ロジックの変更
   ・文言・スタイルの修正、画像差し替え
   ・JSだけで完結するバグfix

❌ OTAで配信できない（= 新しいネイティブビルドが必須）
   ・新しいネイティブモジュールの追加（例：expo-camera を新規導入）
   ・Expo SDK のアップグレード
   ・app.config.ts のネイティブに影響する変更（権限・scheme等）
```

なぜか。OTAが差し替えるのは端末内のJSバンドルだけで、**ネイティブバイナリには触れない**から。新しいネイティブモジュールを呼ぶJSを、それを含まない古いバイナリへ配ると、**実行時に「モジュールが無い」で即クラッシュ**します。

### 5-2. 事故を防ぐ仕組み：runtimeVersion

この事故をフレームワークが防ぐ仕組みが **`runtimeVersion`** です。**OTA更新は、自分と同じ `runtimeVersion` を持つビルドにしか配信されません**。ネイティブが変われば runtimeVersion を変える——これを手動でやると必ず忘れるので、**`fingerprint` ポリシー**を使います。

```ts
// app.config.ts（抜粋）
export default (): ExpoConfig => ({
  // ...
  runtimeVersion: { policy: 'fingerprint' }, // ★推奨
  updates: { url: 'https://u.expo.dev/<your-project-id>' },
});
```

`fingerprint` ポリシーは、**ネイティブ層の構成（依存・設定・config plugin の結果）をハッシュ化**して runtimeVersion を自動算出します。つまり：

- JSだけ変えた → fingerprint は同じ → **OTAが届く**（正しい）
- ネイティブを変えた → fingerprint が変わる → **古いビルドにはOTAが届かない**（＝クラッシュを未然に防止）

「うっかりネイティブ変更をOTAで配って全端末が落ちる」という最悪が、設定ひとつで構造的に起きなくなります。

### 5-3. チャンネルとブランチ：配信先の制御

- **チャンネル（channel）**：配信先ビルドの論理グループ。`eas.json` のプロファイルで指定（`preview` / `production`）。
- **ブランチ（branch）**：チャンネルに紐づく更新履歴の系列。git のブランチに似た概念。

```bash
# preview ビルド群へ更新を配信（QA確認用）
eas update --channel preview --message "スコア表示の桁ズレ修正"

# 本番へ配信
eas update --channel production --message "hotfix: 打順入力のクラッシュ修正"
```

`preview` で検証 → 問題なければ `production`、という**昇格フロー**が自然に組めます。これが「審査待ち数日」を「数分」に変える実体です。

### 5-4. 回復性：壊れた更新からの即時ロールバック

OTAは速い分、壊れた更新も速く広がります。**ロールバック手段を常備**するのが本番の作法です。直前の正常な更新へ即座に戻せます。

```bash
# 不正な更新を無効化し、直前の正常版へ戻す
eas update:rollback --channel production
```

加えて、アプリ側で更新の取得を制御し、**失敗時に握り潰さず観測**します。

```tsx
// 起動時に最新更新を取りに行く（既定はバックグラウンド取得→次回起動で反映）
import * as Updates from 'expo-updates';

export async function syncUpdates() {
  if (__DEV__) return; // 開発中はスキップ
  try {
    const result = await Updates.checkForUpdateAsync();
    if (result.isAvailable) {
      await Updates.fetchUpdateAsync();
      await Updates.reloadAsync(); // 重要更新は即時適用
    }
  } catch (e) {
    // 失敗を黙殺しない：監視基盤へ送る（Sentry等）
    reportError('eas-update-sync-failed', e);
  }
}
```

---

## 6. EAS Workflows：ビルド〜配信をCIで束ねる

**EAS Workflows** は、ビルド・更新・提出を**クラウドのCI/CDジョブ**として宣言的に連結します（`.eas/workflows/` 配下のYAML）。「mainにマージしたらpreviewへOTA、タグを打ったら本番ビルド」を自動化できます。

```yaml
# .eas/workflows/deploy-preview.yml
name: Deploy Preview on merge
on:
  push:
    branches: ['main']
jobs:
  publish_preview_update:
    name: Publish OTA to preview
    type: update
    params:
      channel: preview
      message: 'auto: merged to main'
```

```yaml
# .eas/workflows/release.yml — タグで本番ビルド＆提出
name: Production release
on:
  push:
    tags: ['v*']
jobs:
  build:
    type: build
    params: { platform: all, profile: production }
  submit:
    needs: [build]
    type: submit
    params: { platform: all, profile: production }
```

ローカルからは `eas workflow:run deploy-preview.yml` で手動起動も可能。**個人開発でも「人間がコマンドを覚えている」状態を排除**でき、属人性が消えます。

---

## 7. 本番運用の勘所（可観測性・回復性・冪等性・セキュリティ・a11y）

ここまでが「Expoの機能」。最後に、**本番に耐えるための横断的な設計**を、実運用で効いた順にまとめます。

### 7-1. セキュリティ：秘密はバンドルに埋めない

最頻出の事故が**秘密情報のバンドル混入**です。鉄則は2つ。

- **`EXPO_PUBLIC_` 接頭辞の環境変数は、JSバンドルに平文で焼き込まれます。** 公開して構わない値（公開APIのベースURL等）専用です。**APIキー・シークレットを絶対に入れない**。
- ユーザーのトークン等の機微情報は **`expo-secure-store`**（iOS Keychain / Android Keystore）に保存します。`AsyncStorage` は暗号化されないため不適です。

```ts
import * as SecureStore from 'expo-secure-store';

// OSの安全領域に保存（端末暗号化ストレージ）
await SecureStore.setItemAsync('session_token', token, {
  keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
});
const token = await SecureStore.getItemAsync('session_token');
```

> 秘密を要するサーバー処理は、§2-5の `+api.ts` か専用バックエンドに置く。**「クライアントを信じない」**は、私のモバイル設計の一貫した背骨です（同主旨を[こちらの記事](/blog/untrusted-client-postgres-rls-offline-first)で詳述）。

### 7-2. 可観測性：本番のクラッシュとOTA成否を見る

`expo-updates` の状態（どのupdate IDが効いているか）と、クラッシュを必ず計測します。OTA配信後に「どの更新で落ちたか」を `Updates.updateId` で突き合わせられると、原因切り分けが一瞬になります。エラー監視（Sentry等）には**ソースマップをEASビルドから自動アップロード**できるため、ミニファイ後のスタックでも読めます。

### 7-3. 冪等性・回復性：モバイルは「切れる前提」で書く

電波の悪い環境では、リクエストは**重複し・順序が乱れ・失敗**します。これを前提に、書き込みは**決定的な冪等性キー**で重複吸収し、ローカル永続キュー → バックグラウンド送信で**オフラインファースト**にする。この具体実装（AsyncStorageの永続キューと単一ドレインワーカー、Supabase RLSへの寄せ）は、[実コード付きの別記事](/blog/untrusted-client-postgres-rls-offline-first)に書きました。本記事のExpo土台の上に、この層が乗ります。

### 7-4. パフォーマンス：New ArchitectureとHermesはもうデフォルト

SDK 56 では **New Architecture（無効化不可）** と **Hermes v1**（デフォルトエンジン）が標準です。特別な設定なしに起動時間とブリッジ越しのオーバーヘッドが改善されています。アプリ側でやるべきは王道——リストは `FlatList` / `FlashList` で仮想化、重い計算は `useMemo`、画像は `expo-image`（ディスクキャッシュ・優先度制御つき）を使う、です。

### 7-5. a11y：宣言的に役割とラベルを付ける

§2のコードで触れた通り、`accessibilityRole` / `accessibilityLabel` を**要素を作る時点で**付けます。後付けは漏れます。タッチ領域は最低44×44pt、フォーカス順序とコントラストを守る——これは「対応」ではなく**最初からの前提**として設計に織り込みます。

---

## 8. いつExpoを選び、いつ避けるか（正直な判断軸）

技術選定で嘘をつかないために、限界も書きます。

**Expoが最適なケース：**

- iOS / Android / Web を**少人数で**同時に出したい
- ストア審査を待たずに**素早く直したい**運用（OTA）
- ネイティブを宣言的に管理し、**SDKアップグレードのコストを下げたい**

**慎重に検討すべきケース：**

- **超低レベルなネイティブ機能**やニッチなSDK連携が中核で、config pluginでも吸収しきれない（→ bare/brownfield、あるいは素のネイティブ）
- ゲームのような**専用エンジン**前提のアプリ
- バイナリサイズに極端な制約がある（Expoランタイムのぶん増える）

私の実プロジェクトは「少人数・マルチプラットフォーム・高速な運用更新」に全部当てはまったため、Expoは最良の選択でした。**逆に当てはまらないなら、無理に寄せません**——これは技術選定の誠実さの問題です。

---

## まとめ：Expoは「速くて安全に運用できる」本番基盤

2026年のExpoは、**設定（app.config.ts）を真実とし、CNGでネイティブを再生成し、EASでビルド・配信し、OTAで無停止更新する**——という一貫した運用基盤です。要点を最後に5行で。

1. **`app.config.ts` が唯一の真実**。`android/`/`ios/` は生成物。CNG + config plugin で宣言的に管理する。
2. **Expo Router v7** はファイル構造＝画面遷移。`typedRoutes` で遷移を型安全に。
3. **EAS Build/Submit** でMac不要の本番ビルドとストア提出。`preview` プロファイルで審査前共有。
4. **EAS Update（OTA）** は強力だが、配れるのは**JS/アセットだけ**。`runtimeVersion: { policy: 'fingerprint' }` で事故を構造的に防ぐ。ロールバックを常備。
5. **秘密はバンドルに埋めない**（`EXPO_PUBLIC_` の罠、`expo-secure-store`）。冪等性・オフライン耐性・可観測性は最初から設計に織り込む。

「一人 × 生成AI（Claude Code）で、速く・安く・安全に」モバイルからWeb管理画面・DB・CI/CDまで一気通貫で作る——その実例が、本記事のコードの出どころである[リアルタイム試合記録アプリ](/case-studies/realtime-sports-scoring-app)です。Expo / React Native でのアプリ開発・本番運用設計のご相談は、[お問い合わせ](/contact)からどうぞ。
