콘텐츠로 이동

Cloudflare Pages Functions + D1 + R2

개념

Astro 같은 정적 사이트에 서버 기능을 붙이는 방법. 별도 서버 없이 Cloudflare 인프라만으로 API + DB + 파일저장까지 가능.

비유: 정적 사이트가 "전단지"라면, Pages Functions는 "전단지 뒤에 붙인 접수 창구"

구성 요소

서비스 역할 비유 무료 범위
Pages 사이트 호스팅 웹사이트 건물 무제한
Pages Functions API 서버 건물 안 접수 창구 하루 10만 요청
D1 SQLite DB 서류 보관함 하루 500만 읽기
R2 파일 저장소 사진 보관함 10GB, 트래픽 무료

프로젝트 구조

my-site/
  src/              # Astro 소스
  dist/             # 빌드 결과물
  functions/        # ← 이 폴더가 Pages Functions
    api/
      boards/
        [boardId].ts    # /api/boards/abc → params.boardId = "abc"
      upload.ts         # /api/upload
  wrangler.toml     # D1, R2 바인딩 설정

핵심: functions/ 폴더에 ts 파일을 넣으면 자동으로 API 엔드포인트가 됨. 파일 경로 = URL 경로. [param] = 동적 경로.

wrangler.toml (바인딩)

name = "my-project"
pages_build_output_dir = "dist"

[[d1_databases]]
binding = "DB"          # 코드에서 env.DB로 접근
database_name = "my-db"
database_id = "xxx-xxx"

[[r2_buckets]]
binding = "IMAGES"      # 코드에서 env.IMAGES로 접근
bucket_name = "my-bucket"

Pages Function 기본 형태

interface Env {
  DB: D1Database;
  IMAGES: R2Bucket;
}

export const onRequest: PagesFunction<Env> = async (context) => {
  const { request, env, params } = context;

  // request: HTTP 요청 정보
  // env: D1, R2 등 바인딩
  // params: URL 동적 파라미터 ([boardId] 등)

  if (request.method === 'GET') {
    const { results } = await env.DB.prepare(
      'SELECT * FROM items WHERE id = ?'
    ).bind(params.id).all();

    return new Response(JSON.stringify(results), {
      headers: { 'Content-Type': 'application/json' },
    });
  }
};

D1 (SQLite) 주요 명령

# DB 생성
npx wrangler d1 create my-db

# 스키마 적용
npx wrangler d1 execute my-db --remote --file=schema.sql

# SQL 직접 실행
npx wrangler d1 execute my-db --remote --command="SELECT * FROM items;"

# 컬럼 추가
npx wrangler d1 execute my-db --remote --command="ALTER TABLE items ADD COLUMN name TEXT;"

코드에서 사용:

// 조회
const { results } = await env.DB.prepare('SELECT * FROM items WHERE id = ?').bind(id).all();

// 단건 조회
const item = await env.DB.prepare('SELECT * FROM items WHERE id = ?').bind(id).first();

// 삽입
await env.DB.prepare('INSERT INTO items (id, name) VALUES (?, ?)').bind(id, name).run();

// 수정
await env.DB.prepare('UPDATE items SET name = ? WHERE id = ?').bind(name, id).run();

// 삭제
await env.DB.prepare('DELETE FROM items WHERE id = ?').bind(id).run();

R2 (파일 저장) 주요 패턴

# 버킷 생성
npx wrangler r2 bucket create my-bucket

코드에서 사용:

// 업로드
await env.IMAGES.put(key, file.stream(), {
  httpMetadata: { contentType: file.type },
});

// 다운로드 (서빙)
const object = await env.IMAGES.get(key);
return new Response(object.body, {
  headers: { 'Content-Type': object.httpMetadata?.contentType || 'image/png' },
});

// 삭제
await env.IMAGES.delete(key);

배포

# 빌드 + 배포 (수동)
npm run build
npx wrangler pages deploy dist --project-name my-project --commit-dirty=true

# Pages 프로젝트 생성 (최초 1회)
npx wrangler pages project create my-project --production-branch master

GitHub 자동 배포: 클플 대시보드 > Pages > Settings > Git 연결

실전 삽질 방지

1. 디렉토리 구조 주의

  • functions/api/cards/[cardId].tsfunctions/api/cards/[cardId]/like.ts를 동시에 쓸 수 없음
  • 해결: [cardId].ts[cardId]/index.ts로 변경

2. D1 멀티라인 SQL

  • wrangler CLI에서 줄바꿈 있는 SQL은 --command로 안 됨
  • 해결: --file=schema.sql로 파일 전달

3. CORS

  • 같은 Pages 프로젝트 안이면 CORS 불필요
  • 다른 사이트에서 호출하려면 Access-Control-Allow-Origin: * 필수

4. IP 가져오기

  • Cloudflare 환경에서는 request.headers.get('CF-Connecting-IP') 사용
  • 로컬에서는 없으므로 fallback 필요

5. 환경변수

  • 민감한 값(비밀번호 등)은 클플 대시보드 > Pages > Settings > Environment variables
  • 코드에서 env.MY_VAR로 접근

Firebase 대비 장점

항목 Cloudflare Firebase
이미지 트래픽 비용 무료 유료 (다운로드 수 과금)
DB SQLite (익숙) NoSQL (학습 필요)
새 계정 필요 X (이미 사용 중) O (Google 계정 별도)
배포 wrangler 한 줄 firebase deploy

우리 프로젝트에서 쓴 구조

강의 프롬프트 사이트 (Astro 정적)
  └─ /260408-nhis/           프롬프트 페이지
  └─ /260408-nhis/board/     결과물 공유 보드
       ├─ POST /api/boards/{id}     카드 생성
       ├─ GET  /api/boards/{id}     카드 목록
       ├─ PUT  /api/cards/{id}      카드 수정
       ├─ DELETE /api/cards/{id}    카드 삭제 (관리자)
       ├─ POST /api/cards/{id}/like 좋아요 토글
       ├─ POST /api/upload          이미지 업로드 → R2
       └─ GET  /api/images/{key}    R2 이미지 서빙

boardId만 바꾸면 다른 강의에서도 재사용 가능.