Use Cases

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/필요한 복합 인덱스를 포함하세요.
"
화면whereorderBy인덱스
글 목록status == "published"publishedAt descstatus, publishedAt desc
작성자authorId, statuspublishedAt descauthorId, status, publishedAt desc
관리자status == "draft"updatedAt descstatus, 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로 쿼리 표를 만들게 한 단계였습니다.

#claude-code #gcp #firestore #database #typescript #query-design
무료 제공

무료 PDF: 5분 완성 Claude Code 치트시트

이메일 주소만 등록하시면 A4 한 장짜리 치트시트 PDF를 즉시 보내드립니다.

개인정보는 엄격하게 관리하며 스팸은 보내지 않습니다.

Claude Code 워크플로우를 한 단계 업그레이드하세요

지금 바로 Claude Code에 복사해 쓸 수 있는 검증된 프롬프트 템플릿 50선.

Masa

이 글을 작성한 사람

Masa

Claude Code를 적극 활용하는 엔지니어. 10개 언어, 2,000페이지 이상의 테크 미디어 claudecode-lab.com을 운영 중.