こんにちは!モバイルアプリ開発、特にReact Nativeの日々のビルドやデバッグ、本当にお疲れ様です。
「なんだか突然アプリがフリーズして落ちるけれど、ログには何も残っていない……これってJavaScript側のエラー? それともiOS/Androidのネイティブ側のクラッシュ?」
React Nativeで開発をしていると、この「境界線のない闇」に頭を抱えた夜が一度や二度ではないはずです。JSの非同期処理で起きたエラーがネイティブ側を巻き込んでアプリ全体を沈没させたり、逆にネイティブモジュールの不具合がJSスレッドを巻き込んだり。
今回は、モバイルアプリの安定稼働において最強の守護神である「Firebase Crashlytics」を使い、このJS層とネイティブ層の混沌としたエラーを完璧に切り分け、一元管理するための極意を伝授します。
これをマスターすれば、「原因不明のクラッシュ」に怯える日々から解放され、スマートにバグを駆逐できるようになりますよ。それでは、一緒に見ていきましょう!
—
1. なぜReact Nativeのクラッシュ解析は難しいのか?
まず敵を知ることから始めましょう。React Nativeは、いわば「2つの世界」が同居する特異な環境です。
1. JavaScriptの世界(JSI / Hermes / メインJSスレッド)
- あなたが普段書いているTypeScript/JavaScriptが動く世界。
2. ネイティブの世界(iOS: Swift/Objective-C, Android: Kotlin/Java)
- OSの機能やカメラ、ストレージなどを直接叩く、あるいはReact Nativeの基盤が動く世界。
Crashlyticsは本来、ネイティブアプリ(iOS/Android)のために生まれたツールです。そのため、何も考えずに導入すると、JS側で起きたエラーは「よくわからないネイティブのクラッシュ(例: `SIGABRT` や `EXC_BAD_ACCESS`)」として片付けられてしまいます。これでは、どのコンポーネントの何のコードが原因で落ちたのか、全く見当がつきませんよね。
だからこそ、「JSのエラーを綺麗にキャッチし、ネイティブのCrashlyticsへと文脈(スタックトレースやカスタムログ)を添えて引き渡すブリッジ」が必要なのです。
—
2. 基本セットアップ:CrashlyticsをReact Nativeに迎え撃つ
まずは、プロジェクトの土台を作ります。ここでは、現在主流である Firebase JS SDK(または `@react-native-firebase/app`) をベースに解説します。
ステップ①: ライブラリのインストール
ターミナルを開き、プロジェクトのルートで以下のコマンドを実行します。
FirebaseのコアとCrashlyticsモジュールをインストール
yarn add @react-native-firebase/app @react-native-firebase/crashlytics
iOSの場合はCocoaPodsのインストールを忘れずに
cd ios && pod install && cd ..
> 先輩のアドバイス: React Native Firebaseを使う際は、iOS側で必ず `GoogleService-Info.plist` を、Android側で `google-services.json` をそれぞれのネイティブプロジェクトの適切な位置に配置してください。ここをミスるとビルドで盛大にコケます。
ステップ②: 自動クラッシュ収集の有効化確認
Crashlyticsはデフォルトで、アプリ起動時の未処理クラッシュを自動収集します。
アプリのエントリーポイント(多くは `App.tsx` や `index.js`)で、モジュールが正しく読み込まれているか確認しましょう。
import ‘@react-native-firebase/app’;
import crashlytics from ‘@react-native-firebase/crashlytics’;
// 例: アプリ起動時にCrashlyticsが有効かチェックするコード
const initializeCrashlytics = async () => {
await crashlytics().setCrashlyticsCollectionEnabled(true);
console.log(‘🔥 Crashlytics is ready to guard your app!’);
};
—
3. 【核心】JSエラーとネイティブクラッシュを美しく切り分けるブリッジ設定
ここからが本題です。JavaScript層で発生した未処理の例外(Uncaught Exception)や、Promiseの拒否(Unhandled Rejection)をキャッチし、Crashlyticsに「JSのエラーですよ」と明確に伝えて送信する仕組みを作ります。
実装:グローバルエラーハンドラーの構築
React Nativeには、グローバルなエラーをフックする仕組みが備わっています。これを利用して、JSのエラーをCrashlyticsの `recordError` メソッドに流し込みましょう。
プロジェクトの根幹(例: `src/services/errorReporter.ts` や `App.tsx` の初期化部分)に以下のようなコードを記述します。
import crashlytics from ‘@react-native-firebase/crashlytics’;
import { Platform } from ‘react-native’;
/
- JS層で発生したエラーをCrashlyticsに集約するための設定
/
export const setupGlobalErrorHandling = () => {
// 1. ErrorUtils(React Nativeのグローバルエラーハンドラー)をフック
const defaultGlobalHandler = ErrorUtils.getGlobalHandler();
ErrorUtils.setGlobalHandler((error: Error, isFatal?: boolean) => {
// 開発環境では通常通りコンソールにも流す
console.error(‘Captured JS Error:’, error);
// Crashlyticsへエラーと致命的フラグを記録
crashlytics().recordError(error);
// カスタム属性でJS層でのエラーであることを明示
crashlytics().setAttribute(‘error_origin’, ‘javascript’);
crashlytics().setAttribute(‘is_fatal’, String(isFatal));
// デフォルトの挙動(アプリを落とすなど)を維持するために呼び出す
if (defaultGlobalHandler) {
defaultGlobalHandler(error, isFatal);
}
});
// 2. PromiseのUncaught Rejection(非同期処理のエラー)も拾う
// ※ 近年のReact Native/Hermes環境ではグローバルでハンドリング可能です
const tracking = require(‘promise/setimmediate/rejection-tracking’);
tracking.enable({
allRejections: true,
onUnhandled: (id: string, error: Error) => {
console.warn(‘Unhandled Promise Rejection:’, error);
crashlytics().recordError(error);
crashlytics().setAttribute(‘error_origin’, ‘javascript_promise’);
},
});
};
これをアプリの起動最上流(`index.js` など)で呼び出します。
import { AppRegistry } from ‘react-native’;
import App from ‘./App’;
import { name as appName } from ‘./app.json’;
import { setupGlobalErrorHandling } from ‘./src/services/errorReporter’;
// アプリ起動と同時にエラーハンドリングをスタンバイ
setupGlobalErrorHandling();
AppRegistry.registerComponent(appName, () => App);
—
4. 精度高い「Hello World」動作確認(テストクラッシュの実行)
正しく設定できているか、実際にエラーとクラッシュを起こして確認してみましょう。「意図的に壊す」ことは、オブザーバビリティにおいて最も確実なテスト手法です。
ボタンを押したときに、それぞれ異なるレイヤーでエラーが発生するコンポーネントを作ってみます。
import React from ‘react’;
import { StyleSheet, Text, TouchableOpacity, View } from ‘react-native’;
import crashlytics from ‘@react-native-firebase/crashlytics’;
export default function CrashTestScreen() {
// パターンA: JS層での通常のエラー(例外を投げる)
const triggerJsError = () => {
throw new Error(‘💥 テストだ!JavaScript層からの意図的なエラーです’);
};
// パターンB: ネイティブ層のハードクラッシュ(Crashlyticsのネイティブ強制終了機能を使用)
const triggerNativeCrash = () => {
console.log(‘🔥 ネイティブクラッシュを発生させます…’);
crashlytics().crash(); // これによりネイティブ側からアプリが強制終了されます
};
return (
);
}
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: ‘center’, alignItems: ‘center’, backgroundColor: ‘#f5f6fa’ },
title: { fontSize: 18, fontWeight: ‘bold’, marginBottom: 24, color: ‘#333’ },
button: { paddingVertical: 12, paddingHorizontal: 24, borderRadius: 8, marginVertical: 8 },
buttonText: { color: ‘#fff’, fontSize: 16, fontWeight: ‘600’ },
});
Firebaseコンソールでの確認ポイント
1. 「1. JSエラーを送信する」を押した場合:
- FirebaseコンソールのCrashlyticsダッシュボードに、「Non-fatal(非致命的)」エラーとして記録されます。
- スタックトレースにTypeScript/JavaScriptのファイル名や行数が表示されていることを確認してください(ソースマップが正しく適用されていれば、コンパイル前の綺麗なコード位置まで特定できます)。
- 先ほど仕込んだカスタム属性 `error_origin: javascript` が付いているため、ネイティブエラーと一発で区別できます。
2. 「2. ネイティブクラッシュを起こす」を押した場合:
- アプリが即座に落ちます。
- 再度アプリを起動すると(Crashlyticsは再起動時にレポートを送信します)、ダッシュボードに「Fatal」なクラッシュとして着弾します。
—
5. 現場で役立つプロの技:ユーザーの文脈を残す
クラッシュログを見たときに、「誰の、どんな操作のあとに起きたか」が分からないと、デバッグは難航します。Crashlyticsには、クラッシュする直前の「文脈」を記録する機能があります。
以下のように、画面遷移やユーザーIDをこまめに記録しておきましょう。
// ユーザーがログインした時
await crashlytics().setUserId(user.id);
// 画面が切り替わった時や重要なアクションの時
await crashlytics().log(`User tapped checkout button on cart screen`);
これらがクラッシュレポートと一緒にタイムライン形式で記録されるため、「ユーザーがカート画面でチェックアウトボタンを押した直後にネイティブのメモリ不足で落ちた」といったストーリーが手に取るようにわかるようになります。
—
まとめ
いかがでしたでしょうか?
React NativeにおけるCrashlyticsの導入と切り分けは、一見すると複雑に思えますが、「JSのエラーは `recordError` でキャッチし、カスタム属性でレイヤーを明示する」という原則 さえ押さえてしまえば、非常にシンプルかつ強力な武器になります。
これをマスターすれば、もう「原因不明のクラッシュにおびえる夜」とはおさらばです。プロダクトの品質をぐっと引き上げ、ユーザーに最高の体験を届けるために、ぜひ今日からあなたのプロジェクトに取り入れてみてくださいね。あなたの開発ライフがより快適でスリリングなものになりますように!