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: 'alice@example.com',
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: 'user@example.com', 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: