Firebase Complete Guide | Firestore, Auth, Storage, Functions & Real-Time

Firebase Complete Guide | Firestore, Auth, Storage, Functions & Real-Time

이 글의 핵심

Firebase gives you a real-time database, auth, file storage, and serverless functions — all managed. This guide covers Firestore data modeling, security rules, Firebase Auth flows, Cloud Storage, and React integration with the v10 SDK.

Firebase SDK Setup

# Create Firebase project: console.firebase.google.com

npm install firebase
// src/lib/firebase.ts
import { initializeApp } from 'firebase/app';
import { getFirestore } from 'firebase/firestore';
import { getAuth } from 'firebase/auth';
import { getStorage } from 'firebase/storage';

const firebaseConfig = {
  apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
  authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
  projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
  storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
  appId: import.meta.env.VITE_FIREBASE_APP_ID,
};

const app = initializeApp(firebaseConfig);

export const db = getFirestore(app);
export const auth = getAuth(app);
export const storage = getStorage(app);

Firestore — Data Modeling

Firestore is a NoSQL document database. The key difference from SQL: model data for how you query it, not for normalization.

Structure:
  Collection (table equivalent)
    └── Document (row equivalent) — { field: value, ... }
          └── Subcollection (nested collection)
                └── Document
// Document structure example
// Collection: "posts"
// Document ID: auto-generated or custom

{
  id: "abc123",                    // Document ID
  title: "Getting Started with Firebase",
  content: "...",
  authorId: "user456",
  authorName: "Alice",             // Denormalized — avoids joins
  published: true,
  tags: ["firebase", "tutorial"],
  createdAt: Timestamp,
  viewCount: 0,
}

Denormalization is intentional — Firestore has no joins. Store the data you need together.


CRUD Operations

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

// CREATE — add with auto-generated ID
const docRef = await addDoc(collection(db, 'posts'), {
  title: 'My First Post',
  content: 'Hello Firebase!',
  authorId: 'user123',
  published: false,
  createdAt: serverTimestamp(),   // Server-side timestamp (consistent)
});
console.log('Created:', docRef.id);

// CREATE — set with specific ID
await setDoc(doc(db, 'users', 'user123'), {
  name: 'Alice',
  email: '[email protected]',
  role: 'admin',
  createdAt: serverTimestamp(),
});

// READ — single document
const docSnap = await getDoc(doc(db, 'posts', 'abc123'));
if (docSnap.exists()) {
  const post = { id: docSnap.id, ...docSnap.data() };
}

// READ — query collection
const q = query(
  collection(db, 'posts'),
  where('published', '==', true),
  where('authorId', '==', 'user123'),
  orderBy('createdAt', 'desc'),
  limit(10)
);
const snapshot = await getDocs(q);
const posts = snapshot.docs.map(d => ({ id: d.id, ...d.data() }));

// UPDATE — partial update
await updateDoc(doc(db, 'posts', 'abc123'), {
  title: 'Updated Title',
  updatedAt: serverTimestamp(),
});

// UPDATE — atomic operations
await updateDoc(doc(db, 'posts', 'abc123'), {
  viewCount: increment(1),                    // Atomic increment
  tags: arrayUnion('featured'),               // Add to array
  tags: arrayRemove('draft'),                 // Remove from array
});

// DELETE
await deleteDoc(doc(db, 'posts', 'abc123'));

Real-Time Listeners

import { onSnapshot, query, collection, where, orderBy } from 'firebase/firestore';

// Listen to a single document
const unsubscribe = onSnapshot(doc(db, 'posts', 'abc123'), (docSnap) => {
  if (docSnap.exists()) {
    console.log('Post updated:', docSnap.data());
  }
});

// Listen to a query
const q = query(
  collection(db, 'messages'),
  where('chatId', '==', 'chat123'),
  orderBy('createdAt', 'asc')
);

const unsubscribeMsgs = onSnapshot(q, (snapshot) => {
  snapshot.docChanges().forEach(change => {
    if (change.type === 'added') console.log('New message:', change.doc.data());
    if (change.type === 'modified') console.log('Message updated:', change.doc.data());
    if (change.type === 'removed') console.log('Message deleted:', change.doc.id);
  });
});

// Cleanup
unsubscribe();           // Call when component unmounts
unsubscribeMsgs();

React Hook for Real-Time Data

import { useState, useEffect } from 'react';
import { onSnapshot, query, collection, where, orderBy } from 'firebase/firestore';

function useMessages(chatId: string) {
  const [messages, setMessages] = useState<Message[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const q = query(
      collection(db, 'messages'),
      where('chatId', '==', chatId),
      orderBy('createdAt', 'asc')
    );

    const unsubscribe = onSnapshot(q, (snapshot) => {
      const msgs = snapshot.docs.map(d => ({ id: d.id, ...d.data() } as Message));
      setMessages(msgs);
      setLoading(false);
    });

    return () => unsubscribe();   // Cleanup on unmount
  }, [chatId]);

  return { messages, loading };
}

Security Rules

Security Rules run on Firebase servers before any client read/write. They’re the primary security mechanism.

// firestore.rules

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // Helper functions
    function isAuthenticated() {
      return request.auth != null;
    }

    function isOwner(userId) {
      return isAuthenticated() && request.auth.uid == userId;
    }

    function isAdmin() {
      return isAuthenticated() &&
        get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'admin';
    }

    // Users collection
    match /users/{userId} {
      allow read: if isAuthenticated();
      allow create: if isOwner(userId);
      allow update: if isOwner(userId) || isAdmin();
      allow delete: if isAdmin();
    }

    // Posts collection
    match /posts/{postId} {
      // Anyone can read published posts
      allow read: if resource.data.published == true || isAuthenticated();

      // Authenticated users can create posts
      allow create: if isAuthenticated() &&
        request.resource.data.authorId == request.auth.uid &&
        request.resource.data.title is string &&
        request.resource.data.title.size() >= 1;

      // Only author or admin can update/delete
      allow update: if isOwner(resource.data.authorId) || isAdmin();
      allow delete: if isOwner(resource.data.authorId) || isAdmin();
    }

    // Private user data (subcollection)
    match /users/{userId}/private/{document} {
      allow read, write: if isOwner(userId);
    }
  }
}

Firebase Authentication

import {
  createUserWithEmailAndPassword,
  signInWithEmailAndPassword,
  signOut,
  sendPasswordResetEmail,
  GoogleAuthProvider,
  signInWithPopup,
  onAuthStateChanged,
  updateProfile,
} from 'firebase/auth';
import { auth } from './firebase';

// Email/password auth
async function register(email: string, password: string, name: string) {
  const { user } = await createUserWithEmailAndPassword(auth, email, password);
  await updateProfile(user, { displayName: name });

  // Create user document in Firestore
  await setDoc(doc(db, 'users', user.uid), {
    name,
    email,
    createdAt: serverTimestamp(),
  });

  return user;
}

async function login(email: string, password: string) {
  const { user } = await signInWithEmailAndPassword(auth, email, password);
  return user;
}

async function logout() {
  await signOut(auth);
}

// Google sign-in
async function signInWithGoogle() {
  const provider = new GoogleAuthProvider();
  const { user } = await signInWithPopup(auth, provider);
  return user;
}

// Password reset
await sendPasswordResetEmail(auth, email);

Auth State Hook

import { useState, useEffect } from 'react';
import { onAuthStateChanged, User } from 'firebase/auth';
import { auth } from '../lib/firebase';

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

  useEffect(() => {
    const unsubscribe = onAuthStateChanged(auth, (user) => {
      setUser(user);
      setLoading(false);
    });
    return unsubscribe;
  }, []);

  return { user, loading };
}

function App() {
  const { user, loading } = useAuth();

  if (loading) return <Spinner />;
  if (!user) return <LoginPage />;
  return <Dashboard user={user} />;
}

Cloud Storage

import { ref, uploadBytes, uploadBytesResumable, getDownloadURL, deleteObject } from 'firebase/storage';
import { storage } from './firebase';

// Simple upload
async function uploadAvatar(userId: string, file: File) {
  const storageRef = ref(storage, `avatars/${userId}/${file.name}`);
  const snapshot = await uploadBytes(storageRef, file);
  const downloadUrl = await getDownloadURL(snapshot.ref);
  return downloadUrl;
}

// Upload with progress
function uploadWithProgress(file: File, path: string, onProgress: (pct: number) => void) {
  const storageRef = ref(storage, path);
  const uploadTask = uploadBytesResumable(storageRef, file);

  return new Promise<string>((resolve, reject) => {
    uploadTask.on(
      'state_changed',
      (snapshot) => {
        const progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
        onProgress(progress);
      },
      reject,
      async () => {
        const url = await getDownloadURL(uploadTask.snapshot.ref);
        resolve(url);
      }
    );
  });
}

// Delete file
async function deleteFile(filePath: string) {
  const fileRef = ref(storage, filePath);
  await deleteObject(fileRef);
}

Cloud Functions

// functions/src/index.ts
import { onDocumentCreated, onDocumentDeleted } from 'firebase-functions/v2/firestore';
import { onCall } from 'firebase-functions/v2/https';
import { getFirestore, FieldValue } from 'firebase-admin/firestore';

const db = getFirestore();

// Trigger when a new post is created
export const onPostCreated = onDocumentCreated('posts/{postId}', async (event) => {
  const post = event.data?.data();
  if (!post) return;

  // Update author's post count
  await db.doc(`users/${post.authorId}`).update({
    postCount: FieldValue.increment(1),
  });

  // Send notification (could call an email service here)
  console.log(`New post by ${post.authorId}: ${post.title}`);
});

// Trigger when a post is deleted
export const onPostDeleted = onDocumentDeleted('posts/{postId}', async (event) => {
  const post = event.data?.data();
  if (!post) return;

  await db.doc(`users/${post.authorId}`).update({
    postCount: FieldValue.increment(-1),
  });
});

// Callable function (called from client SDK)
export const sendWelcomeEmail = onCall(async (request) => {
  if (!request.auth) throw new Error('Unauthenticated');

  const { email, name } = request.data;
  // Send welcome email via SendGrid, etc.
  return { success: true };
});
// Client: call a Cloud Function
import { getFunctions, httpsCallable } from 'firebase/functions';

const functions = getFunctions();
const sendWelcome = httpsCallable(functions, 'sendWelcomeEmail');

const result = await sendWelcome({ email: '[email protected]', name: 'Alice' });

Performance Tips

// 1. Use subcollections for private/large data
// ✅ users/{uid}/private/settings  (only the user can read)
// ✅ posts/{postId}/comments (paginate separately)

// 2. Limit query results
const q = query(collection(db, 'posts'), limit(20));  // Always limit

// 3. Use composite indexes for complex queries
// Firestore requires explicit indexes for queries with multiple filters
// Firebase Console shows "Index required" errors with a direct link to create them

// 4. Pagination with cursors
import { startAfter, endBefore } from 'firebase/firestore';

const firstPage = await getDocs(query(col, orderBy('createdAt', 'desc'), limit(20)));
const lastDoc = firstPage.docs[firstPage.docs.length - 1];

const nextPage = await getDocs(
  query(col, orderBy('createdAt', 'desc'), startAfter(lastDoc), limit(20))
);

Related posts: