コンテンツにスキップ

01. Firebaseでユーザー認証とクラウド保存を実装する

基本のMVPアプリをリリースした次のステージとして、多くのユーザーから最も要望されるのが「端末を変えてもデータを引き継ぎたい」「タブレットや別のスマホでも同じデータを共有したい」というクラウド同期機能です。

端末内保存(AsyncStorage)からクラウドデータベース(Cloud Firestore)と認証(Firebase Authentication)へステップアップすることで、アプリの利便性と信頼性は劇的に向上します。

この記事では、ExpoアプリにFirebaseを導入し、ユーザーごとの安全なログイン、既存ローカルデータのクラウドマイグレーション、そしてリアルタイムなデータ同期を組み込む実践手順を詳しく解説します。


  1. ローカル保存からクラウド連携へステップアップするメリット
  2. ExpoへのFirebase SDK導入と初期化設定
  3. Firebase Authを使った手軽なサインイン実装(匿名認証と段階的連携)
  4. Cloud Firestoreでのデータモデル設計とリアルタイム同期
  5. 既存AsyncStorageデータからFirestoreへの自動マイグレーション
  6. 他人のデータを守るFirestoreセキュリティルールの必須設定
  7. まとめと次のステップ

1. ローカル保存からクラウド連携へステップアップするメリット

Section titled “1. ローカル保存からクラウド連携へステップアップするメリット”

AsyncStorageによる完全ローカル管理は手軽ですが、以下の課題があります。

  • 機種変更時やスマホ故障・紛失時にデータが二度と復旧できない
  • iPhoneとiPad、あるいはPCなど複数端末でのシームレスな同期ができない
  • クラウドバックアップがないため、利用継続の妨げになることがある(LTVは継続率そのものではなく、顧客生涯価値)

Firebaseを導入することで、自前でサーバーを用意することなく、Googleの堅牢なクラウドインフラ上でユーザー認証と自動同期を実現できます。


2. ExpoへのFirebase SDK導入と初期化設定

Section titled “2. ExpoへのFirebase SDK導入と初期化設定”

Firebaseの公式JavaScript SDKをインストールします。

Terminal window
npx expo install firebase @react-native-async-storage/async-storage

FirebaseコンソールでWebアプリを登録して設定値を取得します。ここではFirebase JS SDKを使います。ネイティブ向けのAnalytics等とは設定が異なります。ExpoのFirebase導入ガイドで互換バージョンを確認してください。

プロジェクト内に lib/firebase.ts を作成し、設定値を環境変数(.env)から読み込みます。EXPO_PUBLIC_ の値は配布アプリから読めるため、管理者の秘密鍵を入れないでください。

lib/firebase.ts
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回だけ呼び、各画面へ共有してください。ログアウトやアカウント切り替えを設ける場合は、自動匿名ログインの再実行と古いユーザーデータの表示を制御する状態も追加します。

hooks/useAuth.ts
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つの処理だけを動かします。途中で通信が切れても、再実行でデータが増殖したり、移行済みのクラウドデータを古い値で上書きしたりしないようにします。

lib/migration.ts
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;
}
}
}

このルールは、クライアントから習慣データへアクセスできるユーザーを本人に限定する最小例です。書き込むフィールド・型・値の検証も追加し、セキュリティ編のルール検証を公開前に実施してください。管理用の有料権利などを同じ許可範囲に入れないでください。


クラウド同期と認証が加わり、アプリの本格感と信頼性が一段と高まりました。

次の記事では、毎日決まった時間にアプリを開いてもらい、習慣化とアクティブ率(継続率)を劇的に向上させる「02. Push通知を実装してアクティブ率を向上させる」に進みましょう。