おそらく、アプリは動作しており、ユーザーもログイン済みで、プロダクトチームがネイティブな再エンゲージメントフロー(カートリマインダー、レビュープロンプト、新着メッセージ通知、リリース告知など)を求めている段階だと思います。最初の衝動は「とりあえずプッシュ通知を繋ぐ」ことですが、1週間後には、なぜ特定のデバイスだけが通知を受け取っているのか、シミュレータでは正常に登録されているのに、なぜタップで正しい画面が開かないのか、誰も説明できないデバッグに追われることになります。
Expoのプッシュ通知は、心地よいほどシンプルであるか、驚くほど脆いものであるかのどちらかです。
ExpoはAPNsとFCMの上に実用的なレイヤーを提供しており、多くのReact Nativeチームが使う理由もそこにあります。しかし、デモと本番対応の実装の間には確かな隔たりがあります。トークンのライフサイクル、権限を求めるタイミング、リスナー設定、ペイロード設計、バックエンドのクリーンアップはどれも重要です。アプリロジックの変更を頻繁にリリースしているなら、運用の徹底はさらに重要になります。特に、リテンション施策が信頼性の高いメッセージ配信とリリース速度に依存している場合はなおさらです。配信は、その周辺のユーザー体験が予測可能でなければ意味がありません。
目次
- Expoプッシュ通知でユーザーエンゲージメントを高める基盤
- プロジェクトの初期セットアップと設定
- 権限のリクエストとPushトークンの取得
- サーバーからの通知送信
- アプリ内での受信通知のハンドリング
- 本番環境でのベストプラクティスとよくある落とし穴
Expoプッシュ通知でユーザーエンゲージメントを高める基盤
一つの Expoプッシュ通知 セットアップが魅力的な理由は、何よりも一つだけあります。チームが初日から管理したくないネイティブメッセージングの複雑さを排除してくれるからです。まずAPNsやFCMの直接接続を構築する代わりに、Expoのゲートウェイを使って、プロダクトの動作、ルーティング、権限UX、バックエンドのメッセージロジックに集中できます。
この抽象化はプッシュを「現実的でない」ものにはしません。エンジニアリングの労力がどこに向かうかを変えるだけです。
このサービスは十分に高速で、パフォーマンスが最初に心配すべきことであることは通常ありません。2023年3月14日から2023年6月12日までのExpo push notification API は 中央値42ミリ秒の応答時間, p99レイテンシ273ミリ秒、および 平均日次エラー率 0.17% を 1日数千万件のメッセージ、出典: Knock の Expo Push API ベンチマーク分析これで、Expoはプロトタイプ専用なのかと気にしているチームも安心できるはずです。
Expoが実際に抽象化しているもの
チームが「Expo push」と言うとき、実際にはいくつかの別々の関心事がまとめられたものを指していることが多いです:
- プロバイダーのルーティング: Expoは以下のサービスへメッセージを転送します: APNs (iOS用)および FCM Android用
- トークンの形式: プラットフォーム固有のトークン管理を最初に行う代わりに、サーバー側でExpo Push Tokenを保存・送信します。
- リクエストの契約: ネイティブプロバイダーのAPIに直接統合するのではなく、ExpoのプッシュAPIにペイロードをPOSTします。
これは便利ですが、同時に一般的な誤解を生む原因にもなります。チームによっては、Expoがすべての配信問題を管理していると思い込んでいる場合があります。実際には、多くの失敗はアプリのcode、古いトークン、不正なペイロード、または不適切な権限フローに起因しています。
実践的なルール: Expoは信頼できるトランスポートレイヤーとして扱い、健全なクライアントおよびバックエンド設計の代替手段として扱わないでください。
本番対応の本当の意味
動作するデモは、単に1つのデバイスが1つのペイロードを一度受け入れただけであることを示すだけです。本番環境での準備完了とは、別のことを意味します:
| 課題 | デモ思考 | 本番思考 |
|---|---|---|
| 権限 | 今すぐ質問 | 価値が明確になったら文脈で質問 |
| トークン | 一度だけ保存 | 更新、重複排除、期限切れ、調整 |
| ペイロード | すべて入れる data |
ペイロードは小さく、行動に焦点を |
| アプリの動作 | アラートを表示する | 正しくナビゲーションし、フォアグラウンド状態を処理する |
| 運用 | 手動テスト | レシート、クリーンアップ、ログ、インシデント対応 |
「通知を送信できること」と「通知が実際のプロダクトワークフローを支えられること」には、大きな違いがあります。
プロジェクトの初期セットアップと設定
Expoのプッシュ通知におけるトラブルの多くは、最初の権限プロンプトが出る前に発生しています。プロジェクトの設定がずさんだと、クライアント側の code は正しく見えても、ビルドごとにアプリの挙動が不安定になることがあります。

まずは適切なライブラリをインストールし、ビルドパスに合った開発環境を整えましょう。Expo Goの枠を超えて開発するなら、リリースワークフローを次のガイドと揃えておくと役立ちます。 Capgo の Expo EAS 代替ガイド通知の挙動は、単なるサンドボックス実行よりも、本番環境に近いビルドで検証する必要があることが多いためです。
通知パッケージのインストール
最低限、通常は以下が必要です:
expo-notifications権限リクエスト、トークン取得、リスナー、通知の表示に使います。expo-deviceトークン取得は以下でガードすべきだからです:Device.isDevice.
一般的なインストールコマンドはパッケージマネージャーによりますが、重要なのは Expo SDK とのバージョン整合性です。任意のパッケージバージョンを混ぜないようにしましょう。互換性のあるバージョンは Expo に解決させてください。
プロジェクトレベルの設定を追加
設定は明示的に保ちましょう。最小限の app.json または app.config.js 設定は、通知がアプリの契約の一部であり、後付けではないという事実を反映すべきです。
{
"expo": {
"name": "MyApp",
"slug": "my-app",
"plugins": ["expo-notifications"],
"ios": {
"bundleIdentifier": "com.example.myapp"
},
"android": {
"package": "com.example.myapp"
},
"extra": {
"eas": {
"projectId": "your-project-id"
}
}
}
}
ここでのいくつかの詳細が重要です:
- Bundle identifier とパッケージ名 は、実際にリリースするアプリと一致させる必要があります。
- 通知プラグイン ビルド時にネイティブプロジェクトに必要な設定が行われることを保証します。
- EAS プロジェクト ID トークンの取得が、アプリが正しい Expo プロジェクトに関連付けられていることを期待する場合に重要です。
通知ハンドラーは早めに設定する
多くの基本的なチュートリアルは、通知の動作定義を遅くまで先延ばしにしがちです。そうしないでください。アプリの起動直後に設定し、フォアグラウンドでの動作を予測可能にしてください。
import * as Notifications from 'expo-notifications';
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: false,
shouldSetBadge: true,
}),
});
フォアグラウンド通知でアラートを表示するか、サウンドを再生するか、バッジに影響を与えるかはチームが決定します。具体的な動作はプロダクトによって異なります。チャットアプリと決済アプリでは、同じ選択はしないでしょう。
フォアグラウンドの動作を意図的に定義しないと、実際には受信していたがプロダクトが期待する形で表示されなかったため「欠落」と見なされた通知のデバッグにチームが追われることになります。
Androidにはチャンネル設定が必要
Android の通知チャネルは、実質的に省略できません。スキップすると、アラートが一貫性を欠いたり、ユーザーの期待と一致しなかったりすることがあります。
import { Platform } from 'react-native';
import * as Notifications from 'expo-notifications';
export async function configureAndroidNotifications() {
if (Platform.OS !== 'android') return;
await Notifications.setNotificationChannelAsync('default', {
name: 'Default',
importance: Notifications.AndroidImportance.MAX,
});
}
アプリの初期化時にこれを設定してください。その後、チャネル ID は安定させてください。安易に変更すると、後で通知の動作を推論しにくくなります。
権限のリクエストとPushトークンの取得
多くのチームがスニペットからコピーし、後で後悔するのがこの部分です。
権限リクエストにはタイミング、プラットフォームごとの考慮、規律ある非同期処理が必要です。トークン取得は実機でのみ、権限解決後のみ、そして結果をすぐにバックエンドへ保存できる場合にのみ行ってください。

ベースラインとなるクライアント関数
次のような関数を出発点にしてください:
import * as Device from 'expo-device';
import * as Notifications from 'expo-notifications';
import Constants from 'expo-constants';
import { Platform } from 'react-native';
type RegisterResult =
| { ok: true; token: string }
| { ok: false; reason: string };
export async function registerForExpoPushNotificationsAsync(): Promise<RegisterResult> {
if (!Device.isDevice) {
return { ok: false, reason: 'Push notifications require a physical device.' };
}
if (Platform.OS === 'android') {
await Notifications.setNotificationChannelAsync('default', {
name: 'Default',
importance: Notifications.AndroidImportance.MAX,
});
}
const permissions = await Notifications.getPermissionsAsync();
let finalStatus = permissions.status;
if (finalStatus !== 'granted') {
const request = await Notifications.requestPermissionsAsync();
finalStatus = request.status;
}
if (finalStatus !== 'granted') {
return { ok: false, reason: 'Notification permission was not granted.' };
}
const projectId =
Constants.expoConfig?.extra?.eas?.projectId ??
Constants.easConfig?.projectId;
if (!projectId) {
return { ok: false, reason: 'Missing EAS project ID configuration.' };
}
const tokenResponse = await Notifications.getExpoPushTokenAsync({ projectId });
return { ok: true, token: tokenResponse.data };
}
順序が重要です。まずデバイスタイプを確認し、Androidのチャンネル動作を設定し、権限を解決し、プロジェクト設定を検証してから、Expoトークンをリクエストします。
理由 Device.isDevice は必須です
これは、無害に見えて多くのノイズを生む数少ない間違いの一つです。熟練したチームは、次の場合にのみ条件付きで権限をリクエストします Device.isDevice が true の場合、よくある落とし穴はこのガードをスキップすることです。これにより、開発者が無効なシミュレータートークンに通知を送信し、問題がアプリの構成にあるにもかかわらずExpoのせいにすることになります。詳細は EagerworksのExpo通知実装メモ.
だからこそ、チェックは関数の先頭に置きます。ヘルパーに隠さないでください。明確に示しましょう。
シミュレータの結果はUIテストには有用ですが、プッシュトークンの登録を検証するには信頼できません。
AIに聞く
スプラッシュ画面で確認しないでください。ユーザーが価値を理解する前に確認しないでください。最適なタイミングは、通知の利点が具体的になるユーザーアクションの後です。例えば、配信更新を有効にしたり、会話に参加したり、ウォッチリストに追加したりした直後などです。
良い実装は、通常このフローに従います:
- ユーザーが意味のある機能の区切りに到達します。
- アプリ独自のUIで、通知の価値を説明します。
- アプリがシステム権限をリクエストします。
- 権限が許可されたら、即座にトークンをバックエンドに保存します。
多くのアプリが失敗するのは、この最後のステップです。トークンを取得してローカルにログを残すだけで、バックエンドへの登録を後回しにしてしまいます。これでは、後からサポート担当者が「どのデバイスが、いつ、どのトークンを持っていたか」を特定できなくなります。
登録後にトークンを保存する簡単な例は以下の通りです:
export async function enablePushForCurrentUser(userId: string) {
const result = await registerForExpoPushNotificationsAsync();
if (!result.ok) {
return result;
}
await fetch('https://api.example.com/push-tokens', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer user-session-token',
},
body: JSON.stringify({
userId,
token: result.token,
platform: Platform.OS,
}),
});
return result;
}
ワークフローの後半では、このウォークスルーが視覚的なリファレンスとして役立ちます:
リリースの多いアプリを開発するチームにとって、トークン登録は単なるオンボーディングの一部ではなく、アプリの運用状態(operational state)の一部として考えるのが有効です。この考え方は、より広範な Expoアプリのデリバリーワークフローにおいて、アプリの挙動が頻繁に変化し、バックエンドの状態を同期し続ける必要がある場面によく当てはまります。
サーバーからの通知送信
バックエンドに有効なExpo Push Tokenがあれば、通知の送信は簡単です。難しいのはリクエストそのものではなく、ペイロードに何を含めるか、そしてクライアントの状態をどこまで信頼するかという判断です。
以下は、Node スタイルを使った最小限の例です。 fetch:
type ExpoPushMessage = {
to: string;
title: string;
body: string;
sound?: 'default' | null;
data?: Record<string, unknown>;
};
export async function sendExpoPushNotification(token: string) {
const message: ExpoPushMessage = {
to: token,
title: 'New review received',
body: 'Tap to open the order details.',
sound: 'default',
data: {
type: 'new_review',
orderId: 'ord_123',
screen: 'OrderDetails',
},
};
const response = await fetch('https://exp.host/--/api/v2/push/send', {
method: 'POST',
headers: {
Accept: 'application/json',
'Accept-encoding': 'gzip, deflate',
'Content-Type': 'application/json',
},
body: JSON.stringify(message),
});
const result = await response.json();
return result;
}
各ペイロードフィールドの役割
ペイロードを何でも放り込む場所にしないでください。各フィールドには意図を持たせましょう。
| 項目 | 目的 | 実践的なアドバイス |
|---|---|---|
to |
対象の Expo Push Token | 現在のデバイスレコードに属していることを検証します |
title |
通知の見出し | 短く、人間が読めるようにしてください |
body |
メインの表示テキスト | アクションを明確にしてください |
sound |
システムサウンドの挙動 | 高価値なアラートにのみ、控えめに使用してください |
data |
アプリ固有のメタデータ | リッチコンテンツよりIDやルートヒントを優先してください |
この data object はプロダクトのワークフローで役立ちます。type とレコードIDを渡し、ユーザーがタップした際にアプリが最新データを取得するようにします。これは、大きなデータや機密データをペイロードに直接埋め込むよりも安全です。
ペイロードは軽量かつシンプルに保つ
によると、 Expo通知のCourier向けガイド、Expo Push Tokenは一時的なものとして扱うべきで、サイズ制限(約 4 KB )を超えるペイロードは破棄される可能性があるため、信頼できるパターンは、 { "type": "new_review", "id": 123 } 巨大なJSONやインラインメディアではなく。このアドバイスは実際のシステムでうまくいく方法と一致しています。小さなペイロードは失敗が少なく、アプリのロジックが変わっても長持ちします。
ユーザーのルーティングに必要な分だけ送信し、残りはアプリ起動後に取得します。
サーバー側で役立つ習慣
テスト用ならシンプルな送信関数で十分ですが、本番用では通常いくつか責務が増えます:
- 送信試行を永続化: 通知の意図をユーザーID、トークン、ペイロードタイプ、タイムスタンプとともに保存します。
- コンテンツ生成と転送を分離: メッセージ本文の組み立てと、Expo API リクエストを別レイヤーに分けます。
- 無効化フィードバックを処理: Expoが後から報告した場合、
DeviceNotRegisteredそのトークンを stale としてマークし、やみくもなリトライを止めます。 - Webhookに適した設計: 既存のシステムですでにイベントを発行している場合は、通知トリガーを、他で使っているのと同じ バックエンドのwebhook処理パターン に通してください。
クライアント側のリスナーをデバッグする前に、まず手動でテストプッシュを送信しましょう。トークンが小さなペイロードの通常の通知を受信できれば、配信経路はおそらく正常です。受信できない場合でも、いきなりナビゲーションの code を変更するのではなく、まずトークン、ペイロードの形式、権限の状態を検証してください。
アプリ内での受信通知のハンドリング
配信は機能の半分に過ぎません。通知が届いたとき、そしてユーザーがそれをタップしたときに、アプリが一貫した動作をする必要があります。
つまり、以下の2つの異なるタイミングを扱う必要があります:
- アプリを開いているときに通知が届いたとき
- ユーザーがシステムトレイやロック画面から通知を操作したとき

フォアグラウンドでの受信とユーザーの応答は、それぞれ別のイベントです
信頼性の高いセットアップには、通常両方のリスナーが含まれます:
import { useEffect } from 'react';
import * as Notifications from 'expo-notifications';
export function useNotificationObservers(
onForegroundMessage: (notification: Notifications.Notification) => void,
onNotificationTap: (response: Notifications.NotificationResponse) => void
) {
useEffect(() => {
const receivedSub = Notifications.addNotificationReceivedListener(
(notification) => {
onForegroundMessage(notification);
}
);
const responseSub = Notifications.addNotificationResponseReceivedListener(
(response) => {
onNotificationTap(response);
}
);
return () => {
receivedSub.remove();
responseSub.remove();
};
}, [onForegroundMessage, onNotificationTap]);
}
addNotificationReceivedListener アプリがアクティブなときに実行されます。 addNotificationResponseReceivedListener ユーザーが届いた通知をタップしたときに実行されます。これらは混同しないでください。それぞれ異なるUX導線を担います。
データペイロードを読み取り、意図した画面へ遷移させる
タップ時のハンドリングにおける実践的なパターンは以下の通りです:
type NotificationData = {
type?: string;
orderId?: string;
screen?: string;
};
export function handleNotificationTap(
response: Notifications.NotificationResponse,
navigation: any
) {
const data =
response.notification.request.content.data as NotificationData;
if (data.screen === 'OrderDetails' && data.orderId) {
navigation.navigate('OrderDetails', { orderId: data.orderId });
return;
}
if (data.type === 'new_review') {
navigation.navigate('Inbox');
return;
}
navigation.navigate('Home');
}
このパターンは、ペイロードにドキュメント全体ではなくルーティングヒントを含めることで堅牢性を保ちます。通知の送信後に注文内容が変わっていても、遷移後にアプリが最新のサーバー状態を取得できます。
通知をタップしたら、明確な1つの遷移先に導くべきです。フォールバックの遷移先が曖昧だと、ユーザーはすぐに違和感を覚えます。
フォアグラウンドでの挙動はユーザーのコンテキストに合わせる
アプリがすでに開いているときに、何も考えずにシステム風のアラートを表示すると不格好に感じられます。アプリ内バナー、バッジ更新、サイレントリフレッシュの方が適切なこともあります。ユーザーがすでにその会話を読んでいるなら、サポート受信箱画面では目に見えるアラートは不要かもしれません。
そのため、フォアグラウンドのリスナーはルートと通知タイプによって処理を分岐させるべきです。例:
- チャット画面が開いている場合: メッセージを追加し、重複するバナーの表示を避ける
- ダッシュボードが開いている場合: 軽量なアプリ内トーストを表示する
- 重要なアカウントイベント: より強いUI処理を表示する
シンプルなアプローチは次のようになります:
export function handleForegroundNotification(
notification: Notifications.Notification,
currentRouteName: string
) {
const data = notification.request.content.data as { type?: string };
if (currentRouteName === 'ChatThread' && data.type === 'new_message') {
// refresh local thread state
return;
}
// otherwise show your own in-app UI or update badges
}
アプリがこれらの文脈を区別しない場合、配信が技術的に正しくても、ユーザーはより早く通知疲れを感じます。
本番環境でのベストプラクティスとよくある落とし穴
壊れたExpoプッシュ通知のセットアップの多くは、Expoが制限的だからではなく、チームがトークンが永続的だと仮定し、ペイロードに何でも含められると信じ、アプリの更新が通知ロジックに影響しないと考えるからです。
その前提は本番環境では通用しません。

トークンは一時的なものであり、ID レコードではありません
Expo Push Token は、一生続くデバイス識別子ではなく、リース(貸与)として扱うのが最適です。再インストール、OS の変更、その他のライフサイクルイベント後にトークンはローテーションされることがあります。トークンが最終的に返却された場合 DeviceNotRegistered、バックエンドはそれをアクティブとして扱わなくなります。
実用的なバックエンドモデルは以下を保存します:
- ユーザー ID
- プラットフォーム
- インストールスコープのメタデータ
- 現在のトークン
- 最終確認タイムスタンプ
- active、stale、revoked などのステータス
ユーザーテーブルにトークンフィールドを1つだけ保存して終わり、とはしないでください。ユーザーは複数のデバイスを持ち、デバイスの状態も変化します。
リフレッシュ戦略は、多くのチュートリアルが認める以上に重要です
公式のエコシステムガイダンスには、ここでの実際の運用上のギャップが残っています。既存のExpoプッシュ通知に関するコンテンツは、トークンの有効性をどのように維持するかを説明していないことが多く、 App StoreのレビューサイクルやOTAアップデート、特にライブ変更をリリースするチームにとって重要です。プッシュの信頼性は、現在のトークン状態とバックエンドの同期に依存するためです。これは Expo通知ドキュメント.
で述べられています。これはリフレッシュトリガーの設計に影響します。トークン状態を調整する適切なタイミングには以下が含まれます:
- アップデート後のアプリ起動時
- ユーザーのサインイン時
- 権限設定の変更時
- リリースプロセスでの認証情報のローテーション作業
- プッシュ関連のサポートチケット後のリカバリーフロー
セキュリティとコンプライアンスはスプリントの最後に回すべきではない
多くのExpoチュートリアルは仕組みに焦点を当て、運用上のリスクを無視しがちです。趣味のアプリなら問題ありません。しかし、ヘルスケア関連、フィンテック、または規制対象のEC製品では許されません。
CourierによるExpoの通知機能の欠陥に関するエンタープライズ向け議論 同意ログ、監査証跡、機密ペイロードの露出最小化に関する実践的なガイダンスの不足を指摘しています。エンジニアリング上の直接的な教訓はシンプルです:
- 機密のビジネスデータを通知テキストやペイロードメタデータに含めないでください。
- 同意の変更をサーバーサイドでログに記録してください。
- どの通知意図がどのトークンに送信されたかを記録してください。
- ペイロードにはIDを使用し、アプリ起動後に保護されたコンテンツを取得してください。
リリース運用をより広範な App StoreのコンプライアンスやAPIのセキュリティプラクティスと整合させるチームには、プッシュ通知は認証、アナリティクス、バックエンドイベントログと同じレビュー規律の対象に含めるべきです。
プッシュ通知はユーザー向けのメッセージですが、分散システムの問題でもあります。認証状態や決済イベントに適用するのと同じ慎重さで扱ってください。
通常動作するものと、通常壊れるもの
| 通常動作するもの | 通常壊れるもの |
|---|---|
| 明確な価値説明の後に権限をリクエストする | 初回フレームでプロンプトを表示する |
| 実機でのテスト | シミュレーターの登録を信頼する |
| デバイスコンテキスト付きでトークンを保存する | ユーザーレコードごとに1つのトークン |
| 小さなメタデータペイロードを送信する | 大きなBlobや機密性の高いBlobを埋め込む |
| フォアグラウンドイベントとタップイベントを個別に処理する | すべての通知が同じパスを通ると想定する |
| 古いトークンを積極的に失効させる | 無効なトークンへのリトライを永久に繰り返す |
堅実なExpoプッシュ通知の設定は複雑ではありません。必要なのは規律です。
頻繁にアプリロジックの変更をリリースし、リリース時の挙動、ロールバック、配信状況の可視性をより細かく制御したいチームには、 Capgo 一見の価値があります。ストアの審査を待たずにモバイルチームが迅速にアップデートを配信できるため、通知フロー、ルーティングロジック、クライアントサイドの修正を素早くユーザーに届けたい場合に特に役立ちます。