Cloudflare Pages 배포: GitHub 연동, Wrangler CLI, Pages Functions와 Vercel·Netlify 비교
이 글의 핵심
Cloudflare Pages로 정적 사이트·SSR 앱을 무료로 배포하고, Edge Functions로 서버 로직을 실행하는 방법을 다룹니다. GitHub 연동, Wrangler CLI, 환경 변수, 커스텀 도메인, 빌드 최적화까지 실전 예제로 설명합니다.
들어가며
Cloudflare Pages는 정적 사이트(Static Site)와 서버 사이드 렌더링(SSR) 앱을 글로벌 Edge 네트워크에 배포할 수 있는 플랫폼입니다. Vercel·Netlify와 비슷하지만, 무료 대역폭 무제한, 300개 이상 도시의 CDN, Workers·D1·R2와의 통합이 강점입니다.
이 글에서는 GitHub 저장소 연동, Wrangler CLI 배포, 환경 변수, 커스텀 도메인, Functions(Edge 서버 로직), 빌드 최적화, 그리고 Vercel과의 비교를 실전 예제로 다룹니다.
먼저 알아 둘 변화가 하나 있습니다. 2025년부터 Cloudflare는 Workers에 정적 자산(static assets)을 함께 올리는 방식을 새 프로젝트의 기본 경로로 권장하고 있고, 새로운 기능(Durable Objects 연동, Cron 트리거, 관측 도구 등)도 Workers 쪽에 먼저 들어갑니다. Pages는 계속 지원되고 기존 프로젝트를 옮길 의무도 없지만, 새로 시작한다면 아래 “Pages냐 Workers냐” 절을 먼저 읽고 결정하는 편이 좋습니다. 이 블로그(pkglog.com)도 Astro 정적 빌드를 Cloudflare Pages에 올려 운영하고 있어서, 본문 중간중간에 직접 겪은 한도와 함정을 적어 두었습니다.
Node.js 앱 배포 전반은 Node.js 배포 가이드 (PM2, Docker, AWS, Nginx)에서, CI/CD 파이프라인은 Node.js + GitHub Actions CI/CD에서 확인할 수 있습니다.
무료 대역폭과 Edge 배포라는 Pages의 성격
Pages의 주요 특징
| 항목 | 설명 |
|---|---|
| CDN | 전 세계 300+ 도시, Cloudflare 네트워크 |
| 무료 플랜 | 월 500회 빌드(동시 빌드 1개), 정적 요청·대역폭 무제한, 배포당 파일 20,000개 |
| 지원 프레임워크 | React, Vue, Astro, Next.js, SvelteKit, Remix 등 |
| SSR/Edge | Cloudflare Workers 기반 Functions |
| 빌드 환경 | Node.js, Python, Ruby, Go 등 |
Pages가 잘 맞는 프로젝트
- 정적 사이트: 블로그, 문서, 랜딩 페이지
- JAMstack: Astro, Hugo, Jekyll, Eleventy
- SSR 앱: Next.js App Router, SvelteKit, Remix
- Edge API: Cloudflare Workers + D1(SQLite) + R2(S3 호환) 트래픽이 글로벌하거나, 대역폭 비용이 걱정되거나, Edge에서 서버 로직을 돌리고 싶다면 Cloudflare Pages가 강력한 선택지입니다.
대시보드에서 GitHub 저장소 연결하기
가장 간단한 방법은 Cloudflare 대시보드에서 GitHub 저장소를 연결하는 것입니다.
빌드 명령과 출력 디렉터리 설정
- Cloudflare 대시보드 → Pages → Create a project
- Connect to Git → GitHub 계정 연동
- 저장소 선택 → 브랜치(
main또는production) - 빌드 설정:
- Framework preset: Astro, Next.js, React 등 자동 감지
- Build command:
npm run build - Build output directory:
dist(Astro·Vite),out(Next.js 정적 export),build(CRA·SvelteKit 정적)
- Save and Deploy
브랜치 푸시별 자동 배포와 프리뷰
main브랜치에 푸시하면 자동으로 빌드·배포- PR마다 Preview 배포 생성 (URL:
<branch>.<project>.pages.dev) - 빌드 로그는 대시보드에서 실시간 확인
장점: 설정이 쉽으며, PR 프리뷰가 자동.
단점: Cloudflare 빌드 환경(무료 500회/월)을 소모하며, 빌드 시간·캐시 제어가 제한적.
Wrangler CLI로 직접 업로드하기
Wrangler는 Cloudflare의 공식 CLI로, 로컬 빌드 → 업로드만 하면 Cloudflare 빌드 횟수를 아낄 수 있습니다.
Wrangler 설치
npm install -g wrangler
# 또는 프로젝트 로컬
npm install --save-dev wrangler
wrangler login 인증
wrangler login
브라우저에서 Cloudflare 계정 인증.
pages deploy 실행
# 로컬에서 빌드
npm run build
# dist 폴더를 Cloudflare Pages에 업로드
wrangler pages deploy dist --project-name=my-project
첫 배포 시 프로젝트가 없으면 자동 생성됩니다.
GitHub Actions에서 Wrangler 사용
name: Deploy to Cloudflare Pages
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
env:
NODE_OPTIONS: '--max-old-space-size=4096'
- name: Deploy to Cloudflare Pages
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy dist --project-name=my-project
필요한 시크릿:
CLOUDFLARE_API_TOKEN: 대시보드 → API Tokens에서 커스텀 토큰 생성, Account → Cloudflare Pages → Edit 권한만 부여 (Workers 템플릿을 그대로 쓰면 필요 이상 권한이 붙음)CLOUDFLARE_ACCOUNT_ID: 대시보드 URL에서 확인 (dash.cloudflare.com/<ACCOUNT_ID>/...) 장점: 빌드 환경 완전 제어, 캐시 전략 자유, Cloudflare 빌드 횟수 절약.
환경 변수와 시크릿 관리
대시보드에서 Production/Preview 변수 설정
Pages 프로젝트 → Settings → Environment variables
- Production:
main브랜치 배포 시 사용 - Preview: PR·브랜치 배포 시 사용
DATABASE_URL=postgresql://...
API_KEY=abc123
빌드 시(npm run build)와 런타임(Functions) 모두 접근 가능합니다.
빌드 타임과 Functions에서 변수 읽기
빌드 타임 (Node.js):
// astro.config.mjs, next.config.js 등
const apiKey = process.env.API_KEY;
런타임 (Cloudflare Functions):
// functions/api/data.js
export async function onRequest(context) {
const apiKey = context.env.API_KEY;
return new Response(JSON.stringify({ key: apiKey }));
}
.dev.vars로 로컬 개발
.dev.vars 파일 (.gitignore에 추가):
DATABASE_URL=postgresql://localhost/dev
API_KEY=dev-key-123
wrangler pages dev dist
커스텀 도메인과 apex 도메인 연결
커스텀 도메인 추가
Pages 프로젝트 → Custom domains → Set up a custom domain
- 도메인 입력 (예:
blog.example.com) - DNS 레코드 추가:
- CNAME:
blog→<project>.pages.dev - 또는 A/AAAA: Cloudflare가 제공하는 IP
- CNAME:
- SSL 인증서 자동 발급 (무료)
Apex 도메인 (example.com)과 CNAME flattening
Cloudflare에서 도메인(존)을 관리 중이라면 CNAME flattening이 자동 적용되어 example.com → <project>.pages.dev CNAME을 그대로 쓸 수 있습니다.
주의할 점은 Pages에 apex 도메인을 붙이려면 네임서버를 Cloudflare로 옮겨야 한다는 것입니다. 외부 DNS(가비아, Route 53 등)를 계속 쓰면서 연결할 수 있는 건 www.example.com 같은 서브도메인(CNAME)뿐입니다. 외부 DNS에 Cloudflare IP를 A 레코드로 박아 두는 방식은 지원되지 않으며, 동작하는 것처럼 보여도 인증서 발급이나 라우팅이 깨질 수 있습니다. 네임서버를 옮기기 어렵다면 www를 정식 주소로 쓰고 apex는 기존 DNS 업체의 포워딩 기능으로 www로 넘기는 구성이 현실적입니다.
Pages Functions로 Edge에서 서버 코드 실행
Cloudflare Pages Functions는 Cloudflare Workers 기반으로, Edge에서 서버 코드를 실행합니다.
functions/ 디렉터리 구조
my-project/
├── functions/
│ ├── api/
│ │ ├── hello.js # /api/hello
│ │ └── users/[id].js # /api/users/:id
│ └── _middleware.js # 모든 요청에 적용
├── public/
└── dist/
API 엔드포인트 만들기
// functions/api/hello.js
export async function onRequest(context) {
const { request, env, params } = context;
return new Response(
JSON.stringify({ message: 'Hello from Edge!' }),
{ headers: { 'Content-Type': 'application/json' } }
);
}
배포 후 https://my-project.pages.dev/api/hello로 접근.
[id].ts 동적 라우트
// functions/api/users/[id].js
export async function onRequest(context) {
const userId = context.params.id;
// D1 (Cloudflare SQLite) 예시
const db = context.env.DB;
const user = await db.prepare('SELECT * FROM users WHERE id = ?')
.bind(userId)
.first();
return new Response(JSON.stringify(user), {
headers: { 'Content-Type': 'application/json' }
});
}
_middleware.ts
// functions/_middleware.js
export async function onRequest(context) {
const start = Date.now();
// 다음 핸들러 실행
const response = await context.next();
// 응답 헤더 추가
response.headers.set('X-Response-Time', `${Date.now() - start}ms`);
return response;
}
Astro·Next.js SSR 어댑터
Astro SSR:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';
export default defineConfig({
output: 'server', // 또는 'hybrid'
adapter: cloudflare()
});
Astro 5부터는 output: 'hybrid'가 없어지고 'static'이 그 역할을 흡수했습니다. 정적 빌드를 기본으로 두고, 서버 렌더링이 필요한 페이지에만 export const prerender = false를 적으면 됩니다.
Next.js: 예전 자료에 나오는 @cloudflare/next-on-pages는 모든 라우트를 Edge 런타임으로 강제해야 했고 Node.js API를 쓰는 라이브러리와 충돌이 잦았습니다. 지금은 OpenNext의 Cloudflare 어댑터가 권장 경로이며, Pages가 아니라 Workers(Node.js 호환 모드)에 배포합니다.
npm install --save-dev @opennextjs/cloudflare wrangler
npx opennextjs-cloudflare build
npx opennextjs-cloudflare deploy
빌드 캐시와 대량 페이지 빌드 시간 줄이기
GitHub Actions 빌드 캐시
- name: Cache dependencies
uses: actions/cache@v4
with:
path: |
node_modules
.astro
.next/cache
key: ${{ runner.os }}-build-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-
병렬 처리로 빌드 시간 단축
병렬 처리:
// scripts/build.mjs
import { exec, execSync } from 'node:child_process';
import { promisify } from 'node:util';
const run = promisify(exec);
const tasks = [
'node scripts/generate-og-images.mjs',
'node scripts/generate-sitemap.mjs',
'node scripts/generate-rss.mjs'
];
// execSync는 끝날 때까지 블로킹하므로 Promise.all로 감싸도 순차 실행됨 → 비동기 exec 사용
await Promise.all(tasks.map((cmd) => run(cmd)));
execSync('astro build', { stdio: 'inherit' });
execSync를 Promise.all에 넣는 코드가 인터넷 예제에 꽤 흔한데, execSync가 반환하는 건 Promise가 아니라 이미 끝난 결과라서 병렬 효과가 전혀 없습니다. 병렬로 돌리려면 위처럼 비동기 exec나 spawn을 써야 합니다.
증분 빌드:
- Astro:
.astro폴더 캐시 - Next.js:
.next/cache폴더 캐시
1,000페이지 이상 사이트의 빌드
문제: 1,000개 이상 페이지 → 빌드 10분+ 해결:
- OG 이미지 캐시: 변경된 글만 재생성
- 정적 페이지 우선:
output: 'static'또는hybrid - 병렬 렌더링: Astro의
build.concurrency(기본값 1)로 페이지를 동시에 렌더링
// astro.config.mjs
export default defineConfig({
output: 'static',
build: {
concurrency: 4 // 기본은 1(순차). 올리면 빨라지지만 메모리 사용량도 늘어남
}
});
이 블로그는 글이 2,000개를 넘어가면서 기본 힙 크기로는 빌드 도중 Node.js가 메모리 부족(heap out of memory)으로 죽기 시작했고, 빌드 스크립트에서 --max-old-space-size=8192를 지정해 해결했습니다. concurrency를 올리면 이 메모리 압박이 더 커지니, 대형 사이트는 병렬도보다 메모리 한도를 먼저 확인하세요. 또 무료 플랜은 배포당 파일 20,000개 제한이 있어서, 글마다 OG 이미지를 여러 장 생성하는 사이트는 페이지 수보다 파일 수가 먼저 한도에 걸립니다.
Vercel·Netlify와 무엇이 다른가
기능 비교표
| 항목 | Cloudflare Pages | Vercel | Netlify |
|---|---|---|---|
| 무료 대역폭 | 정적 자산 무제한 | 월 한도 있음 | 월 한도(크레딧) 있음 |
| 서버 코드 런타임 | Workers (V8 isolate, Node.js 호환 옵션) | Node.js 함수(Fluid compute) 중심, Edge 런타임도 제공 | Node.js Functions + Deno 기반 Edge Functions |
| 데이터 서비스 | D1(SQLite), R2(S3 호환, 송신 요금 없음), KV | 마켓플레이스 연동(Neon, Upstash 등), Blob | Netlify Blobs, DB 연동 |
| Next.js 지원 | OpenNext 어댑터 | 네이티브(개발사) | 어댑터 |
| DX | 보통 | 매우 좋음 | 좋음 |
무료 플랜의 구체적인 수치(빌드 분, 대역폭 GB, 함수 호출 수)는 세 회사 모두 1~2년 주기로 바뀌어 왔으니 도입 시점에 공식 가격 페이지를 확인하세요. 구조적인 차이는 비교적 안정적입니다. Cloudflare는 정적 자산 대역폭에 과금하지 않는 구조라 트래픽이 튀어도 청구서가 놀랍지 않고, Vercel은 Next.js 기능을 가장 먼저·가장 완전하게 지원하며, Netlify는 폼·인증 같은 부가 기능을 플랫폼에 묶어 제공합니다.
Pages냐 Workers냐
새 프로젝트라면 이제 “Cloudflare Pages vs Vercel”보다 “Pages vs Workers(정적 자산)“를 먼저 정해야 합니다. 두 방식 모두 같은 네트워크에서 정적 파일을 서빙하고 무료 정적 요청은 과금되지 않지만, 차이는 이렇습니다.
- Pages: Git 연동 자동 빌드, 브랜치별 프리뷰 URL,
functions/폴더 기반 라우팅이 기본 제공되어 정적 사이트 + 약간의 API에 가장 손이 덜 갑니다. - Workers + static assets:
wrangler.jsonc하나로 정적 자산과 서버 코드를 함께 배포하고, Durable Objects·Queues·Cron·Workflows 같은 Workers 기능을 제약 없이 씁니다. Cloudflare가 신규 기능을 우선 제공하는 쪽이며, Workers Builds로 Git 연동 빌드도 가능해졌습니다.
제 판단 기준은 단순합니다. 서버 코드가 거의 없는 블로그·문서 사이트이고 이미 Pages로 잘 돌고 있다면 옮길 이유가 없습니다. 반대로 SSR 비중이 크거나 Next.js(OpenNext)를 쓰거나 Durable Objects가 필요할 것 같다면 처음부터 Workers로 시작하는 편이 나중에 이전하는 비용을 아낍니다.
어떤 플랫폼을 고를지
Cloudflare Pages를 선택하면 좋은 경우:
- 글로벌 트래픽이 많고 대역폭 비용이 걱정될 때
- Workers·D1·R2 등 Cloudflare 생태계를 쓸 때
- 무료 플랜으로 무제한 대역폭이 필요할 때
- Astro·Hugo 같은 정적 생성기로 블로그를 만들 때 (Astro 블로그 가이드 참고) Vercel을 선택하면 좋은 경우:
- Next.js App Router를 쓰며, 개발자 경험을 최우선할 때
- 빌드 시간이 중요하며, 프리뷰 배포가 많을 때
- Vercel Analytics·Speed Insights를 쓸 때 Netlify를 선택하면 좋은 경우:
- Form 처리, Identity(인증), Split Testing이 필요할 때
- Deno 기반 Edge Functions를 선호할 때
Astro 블로그를 Pages에 올리는 전체 흐름
프로젝트 구조
my-blog/
├── src/
│ ├── pages/
│ ├── content/
│ └── components/
├── public/
├── functions/
│ └── api/
│ └── views.js # 조회수 API
├── astro.config.mjs
├── wrangler.toml
└── package.json
GitHub Actions 워크플로
name: Deploy to Cloudflare Pages
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Validate frontmatter
run: npm run validate
- name: Build
run: npm run build
env:
NODE_OPTIONS: '--max-old-space-size=4096'
- name: Deploy to Cloudflare Pages
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy dist --project-name=my-blog --commit-message="Deploy from GitHub Actions"
Functions + D1로 조회수 API 만들기
// functions/api/views.js
export async function onRequest(context) {
const { request, env } = context;
const url = new URL(request.url);
const slug = url.searchParams.get('slug');
if (!slug) {
return new Response('Missing slug', { status: 400 });
}
// D1 (Cloudflare SQLite)
const db = env.DB;
if (request.method === 'POST') {
// 조회수 증가
await db.prepare('INSERT INTO views (slug, count) VALUES (?, 1) ON CONFLICT(slug) DO UPDATE SET count = count + 1')
.bind(slug)
.run();
}
// 조회수 조회
const result = await db.prepare('SELECT count FROM views WHERE slug = ?')
.bind(slug)
.first();
return new Response(
JSON.stringify({ slug, views: result?.count || 0 }),
{ headers: { 'Content-Type': 'application/json' } }
);
}
D1 바인딩 (wrangler.toml):
name = "my-blog"
pages_build_output_dir = "dist"
[[d1_databases]]
binding = "DB"
database_name = "my-blog-db"
database_id = "abc123..."
_redirects·프리뷰 제한·롤백
_redirects로 리다이렉트
_redirects 파일 (빌드 출력 폴더에 포함):
/old-url /new-url 301
/blog/* /posts/:splat 302
_redirects에는 개수 한도가 있습니다(문서상 정적 규칙 2,000개 + 동적 규칙 100개). 이 블로그는 글을 합치고 URL을 정리하면서 규칙이 1,700개를 넘었는데, 배포는 에러 없이 성공했지만 실제로 요청해 보니 파일 뒤쪽 규칙들이 적용되지 않고 404가 나는 현상을 겪었습니다. 경고가 없어서 발견이 늦었고, 결국 패턴이 반복되는 규칙(/X-en/ → /X/ 같은 것)을 존(zone) 레벨의 Redirect Rules(Bulk Redirects/Single Redirects)로 옮겨 파일을 수백 줄로 줄였습니다. 리다이렉트가 수백 개를 넘어갈 것 같다면 처음부터 대시보드의 Bulk Redirects를 쓰고, _redirects를 쓰더라도 배포 후 마지막 몇 개 규칙을 curl -I로 직접 확인하는 습관을 들이세요.
또는 _headers:
/*
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
_headers도 직관과 다른 점이 있습니다. 여러 규칙이 같은 요청에 매치되면 뒤의 규칙이 앞을 덮어쓰는 게 아니라 같은 헤더 값이 콤마로 합쳐집니다. /*에 Cache-Control: public, max-age=3600을 주고 /assets/*에 max-age=31536000을 따로 주면 두 값이 합쳐진 이상한 헤더가 나가니, 겹치는 패턴에는 같은 헤더를 중복 지정하지 말고 필요하면 ! Cache-Control로 상위 규칙의 헤더를 먼저 제거한 뒤 다시 지정하세요.
프리뷰 브랜치 제한
# .github/workflows/deploy-cloudflare.yml
on:
push:
branches: [main]
# PR 프리뷰는 Cloudflare 자동 배포에 맡기기
빌드 실패 알림
- name: Notify on failure
if: failure()
run: |
curl -X POST ${{ secrets.SLACK_WEBHOOK }} \
-H 'Content-Type: application/json' \
-d '{"text":"Cloudflare Pages 배포 실패!"}'
이전 배포로 롤백
대시보드:
- Deployments → 이전 배포 선택 → Rollback to this deployment
CLI:
wrangler pages deployment list --project-name=my-blog로 배포 목록은 볼 수 있지만, Pages 롤백 자체는 대시보드나 API로 합니다. 롤백은 이전 빌드 산출물로 즉시 되돌리는 것이라 빌드를 다시 돌리지 않아 수 초 만에 끝나고, Git 히스토리와는 별개라서 롤백 후 다음 푸시가 들어오면 그 커밋으로 다시 배포된다는 점을 기억하세요.
Cloudflare Pages 배포 요약
Cloudflare Pages 장점:
- 무료 대역폭 무제한
- 300+ 도시 글로벌 CDN
- Workers·D1·R2 통합
- SSR/Edge Functions 지원 배포 방법:
- GitHub 연동: 가장 쉬움, 자동 프리뷰
- Wrangler CLI: 빌드 제어, 횟수 절약 추천 구성:
- 로컬/CI: GitHub Actions에서 빌드 + Wrangler 업로드
- 프리뷰: Cloudflare 자동 배포
- 환경 변수: 대시보드에서 Production/Preview 분리
- 모니터링: Cloudflare Analytics + Sentry
배포 전후 확인 항목
배포 전:
-
빌드 명령어 확인 (
npm run build) -
출력 디렉터리 확인 (
dist,out,.next) -
환경 변수 설정 (API 키, DB URL)
-
.gitignore에.env,.dev.vars추가 배포 후: -
커스텀 도메인 DNS 전파 확인 (최대 24시간)
-
SSL 인증서 발급 확인
-
Functions 동작 테스트
-
404 페이지 확인
함께 읽을 글과 공식 문서
Cloudflare Pages와 함께 쓰면 좋은 글:
-
Astro + Cloudflare Pages 스택 분석 — Vercel·Netlify·WordPress와 비교
-
기술 블로그 방문자 늘리기 — 배포 후 검색 유입 구조 잡기 참고 자료:
자주 묻는 질문 (FAQ)
Q. _redirects에 넣은 규칙 일부가 동작하지 않아요.
A. 문법 오류가 없는데 파일 뒤쪽 규칙만 안 된다면 개수 한도를 의심하세요. 규칙이 수백 개를 넘으면 반복 패턴은 존 레벨 Redirect Rules나 Bulk Redirects로 옮기고, 배포 후 마지막 규칙을 curl -I로 확인하는 것이 안전합니다.
Q. 외부 DNS를 쓰는데 apex 도메인(example.com)을 연결할 수 있나요?
A. Pages에서 apex 도메인을 쓰려면 네임서버를 Cloudflare로 옮겨야 합니다. 외부 DNS에서는 www 같은 서브도메인만 CNAME으로 연결할 수 있습니다.