NestBook / Cloudflare + D1

Cloudflare D1이란?

D1은 Cloudflare가 제공하는 서버리스 SQL 데이터베이스다. 내부적으로 SQLite 엔진을 사용하며, Workers 런타임과 같은 엣지 인프라 위에서 동작한다.

기존에 SQL 데이터베이스를 쓰려면 별도의 서버를 띄우고, 연결 풀을 관리하고, 네트워크 지연을 감수해야 했다. D1은 이 모든 것을 없앤다. wrangler d1 create 한 줄로 DB가 생기고, Workers 코드에서 env.DB.prepare("SELECT ...") 한 줄로 쿼리를 날린다.

일반적인 아키텍처:
  사용자 → CDN → 서버(EC2/GCP) → DB 서버(RDS/Cloud SQL)
                                    ↑ 네트워크 지연, 연결 풀 관리 필요

D1 아키텍처:
  사용자 → Cloudflare 엣지 → Workers + D1
                              ↑ 같은 인프라 위에서 동작, 저지연

KV와 D1의 결정적 차이

앞 챕터에서 KV를 배웠다. D1을 배우기 전에 두 저장소의 차이를 명확히 해두자.

질문KVD1
”user:1001 데이터 줘”✅ 완벽✅ 가능
”나이 30세 이상 사용자 전체 줘”❌ 불가능✅ WHERE age >= 30
”매출 상위 10개 상품 줘”❌ 불가능✅ ORDER BY, LIMIT
”주문과 사용자 테이블 합쳐 줘”❌ 불가능✅ JOIN
”재고 1 감소 + 주문 생성 동시에”❌ 일관성 보장 안 됨✅ 트랜잭션
”초당 수만 건 읽기”✅ 엣지 캐시로 빠름⚠️ 상대적으로 느림

법칙: “조건, 정렬, 집계, JOIN이 필요하면 D1. 단순 키 조회·캐싱이면 KV.”


D1의 특성 — SQLite 기반임을 이해하기

D1은 PostgreSQL이나 MySQL이 아닌 SQLite다. 이것이 의미하는 바:

장점

  • 별도 서버 프로세스 없음 — Workers와 같은 프로세스 내에서 동작, 연결 오버헤드 0
  • 경량 — SQLite 파일은 수 MB부터 시작, 필요한 만큼만 커짐
  • 트랜잭션 지원 — ACID 보장
  • 표준 SQL — 대부분의 SQL 문법 그대로 사용 가능

한계

  • 단일 쓰기 노드 — 쓰기(INSERT/UPDATE/DELETE)는 하나의 주 노드(Primary)에서 처리. 초당 수천 건 이상의 쓰기 부하에는 적합하지 않음
  • 읽기 복제 — 읽기는 여러 복제본에서 처리되어 확장성 있음
  • 파일 크기 제한 — 데이터베이스 하나당 최대 10GB (2024 기준)
  • 동시 접속 수 — 초당 수천 건의 쓰기가 필요한 경우 Hyperdrive + PostgreSQL을 고려
💡 D1은 어떤 규모에 적합한가
스타트업, 사이드 프로젝트, B2B SaaS 등 DAU 수십만 명 이하의 서비스에 완벽히 적합하다. 초당 수만 건의 쓰기가 필요한 소셜 미디어 피드 같은 워크로드는 D1보다 Cloudflare Queues + Hyperdrive 조합을 검토한다.

D1 요금

항목무료 (Workers Free)유료 (Workers Paid, $5/월)
읽기 행 수500만 행/일250억 행/월 포함
쓰기 행 수10만 행/일5천만 행/월 포함
스토리지5GB5GB 포함, 이후 $0.75/GB/월
데이터베이스 수최대 10개최대 50,000개

무료 플랜으로도 웬만한 사이드 프로젝트는 충분히 운용할 수 있다.


D1 데이터베이스 생성 — wrangler d1 create

1. Workers 프로젝트 준비

D1은 단독으로 존재하지 않는다. Workers 프로젝트에 바인딩해서 사용한다. 이미 Workers 프로젝트가 있다고 가정한다 (없으면 npm create cloudflare@latest 로 생성).

# 프로젝트 디렉토리 확인
ls
# wrangler.toml  package.json  src/index.ts

2. D1 데이터베이스 생성

wrangler d1 create my-app-db

# 출력:
# ⛅️ wrangler 3.x.x
# -------------------
# ✅ Successfully created DB 'my-app-db' in region APAC
# Created your new D1 database.
#
# [[d1_databases]]
# binding = "DB"                          # 코드에서 env.DB 로 접근
# database_name = "my-app-db"
# database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"  ← 복사 필요

database_id는 이 DB의 고유 식별자다. 반드시 메모해 둔다.

3. wrangler.toml에 바인딩 추가

name = "my-worker"
main = "src/index.ts"
compatibility_date = "2024-09-23"

# D1 바인딩 추가
[[d1_databases]]
binding = "DB"                    # 코드에서 env.DB 로 접근
database_name = "my-app-db"       # D1 콘솔에 표시되는 이름
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"  # 위에서 복사한 ID
⚠️ binding 이름은 자유롭게 정할 수 있다
binding = "DB"로 설정하면 코드에서 env.DB로 접근한다. binding = "MY_DATABASE"로 하면 env.MY_DATABASE가 된다. 하나의 Worker에 여러 D1 DB를 바인딩할 때는 이름을 다르게 설정한다.

4. TypeScript 타입 생성

wrangler types

# worker-configuration.d.ts 파일이 자동 생성된다:
# interface Env {
#   DB: D1Database;
# }

첫 번째 쿼리 실행해보기

바인딩이 완료되면 코드에서 즉시 쿼리를 날릴 수 있다.

// src/index.ts
export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    // D1 버전 확인 — SQLite 버전이 출력된다
    const result = await env.DB.prepare('SELECT sqlite_version()').first();
    
    return Response.json({
      message: 'D1 연결 성공!',
      sqliteVersion: result,
    });
  },
};
# 로컬에서 실행
wrangler dev

# 브라우저에서 http://localhost:8787 접속 시:
# { "message": "D1 연결 성공!", "sqliteVersion": { "sqlite_version()": "3.46.0" } }

D1 데이터베이스 목록 & 관리

# 생성된 D1 DB 목록 확인
wrangler d1 list

# 출력:
# ┌──────────────────────────────────────┬─────────────┬──────────┐
# │ uuid                                 │ name        │ version  │
# ├──────────────────────────────────────┼─────────────┼──────────┤
# │ xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx │ my-app-db   │ beta     │
# └──────────────────────────────────────┴─────────────┴──────────┘

# DB 상세 정보 확인
wrangler d1 info my-app-db

# SQL 쿼리 직접 실행 (로컬)
wrangler d1 execute my-app-db --local --command="SELECT 1+1 AS result"

# SQL 쿼리 직접 실행 (프로덕션 원격)
wrangler d1 execute my-app-db --command="SELECT 1+1 AS result"

로컬 개발 vs 프로덕션 D1

wrangler dev로 로컬 개발 시 D1은 로컬 SQLite 파일을 사용한다. 실제 Cloudflare 서버의 D1과 분리되어 있어 개발 중 데이터를 마음껏 건드려도 프로덕션에 영향이 없다.

로컬 개발 (wrangler dev):
  Workers 코드 → 로컬 SQLite (.wrangler/state/d1/my-app-db.sqlite3)

프로덕션 배포 (wrangler deploy):
  Workers 코드 → Cloudflare D1 (원격 DB)
# 로컬 D1에 직접 SQL 실행
wrangler d1 execute my-app-db --local --command="CREATE TABLE test (id INTEGER PRIMARY KEY)"

# 프로덕션 D1에 직접 SQL 실행
wrangler d1 execute my-app-db --command="SELECT COUNT(*) FROM users"
📌 로컬 DB 파일 위치
로컬 D1 파일은 .wrangler/state/v3/d1/ 하위에 생성된다. DB Browser for SQLite 같은 도구로 이 파일을 직접 열어 데이터를 확인할 수 있다. 개발 중 데이터를 초기화하고 싶으면 이 폴더를 삭제하면 된다.

Cloudflare 대시보드에서 D1 확인

CLI 외에 웹 대시보드에서도 D1을 관리할 수 있다.

  1. dash.cloudflare.com 로그인
  2. 왼쪽 메뉴 → Workers & PagesD1
  3. 생성한 DB 클릭 → Console 탭에서 SQL 직접 실행 가능
  4. Tables 탭에서 테이블 구조와 데이터 조회 가능

초보자 흔한 오해와 주의사항

오해 1: “D1은 느리다” Workers와 같은 인프라에서 동작하기 때문에 일반적인 클라우드 DB(EC2 → RDS)보다 오히려 빠를 수 있다. 단, 쓰기는 Primary 노드에서 처리되므로 읽기보다 상대적으로 느리다.

오해 2: “wrangler d1 create를 여러 번 실행해도 된다” 실행할 때마다 새로운 DB가 생성된다. 같은 이름으로 실행해도 덮어쓰지 않고 새 DB가 생긴다. wrangler d1 list로 중복 여부를 확인한다.

오해 3: “로컬 D1과 프로덕션 D1은 자동으로 동기화된다” 완전히 분리된 DB다. 스키마(테이블 구조) 변경은 마이그레이션 파일로 양쪽에 모두 적용해야 한다. 이것이 다음 챕터의 주제다.


다음 챕터에서는 D1 스키마 & 마이그레이션을 다룬다. CREATE TABLE로 테이블을 만들고, wrangler d1 migrations로 스키마 변경을 버전 관리하는 방법을 배운다. 프로덕션 DB에서 스키마를 안전하게 변경하는 것은 개발만큼 중요한 기술이다.