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 (파일 저장) 주요 패턴¶
코드에서 사용:
// 업로드
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].ts와functions/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만 바꾸면 다른 강의에서도 재사용 가능.