01. Firebaseでユーザー認証とクラウド保存を実装する
基本のMVPアプリをリリースした次のステージとして、多くのユーザーから最も要望されるのが「端末を変えてもデータを引き継ぎたい」「タブレットや別のスマホでも同じデータを共有したい」というクラウド同期機能です。
端末内保存(AsyncStorage)からクラウドデータベース(Cloud Firestore)と認証(Firebase Authentication)へステップアップすることで、アプリの利便性と信頼性は劇的に向上します。
この記事では、ExpoアプリにFirebaseを導入し、ユーザーごとの安全なログイン、既存ローカルデータのクラウドマイグレーション、そしてリアルタイムなデータ同期を組み込む実践手順を詳しく解説します。
この記事のサブコンテンツ
Section titled “この記事のサブコンテンツ”- ローカル保存からクラウド連携へステップアップするメリット
- ExpoへのFirebase SDK導入と初期化設定
- Firebase Authを使った手軽なサインイン実装(匿名認証と段階的連携)
- Cloud Firestoreでのデータモデル設計とリアルタイム同期
- 既存AsyncStorageデータからFirestoreへの自動マイグレーション
- 他人のデータを守るFirestoreセキュリティルールの必須設定
- まとめと次のステップ
1. ローカル保存からクラウド連携へステップアップするメリット
Section titled “1. ローカル保存からクラウド連携へステップアップするメリット”AsyncStorageによる完全ローカル管理は手軽ですが、以下の課題があります。
- 機種変更時やスマホ故障・紛失時にデータが二度と復旧できない
- iPhoneとiPad、あるいはPCなど複数端末でのシームレスな同期ができない
- クラウドバックアップがないため、利用継続の妨げになることがある(LTVは継続率そのものではなく、顧客生涯価値)
Firebaseを導入することで、自前でサーバーを用意することなく、Googleの堅牢なクラウドインフラ上でユーザー認証と自動同期を実現できます。
2. ExpoへのFirebase SDK導入と初期化設定
Section titled “2. ExpoへのFirebase SDK導入と初期化設定”Firebaseの公式JavaScript SDKをインストールします。
npx expo install firebase @react-native-async-storage/async-storageFirebaseコンソールでWebアプリを登録して設定値を取得します。ここではFirebase JS SDKを使います。ネイティブ向けのAnalytics等とは設定が異なります。ExpoのFirebase導入ガイドで互換バージョンを確認してください。
プロジェクト内に lib/firebase.ts を作成し、設定値を環境変数(.env)から読み込みます。EXPO_PUBLIC_ の値は配布アプリから読めるため、管理者の秘密鍵を入れないでください。
import { initializeApp, getApps, getApp } from 'firebase/app';import { initializeAuth, getReactNativePersistence } from 'firebase/auth';import { getFirestore } from 'firebase/firestore';import AsyncStorage from '@react-native-async-storage/async-storage';
const firebaseConfig = { apiKey: process.env.EXPO_PUBLIC_FIREBASE_API_KEY, authDomain: process.env.EXPO_PUBLIC_FIREBASE_AUTH_DOMAIN, projectId: process.env.EXPO_PUBLIC_FIREBASE_PROJECT_ID, storageBucket: process.env.EXPO_PUBLIC_FIREBASE_STORAGE_BUCKET, messagingSenderId: process.env.EXPO_PUBLIC_FIREBASE_MESSAGING_SENDER_ID, appId: process.env.EXPO_PUBLIC_FIREBASE_APP_ID,};
const app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApp();
// React Native用の永続化ストレージを設定const auth = initializeAuth(app, { persistence: getReactNativePersistence(AsyncStorage),});
const db = getFirestore(app);
export { app, auth, db };3. Firebase Authを使った手軽なサインイン実装(匿名認証と段階的連携)
Section titled “3. Firebase Authを使った手軽なサインイン実装(匿名認証と段階的連携)”いきなりメールアドレスやパスワードを入力させると、ユーザーの離脱を招きます。
おすすめは、「最初は匿名ログイン(ユーザー登録不要ですぐ開始) → 必要な時にGoogle/Appleアカウントに連携」 という段階的認証(プログレッシブサインイン)です。Firebaseコンソールで匿名認証を有効にしてください。
匿名アカウントだけでは、再インストールや別端末で同じUIDを復元できません。バックアップ用途では、現在の匿名ユーザーに linkWithCredential などで認証プロバイダを連携し、UIDを維持します。別アカウントへ単にサインインし直すと、元のUID配下のデータは自動では引き継がれません。
以下のフックは認証状態の監視例です。アプリのProviderで1回だけ呼び、各画面へ共有してください。ログアウトやアカウント切り替えを設ける場合は、自動匿名ログインの再実行と古いユーザーデータの表示を制御する状態も追加します。
import { useState, useEffect } from 'react';import { signInAnonymously, onAuthStateChanged, User } from 'firebase/auth';import { auth } from '../lib/firebase';
export function useAuth() { const [user, setUser] = useState<User | null>(null); const [loading, setLoading] = useState(true);
useEffect(() => { const unsubscribe = onAuthStateChanged(auth, async (currentUser) => { if (currentUser) { setUser(currentUser); } else { // 未ログイン時は自動で匿名アカウントを発行 try { const anon = await signInAnonymously(auth); setUser(anon.user); } catch (error) { console.error('Anonymous auth failed', error); } } setLoading(false); }); return unsubscribe; }, []);
return { user, loading };}4. Cloud Firestoreでのデータモデル設計とリアルタイム同期
Section titled “4. Cloud Firestoreでのデータモデル設計とリアルタイム同期”Firestoreでは、users/{userId}/habits/{habitId} という階層構造でデータを管理するのが王道です。
// データの書き込み例import { collection, addDoc, serverTimestamp } from 'firebase/firestore';import { db } from '../lib/firebase';
export async function addHabitToCloud(userId: string, title: string) { const habitsRef = collection(db, 'users', userId, 'habits'); await addDoc(habitsRef, { title, streakDays: 0, isCompletedToday: false, createdAt: serverTimestamp(), });}onSnapshot リスナーを使えば、別の端末でチェックを入れた瞬間に、手元の端末の画面もリアルタイムに更新されます。
5. 既存AsyncStorageデータからFirestoreへの自動マイグレーション
Section titled “5. 既存AsyncStorageデータからFirestoreへの自動マイグレーション”これまでローカルに保存していたユーザーのデータを失わないよう、初めてクラウドログインした際に自動移行する処理を入れます。
移行は本人が同意して開始し、認証確定後に1つの処理だけを動かします。途中で通信が切れても、再実行でデータが増殖したり、移行済みのクラウドデータを古い値で上書きしたりしないようにします。
import AsyncStorage from '@react-native-async-storage/async-storage';import { doc, runTransaction, serverTimestamp } from 'firebase/firestore';import { auth, db } from './firebase';
type LocalHabit = { id: string; title: string; streakDays: number; isCompletedToday: boolean; lastCompletedDate?: string;};
function isLocalHabit(value: unknown): value is LocalHabit { if (!value || typeof value !== 'object') return false; const h = value as Record<string, unknown>; return typeof h.id === 'string' && h.id.length > 0 && h.id.length <= 120 && typeof h.title === 'string' && h.title.length > 0 && h.title.length <= 100 && typeof h.streakDays === 'number' && Number.isInteger(h.streakDays) && h.streakDays >= 0 && typeof h.isCompletedToday === 'boolean' && (h.lastCompletedDate === undefined || (typeof h.lastCompletedDate === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(h.lastCompletedDate)));}
export async function migrateLocalDataToCloud(userId: string) { if (auth.currentUser?.uid !== userId) throw new Error('認証状態を確認してください。'); const sourceKey = '@habit_flow_data'; const ownerKey = '@habit_flow_migration_owner'; const doneKey = `@habit_flow_migrated:${userId}`; if (await AsyncStorage.getItem(doneKey)) return;
const raw = await AsyncStorage.getItem(sourceKey); if (!raw) return; const parsed: unknown = JSON.parse(raw); if (!Array.isArray(parsed) || !parsed.every(isLocalHabit)) { throw new Error('移行元データの形式を確認してください。'); } const habits: LocalHabit[] = parsed; if (new Set(habits.map((h) => h.id)).size !== habits.length) { throw new Error('習慣IDが重複しています。移行前に確認してください。'); }
// 同じ端末のデータを別アカウントへ無断で再取り込みしない const owner = await AsyncStorage.getItem(ownerKey); if (owner && owner !== userId) throw new Error('元のアカウントで移行を再開してください。'); await AsyncStorage.setItem(ownerKey, userId);
for (const habit of habits) { if (auth.currentUser?.uid !== userId) throw new Error('アカウントが切り替わりました。'); const ref = doc(db, 'users', userId, 'habits', `local_${encodeURIComponent(habit.id)}`); await runTransaction(db, async (transaction) => { const existing = await transaction.get(ref); if (existing.exists()) return; // 再試行で既存のクラウド記録を上書きしない transaction.set(ref, { title: habit.title, streakDays: habit.streakDays, isCompletedToday: habit.isCompletedToday, ...(habit.lastCompletedDate ? { lastCompletedDate: habit.lastCompletedDate } : {}), migratedAt: serverTimestamp(), }); }); } await AsyncStorage.setItem(doneKey, 'true');}トランザクションはオンラインで実行します。例は小規模なMVPの初回移行向けです。移行中は編集・アカウント切り替えを止め、成功後はクラウドを正本に切り替えてください。ローカル原本は確認用に保持し、ログアウト時のデータ分離や削除方法も決めます。大量データでは、再開位置・進捗表示・削除済み記録の扱いを追加します。
6. 他人のデータを守るFirestoreセキュリティルールの必須設定
Section titled “6. 他人のデータを守るFirestoreセキュリティルールの必須設定”データベースを公開する際は、他人のデータを読み書きできないよう、Firebaseコンソールでセキュリティルールを記述します。
rules_version = '2';service cloud.firestore { match /databases/{database}/documents { // ユーザー自身の配下のドキュメントのみ、本人のみ読み書きを許可 match /users/{userId}/habits/{habitId} { allow read, write: if request.auth != null && request.auth.uid == userId; } }}このルールは、クライアントから習慣データへアクセスできるユーザーを本人に限定する最小例です。書き込むフィールド・型・値の検証も追加し、セキュリティ編のルール検証を公開前に実施してください。管理用の有料権利などを同じ許可範囲に入れないでください。
7. まとめと次のステップ
Section titled “7. まとめと次のステップ”クラウド同期と認証が加わり、アプリの本格感と信頼性が一段と高まりました。
次の記事では、毎日決まった時間にアプリを開いてもらい、習慣化とアクティブ率(継続率)を劇的に向上させる「02. Push通知を実装してアクティブ率を向上させる」に進みましょう。