Cloudflare Pages 배포: GitHub 연동, Wrangler CLI, Pages Functions와 Vercel·Netlify 비교
이 글의 핵심
Cloudflare Pages로 정적 사이트·SSR 앱을 무료로 배포하고, Pages Functions로 서버 로직을 실행하는 방법을 다룹니다. GitHub 연동, Wrangler CLI, 환경 변수, 커스텀 도메인, 빌드 최적화를 예제로 설명합니다.
들어가며
Cloudflare Pages는 정적 사이트와 서버 사이드 렌더링(SSR) 앱을 Cloudflare의 글로벌 네트워크에 배포하는 플랫폼입니다. Vercel·Netlify와 비슷한 역할을 하지만, 정적 자산 대역폭에 과금하지 않고 Workers·D1·R2 같은 Cloudflare 서비스와 바로 연결된다는 점이 다릅니다.
먼저 알아 둘 변화가 하나 있습니다. 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의 주요 특징
| 항목 | 설명 |
|---|---|
| 네트워크 | Cloudflare의 전 세계 300개 이상 도시 데이터센터 |
| 무료 플랜 | 월 500회 빌드(동시 빌드 1개), 정적 요청·대역폭 무제한, 배포당 파일 20,000개 |
| 지원 프레임워크 | React, Vue, Astro, SvelteKit, Remix, Hugo 등 |
| 서버 코드 | Workers 런타임 기반 Pages Functions |
| 빌드 환경 | Node.js, Python, Ruby, Go 등 |
파일 하나의 크기에도 한도가 있어서, 정적 자산은 파일당 25MiB를 넘길 수 없습니다. 실제로 shrinkto.app을 만들 때 32MB짜리 ffmpeg.wasm 코어를 사이트에 넣지 못해 CDN에서 불러오도록 바꿨습니다. 큰 파일이 필요하면 R2 같은 별도 저장소나 외부 CDN을 쓰세요.
Pages가 가장 잘 맞는 것은 블로그·문서·랜딩 페이지 같은 정적 사이트와, Astro·Hugo·Eleventy처럼 정적 HTML을 만들어 내는 생성기입니다. SvelteKit의 SSR도 어댑터로 돌릴 수 있고(Astro SSR은 어댑터 13부터 Workers 전용), D1(SQLite)이나 R2(S3 호환 스토리지)를 붙여 간단한 API를 만들 수도 있습니다. 트래픽이 여러 나라에 걸쳐 있거나 대역폭 비용이 걱정된다면 특히 검토해 볼 만합니다. 다만 Next.js는 뒤에서 설명하듯 Pages보다 Workers 쪽이 권장 경로입니다.
대시보드에서 GitHub 저장소 연결하기
가장 간단한 방법은 Cloudflare 대시보드에서 GitHub 저장소를 연결하는 것입니다.
빌드 명령과 출력 디렉터리 설정
- Cloudflare 대시보드 → Workers & Pages → Create → Pages 탭
- Connect to Git → GitHub 계정 연동
- 저장소와 프로덕션 브랜치(보통
main) 선택 - 빌드 설정:
- Framework preset: Astro, Next.js(정적 export), React 등에서 선택
- Build command:
npm run build - Build output directory:
dist(Astro·Vite),out(Next.js 정적 export),build(CRA, SvelteKit adapter-static)
- Save and Deploy
브랜치 푸시별 자동 배포와 프리뷰
프로덕션 브랜치에 푸시하면 자동으로 빌드·배포되고, 다른 브랜치에 푸시하면 프리뷰 배포가 만들어집니다. 프리뷰는 배포마다 고유한 <hash>.<project>.pages.dev 주소와, 브랜치 이름으로 된 <branch>.<project>.pages.dev 별칭을 받습니다. GitHub PR에는 프리뷰 URL이 댓글로 달리고, 빌드 로그는 대시보드에서 실시간으로 볼 수 있습니다.
설정이 쉽고 프리뷰가 자동으로 생긴다는 점이 장점입니다. 대신 Cloudflare 빌드 횟수(무료 월 500회)를 소모하고, 빌드 머신의 메모리나 캐시를 세밀하게 제어하기 어렵습니다.
Wrangler CLI로 직접 업로드하기
Wrangler는 Cloudflare의 공식 CLI입니다. 빌드는 로컬이나 CI에서 하고 결과물만 업로드하면 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
대화형 터미널에서 실행하면 프로젝트가 없을 때 새로 만들지 묻습니다. CI처럼 대화형 입력이 없는 환경에서는 wrangler pages project create 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: '24'
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>/...)에서 확인합니다.
이 방식에서는 Node 버전, 의존성 캐시, 힙 크기 같은 빌드 환경을 워크플로에서 직접 정할 수 있습니다.
환경 변수와 시크릿 관리
대시보드에서 Production/Preview 변수 설정
Pages 프로젝트 → Settings → Variables and Secrets에서 환경별로 값을 넣습니다. Production 값은 프로덕션 브랜치 배포에, Preview 값은 그 밖의 브랜치 배포에 쓰입니다.
DATABASE_URL=postgresql://...
API_KEY=abc123
대시보드에서 설정한 값은 Cloudflare 빌드 중(npm run build)과 런타임(Functions) 모두에서 읽을 수 있습니다. Wrangler로 직접 업로드하는 경우 빌드는 CI에서 일어나므로, 빌드 타임 변수는 CI 쪽에 따로 넣어야 합니다.
빌드 타임과 Functions에서 변수 읽기
빌드 타임(Node.js)에는 process.env로 읽습니다.
// astro.config.mjs, next.config.js 등
const apiKey = process.env.API_KEY;
Functions 런타임에는 process.env가 아니라 context.env로 읽습니다.
// functions/api/data.js
export async function onRequest(context) {
const apiKey = context.env.API_KEY;
// 시크릿을 응답에 그대로 내보내지 않도록 주의
return new Response(JSON.stringify({ configured: Boolean(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 - SSL 인증서는 자동으로 발급됩니다.
대시보드에서 도메인을 먼저 추가하지 않고 DNS에 CNAME만 만들면 522 에러가 납니다. 도메인 추가가 먼저입니다.
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에서 서버 코드 실행
Pages Functions는 Workers 런타임 위에서 동작하며, functions/ 폴더의 파일 경로가 그대로 URL 경로가 됩니다.
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로 접근합니다. onRequest는 모든 메서드를 받고, onRequestGet·onRequestPost처럼 메서드별 핸들러를 따로 export할 수도 있습니다.
[id].js 동적 라우트
// functions/api/users/[id].js
export async function onRequest(context) {
const userId = context.params.id;
// D1 (Cloudflare SQLite) 예시: DB 바인딩이 설정되어 있어야 함
const db = context.env.DB;
const user = await db.prepare('SELECT * FROM users WHERE id = ?')
.bind(userId)
.first();
if (!user) {
return new Response('Not found', { status: 404 });
}
return new Response(JSON.stringify(user), {
headers: { 'Content-Type': 'application/json' }
});
}
_middleware.js
// functions/_middleware.js
export async function onRequest(context) {
const start = Date.now();
// 다음 핸들러(또는 정적 자산) 실행
const res = await context.next();
// 받은 응답의 헤더는 변경할 수 없을 수 있으므로 복사본을 만들어 수정
const response = new Response(res.body, res);
response.headers.set('X-Response-Time', `${Date.now() - start}ms`);
return response;
}
context.next()가 돌려준 응답, 특히 정적 자산 응답의 헤더는 불변(immutable)일 수 있어서 바로 headers.set을 호출하면 TypeError: Can't modify immutable headers.가 날 수 있습니다. 위처럼 new Response(res.body, res)로 복사한 뒤 수정하는 것이 안전합니다.
Astro·Next.js SSR 어댑터
Astro SSR은 @astrojs/cloudflare 어댑터를 씁니다. 다만 Astro 6과 함께 나온 어댑터 13부터는 Cloudflare Pages 공식 지원이 빠지고 Workers(정적 자산 포함) 배포만 지원합니다. 지금 설치되는 버전으로 아래 설정을 쓰면 빌드 결과가 Workers용(wrangler.jsonc, wrangler deploy)으로 나오므로, Pages 프로젝트에 그대로 올리면 Functions가 동작하지 않습니다. 순수 정적 Astro 사이트는 어댑터 없이 dist/만 Pages에 올리면 되므로 이 변화와 무관합니다.
// astro.config.mjs
import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';
export default defineConfig({
output: 'server', // Astro 5 이후 값은 'static' 또는 'server'
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-
Astro는 .astro 폴더(콘텐츠 데이터 저장소)를, Next.js는 .next/cache를 재사용하면 다음 빌드에서 변경되지 않은 부분의 처리를 줄일 수 있습니다. 다만 캐시가 오래된 결과를 내보내는 일도 있어서, 마크다운 플러그인 설정을 바꿨는데 HTML이 그대로라면 캐시부터 지워 보는 것이 좋습니다.
빌드 전 작업 병렬 처리
// 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을 써야 합니다. 서로 독립적인 작업만 병렬로 묶어야 한다는 점도 잊지 마세요.
페이지가 많은 사이트의 빌드
페이지가 수천 개가 되면 빌드 시간과 메모리가 함께 문제가 됩니다. OG 이미지처럼 비싼 산출물은 변경된 글만 다시 만들도록 캐시하고, 서버 렌더링이 필요 없는 페이지는 정적으로 남겨 두는 것이 기본입니다. 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 어댑터(Workers) | 네이티브(개발사) | 어댑터 |
무료 플랜의 구체적인 수치(빌드 분, 대역폭 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를 함께 쓸 계획이거나, Astro·Hugo 같은 정적 생성기로 블로그를 만들 때 잘 맞습니다(Astro 블로그 가이드 참고). Vercel은 Next.js App Router의 최신 기능을 그대로 쓰고 싶거나 Vercel Analytics·Speed Insights 같은 도구를 함께 쓸 때 유리합니다. Netlify는 폼 처리나 분할 테스트(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: '24'
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"
npm run validate는 예시로 넣은 프로젝트 자체 스크립트입니다. 배포 전에 frontmatter 형식 같은 것을 검사해 빌드 실패를 일찍 잡는 단계로 생각하면 됩니다.
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') {
// 조회수 증가 (slug에 UNIQUE 또는 PRIMARY KEY 제약이 있어야 ON CONFLICT가 동작)
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·_headers·롤백
_redirects로 리다이렉트
빌드 출력 폴더에 _redirects 파일을 넣습니다.
/old-url /new-url 301
/blog/* /posts/:splat 302
_redirects에는 개수 한도가 있습니다(문서상 정적 규칙 2,000개 + 동적 규칙 100개). 이 블로그는 글을 합치고 URL을 정리하면서 규칙이 1,000개를 훌쩍 넘었는데, 배포는 에러 없이 성공했지만 실제로 요청해 보니 파일 뒤쪽 규칙들이 적용되지 않고 404가 나는 현상을 겪었습니다. 확인해 보니 1,400번째 줄 근처부터 규칙이 무시되고 있었고, 별도로 *·:splat을 쓰는 동적 규칙을 파일 앞쪽에 두었을 때는 그 뒤의 규칙까지 동적 규칙 100개 한도에 걸려 처음 100여 개만 동작했습니다. 경고가 없어서 발견이 늦었고, 결국 패턴이 반복되는 규칙(/X-en/ → /X/ 같은 것)을 존(zone) 레벨의 Redirect Rules로 옮겨 파일을 줄였습니다. 리다이렉트가 수백 개를 넘어갈 것 같다면 처음부터 대시보드의 Bulk Redirects를 쓰고, _redirects를 쓰더라도 동적 규칙은 파일 끝에 모아 두고 배포 후 마지막 몇 개 규칙을 curl -I로 직접 확인하는 습관을 들이세요.
2026-11-09 재확인: 지금도 앞쪽 약 100개만 적용된다
이 블로그의 _redirects는 1,014개 규칙이고, 라이브에서 규칙마다 요청해 본 결과 앞쪽 약 100개(102번째 규칙까지)는 301이었고 그 뒤 출발 주소는 301이 아니라 404였습니다. 배포 로그에는 경고가 나오지 않았습니다. 문서상 한도(정적 2,000개)와 다른 동작이라 원인은 확인하지 못했고, 증상만 확인했습니다. 대응으로 나머지 옛 주소는 Astro의 redirects 설정으로 정적 리다이렉트 HTML(meta refresh)을 만들게 했습니다. 서버 301은 아니므로 장기적으로는 위에서 말한 Redirect Rules나 Bulk Redirects로 옮기는 쪽이 낫습니다. 규칙이 100개를 넘기 시작하면 배포 후 파일 뒤쪽 주소 몇 개를 직접 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로 상위 규칙의 헤더를 먼저 제거한 뒤 다시 지정하세요.
빌드 실패 알림
- 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를 누르면 됩니다. wrangler pages deployment list --project-name=my-blog로 배포 목록은 볼 수 있지만, Pages 롤백 자체는 대시보드나 API로 합니다. 롤백은 이전 빌드 산출물로 되돌리는 것이라 빌드를 다시 돌리지 않아 금방 끝나고, Git 히스토리와는 별개라서 롤백 후 다음 푸시가 들어오면 그 커밋으로 다시 배포된다는 점을 기억하세요.