Firebase

モバイル開発バックエンドサービスリアルタイムデータベース認証プッシュ通知分析

モバイルプラットフォーム

Firebase

概要

FirebaseはGoogleが提供するモバイル・Webアプリ開発プラットフォームです。認証、データベース、プッシュ通知、分析等の機能を統合し、高品質なアプリを迅速に構築可能にします。2025年現在、モバイルアプリ開発のバックエンドサービスとして最も人気が高く、特にスタートアップでの採用率が高く、迅速なプロトタイピングとスケーリングを実現しています。

詳細

Firebaseは、従来のバックエンド開発で必要だった複雑なサーバー構築、データベース設計、インフラ管理を大幅に簡素化し、フロントエンド開発者でも本格的なバックエンド機能を持つアプリを開発できるようにします。

2025年の最新アップデートでは、ML Kit による機械学習機能の強化、App Check によるセキュリティ向上、Cloud Functions の第2世代による性能改善が実装されています。また、マルチテナント機能により、単一のプロジェクトで複数の顧客やテナントを管理できるエンタープライズレベルの機能も提供されています。

Firebaseの最大の特徴は、リアルタイム性と簡単な統合です。Firestore のリアルタイムリスナーにより、データの変更が即座にクライアントに反映され、チャットアプリや協調編集ツールなどのリアルタイムアプリケーションの構築が簡単になります。

メリット・デメリット

メリット

  • 迅速な開発: バックエンドインフラの構築不要で、短期間でのMVP作成が可能
  • リアルタイム機能: Firestoreによるリアルタイムデータ同期で、動的なアプリ体験を実現
  • 包括的な機能: 認証、データベース、ストレージ、分析、ML機能まで一元管理
  • スケーラビリティ: Googleのインフラにより、自動的なスケーリングに対応
  • 無料プラン: 開発初期段階や小規模アプリに十分な無料枠を提供
  • マルチプラットフォーム: iOS、Android、Web、Flutter、React Nativeなど幅広いプラットフォーム対応

デメリット

  • ベンダーロックイン: Firebase エコシステムへの依存度が高い
  • 価格: 大規模になると従量課金によるコスト増加の可能性
  • カスタマイゼーション制限: 特殊なバックエンド処理には制限がある場合がある
  • データエクスポート: 他のプラットフォームへの移行時の複雑さ
  • オフライン機能: 複雑なオフライン処理には限界がある

参考ページ

書き方の例

プロジェクトセットアップと初期化

# Firebase CLI インストール
npm install -g firebase-tools

# Firebase にログイン
firebase login

# プロジェクト初期化
firebase init

# Firebase プロジェクト作成(Console で作成済みの場合はスキップ)
firebase projects:create your-project-id

# Firebase JS SDK インストール
npm install firebase
// firebaseConfig.ts
import { initializeApp } from 'firebase/app';
import { getFirestore } from 'firebase/firestore';
import { getAuth } from 'firebase/auth';
import { getStorage } from 'firebase/storage';
import { getAnalytics } from 'firebase/analytics';

const firebaseConfig = {
  apiKey: "your-api-key",
  authDomain: "your-project.firebaseapp.com",
  projectId: "your-project-id",
  storageBucket: "your-project.appspot.com",
  messagingSenderId: "123456789",
  appId: "your-app-id",
  measurementId: "G-MEASUREMENT_ID"
};

// Firebase アプリ初期化
const app = initializeApp(firebaseConfig);

// サービス初期化
export const db = getFirestore(app);
export const auth = getAuth(app);
export const storage = getStorage(app);
export const analytics = getAnalytics(app);

export default app;

認証システム実装

// services/authService.ts
import { 
  createUserWithEmailAndPassword,
  signInWithEmailAndPassword,
  signOut,
  onAuthStateChanged,
  User,
  updateProfile,
  sendPasswordResetEmail,
  GoogleAuthProvider,
  signInWithPopup
} from 'firebase/auth';
import { auth } from '../firebaseConfig';

export class AuthService {
  // メール・パスワードでユーザー登録
  static async signUp(email: string, password: string, displayName: string) {
    try {
      const userCredential = await createUserWithEmailAndPassword(auth, email, password);
      
      // プロフィール更新
      await updateProfile(userCredential.user, {
        displayName: displayName
      });
      
      return userCredential.user;
    } catch (error) {
      throw error;
    }
  }

  // メール・パスワードでサインイン
  static async signIn(email: string, password: string) {
    try {
      const userCredential = await signInWithEmailAndPassword(auth, email, password);
      return userCredential.user;
    } catch (error) {
      throw error;
    }
  }

  // Google OAuth サインイン
  static async signInWithGoogle() {
    try {
      const provider = new GoogleAuthProvider();
      const userCredential = await signInWithPopup(auth, provider);
      return userCredential.user;
    } catch (error) {
      throw error;
    }
  }

  // サインアウト
  static async signOut() {
    try {
      await signOut(auth);
    } catch (error) {
      throw error;
    }
  }

  // パスワードリセット
  static async resetPassword(email: string) {
    try {
      await sendPasswordResetEmail(auth, email);
    } catch (error) {
      throw error;
    }
  }

  // 認証状態の監視
  static onAuthStateChanged(callback: (user: User | null) => void) {
    return onAuthStateChanged(auth, callback);
  }
}

// React Hook での使用例
import { useState, useEffect } from 'react';

export const useAuth = () => {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const unsubscribe = AuthService.onAuthStateChanged((user) => {
      setUser(user);
      setLoading(false);
    });

    return () => unsubscribe();
  }, []);

  return { user, loading };
};

Firestore データベース操作

// services/firestoreService.ts
import { 
  collection,
  doc,
  getDocs,
  getDoc,
  addDoc,
  updateDoc,
  deleteDoc,
  query,
  where,
  orderBy,
  limit,
  onSnapshot,
  serverTimestamp,
  increment,
  arrayUnion,
  arrayRemove
} from 'firebase/firestore';
import { db } from '../firebaseConfig';

export interface User {
  id: string;
  name: string;
  email: string;
  createdAt: any;
  posts?: string[];
}

export interface Post {
  id: string;
  title: string;
  content: string;
  authorId: string;
  authorName: string;
  createdAt: any;
  updatedAt: any;
  likes: number;
  tags: string[];
}

export class FirestoreService {
  // ユーザー作成
  static async createUser(userData: Omit<User, 'id'>) {
    try {
      const userRef = await addDoc(collection(db, 'users'), {
        ...userData,
        createdAt: serverTimestamp()
      });
      return userRef.id;
    } catch (error) {
      throw error;
    }
  }

  // ユーザー取得
  static async getUser(userId: string): Promise<User | null> {
    try {
      const userDoc = await getDoc(doc(db, 'users', userId));
      if (userDoc.exists()) {
        return { id: userDoc.id, ...userDoc.data() } as User;
      }
      return null;
    } catch (error) {
      throw error;
    }
  }

  // 投稿作成
  static async createPost(postData: Omit<Post, 'id' | 'createdAt' | 'updatedAt' | 'likes'>) {
    try {
      const postRef = await addDoc(collection(db, 'posts'), {
        ...postData,
        createdAt: serverTimestamp(),
        updatedAt: serverTimestamp(),
        likes: 0
      });
      return postRef.id;
    } catch (error) {
      throw error;
    }
  }

  // 投稿一覧取得(リアルタイム)
  static subscribeToPosts(callback: (posts: Post[]) => void) {
    const q = query(
      collection(db, 'posts'),
      orderBy('createdAt', 'desc'),
      limit(20)
    );

    return onSnapshot(q, (snapshot) => {
      const posts = snapshot.docs.map(doc => ({
        id: doc.id,
        ...doc.data()
      })) as Post[];
      callback(posts);
    });
  }

  // ユーザーの投稿取得
  static async getUserPosts(userId: string): Promise<Post[]> {
    try {
      const q = query(
        collection(db, 'posts'),
        where('authorId', '==', userId),
        orderBy('createdAt', 'desc')
      );
      
      const snapshot = await getDocs(q);
      return snapshot.docs.map(doc => ({
        id: doc.id,
        ...doc.data()
      })) as Post[];
    } catch (error) {
      throw error;
    }
  }

  // 投稿更新
  static async updatePost(postId: string, updates: Partial<Post>) {
    try {
      await updateDoc(doc(db, 'posts', postId), {
        ...updates,
        updatedAt: serverTimestamp()
      });
    } catch (error) {
      throw error;
    }
  }

  // いいね追加
  static async likePost(postId: string) {
    try {
      await updateDoc(doc(db, 'posts', postId), {
        likes: increment(1)
      });
    } catch (error) {
      throw error;
    }
  }

  // タグ追加
  static async addTagToPost(postId: string, tag: string) {
    try {
      await updateDoc(doc(db, 'posts', postId), {
        tags: arrayUnion(tag)
      });
    } catch (error) {
      throw error;
    }
  }

  // 投稿削除
  static async deletePost(postId: string) {
    try {
      await deleteDoc(doc(db, 'posts', postId));
    } catch (error) {
      throw error;
    }
  }
}

Firebase Cloud Functions バックエンド統合

// functions/src/index.ts (Cloud Functions)
import { onDocumentCreated, onDocumentUpdated } from 'firebase-functions/v2/firestore';
import { onCall, HttpsError } from 'firebase-functions/v2/https';
import { onSchedule } from 'firebase-functions/v2/scheduler';
import { initializeApp } from 'firebase-admin/app';
import { getFirestore } from 'firebase-admin/firestore';
import { getMessaging } from 'firebase-admin/messaging';

initializeApp();
const db = getFirestore();
const messaging = getMessaging();

// 新しい投稿作成時のトリガー
export const onPostCreated = onDocumentCreated('posts/{postId}', async (event) => {
  const postData = event.data?.data();
  const postId = event.params.postId;

  if (!postData) return;

  try {
    // 作者の情報を取得
    const authorDoc = await db.collection('users').doc(postData.authorId).get();
    const authorData = authorDoc.data();

    // フォロワーに通知を送信
    const followersSnapshot = await db
      .collection('follows')
      .where('followingId', '==', postData.authorId)
      .get();

    const notificationPromises = followersSnapshot.docs.map(async (followerDoc) => {
      const followerId = followerDoc.data().followerId;
      
      // 通知ドキュメント作成
      await db.collection('notifications').add({
        userId: followerId,
        type: 'new_post',
        title: '新しい投稿',
        message: `${authorData?.name}さんが新しい投稿をしました`,
        postId: postId,
        read: false,
        createdAt: new Date()
      });

      // プッシュ通知送信
      const userDoc = await db.collection('users').doc(followerId).get();
      const fcmToken = userDoc.data()?.fcmToken;
      
      if (fcmToken) {
        await messaging.send({
          token: fcmToken,
          notification: {
            title: '新しい投稿',
            body: `${authorData?.name}さんが新しい投稿をしました`,
          },
          data: {
            type: 'new_post',
            postId: postId
          }
        });
      }
    });

    await Promise.all(notificationPromises);
  } catch (error) {
    console.error('Error in onPostCreated:', error);
  }
});

// API エンドポイント例
export const getUserStats = onCall(async (request) => {
  const userId = request.auth?.uid;
  
  if (!userId) {
    throw new HttpsError('unauthenticated', 'User must be authenticated');
  }

  try {
    // ユーザーの投稿数取得
    const postsSnapshot = await db
      .collection('posts')
      .where('authorId', '==', userId)
      .get();

    // フォロワー数取得
    const followersSnapshot = await db
      .collection('follows')
      .where('followingId', '==', userId)
      .get();

    // フォロー数取得
    const followingSnapshot = await db
      .collection('follows')
      .where('followerId', '==', userId)
      .get();

    return {
      postsCount: postsSnapshot.size,
      followersCount: followersSnapshot.size,
      followingCount: followingSnapshot.size
    };
  } catch (error) {
    throw new HttpsError('internal', 'Failed to get user stats');
  }
});

// 定期実行タスク
export const dailyCleanup = onSchedule('0 2 * * *', async (event) => {
  try {
    // 30日以上古い通知を削除
    const thirtyDaysAgo = new Date();
    thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);

    const oldNotificationsSnapshot = await db
      .collection('notifications')
      .where('createdAt', '<', thirtyDaysAgo)
      .get();

    const deletePromises = oldNotificationsSnapshot.docs.map(doc => doc.ref.delete());
    await Promise.all(deletePromises);

    console.log(`Deleted ${oldNotificationsSnapshot.size} old notifications`);
  } catch (error) {
    console.error('Error in dailyCleanup:', error);
  }
});

プッシュ通知実装

// services/messagingService.ts
import { getMessaging, getToken, onMessage } from 'firebase/messaging';
import { doc, updateDoc } from 'firebase/firestore';
import { db } from '../firebaseConfig';

const messaging = getMessaging();

export class MessagingService {
  // FCM トークン取得
  static async getToken(): Promise<string | null> {
    try {
      const currentToken = await getToken(messaging, {
        vapidKey: 'your-vapid-key'
      });
      
      if (currentToken) {
        return currentToken;
      } else {
        console.log('No registration token available.');
        return null;
      }
    } catch (error) {
      console.error('An error occurred while retrieving token. ', error);
      return null;
    }
  }

  // FCMトークンをFirestoreに保存
  static async saveFCMToken(userId: string) {
    try {
      const token = await this.getToken();
      if (token) {
        await updateDoc(doc(db, 'users', userId), {
          fcmToken: token,
          tokenUpdatedAt: new Date()
        });
      }
    } catch (error) {
      console.error('Error saving FCM token:', error);
    }
  }

  // フォアグラウンドメッセージ受信
  static setupForegroundMessageListener() {
    onMessage(messaging, (payload) => {
      console.log('Message received in foreground: ', payload);
      
      // カスタム通知表示
      if (payload.notification) {
        this.showNotification(
          payload.notification.title || 'New Message',
          payload.notification.body || ''
        );
      }
    });
  }

  // カスタム通知表示(Web)
  static showNotification(title: string, body: string) {
    if ('Notification' in window && Notification.permission === 'granted') {
      new Notification(title, {
        body,
        icon: '/icon-192x192.png'
      });
    }
  }

  // 通知権限リクエスト
  static async requestPermission(): Promise<boolean> {
    if ('Notification' in window) {
      const permission = await Notification.requestPermission();
      return permission === 'granted';
    }
    return false;
  }
}

// service-worker.js(プッシュ通知のバックグラウンド受信)
import { initializeApp } from 'firebase/app';
import { getMessaging, onBackgroundMessage } from 'firebase/messaging/sw';

const firebaseConfig = {
  // Firebase設定
};

const app = initializeApp(firebaseConfig);
const messaging = getMessaging(app);

onBackgroundMessage(messaging, (payload) => {
  console.log('Received background message ', payload);
  
  const notificationTitle = payload.notification?.title || 'Background Message';
  const notificationOptions = {
    body: payload.notification?.body || '',
    icon: '/icon-192x192.png'
  };

  self.registration.showNotification(notificationTitle, notificationOptions);
});

Firebase Analytics 実装

// services/analyticsService.ts
import { logEvent, setUserId, setUserProperties } from 'firebase/analytics';
import { analytics } from '../firebaseConfig';

export class AnalyticsService {
  // カスタムイベントログ
  static logCustomEvent(eventName: string, parameters?: Record<string, any>) {
    logEvent(analytics, eventName, parameters);
  }

  // ページビュー記録
  static logPageView(pageName: string, pageTitle?: string) {
    logEvent(analytics, 'page_view', {
      page_title: pageTitle || pageName,
      page_location: window.location.href,
      page_name: pageName
    });
  }

  // ユーザーアクション記録
  static logUserAction(action: string, category?: string, label?: string) {
    logEvent(analytics, 'user_action', {
      action,
      category,
      label,
      timestamp: new Date().toISOString()
    });
  }

  // コンバージョン追跡
  static logConversion(eventName: string, value?: number, currency?: string) {
    logEvent(analytics, eventName, {
      currency: currency || 'USD',
      value: value || 0
    });
  }

  // ユーザー情報設定
  static setUser(userId: string, properties?: Record<string, any>) {
    setUserId(analytics, userId);
    if (properties) {
      setUserProperties(analytics, properties);
    }
  }

  // エラー追跡
  static logError(error: Error, context?: string) {
    logEvent(analytics, 'exception', {
      description: error.message,
      fatal: false,
      context,
      stack: error.stack
    });
  }

  // パフォーマンス追跡
  static logPerformance(metricName: string, value: number, unit?: string) {
    logEvent(analytics, 'performance_metric', {
      metric_name: metricName,
      metric_value: value,
      metric_unit: unit || 'ms'
    });
  }
}

// React Hook での自動追跡
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

export const useAnalytics = () => {
  const location = useLocation();

  useEffect(() => {
    AnalyticsService.logPageView(location.pathname);
  }, [location]);

  return {
    logEvent: AnalyticsService.logCustomEvent,
    logAction: AnalyticsService.logUserAction,
    logError: AnalyticsService.logError
  };
};

Firebase Security Rules とデプロイ

// firestore.rules - Firestore セキュリティルール
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // ユーザードキュメント: 本人のみ読み書き可能
    match /users/{userId} {
      allow read, write: if request.auth != null && request.auth.uid == userId;
    }
    
    // 投稿: 認証済みユーザーは読み取り可能、作成者のみ書き込み可能
    match /posts/{postId} {
      allow read: if request.auth != null;
      allow create: if request.auth != null && 
                   request.auth.uid == resource.data.authorId;
      allow update, delete: if request.auth != null && 
                            request.auth.uid == resource.data.authorId;
    }
    
    // フォロー関係: 本人のみ管理可能
    match /follows/{followId} {
      allow read: if request.auth != null;
      allow create, update, delete: if request.auth != null && 
                                   request.auth.uid == resource.data.followerId;
    }
    
    // 通知: 本人のみ読み書き可能
    match /notifications/{notificationId} {
      allow read, write: if request.auth != null && 
                         request.auth.uid == resource.data.userId;
    }
  }
}
// firebase.json - Firebase プロジェクト設定
{
  "firestore": {
    "rules": "firestore.rules",
    "indexes": "firestore.indexes.json"
  },
  "functions": [
    {
      "source": "functions",
      "codebase": "default",
      "ignore": [
        "node_modules",
        ".git",
        "firebase-debug.log",
        "firebase-debug.*.log"
      ]
    }
  ],
  "hosting": {
    "public": "dist",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites": [
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  },
  "storage": {
    "rules": "storage.rules"
  }
}
# Firebase デプロイ
firebase deploy

# 特定のサービスのみデプロイ
firebase deploy --only firestore:rules
firebase deploy --only functions
firebase deploy --only hosting

# 複数のプロジェクトでの管理
firebase use production
firebase deploy

firebase use staging
firebase deploy