Claude Code로 Firestore 설계를 실패하지 않는 방법: 컬렉션보다 쿼리부터
Claude Code로 Firestore 스키마를 쿼리, 인덱스, 비용, 보안 규칙 관점에서 설계하는 실전 워크플로우입니다.
Firestore 설계는 테이블 설계가 아니라 질문 설계입니다
claudecode-lab.com을 운영하는 Masa입니다.
Firestore를 처음 사용할 때 저는 RDB처럼 users, posts, comments부터 만들었습니다. 처음에는 깔끔해 보였지만, 실제 화면 요구사항이 생기자 바로 흔들렸습니다. 최신 공개 글, 작성자별 글, draft 관리 화면, 태그 페이지를 만들려면 처음 만든 스키마가 쿼리와 맞지 않았습니다.
Firestore는 나중에 JOIN으로 정리하는 데이터베이스가 아닙니다. 먼저 앱이 던지는 질문, 즉 쿼리를 정리하고 그 쿼리에 맞춰 문서를 설계해야 합니다.
참고한 공식 문서:
Step 1: Claude Code에 쿼리 목록부터 만들게 합니다
claude -p "
미디어 CMS의 Firestore 설계를 하고 싶습니다.
컬렉션을 제안하기 전에 필요한 쿼리 목록을 표로 만들어 주세요.
화면:
- 공개 글 목록: publishedAt 내림차순
- 작성자 페이지: authorId 기준
- 관리자 draft 목록: updatedAt 내림차순
- 태그 페이지
- 문의 관리: status와 createdAt 기준
where/orderBy/limit/필요한 복합 인덱스를 포함하세요.
"
| 화면 | where | orderBy | 인덱스 |
|---|---|---|---|
| 글 목록 | status == "published" | publishedAt desc | status, publishedAt desc |
| 작성자 | authorId, status | publishedAt desc | authorId, status, publishedAt desc |
| 관리자 | status == "draft" | updatedAt desc | status, updatedAt desc |
Step 2: 목록 표시용 작은 필드는 의도적으로 중복합니다
export interface PostDoc {
id: string;
slug: string;
title: string;
description: string;
status: "draft" | "published" | "archived";
lang: "ja" | "en" | "es" | "ko";
authorId: string;
authorName: string;
authorAvatarUrl: string;
tagSlugs: string[];
tagNames: string[];
publishedAt: FirebaseFirestore.Timestamp | null;
updatedAt: FirebaseFirestore.Timestamp;
createdAt: FirebaseFirestore.Timestamp;
}
중복은 나쁜 것이 아니라 트레이드오프입니다. 목록 20개를 보여주기 위해 작성자 문서를 20번 더 읽는 것보다, 작은 표시 필드를 문서에 저장하는 편이 낫습니다.
Step 3: Zod로 저장 전 검증을 고정합니다
import { z } from "zod";
export const CreatePostSchema = z.object({
slug: z.string().min(3).max(120).regex(/^[a-z0-9-]+$/),
title: z.string().min(1).max(120),
description: z.string().max(160),
lang: z.enum(["ja", "en", "es", "ko"]),
authorId: z.string().min(1),
authorName: z.string().min(1),
authorAvatarUrl: z.string().url(),
tagSlugs: z.array(z.string()).max(8),
tagNames: z.array(z.string()).max(8),
});
export async function createDraftPost(input: unknown) {
const parsed = CreatePostSchema.parse(input);
const ref = db.collection("posts").doc();
await ref.set({
id: ref.id,
...parsed,
status: "draft",
publishedAt: null,
createdAt: FieldValue.serverTimestamp(),
updatedAt: FieldValue.serverTimestamp(),
});
}
Step 4: 인덱스도 Git으로 관리합니다
{
"indexes": [
{
"collectionGroup": "posts",
"queryScope": "COLLECTION",
"fields": [
{ "fieldPath": "lang", "order": "ASCENDING" },
{ "fieldPath": "status", "order": "ASCENDING" },
{ "fieldPath": "publishedAt", "order": "DESCENDING" }
]
}
],
"fieldOverrides": [
{
"collectionGroup": "posts",
"fieldPath": "body",
"indexes": []
}
]
}
Step 5: 보안 규칙은 공격 시나리오와 함께 검토합니다
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function isAdmin() {
return request.auth != null
&& request.auth.token.role == "admin";
}
match /posts/{postId} {
allow read: if resource.data.status == "published" || isAdmin();
allow create, update, delete: if isAdmin();
}
}
}
익명 사용자가 draft를 읽는 경우, 일반 사용자가 status를 바꾸는 경우, 문의 폼에 관리자 필드를 섞는 경우까지 테스트해야 합니다.
실제로 적용해 본 결과
기사 관리, 문의 관리, 로그 저장 설계에 이 흐름을 적용하자 첫 스키마의 절반 이상을 다시 만들었습니다. 가장 효과가 컸던 것은 코드 생성을 요청하기 전에 Claude Code로 쿼리 표를 만들게 한 단계였습니다.
무료 PDF: 5분 완성 Claude Code 치트시트
이메일 주소만 등록하시면 A4 한 장짜리 치트시트 PDF를 즉시 보내드립니다.
개인정보는 엄격하게 관리하며 스팸은 보내지 않습니다.
Claude Code 워크플로우를 한 단계 업그레이드하세요
지금 바로 Claude Code에 복사해 쓸 수 있는 검증된 프롬프트 템플릿 50선.
이 글을 작성한 사람
Masa
Claude Code를 적극 활용하는 엔지니어. 10개 언어, 2,000페이지 이상의 테크 미디어 claudecode-lab.com을 운영 중.
관련 글
Codex Automations란? 잠자는 동안 AI가 콘텐츠 운영을 처리하게 하는 방법
Codex Automations로 트래픽 분석, 주제 선정, 글 작성, CTA 개선, 배포까지 자동화하는 실전 가이드.
Claude Code × GCP Cloud Functions 완전 가이드 | 서버리스 함수 초고속 개발
Claude Code로 GCP Cloud Functions를 효율화. HTTP/Pub/Sub/Firestore 트리거 구현부터 로컬 테스트·배포 자동화까지, Masa의 실무 경험을 토대로 실제 코드로 해설.
Claude Code × GCP Cloud Run 완전 가이드 | 서버리스 컨테이너 자동 배포
Claude Code로 GCP Cloud Run 배포를 빠르게. Dockerfile 생성, 자동 스케일링, CI/CD 파이프라인, Secret Manager 연동까지 실제 코드로 완전 해설.