안녕하자, junetapa다. 오늘은 제가 실제 프로젝트에서 꽤 오래 써온 Firebase Realtime Database에 대해 솔직하게 이야기해보려 한다. "실시간 데이터베이스"라는 이름답게 정말 매력적인 녀석인데, 막상 실전에서 쓰다 보면 알아야 할 것들이 꽤 많거든. 이 글에서는 기본 개념부터 실전 팁, 그리고 장단점까지 빠짐없이 다뤄보겠다.
최신 현황을 반영해 본문을 손봤다. Firebase JavaScript SDK는 v12 대(modular API)가 표준이며, v8 이하의 네임스페이스(체이닝) 방식과 호환용 compat 라이브러리는 향후 메이저 버전에서 제거 예정이다. 본문 예제는 모두 트리 셰이킹이 가능한 모듈러 문법으로 갱신했다. 또한 Blaze 요금제에서 한 프로젝트당 최대 1,000개의 Realtime Database 인스턴스를 만들어 샤딩할 수 있고, 보안 규칙 CLI에 타입 체크와 String.replace() 같은 편의 기능이 추가됐다.
Firebase Realtime Database란 무엇인가
핵심 개념 한눈에 보기
Firebase Realtime Database는 Google이 제공하는 클라우드 기반 NoSQL 데이터베이스다. 가장 큰 특징은 이름 그대로 "실시간 동기화"인데, 클라이언트가 데이터를 변경하면 연결된 모든 기기에 밀리초 단위로 반영된다. REST API나 폴링 없이도 데이터가 알아서 흘러들어오는 셈이다.
데이터는 하나의 거대한 JSON 트리 구조로 저장된다. 관계형 DB의 테이블·행·열 개념 대신, 키-값 쌍이 트리 형태로 중첩되는 구조라서 스키마 없이 유연하게 데이터를 넣고 뺄 수 있다. 이 점이 빠른 프로토타이핑에 아주 유리한다.
Firestore와 뭐가 다를까
Firebase를 처음 접하면 "Realtime Database랑 Cloud Firestore 중 뭘 써야 하지?"라는 고민을 반드시 하게 된다. 간단히 정리하면 이렇다.
- Realtime Database: 단순한 JSON 트리, 낮은 레이턴시, 동시 접속자 관리에 강함. 채팅·실시간 대시보드 등에 적합. 과금은 주로 저장 용량(GB)과 다운로드 대역폭 기준이라 읽기/쓰기가 폭발적으로 많은 앱에서 오히려 저렴할 수 있다.
- Cloud Firestore: 컬렉션-문서 구조, 복합 쿼리 지원, 대규모 데이터에 유리. 일반적인 앱 백엔드에 더 범용적. 과금은 문서 단위 읽기/쓰기/삭제 횟수 기준이다. 2026년부터는 대규모 트래픽을 겨냥한 Firestore Enterprise 에디션이 추가되어, 과금 단위가 문서 단위에서 단위(tranche) 기반으로 바뀌고 실시간 업데이트 비용이 분리되었다.
둘 다 NoSQL이지만, 제 경험상 실시간 반응성이 핵심인 기능에는 Realtime Database가 여전히 한 수 위다. 반면 복잡한 필터링이나 페이지네이션, 정교한 인덱스가 필요하다면 Firestore 쪽이 편하다. 2026년 기준 구글 공식 가이드도 "신규 프로젝트의 기본값은 Firestore, 초저지연 동기화와 잦은 상태 갱신이 핵심이면 Realtime Database"라는 식으로 정리한다. 참고로 같은 프로젝트 안에서 둘을 함께 쓰는 것도 가능하니, 굳이 하나만 고집할 필요는 없다.
어떤 프로젝트에 어울리는가
제가 Realtime Database를 선택했던 프로젝트들의 공통점은 명확한다. 실시간 채팅, 라이브 투표 시스템, 협업 도구의 온라인 상태 표시, IoT 센서 데이터 모니터링처럼 데이터가 빠르게 변하고, 그 변화를 즉시 화면에 반영해야 하는 경우였다.
실전에서 꼭 알아야 할 데이터 설계 전략
플랫하게, 더 플랫하게 — 데이터 비정규화
Realtime Database를 쓸 때 가장 흔히 하는 실수가 관계형 DB처럼 데이터를 깊게 중첩하는 겁니다. 예를 들어 사용자 정보 안에 게시글, 게시글 안에 댓글을 전부 넣으면 사용자 한 명의 데이터를 읽을 때 불필요한 댓글 수천 개까지 함께 내려옵니다.
핵심 원칙은 데이터를 최대한 플랫(flat)하게 유지하는 것다. users, posts, comments를 각각 최상위 노드로 분리하고, 필요한 곳에서 ID로 참조하자. 데이터가 중복되더라도 읽기 성능이 훨씬 좋아집니다. NoSQL에서는 "저장 공간보다 읽기 속도가 비싸다"는 사실을 기억하자.
보안 규칙은 처음부터 잡아라
Firebase 콘솔에서 Realtime Database를 만들 때는 잠금 모드(locked mode)와 테스트 모드 중 하나를 고르게 된다. 테스트 모드를 고르면 일정 기간 동안 누구나 읽고 쓸 수 있는데, 개발 초기에는 편하지만 이대로 배포하면 누구나 여러분의 데이터를 읽고 지울 수 있다. 실제로 이 실수로 데이터가 통째로 날아간 사례를 여러 번 봤다. 2026년에도 "테스트 모드 트랩"은 가장 흔한 사고 원인으로 꼽힌다.
규칙은 프로젝트 초기부터 설정하는 습관을 들이자. .read와 .write를 auth 상태 기반으로 제한하고, .validate로 데이터 형식까지 검증하면 훨씬 안전하다. 아래는 본인 데이터만 읽고 쓰게 막고, 메시지 형식까지 검증하는 실제 규칙 예시다.
"rules": {
"users": {
"$uid": {
".read": "auth != null && auth.uid === $uid",
".write": "auth != null && auth.uid === $uid"
}
},
"messages": {
".indexOn": ["createdAt"],
"$msgId": {
".validate": "newData.hasChildren(['text', 'uid', 'createdAt'])",
"text": { ".validate": "newData.isString() && newData.val().length <= 1000" }
}
}
}
}
규칙은 firebase deploy --only database로 배포한다. 2026년부터 Firebase CLI와 콘솔, 에뮬레이터에서 규칙의 흔한 타입 오류를 미리 잡아주고, 규칙 안에서 String.replace() 같은 문자열 함수도 쓸 수 있게 됐다. 한 프로젝트에 여러 데이터베이스 인스턴스를 둔 경우에도 CLI로 각 인스턴스의 규칙을 따로 배포할 수 있다.
인덱싱으로 쿼리 성능 확보
Realtime Database는 기본적으로 키 기반 정렬만 지원하기 때문에, orderByChild나 equalTo 같은 쿼리를 사용할 때 인덱스를 설정해두지 않으면 경고가 뜨고 성능도 떨어진다. 위 규칙 예시처럼 .indexOn을 활용해서 자주 조회하는 필드에 인덱스를 걸어두자. 단일 인스턴스에는 데이터 크기와 동시 접속 같은 한계가 있으므로, 트래픽이 커지면 데이터를 여러 인스턴스로 쪼개는 샤딩(sharding)을 고려한다. Blaze 요금제 기준 한 프로젝트에서 최대 1,000개 인스턴스까지 만들 수 있다.
실전 활용 팁 — 모듈러 SDK로 읽고 쓰기
2026년 기준 웹에서는 모듈러(modular) Firebase JS SDK v12를 쓰는 것이 표준이다. 과거의 firebase.database().ref() 식 체이닝(네임스페이스) API는 호환용 compat 패키지로만 남아 있고, 신규 기능은 모듈러 쪽에만 추가되니 새로 시작한다면 반드시 모듈러로 가자. 트리 셰이킹 덕분에 번들 크기도 훨씬 작다.
초기화와 데이터 쓰기
먼저 npm install firebase로 설치한 뒤, 필요한 함수만 골라 import한다. 아래는 메시지를 한 건 추가하는 코드다.
import { getDatabase, ref, push, serverTimestamp } from "firebase/database";
const app = initializeApp({
databaseURL: "https://your-project.firebaseio.com",
apiKey: "YOUR_API_KEY",
projectId: "your-project"
});
const db = getDatabase(app);
async function sendMessage(uid, text) {
await push(ref(db, "messages"), {
uid,
text,
createdAt: serverTimestamp()
});
}
실시간으로 구독하기
핵심은 onValue 또는 onChildAdded 리스너다. 한 번 붙여두면 서버 데이터가 바뀔 때마다 콜백이 자동으로 다시 실행된다. 화면을 떠날 때는 반환된 해제 함수를 호출해 누수를 막자.
// 최근 50개 메시지를 createdAt 순으로 구독
const recentMessages = query(
ref(db, "messages"),
orderByChild("createdAt"),
limitToLast(50)
);
const unsubscribe = onValue(recentMessages, (snapshot) => {
const list = [];
snapshot.forEach((child) => {
list.push({ id: child.key, ...child.val() });
});
renderMessages(list); // 받은 데이터로 화면 갱신
});
// 컴포넌트 정리 시점에 호출
// unsubscribe();
오프라인과 원자적 갱신
Realtime Database SDK는 오프라인 캐시를 기본 지원한다. 네트워크가 끊겨도 로컬에 먼저 반영하고, 연결이 복구되면 서버와 동기화한다. 여러 위치를 한꺼번에 일관되게 바꿔야 한다면 update()에 다중 경로(multi-path) 객체를 넘기면 원자적으로 처리된다. 카운터처럼 동시 충돌이 잦은 값은 runTransaction()을 쓰면 안전하다.
// 메시지 본문과 사용자별 마지막 메시지 색인을 동시에 갱신
const updates = {};
updates[`/messages/${msgId}`] = message;
updates[`/userLastMessage/${uid}`] = msgId;
await update(ref(db), updates);
장단점 비교 — 언제 쓰고 언제 피할까
오래 써본 입장에서 솔직하게 정리하면 이렇다. 장점은 분명하다. 첫째, 동기화 지연이 극도로 낮아 채팅·협업·라이브 상태 표시 같은 실시간 기능에서 체감 반응이 빠르다. 둘째, 설정이 간단해서 백엔드 서버 없이도 프로토타입을 며칠 만에 띄울 수 있다. 셋째, 클라이언트 SDK에 오프라인 지원이 내장돼 모바일 환경에서 안정적이다.
단점도 분명하다. 데이터가 단일 JSON 트리라 복합 쿼리와 정렬이 빈약하다. 한 노드를 읽으면 그 아래 전체가 따라 내려오므로 설계가 어긋나면 대역폭이 폭증한다. 그리고 과금이 저장 용량과 다운로드 대역폭 기준이라, 한 화면에서 큰 트리를 통째로 받는 패턴이면 요금이 빠르게 불어난다. 2026년에 흔히 회자되는 "Blaze 요금 폭탄"은 대부분 비효율적인 데이터 설계와 무분별한 리스너에서 비롯된다.
정리하면, 실시간 반응성과 빠른 개발이 우선이면 Realtime Database, 복잡한 쿼리와 대규모 정형 데이터가 우선이면 Firestore다. 둘을 한 프로젝트에서 역할별로 병행하는 것도 2026년 현재 권장되는 현실적인 선택이다.
마무리
Firebase Realtime Database는 "단순한 트리, 빠른 동기화"라는 정체성이 뚜렷한 도구다. 그 결을 따라 데이터를 플랫하게 설계하고, 보안 규칙을 처음부터 잠가두고, 리스너를 꼼꼼히 정리하면 작은 팀이 실시간 기능을 놀랄 만큼 빠르게 만들 수 있다. 반대로 관계형 사고방식으로 깊게 중첩하고 규칙을 미루면 대역폭과 요금, 보안 모두에서 발목을 잡힌다.
2026년 기준 권고를 다시 요약하면 이렇다. 웹은 모듈러 SDK v12로 시작하고, 네임스페이스/compat은 피한다. 신규 프로젝트의 기본은 Firestore지만 초저지연 동기화가 핵심이면 Realtime Database가 여전히 강하다. 보안 규칙은 테스트 모드로 배포하지 말고, CLI 타입 체크와 검증 규칙을 적극 활용하자. 트래픽이 커지면 샤딩으로 인스턴스를 늘려 한계를 넘기면 된다. 도구의 성격을 이해하고 쓰면, Realtime Database는 지금도 충분히 매력적인 선택지다.