モバイルチームが知っておくべき OTA アップデートのベストプラクティス 5 選
React Native チームの OTA 更新ベストプラクティス:プレビューチャネル、フィンガープリント検出、expo-updates API、段階的リリースと迅速なロールバック。
日本語
コピー

モバイル開発者なら誰でも、あの瞬間を経験している。本番環境でバグが出て、修正はもう書けていて、デプロイボタンを見つめながら、どこでまだ問題が起きうるかを頭の中で数えている——そんな瞬間だ。
OTA アップデートはこの状況を変えた。JavaScript とアセットの変更をユーザーのデバイスに直接届けられる。バイナリの再ビルドも、ストアへの申請も、待ち時間もいらない。React Native チームにとって、これは手元にある最も強力なツールのひとつだ。私たちが接してきたチームのほとんどは、すでにこれを難なく使いこなしている。アップデートを出し、ユーザーが受け取り、アプリはいつも通り動く。
だが「動く」と「信頼できる」は別物だ。1 日に何度もリリースしながら平然としているチームに魔法があるわけではない。彼らはいくつかの重要なパターンを中心に習慣を築いているだけだ。アップデートが本番に入る前にどうテストするか、どの変更が OTA で安全に流せてどれが再ビルド必須か、想定外の事態が起きたときどう影響範囲を抑えるか、そしてロールバックを数時間ではなく数分でどう終わらせるか——彼らはそれを知っている。
本記事ではそのうち 5 つのパターンを紹介する。理論的なアドバイスではなく、チームが毎日リリースしながら本番を壊さずに済ませている実際のワークフローだ。
クイックスタート:OTA アップデートの仕組み
Expo Update にすでに慣れているなら、このセクションは飛ばしてよい。
Expo Update を使うと、アプリの JavaScript バンドルとアセットをネイティブコードとは独立して更新できる。アプリは起動時にサーバーへ新しいアップデートがないか確認する。あればバックグラウンドでダウンロードし、次回のコールドスタート時に適用する。
アップデートは channels(ビルドがどのターゲットを指すか)と runtime versions(JavaScript とネイティブバイナリの互換性を保証する)で整理される。Expo Update では channel は内部的に branch に対応するが、branch はほぼ無視して channel を直接使ってよい。
ユーザーに修正をすぐ届ける必要があるなら、expo-updates パッケージを使えば、任意のタイミングでアップデートを確認・適用できる。
TL;DR:OTA アップデートのチェックリスト
ざっと流し読みするだけなら、要点は以下だ:
✓ preview と production の 2 つの channel を使う。本番に出す前に必ず preview ビルドでテストする ✓ fingerprint でネイティブの変更を検出し、アプリバージョンの更新が必要なタイミングを判断する ✓ 再ビルドが必要な変更と OTA で流せる変更を区別する ✓ expo-updates API でアップデートを検出・ダウンロード・適用し、次のコールドスタートを待たずにユーザーへ修正を届ける ✓ 段階的ロールアウトを使い、ロールバックの方法を把握しておく
以下、順に掘り下げる。
1. 本番前にプレビューでテストする
最もシンプルな流れ:preview → production
ほとんどのチームはここから始め、それでうまくいく。本当に重要な channel は 2 つだけだ:
-
Preview:内部テスト用(通常は staging API、TestFlight/内部テストトラックを指す)
-
Production:ユーザーが実際に受け取るもの
ワークフローは単純だ:
# Step 1: Publish to preview
eas update --channel preview --message "Fix login bug"
# Step 2: Test on a preview build (internal distribution or TestFlight)
# Step 3: When validated, publish to production
eas update --channel production --message "Fix login bug"
これで新しいバンドルが production に公開される。テストを通った成果物をそのまま昇格させるのではない。それで問題ない——同じ commit をリリースしているのだから、実質的な差異が生じるリスクは低い。本番に影響しうる問題のほとんど(環境変数、バンドラーの癖、アセットファイルの問題)はプレビューテストの段階で露見する。
これが実務で重要な理由
-
問題がユーザーに届く前に止められる
-
プレビュービルドは staging API を指せるので本番に影響しない
-
習慣にできるくらい単純
さらに一歩:staging 昇格フロー
もう一段の保証を求めるチームもある。ユーザーが受け取るのは、QA が承認したまさにその成果物でなければならない、という考え方だ。「同じコード」は必ずしも「同じビルド成果物」ではない。バンドルは差異が生まれうるもうひとつの工程だからだ。

これには4 つ目の環境が要る。本番と完全に同じ設定(同じ API サーバー、同じフィーチャーフラグ)を持ちながら、内部テスターにのみ配布される staging channel だ。
# Step 1: Publish to staging
eas update --channel staging --message "Fix login bug"
# Step 2: QA tests on a staging build (identical config to production)
# Step 3: Promote the EXACT tested update to production
eas update:republish --channel staging --destination-channel production
どういうときにやる価値があるか:
-
テスト済み成果物にコンプライアンスや監査の要件がある
-
過去にバンドル工程の微妙な差異で痛い目を見た
-
「もう一度リリースする」こと自体が協業上のリスクになる規模のチーム
必要な設定:
-
4 つのアプリバリアント:development、preview、staging、production
-
staging は本番設定と完全に一致させる(API エンドポイント、フィーチャーフラグなど)
始めたばかりのほとんどのチームには、シンプルな preview → production の流れで十分だ。その追加の保証が本当に必要になったときに、staging 昇格のレイヤーを足せばよい。
2. fingerprint で再ビルドが必要なタイミングを判断する
問題: 依存関係のアップデートがすべて JavaScript だけで済むわけではない。ネイティブコードの変更を要する OTA アップデートを配信し、ユーザーがまだ古いバイナリを動かしていたら、アプリはクラッシュする。
なぜ重要か: ランタイムバージョン管理は互換性の防火壁だ。どのバイナリがどのアップデートを読み込めるかを決める。本番で最もよくある失敗の原因は? 依存関係が JS だけだと思い込んでいたら、実際にはネイティブコードを変えていた、というケースだ。
推奨する方法:appVersion + fingerprint 検出
ランタイムバージョンの戦略には appVersion を使う。ランタイムバージョンがアプリのバージョン(1.0.0、1.1.0 など)と紐づくので、どのビルドがどの更新を受け取れるかを判断しやすくなる。
{
"expo": {
"runtimeVersion": { "policy": "appVersion" }
}
}
核心となる問い: アプリバージョンをいつ上げるべきか、どうやって判断するのか。
そこで fingerprint の出番だ。これはランタイム戦略ではなく、検出ツールである。Expo の fingerprint は、コミット間でプロジェクトのネイティブ部分(依存関係、設定、ネイティブコード)を比較する。fingerprint が変わればネイティブコードが変わった証拠であり、OTA 更新を配信する前に新しいビルドを出す必要がある。

fingerprint で OTA 更新にゲートをかける:
# Compare your local project's fingerprint against your production build
eas fingerprint:compare --build-id <BUILD-ID>
fingerprint が一致していれば、OTA 配信に進んで問題ない。一致していなければ、先に新しいビルドが必要だ。
CI で自動化する:
CI パイプラインに fingerprint チェックを追加すれば、ある変更を OTA 更新として配信できるのか、新しいビルドが必要なのかを自動で判定できる。人の勘に頼る必要がなくなり、非互換の更新がユーザーに届くことも防げる。
実例: Hipcamp は fingerprint を使って、変更が OTA に適しているかを自動チェックしている。fingerprint が一致すれば OTA 配信、一致しなければ次のビルドまで待つ。同社いわく「手動チェックが不要になり、人為的ミスも防げる」。
アプリバージョンを上げるべきタイミング:
-
ネイティブコードを含む依存関係を更新した
-
Expo SDK をアップグレードした
-
app.json のネイティブ設定に影響する項目(アイコン、スプラッシュスクリーン、entitlements、scheme など)を変更した
-
ネイティブプロジェクトのファイル(ios/、android/ ディレクトリ)を変更した
fingerprint ツールはこれらすべてをカバーする。ワークフローに組み込めば、記憶に頼る必要はなくなる。
3. OTA 更新で何を配信できるかを把握する
最もよく聞かれる質問:「更新を配信したらアプリがクラッシュした。何でも更新できると思っていたのに?」
これが実務で重要な理由
-
「願望的な OTA」を防げる——チームが変更を配信しながら、新しいバイナリに暗黙のうちに依存してしまうケースだ。
-
プロダクト/エンジニアリングチームが「今 OTA で出す」か「ストアリリースで出す」かを、すっきりと判断できるようになる。
更新に含められるもの:
-
JavaScript コードとビジネスロジック
-
UI コンポーネント、スタイル、レイアウト
-
画像、フォント、その他のアセット
-
バグ修正と文言の変更
新しいビルドが必要なもの:
-
ネイティブモジュールのインストールやアップグレード(ネイティブコードを含む expo-* パッケージも含む)
-
ネイティブコンパイルを要する設定変更(アプリアイコン、スプラッシュスクリーン、ネイティブの権限宣言、config plugins)
-
新しいネイティブ権限(カメラ、位置情報、通知など)
-
Expo SDK のアップグレード(managed workflow しか使っていなくても)
-
ネイティブコードに影響する app.json の設定変更(scheme、associated domains、intent filters など)
-
bare workflow におけるネイティブプロジェクトファイルの変更(ios/、android/ ディレクトリ)
-
React Native 自体の更新

技術的に言えば、JavaScript かアセットであれば OTA で配信できる。ネイティブコードが絡んだ時点で、新しいビルドが必要になる。
判断は fingerprint に任せる
こうしたルールを暗記する必要はない。fingerprint はプロジェクトのネイティブ表面積を本番ビルドと比較することで、ネイティブの変更を自動検出する。fingerprint が一致すれば OTA に進んで問題ない。一致しなければ、先に新しいビルドを作る必要がある。
EAS Workflows で自動化する:
最善の策は、推測を完全に排除することだ。EAS Workflows なら、push のたびに fingerprint チェックを走らせ、変更を正しい経路——OTA 更新か新しいビルドか——に自動で振り分けられる。
# .eas/workflows/deploy.yml
# Android only version
name: Deploy to production
on:
push:
branches: ['main']
jobs:
fingerprint:
name: Check fingerprint
type: fingerprint
environment: production
get_build:
name: Check for existing build
needs: [fingerprint]
type: get-build
params:
fingerprint_hash: ${{ needs.fingerprint.outputs.android_fingerprint_hash }}
profile: production
build:
name: Build (if native changed)
needs: [get_build]
if: ${{ !needs.get_build.outputs.build_id }}
type: build
params:
platform: android
profile: production
update:
name: OTA Update (if native unchanged)
needs: [get_build]
if: ${{ needs.get_build.outputs.build_id }}
type: update
params:
channel: production
この workflow は main への push のたびに fingerprint をチェックする。ネイティブコードが変わっていて互換性のあるビルドがなければ、新しいビルドをトリガーする。互換性のあるビルドがすでにあれば、OTA 更新を配信する。手動チェックも推測も不要で、更新と workflow の相性はいい。
コンプライアンスについて:
両ストアとも OTA 更新に関する規定を設けているので、原文を直接読むことが重要だ。ストアポリシーの全文はこちら:Apple の Developer Program License Agreement と Google Play のダウンロードコードに関するポリシー。
4. expo-updates API で更新をより速く届ける
何が起きるか: ユーザーがアプリをインストールするのは、あなたが OTA 更新を配信した後だ。アプリを開くと更新はバックグラウンドでダウンロードされるが、反映されるのは次回のコールドスタート(強制終了して再度開く)まで待たされる。
これがデフォルトの挙動であり、私たちも推奨している——アプリ起動時のパフォーマンスはリテンションに直結するからだ。ユーザーは初回起動時には同梱のバンドルを使うので速く、2 回目のセッションで最新バージョンを受け取ることになる。

より細かい制御が必要なら、expo-updates API が更新の検出・ダウンロード・適用を担う。ユーザーが次の更新を受け取るまでの時間は、次のコールドスタートを待つ場合よりはるかに短い。更新チェックとダウンロードの進捗を確認し、更新の緊急度やユーザーが今何をしているか、その他の状況を踏まえて適用のタイミングを判断できる。コールドスタートは一切不要だ。
基本的な使い方:
import * as Updates from "expo-updates";
// Check if an update is available
const update = await Updates.checkForUpdateAsync();
if (update.isAvailable) {
await Updates.fetchUpdateAsync();
await Updates.reloadAsync(); // Restarts the app with new update
}
1つ知っておくべきこと: expo-updates は設計上、障害に強い。アプリがオフラインだったり更新サーバーに接続できなかったりしても、手元にある最新の更新を使い続ける。アプリがクラッシュすることはない。ネットワークが復旧するまで新しい更新が届かないだけだ。
クリティカルな更新と通常の更新を扱い分ける
緊急の修正と通常の更新の両方を配信しているなら、両者を区別する仕組みが必要だ。更新メッセージに [CRITICAL] の印があるかどうかを見るだけでは不十分だ。クリティカルな更新を配信したあとに通常の更新を配信すると、ユーザーがチェックしたときには最新の(通常の)更新しか見えない。
正しいやり方は、緊急度を更新そのものとは別に追跡することだ。Updates API Demo で、これを正しく実装する方法を示している。通常の更新が存在する状態でクリティカルな更新を確認する方法も含まれている。
注意点:
-
ループの中で何度もチェック・リロードしない。 フラグを1つ立てて、1セッションにつき1回だけチェックする。
-
強制更新のときはローディング表示を出す。 一瞬の「更新中……」画面のほうが、いきなり再起動されるよりましだ。
-
ネットワークでアプリの起動をブロックしない。 更新チェックがタイムアウトしても、ユーザーはアプリに入れるべきだ。
実際の事例:
MTA のモバイルチームは、日次アクティブ35万人が使う交通系の重要アプリを提供している。本番でバグが出れば、乗客や職員から90秒以内にフィードバックが届く。深刻な問題が起きたときは expo-updates API で修正を配信し、即座に効かせる。コールドスタートは一切不要だ。
「最初のユーザー報告から90秒以内に、問題を特定し、パッチを当て、OTA 修正をデプロイできます」と彼らは話してくれた。彼らの話を全文読む →
5. 段階的ロールアウトを使い、ロールバックできるようにする
欠けている安全弁: ステージング環境とテストがあっても、壊れた更新は本番に紛れ込む。問われるのは、影響範囲を制御して素早く復旧できるかどうかだ。
これが実務で効く理由
-
10% の段階的ロールアウトは「全障害」を「小規模なインシデント」に変える
-
ロールバックが速くなるのは、チームがコマンドに慣れていて、正しい ID をすぐ手元に用意している場合だけだ
段階的ロールアウト
まず小さく配信し、テレメトリを見てから徐々に広げる。

# Start with 10%
eas update --channel production --message "New checkout flow" --rollout-percentage 10
この更新の配信比率を上げる:
# First, get your update group ID from the dashboard or from the publish output
# Then increase the rollout
eas update:edit <update-group-id> --rollout-percentage 50
# Then to 100%
eas update:edit <update-group-id> --rollout-percentage 100
配信を止めて巻き戻す:
# Interactive rollback (CLI will prompt for channel and options)
eas update:rollback
ロールバック
問題が起きたら、前へ進める——修正版を出す——こともできるし、直前の更新にロールバックすることもできる。修正内容がはっきりしているなら、前へ進めるほうがたいてい速い:
# Fix the bug, publish new update
eas update --channel production --message "Fix crash in checkout"
ただし、すぐに戻す必要があるならロールバックコマンドを使う:
eas update:rollback
ロールバックの利点: ユーザーがすでに前のバージョンをインストールしていれば、追加の更新をダウンロードする必要はない。デバイスが前の更新をキャッシュしているので、ロールバックは速い。
よくある落とし穴: インシデントの最中に更新グループ ID が見つからない。Updates コンソールをブックマークに入れ、リリースノートに最後に正常だったグループ ID を記録しておく。
上級テクニック: 更新グループごとに更新の採用率とエラー率を追跡するモニタリングを組む。10% の段階で問題に気づきたい。100% になってからでは遅い。
今週やるならこの3つ
すべてを一度に導入する必要はない。ここから始めよう:
1. preview と production の2つのチャネルを用意する
-
2つのビルドを作る。1つは
previewを指し、もう1つはproductionを指す -
必ず preview に先に配信し、テストを通ってから production に配信する
-
これだけで本番インシデントのほとんどを防げる
2. fingerprint でネイティブコードの変更を検出する
-
設定で
"runtimeVersion": { "policy": "appVersion" }を指定する -
配信前に
eas fingerprint:compareでネイティブコードが変わっていないか確認する -
fingerprint が本番ビルドと一致していれば OTA で配信し、一致しなければ新しいビルドを作る。
3. 段階的ロールアウトをワークフローに組み込む
-
本番更新は毎回 10% の段階的ロールアウトから始める
-
30分観察してから 100% に広げる
-
直近の update group ID をいくつか手元に残し、ロールバックに備える
この3つを変えるだけで、デプロイは速く、安全になる。体に染みついたら、他のパターンを重ねていけばいい。
まとめ
OTA 更新の配信に最も自信を持っているチームは、おおむね同じパターンに従っている:
-
本番に配信する前に preview でテストする
-
fingerprint でネイティブの変更を検出し、app のバージョンを上げるべきタイミングを知る
-
何が更新できて何ができないかを把握している
-
更新のタイミングを細かく制御したいときは expo-updates API を使う
-
段階的ロールアウトを使い、ロールバックの方法を知っている
どのパターンも複雑なツールを必要としないし、プロセスを大きく変えるものでもない。小さな調整が積み重なって、本番インシデントが減り、イテレーションが速くなり、デプロイパイプラインへの信頼が高まる。
目指すのは完璧なデプロイではなく、復旧できるデプロイだ。問題が起きたとき(必ず起きる)、影響範囲を抑え、数時間ではなく数分で直す。それができる状態にしておく。
他のチームがどう OTA 更新でリリースのリズムを整えているか知りたいなら、デプロイパターンの完全ガイドを読んでほしい。これらのパターンが実際にどう働くか見たいなら、Hipcamp がどうやって月次リリースから日次リリースに移行したか、あるいは MTA がどうやって90秒で重要な交通情報の更新を配信したかを読んでみてほしい。