Firebase로 앱 백엔드 구성하기: Authentication, Firestore, Storage, Cloud Functions, 보안 규칙
이 글의 핵심
Firebase 프로젝트 설정부터 인증, Firestore 데이터 읽기·쓰기, Storage 업로드, Cloud Functions, Hosting 배포, 그리고 가장 자주 실수하는 Security Rules 작성법을 다룹니다.
이 글의 핵심
Firebase로 풀스택 앱을 구축하는 글입니다. Authentication, Firestore, Storage, Cloud Functions, Hosting, Analytics까지 실전 예제로 정리했습니다.
실무에서 마주치는 문제들
인증 구현이 복잡해요
JWT, 세션, OAuth를 직접 구현해야 합니다. Firebase는 내장되어 있습니다.
데이터베이스 설정이 번거로워요
PostgreSQL 설치, 스키마 설계가 필요합니다. Firestore는 즉시 사용 가능합니다.
파일 업로드 서버가 필요해요
S3 설정이 복잡합니다. Firebase Storage는 간단합니다.
Firebase의 핵심은 “서버 코드를 줄이는 것”입니다. 전통적인 구조에서는 클라이언트가 API 서버에 요청하고, 서버가 인증을 확인한 뒤 DB에 접근합니다. Firebase에서는 클라이언트가 SDK로 DB와 스토리지에 직접 접근하고, 서버가 하던 권한 검사를 Security Rules가 대신합니다. 그래서 백엔드 서버 없이도 앱이 동작하지만, 반대로 말하면 보안 규칙이 곧 서버의 권한 검사 코드 전부입니다. 규칙을 대충 쓰면 누구나 브라우저 개발자 도구에서 SDK를 호출해 다른 사람의 데이터를 읽고 지울 수 있습니다. 이 글 마지막의 Security Rules 절을 가장 중요하게 읽어야 하는 이유입니다.
트레이드오프도 분명합니다. Firestore는 조인과 집계가 약한 문서형 NoSQL이라 관계가 복잡한 데이터는 비정규화(데이터 중복 저장)로 풀어야 하고, 과금이 문서 읽기 횟수 기준이라 쿼리 설계가 곧 비용 설계가 됩니다. 또 특정 벤더에 강하게 묶이므로, 나중에 다른 인프라로 옮기려면 데이터 모델부터 다시 설계해야 할 수 있습니다.
Firebase란?
핵심 특징
Firebase는 Google의 백엔드 서비스 플랫폼입니다. 주요 서비스:
-
Authentication: 인증
-
Firestore: NoSQL 데이터베이스
-
Storage: 파일 저장소
-
Cloud Functions: 서버리스 함수
-
Hosting: 정적 호스팅
-
Analytics: 사용자 분석 가격:
-
Spark (무료): Firestore 기준 하루 50K reads, 20K writes 등 무료 할당량
-
Blaze (종량제): 무료 할당량 초과분만 과금 (Firestore 읽기는 10만 건당 수 센트 수준, 리전별로 다름)
단가는 자주 바뀌고 리전마다 다르므로 정확한 금액은 공식 요금 페이지에서 확인하세요. 가격표에서 먼저 확인해야 할 것은 단가보다 무엇이 과금 단위인가입니다. Firestore는 쿼리가 반환한 문서 수만큼 읽기가 과금되므로, 목록 화면에서 limit 없이 컬렉션 전체를 가져오거나 실시간 리스너를 여러 화면에 중복으로 걸면 사용자 수에 비례해 읽기가 폭증합니다. 또 Cloud Functions와 예약 함수(스케줄러)는 Blaze 요금제에서만 배포할 수 있어서, 무료 요금제로 시작한 프로젝트가 6장에서 막히는 경우가 많습니다. Blaze로 전환한다면 Google Cloud 콘솔에서 예산 알림을 먼저 설정해 두는 것이 안전합니다.
프로젝트 설정
Firebase 프로젝트 생성
- https://console.firebase.google.com/
- “프로젝트 추가” 클릭
- 프로젝트 이름 입력
SDK 설치
npm install firebase
초기화
// src/lib/firebase.ts
import { initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';
import { getFirestore } from 'firebase/firestore';
import { getStorage } from 'firebase/storage';
const firebaseConfig = {
apiKey: "YOUR_API_KEY",
authDomain: "your-app.firebaseapp.com",
projectId: "your-project-id",
storageBucket: "your-app.appspot.com",
messagingSenderId: "123456789",
appId: "1:123456789:web:abcdef",
};
const app = initializeApp(firebaseConfig);
export const auth = getAuth(app);
export const db = getFirestore(app);
export const storage = getStorage(app);
firebaseConfig에 apiKey가 있어서 “키를 소스에 넣어도 되나?” 하는 질문이 많습니다. Firebase 웹 설정의 apiKey는 비밀번호가 아니라 프로젝트를 식별하는 값이라, 번들된 JavaScript로 누구나 볼 수 있는 것이 정상입니다. 데이터를 보호하는 것은 이 키를 숨기는 것이 아니라 Security Rules입니다. 그래도 남용을 줄이려면 Google Cloud 콘솔에서 키에 HTTP 리퍼러 제한을 걸고, App Check로 정식 앱에서 온 요청만 허용하는 방법을 함께 씁니다. 반면 서버에서 쓰는 서비스 계정 키(JSON)는 모든 규칙을 우회하는 관리자 권한이므로 절대 클라이언트나 저장소에 넣으면 안 됩니다.
firebase/app, firebase/auth처럼 모듈별로 import하는 것은 v9 이후의 모듈식 SDK 방식입니다. 필요한 함수만 번들에 포함되어(tree-shaking) 예전 firebase.auth() 스타일보다 번들 크기가 크게 줄어듭니다. 인터넷에 있는 예제 중 상당수가 옛 네임스페이스 방식이라, 섞어 쓰면 “firebase.auth is not a function” 같은 에러가 납니다. 또 2024년 이후 새로 만든 프로젝트는 storageBucket이 your-app.firebasestorage.app 형식이므로, 콘솔에 표시된 설정 값을 그대로 복사해 쓰세요.
Authentication
이메일/비밀번호
import { auth } from './lib/firebase';
import {
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
signOut,
onAuthStateChanged,
} from 'firebase/auth';
// 회원가입
async function signUp(email: string, password: string) {
const userCredential = await createUserWithEmailAndPassword(auth, email, password);
return userCredential.user;
}
// 로그인
async function signIn(email: string, password: string) {
const userCredential = await signInWithEmailAndPassword(auth, email, password);
return userCredential.user;
}
// 로그아웃
async function logout() {
await signOut(auth);
}
// 인증 상태 감지
onAuthStateChanged(auth, (user) => {
if (user) {
console.log('Logged in:', user.email);
} else {
console.log('Logged out');
}
});
Google 로그인
import { GoogleAuthProvider, signInWithPopup } from 'firebase/auth';
const provider = new GoogleAuthProvider();
async function signInWithGoogle() {
const result = await signInWithPopup(auth, provider);
return result.user;
}
인증 코드에서 가장 자주 하는 실수는 초기 인증 상태를 기다리지 않는 것입니다. 페이지를 새로 고치면 Firebase는 저장된 세션을 비동기로 복원하는데, 그 사이에 auth.currentUser를 읽으면 로그인한 사용자여도 null이 나옵니다. 그래서 새로 고침할 때마다 로그인 화면이 잠깐 깜빡이거나, 보호된 페이지에서 로그인 페이지로 튕기는 문제가 생깁니다. onAuthStateChanged의 첫 번째 콜백이 호출될 때까지 로딩 상태를 두고 라우팅을 결정해야 합니다. 이 함수도 구독 해제 함수를 반환하므로 React라면 useEffect의 정리 함수에서 호출해야 합니다.
에러 처리도 챙겨야 합니다. createUserWithEmailAndPassword는 이미 가입된 이메일이면 auth/email-already-in-use, 비밀번호가 6자 미만이면 auth/weak-password 코드로 예외를 던지므로, error.code를 보고 사용자에게 알맞은 메시지를 보여 줘야 합니다. signInWithPopup은 모바일 브라우저나 팝업 차단 환경에서 auth/popup-blocked로 실패하기 쉬워, 모바일 웹에서는 signInWithRedirect를 쓰는 경우가 많습니다. Google 로그인은 콘솔의 승인된 도메인 목록에 배포 도메인을 추가하지 않으면 auth/unauthorized-domain 에러가 나는데, 로컬에서는 잘 되다가 배포 후에만 실패하는 전형적인 원인입니다.
Firestore
데이터 추가
import { db } from './lib/firebase';
import { collection, addDoc, doc, setDoc } from 'firebase/firestore';
// 자동 ID
const docRef = await addDoc(collection(db, 'users'), {
name: 'John',
email: '[email protected]',
createdAt: new Date(),
});
// 커스텀 ID
await setDoc(doc(db, 'users', 'user123'), {
name: 'John',
email: '[email protected]',
});
addDoc은 Firestore가 무작위 ID를 만들어 주고, setDoc은 내가 정한 ID로 문서를 씁니다. 사용자 프로필처럼 인증 UID와 1:1로 대응하는 문서는 setDoc(doc(db, 'users', user.uid), ...)처럼 UID를 문서 ID로 쓰는 것이 관례입니다. 그래야 Security Rules에서 request.auth.uid == userId로 본인 문서만 허용하는 규칙을 간단히 쓸 수 있습니다. setDoc은 기본적으로 문서를 통째로 덮어쓰므로, 일부 필드만 바꾸려면 updateDoc이나 setDoc(ref, data, { merge: true })를 써야 합니다. 이를 모르고 setDoc으로 이름만 저장하면 기존의 이메일 필드가 사라집니다.
createdAt: new Date()는 클라이언트 기기의 시계를 기준으로 하므로, 시계가 틀린 기기에서 쓴 글은 정렬이 엉망이 됩니다. 순서가 중요한 타임스탬프는 serverTimestamp()를 써서 서버 시각으로 기록하세요.
데이터 조회
import { collection, getDocs, doc, getDoc, query, where, orderBy, limit } from 'firebase/firestore';
// 전체 조회
const querySnapshot = await getDocs(collection(db, 'users'));
querySnapshot.forEach((doc) => {
console.log(doc.id, doc.data());
});
// 단일 조회
const docSnap = await getDoc(doc(db, 'users', 'user123'));
if (docSnap.exists()) {
console.log(docSnap.data());
}
// 쿼리
const q = query(
collection(db, 'posts'),
where('published', '==', true),
orderBy('createdAt', 'desc'),
limit(10)
);
const snapshot = await getDocs(q);
첫 번째 “전체 조회”는 예제로는 편하지만 운영에서는 피해야 하는 패턴입니다. 컬렉션에 문서가 10만 개면 한 번 호출에 10만 건의 읽기가 과금되고, 네트워크로 모두 내려받습니다. 목록은 항상 limit과 startAfter 커서로 페이지를 나눠 가져오세요.
마지막 쿼리는 where(등호)와 orderBy(다른 필드)를 조합하므로 복합 인덱스가 필요합니다. 인덱스 없이 실행하면 “FAILED_PRECONDITION: The query requires an index. You can create it here: https://console.firebase.google.com/…” 에러가 나고, 메시지 안의 링크를 누르면 필요한 인덱스를 바로 만들 수 있습니다. 인덱스 생성에는 몇 분이 걸리며, 이렇게 만든 인덱스는 firebase firestore:indexes로 firestore.indexes.json에 내보내 두어야 다른 환경에 배포할 때 빠지지 않습니다. Firestore는 부등호 필터에 대한 제약, OR 조건과 in 쿼리의 개수 제한 등 SQL에 익숙한 사람이 예상하지 못하는 제약이 있어서, 화면에 필요한 쿼리를 먼저 정하고 그에 맞춰 데이터 구조를 설계하는 것이 Firestore를 쓰는 올바른 순서입니다.
실시간 리스너
import { onSnapshot } from 'firebase/firestore';
const unsubscribe = onSnapshot(collection(db, 'posts'), (snapshot) => {
const posts = snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
}));
console.log('Posts:', posts);
});
// 구독 해제
unsubscribe();
예제는 구독 직후 바로 해제하지만, 실제로는 화면이 사라질 때 해제해야 합니다. React라면 useEffect(() => { const unsub = onSnapshot(...); return unsub; }, [])처럼 정리 함수로 반환합니다. 해제를 빠뜨리면 페이지를 오갈 때마다 리스너가 쌓여, 같은 변경에 콜백이 여러 번 호출되고 읽기 과금도 그만큼 늘어납니다. 리스너가 처음 연결될 때 결과 전체가 읽기로 과금되고, 이후에는 바뀐 문서만 과금된다는 점도 알아 두면 비용 예측에 도움이 됩니다. 이 예제처럼 필터 없이 컬렉션 전체에 리스너를 걸면 첫 연결 비용이 컬렉션 크기만큼 들므로, 실시간 리스너에도 query와 limit을 함께 쓰세요.
Firestore 웹 SDK는 오프라인 상태에서 쓴 데이터를 로컬에 먼저 반영하고 연결이 복구되면 서버로 보냅니다. 그래서 onSnapshot 콜백이 서버 확인 전에 한 번, 확인 후에 한 번 호출될 수 있으며, snapshot.metadata.hasPendingWrites로 둘을 구분할 수 있습니다.
Storage
파일 업로드
import { auth, storage } from './lib/firebase';
import { ref, uploadBytes, getDownloadURL } from 'firebase/storage';
async function uploadFile(file: File) {
const uid = auth.currentUser?.uid;
if (!uid) throw new Error('로그인이 필요합니다');
// 8장 Storage Rules의 uploads/{userId}/{fileName} 경로와 맞춤
const storageRef = ref(storage, `uploads/${uid}/${Date.now()}-${file.name}`);
const snapshot = await uploadBytes(storageRef, file);
const downloadURL = await getDownloadURL(snapshot.ref);
return downloadURL;
}
업로드 경로에 사용자 UID를 넣은 것은 보안 규칙과 맞추기 위해서입니다. 경로를 uploads/${Date.now()}-${file.name}처럼 UID 없이 만들면, 8장의 규칙 match /uploads/{userId}/{fileName}에 해당하는 경로가 아니라서 업로드가 거부되고 storage/unauthorized 에러가 납니다. 규칙과 코드의 경로 구조가 어긋나는 것은 Storage를 처음 설정할 때 가장 흔한 실패 원인입니다. 파일 이름 앞에 Date.now()를 붙인 것은 같은 이름의 파일이 덮어써지는 것을 막기 위해서이고, 여러 사용자가 동시에 올리는 경우까지 고려하면 crypto.randomUUID()가 더 확실합니다.
getDownloadURL이 반환하는 URL에는 접근 토큰이 포함되어 있어서, 이 URL을 아는 사람은 보안 규칙과 상관없이 파일을 내려받을 수 있습니다. 공개 이미지라면 괜찮지만, 비공개 문서라면 URL을 DB에 저장해 공유하는 대신 필요할 때마다 권한을 확인하고 받게 하거나 서명된 URL을 짧은 유효기간으로 발급하는 방식을 고려해야 합니다. 큰 파일이라면 진행률을 보여 줄 수 있고 중단 후 재개가 가능한 uploadBytesResumable이 사용자 경험에 더 좋습니다.
React 예제
import { useState } from 'react';
function FileUpload() {
const [url, setUrl] = useState('');
const [uploading, setUploading] = useState(false);
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setUploading(true);
const downloadURL = await uploadFile(file);
setUrl(downloadURL);
setUploading(false);
};
return (
<div>
<input type="file" onChange={handleUpload} disabled={uploading} />
{uploading && <p>Uploading...</p>}
{url && <img src={url} alt="Uploaded" />}
</div>
);
}
이 컴포넌트는 업로드가 실패하면 영원히 “Uploading…” 상태로 남습니다. uploadFile이 예외를 던지면 setUploading(false)에 도달하지 못하기 때문입니다. try { ... } catch (err) { /* 에러 표시 */ } finally { setUploading(false); }로 감싸야 합니다. 파일 형식과 크기도 클라이언트에서 한 번(file.type.startsWith('image/'), file.size) 확인하는 것이 사용자 경험에 좋지만, 클라이언트 검사는 우회할 수 있으므로 진짜 제한은 Storage Rules의 request.resource.size < 5 * 1024 * 1024, request.resource.contentType.matches('image/.*') 같은 조건으로 걸어야 합니다.
Cloud Functions
설치
npm install -g firebase-tools
firebase login
firebase init functions
함수 작성
// functions/src/index.ts
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
admin.initializeApp();
// HTTP 함수
export const helloWorld = functions.https.onRequest((request, response) => {
response.json({ message: 'Hello from Firebase!' });
});
// Firestore 트리거
export const onUserCreate = functions.firestore
.document('users/{userId}')
.onCreate(async (snap, context) => {
const user = snap.data();
console.log('New user:', user);
// 환영 이메일 전송 등
await admin.firestore().collection('emails').add({
to: user.email,
subject: 'Welcome!',
body: 'Thanks for signing up!',
});
});
// 스케줄 함수
export const dailyCleanup = functions.pubsub
.schedule('0 0 * * *') // 매일 자정
.onRun(async (context) => {
console.log('Running daily cleanup');
// 정리 작업
});
위 코드는 오랫동안 쓰여 온 1세대(v1) API 형식입니다. 최신 firebase-functions 패키지(v6 이상)에서는 기본 import가 2세대 API로 바뀌어, functions.firestore.document(...)를 그대로 쓰면 에러가 납니다. 이 코드를 그대로 쓰려면 import * as functions from 'firebase-functions/v1';로 가져오고, 새 프로젝트라면 2세대 API(import { onDocumentCreated } from 'firebase-functions/v2/firestore';, onSchedule, onRequest)로 작성하는 편이 좋습니다. 2세대는 Cloud Run 기반이라 인스턴스 하나가 여러 요청을 동시에 처리할 수 있어 콜드 스타트와 비용 면에서 유리합니다. 앞에서 말했듯 Cloud Functions 배포에는 Blaze 요금제가 필요합니다.
Firestore 트리거는 최소 한 번(at-least-once) 실행을 보장할 뿐 정확히 한 번을 보장하지 않습니다. 드물게 같은 이벤트로 함수가 두 번 실행될 수 있으므로, 위 예제처럼 환영 이메일 문서를 추가하는 함수는 이메일이 두 번 나갈 수 있습니다. 이벤트 ID(context.eventId)를 문서 ID로 써서 같은 이벤트는 한 번만 처리되게 만드는 등 멱등성을 확보해야 합니다. 또 트리거가 자기가 감시하는 컬렉션에 다시 쓰면 무한 루프가 되어 요금이 폭증할 수 있으니 주의하세요. 스케줄 함수의 '0 0 * * *'는 기본적으로 UTC 자정(한국 시각 오전 9시)이라, 한국 기준 자정에 돌리려면 .timeZone('Asia/Seoul')을 지정해야 합니다. emails 컬렉션에 문서를 쓰는 것만으로 메일이 나가지는 않으며, Trigger Email 확장 프로그램 같은 발송 처리가 따로 필요합니다.
배포
firebase deploy --only functions
Hosting
배포
# 초기화
firebase init hosting
# 빌드
npm run build
# 배포
firebase deploy --only hosting
firebase.json
{
"hosting": {
"public": "dist",
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
"rewrites": [
{
"source": "**",
"destination": "/index.html"
}
],
"headers": [
{
"source": "**/*.@(jpg|jpeg|gif|png|webp)",
"headers": [
{
"key": "Cache-Control",
"value": "max-age=31536000"
}
]
}
]
}
}
rewrites의 "source": "**" → /index.html 규칙은 React Router 같은 클라이언트 라우팅 SPA를 위한 설정입니다. 이 규칙이 없으면 /posts/123 주소로 바로 들어오거나 새로 고침할 때 해당 경로의 파일이 없어 404가 납니다. 반대로 Astro나 Next.js 정적 내보내기처럼 페이지마다 HTML 파일이 따로 있는 사이트에 이 규칙을 넣으면, 없는 경로도 모두 홈 화면을 보여 줘서 404가 사라지고 검색 엔진이 “소프트 404”로 판단할 수 있습니다. public 값은 빌드 결과 폴더(Vite는 dist, CRA는 build)와 맞아야 하며, 틀리면 배포는 성공하는데 Firebase 기본 환영 페이지만 보입니다.
이미지에 max-age=31536000(1년)을 거는 것은 파일 이름에 해시가 들어가 내용이 바뀌면 이름도 바뀌는 경우에만 안전합니다. 같은 이름으로 이미지를 교체하는 사이트라면 사용자는 1년 동안 옛 이미지를 보게 됩니다. index.html은 반대로 캐시를 짧게 둬야 새 배포가 바로 반영됩니다.
Security Rules
Firestore Rules
// firestore.rules
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// 공개 읽기, 인증된 사용자만 쓰기
match /posts/{postId} {
allow read: if true;
allow write: if request.auth != null;
}
// 본인 데이터만 접근
match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
}
}
이 규칙에는 실무에서 그대로 쓰면 안 되는 구멍이 있습니다. posts의 allow write: if request.auth != null은 “로그인만 하면 누구의 글이든 쓰고 고치고 지울 수 있다”는 뜻입니다. 가입만 하면 다른 사람의 게시글을 삭제할 수 있는 셈입니다. 쓰기는 create, update, delete로 나누고, 생성할 때는 request.resource.data.authorId == request.auth.uid로 작성자를 본인으로만 기록하게 하며, 수정·삭제는 resource.data.authorId == request.auth.uid로 기존 작성자만 허용해야 합니다. 여기서 request.resource는 쓰려는 새 데이터, resource는 이미 저장된 데이터라는 차이가 핵심입니다.
필드 검증도 규칙의 몫입니다. 클라이언트가 role: "admin" 같은 필드를 몰래 넣는 것을 막으려면 request.resource.data.keys().hasOnly(['title', 'body', 'authorId', 'createdAt'])처럼 허용 필드를 명시하고, 타입과 길이(request.resource.data.title is string && request.resource.data.title.size() < 200)를 검사합니다. 규칙은 필터가 아니라는 점도 알아 둬야 합니다. 본인 글만 읽을 수 있는 규칙이 있을 때 where('authorId', '==', uid) 없이 컬렉션 전체를 쿼리하면, 본인 글만 걸러 주는 것이 아니라 쿼리 전체가 “Missing or insufficient permissions”로 실패합니다. 쿼리 조건이 규칙을 만족함을 증명할 수 있어야 합니다.
제가 Firebase 프로젝트를 점검할 때 가장 먼저 보는 것이 콘솔의 규칙 탭입니다. 개발 초기에 “테스트 모드”로 시작하면 allow read, write: if request.time < timestamp.date(...)처럼 기한까지 모두 허용하는 규칙이 들어가는데, 이를 잊고 출시했다가 기한이 지나 갑자기 모든 요청이 거부되거나, 반대로 기한을 늘려 두어 데이터베이스가 통째로 공개된 상태로 운영되는 경우가 드물지 않습니다. 규칙은 firestore.rules 파일로 저장소에 두고 Firebase 에뮬레이터와 @firebase/rules-unit-testing으로 테스트한 뒤 배포하는 것이 안전합니다.
Storage Rules
// storage.rules
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /uploads/{userId}/{fileName} {
allow read: if true;
allow write: if request.auth != null && request.auth.uid == userId;
}
}
}
취업·면접과 연결하기
Firestore 규칙·Auth·Functions는 BaaS·보안 질문으로 잘 이어집니다. 개발자 기술 면접 준비: 알고리즘부터 시스템 설계까지와, 이력서에 서비스 규모·기능을 쓰는 법은 개발자 이력서·서류·면접 가이드를 보세요.
정리 및 체크리스트
핵심 요약
- Firebase: Google의 BaaS 플랫폼
- Authentication: 간편한 인증
- Firestore: 실시간 NoSQL DB
- Storage: 파일 저장소
- Cloud Functions: 서버리스
- Hosting: 정적 호스팅
구현 체크리스트
- Firebase 프로젝트 생성
- Authentication 구현
- Firestore CRUD 구현
- Storage 파일 업로드
- Cloud Functions 작성
- Security Rules 설정
- Hosting 배포
같이 보면 좋은 글
자주 묻는 질문 (FAQ)
Q. Firebase vs Supabase, 어떤 게 나은가요?
A. Firebase는 더 성숙하고 기능이 많습니다. Supabase는 오픈소스이고 PostgreSQL을 사용합니다. 빠른 개발은 Firebase, SQL이 필요하면 Supabase를 권장합니다.
Q. 비용이 얼마나 나오나요?
A. 무료 플랜으로 시작 가능하고, 트래픽이 늘거나 Cloud Functions가 필요해지면 종량제(Blaze)로 전환합니다. 비용은 사용량에 따라 크게 달라지며, 대부분은 Firestore 읽기 횟수에서 결정됩니다. 목록 쿼리에 limit을 두고, 실시간 리스너를 해제하고, 예산 알림을 설정해 두면 예상치 못한 청구를 막을 수 있습니다.
Q. Firebase 앱을 프로덕션에 올리기 전에 무엇을 바꿔야 하나요?
A. Security Rules를 테스트 모드에서 실제 규칙으로 바꾸고, 복합 인덱스와 백업(예약 내보내기), 예산 알림을 준비해 두어야 합니다.
Q. 오프라인 지원이 되나요?
A. 네, Firestore는 오프라인 캐싱을 지원합니다. 모바일 SDK는 기본으로 켜져 있고, 웹은 persistentLocalCache 설정으로 IndexedDB 영속 캐시를 켤 수 있습니다.