Firebase Complete Guide | Firestore· Auth
이 글의 핵심
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.
Core Firebase services at a glance
Firebase is a Backend-as-a-Service (BaaS) bundle. In practice you rarely use only one product; the value is how Auth, database, files, and serverless code fit together.
- Authentication — Email/password, federated (Google, Apple, etc.), phone, and anonymous users. Session tokens are verified by other Firebase products and by your Security Rules.
- Cloud Firestore — A managed document database with real-time sync, strong consistency within a region, and client-side offline persistence. It is not a drop-in replacement for PostgreSQL: relationships are expressed via denormalization, references, and sometimes Cloud Functions.
- Cloud Storage — Object storage (think S3) with SDKs for resumable uploads, download URLs, and Security Rules that can enforce path-based access (e.g.
avatars/{userId}/...). - Cloud Functions — Event-driven and HTTPS endpoints on Google Cloud. Use them when rules are not enough: payments, webhooks, trusted aggregation, and anything that must not be forgeable from the client.
Honest take: Firebase shines for mobile and web apps that need fast iteration, real-time UIs, and Google ecosystem integration. Pain points are NoSQL modeling, per-operation billing if you are careless with listeners, and lock-in to Google’s data model and IAM story — acceptable for many products, a deal-breaker for others.
Firebase SDK setup
# Create Firebase project: console.firebase.google.com
npm install firebase
// src/lib/firebase.ts
import { initializeApp } from 'firebase/app';
import { getFirestore, enableIndexedDbPersistence } 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);
// Optional: enable offline persistence (web) — see "Real-time and offline" below
// void enableIndexedDbPersistence(db).catch((err) => {
// if (err.code === 'failed-precondition') console.warn('Multi-tab persistence');
// else if (err.code === 'unimplemented') console.warn('Browser does not support persistence');
// });
Firestore — data modeling strategy
Firestore is a NoSQL document database. The key difference from SQL: model data for how you query it, not for third-normal-form purity.
Structure:
Collection (roughly: table)
└── Document — { field: value, ... }
└── Subcollection
└── Document
Collection vs subcollection
| Pattern | When to use |
|---|---|
Top-level posts + authorId field | List posts globally, filter by author, simple rules. |
users/{id}/posts subcollection | Strong ownership, per-user quotas, or huge post lists you never query across users. |
| Denormalized fields | Avoid extra reads: store authorName on the post, update via a Function when the profile changes (trade-off: write amplification). |
// Document structure example
// Collection: "posts"
// Document ID: auto-generated or custom
{
id: "abc123",
title: "Getting Started with Firebase",
content: "...",
authorId: "user456",
authorName: "Alice", // Denormalized — avoids extra read for display lists
published: true,
tags: ["firebase", "tutorial"],
createdAt: Timestamp,
viewCount: 0,
}
Denormalization is intentional — Firestore has no server-side joins. Either duplicate data you show in lists, or pay extra reads to resolve references client-side (sometimes cached).
Arrays, maps, and references
DocumentReference: use when you need to point at another doc and you are fine resolving it in app code.- Arrays of strings/IDs: great for tags; be careful with array-contains queries and document size.
- Maps: good for static-ish nested config; avoid unbounded growth inside one document (1 MB limit per document).
import { doc, setDoc, serverTimestamp, collection, addDoc } from 'firebase/firestore';
// Root collection with author reference (stored as a path or reference field)
await addDoc(collection(db, 'articles'), {
title: 'Realtime patterns',
authorRef: doc(db, 'users', 'user_42'),
tags: ['firestore', 'react'],
stats: { views: 0, likes: 0 },
createdAt: serverTimestamp(),
});
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(),
});
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),
tags: arrayUnion('featured'),
// Note: you cannot both union and remove different items in one field object key collision;
// use two updates or a transaction if you need both in one write.
});
// 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);
});
});
unsubscribe();
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);
const [error, setError] = useState<Error | null>(null);
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);
},
(err) => {
setError(err);
setLoading(false);
}
);
return () => unsubscribe();
}, [chatId]);
return { messages, loading, error };
}
Real-time updates and offline support
- How it works (web):
onSnapshotopens a stream. When persistence is enabled, the SDK serves cached data immediately, then reconciles with the server. Writes can be queued while offline and synced later. - When it helps: chat, collaborative lists, “live” dashboards. Caution: every active listener has a cost; one component mounting many listeners is an easy way to burn reads.
import { getFirestore, enableIndexedDbPersistence, enableMultiTabIndexedDbPersistence } from 'firebase/firestore';
// After getFirestore(db)
if (import.meta.env.PROD) {
enableMultiTabIndexedDbPersistence(db).catch((err) => {
if (err.code === 'failed-precondition') {
// Another tab has persistence; fall back to single-tab or accept limitations
}
});
}
Pitfall: If you do not need live updates, prefer getDocs (one-time read) over onSnapshot to save money and simplify mental models.
Security Rules — writing guide
Security Rules run on Firebase before any client read or write. They are not a full programming language, but you can use helpers, get() to read other documents (careful: extra reads in evaluation), and request.resource vs resource for create/update.
Principles:
- Default deny — start narrow, then open specific paths.
- Validate shape — check types and required fields on
createandupdate. - Never trust the client — any field a user could forge must be disallowed (e.g. do not let clients set
role: adminon self-signup without server-side or admin-only path).
// firestore.rules
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
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';
}
match /users/{userId} {
allow read: if isAuthenticated();
allow create: if isOwner(userId);
allow update: if isOwner(userId) || isAdmin();
allow delete: if isAdmin();
}
match /posts/{postId} {
allow read: if resource.data.published == true || isAuthenticated();
allow create: if isAuthenticated() &&
request.resource.data.authorId == request.auth.uid &&
request.resource.data.title is string &&
request.resource.data.title.size() >= 1;
allow update: if isOwner(resource.data.authorId) || isAdmin();
allow delete: if isOwner(resource.data.authorId) || isAdmin();
}
match /users/{userId}/private/{document} {
allow read, write: if isOwner(userId);
}
}
}
Storage rules (short example) — keep paths predictable so rules stay simple:
// storage.rules
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /users/{userId}/uploads/{fileName} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
}
}
Authentication patterns
Firebase Auth is flexible: password, federated, phone, and anonymous (upgrade later to a full account). Always create or merge a users/{uid} document only after you trust the uid from onAuthStateChanged.
Email / password and profile bootstrap
import {
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
signOut,
sendPasswordResetEmail,
} from 'firebase/auth';
import { doc, setDoc, serverTimestamp } from 'firebase/firestore';
import { auth, db } from './firebase';
async function register(email: string, password: string, name: string) {
const { user } = await createUserWithEmailAndPassword(auth, email, password);
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);
}
await sendPasswordResetEmail(auth, email);
Google (or other providers)
import { GoogleAuthProvider, signInWithPopup, linkWithCredential } from 'firebase/auth';
async function signInWithGoogle() {
const provider = new GoogleAuthProvider();
const { user } = await signInWithPopup(auth, provider);
return user;
}
Gotcha: On mobile or restricted environments you may use redirect flows instead of popups; test both.
Anonymous auth + upgrade
Use anonymous users for trials or “try before sign-up” flows, then linkWithCredential when the user sets a password or links Google.
import { signInAnonymously, linkWithCredential, EmailAuthProvider } from 'firebase/auth';
const { user } = await signInAnonymously(auth);
// later:
const cred = EmailAuthProvider.credential(email, password);
await linkWithCredential(user, cred);
Auth state in React
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';
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;
}
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);
}
);
});
}
async function deleteFile(filePath: string) {
const fileRef = ref(storage, filePath);
await deleteObject(fileRef);
}
Cloud Functions — practical examples
You typically use Functions to enforce invariants (counters, idempotent webhooks) or to call secret APIs. Gen 2 (v2) is the modern default: define triggers with firebase-functions v2 imports.
// functions/src/index.ts
import { onDocumentCreated, onDocumentDeleted, onDocumentWritten } from 'firebase-functions/v2/firestore';
import { onCall, HttpsError } from 'firebase-functions/v2/https';
import { getFirestore, FieldValue } from 'firebase-admin/firestore';
import { initializeApp } from 'firebase-admin/app';
initializeApp();
const db = getFirestore();
export const onPostCreated = onDocumentCreated('posts/{postId}', async (event) => {
const post = event.data?.data();
if (!post) return;
await db.doc(`users/${post.authorId}`).update({
postCount: FieldValue.increment(1),
});
});
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: verify auth, never trust client-only “role” fields
export const createCheckoutSession = onCall(async (request) => {
if (!request.auth) throw new HttpsError('unauthenticated', 'Sign in required');
const uid = request.auth.uid;
// Call Stripe, etc. with server secrets
return { clientSecret: '...' };
});
Client call:
import { getFunctions, httpsCallable } from 'firebase/functions';
const functions = getFunctions();
const createCheckout = httpsCallable(functions, 'createCheckoutSession');
const { data } = await createCheckout({ priceId: 'price_123' });
Idempotent fan-out (sketch): use onDocumentWritten and compare before / after to avoid double counting when a document flips through intermediate states.
Cost optimization
Firestore bills (on Blaze) around document reads, writes, deletes, and storage. The biggest surprises come from listener fan-out and chatty queries in list views.
- Replace passive listeners with one-time
getDocswhen data does not need to be live. - Paginate with
limit+ cursors; never load an entire collection in the client. - Cache denormalized display fields to avoid N+1 document fetches in feeds.
- Batch writes where possible; prefer transactions when multiple docs must move together.
- Index discipline: composite indexes are required for compound queries—create only what you need, and watch for “query requires index” in development before shipping.
import { startAfter, limit, getDocs, query, orderBy, collection, documentId } from 'firebase/firestore';
// Cursor-based page 2
const first = await getDocs(query(collection(db, 'posts'), orderBy('createdAt', 'desc'), limit(20)));
const cursor = first.docs[first.docs.length - 1];
const second = await getDocs(
query(collection(db, 'posts'), orderBy('createdAt', 'desc'), startAfter(cursor), limit(20))
);
Functions: Minimize cold starts (provisioned concurrency for hot paths if justified), keep dependencies small, and avoid unnecessary Firestore round-trips inside triggers.
Firebase vs Supabase (straight comparison)
| Firebase (Firestore + Auth + Storage + Functions) | Supabase (Postgres + Auth + Storage + Edge Functions) | |
|---|---|---|
| Data model | Document / collection; scale-out friendly | Relational; SQL joins, constraints, mature migrations |
| Realtime | First-class onSnapshot + offline cache | Realtime on tables/channels; different mental model |
| Ecosystem | Mature mobile SDKs, FCM, Remote Config, A/B | Open-source, can self-host, strong Postgres tooling |
| Vendor | Google Cloud | Supabase (hosted) or your own stack |
| When Firebase wins | Mobile-first, heavy real-time, quick ship on GCP | You need SQL, row-level security in Postgres, or on-prem |
| When Supabase wins | Complex reporting, heavy relational integrity | You need Firebase’s mobile + FCM + Firestore sync story |
Neither is “better” in the abstract—match the data shape and team skills to the product.
Production experience (field notes)
In real projects, Firebase teams that succeed design Firestore on paper first (entities, access paths, and rules) before writing UI. Teams that struggle often treat Firestore like MySQL, then fight rules and read costs.
- What worked well: Real-time UIs for small-to-medium datasets, phone auth, Storage + download URLs, and using Callable Functions for payment webhooks. Google’s console and debug tooling for rules are solid if you invest time in the emulator.
- What was painful: Migrating a poorly modeled tree (deep subcollections, inconsistent IDs), accidentally leaving wide open listeners in admin pages, and discovering Blaze was required for Functions after the prototype—plan billing early.
- Operational tip: Use the Firebase Local Emulator Suite for rules + functions before CI; one wrong rule in production is worse than a slow deploy.
Bottom line: Firebase is a productivity multiplier when you accept NoSQL trade-offs and cost-aware access patterns. If your core is relational reporting and complex joins, start with SQL (self-hosted or Supabase) and optionally use Firebase for a slice (e.g. FCM only).
Performance tips (recap)
// 1. Subcollections for per-user or high-volume child data
// users/{uid}/activity/{eventId}
// posts/{postId}/comments (paginate separately)
// 2. Always cap queries
const q = query(collection(db, 'posts'), limit(20));
// 3. Create composite indexes from console links in error messages
// 4. Pagination
import { startAfter } from 'firebase/firestore';
Related posts:
- [React 18 Deep Dive](/en/blog/react-18-deep-dive/
- [Database Comparison: SQL vs NoSQL](/en/blog/database-comparison-sql-nosql-guide/
- [Next.js App Router Guide](/en/blog/nextjs-app-router-rendering-strategies/
More FAQ (inline)
How do I use this in real projects? Use the rules and modeling sections as a checklist: define paths, then write rules, then add client code. Add Functions only where the client must not be trusted.
What should I read next? Follow the related posts above for React rendering and general database trade-offs. For official depth, use the Firebase documentation and the emulator guides.
Where can I go deeper? The Firestore data model and Security Rules pages stay authoritative; this guide is an opinionated map, not a replacement.
Related keywords
Firebase, Firestore, Authentication, Cloud Functions, Cloud Storage, Security Rules, real-time, offline, Supabase comparison, React, Backend-as-a-Service.