Posts

Next.js 정적 사이트의 Workers 이전 기록

목차8
한 사람이 상자 더미를 안고 구름에서 옆 구름으로 출렁다리를 건넌다. 뒤쪽 구름의 화살표 표지판 아래로 방문객들이 줄지어 따라온다
이사는 하루 만에 끝났지만 표지판은 계속 세워 둔다

다른 글에서 클라우드플레어 무료 플랜 하나로 블로그와 문서, 비공개 노트, 방문자 수까지 운영한다고 적었다. 그 글에서는 한도를 표로 정리했고, 스크립트를 거치는 요청을 줄이는 설정은 이 글로 넘겼다. 이 글에는 그 배포를 한 순서대로 적는다.

처음엔 Pages 였다

10월 23일 graphite.is 를 처음 설정할 때는 Pages 로 시작했다. 그리고 같은 날 Workers 정적 자산으로 옮겼다.

클라우드플레어는 새 프로젝트라면 Pages 대신 Workers 정적 자산을 쓰라고 권한다. Pages 도 계속 동작하지만 새 기능은 Workers 에 먼저 들어간다. 옮기는 일은 간단했다. next.config 는 손대지 않았고 wrangler.jsonc 파일 하나만 더했다. 기존 Pages 프로젝트를 옮기는 순서는 클라우드플레어의 Pages 에서 Workers 로 이전 가이드에 따로 정리되어 있다.

서버가 할 일이 남아 있지 않은지

이 방식은 Next.js 사이트를 빌드 시점에 정적 파일로 만들어 올린다. 그러니 요청이 들어온 순간에 서버가 해야 하는 일이 하나라도 남아 있으면 곤란하다. 미들웨어, ISR, next.config 의 rewrites 와 redirects 와 headers, 기본 이미지 최적화는 정적 내보내기에서 동작하지 않는다. 이 가운데 redirects 는 나중에 Worker 코드로 따로 처리했다. 지원하지 않는 기능 전체는 Next.js 정적 내보내기 문서에서 볼 수 있다.

준비물은 많지 않다.

항목조건
Next.js요청 시점에 서버가 필요한 기능을 쓰지 않는 사이트. 미들웨어, ISR, next.config 의 rewrites redirects headers, 기본 이미지 최적화는 정적 내보내기에서 동작하지 않음
Cloudflare사용할 도메인이 Cloudflare 에 연결된 계정
Wrangler4.x. npm i -D wrangler 로 설치하고 npx wrangler login 으로 로그인

output 한 줄

Next.js 쪽에서 할 일은 한 줄이다. next.config.mjs 의 output 을 "export" 로 바꾼다.

next.config.mjs
const config = {
  output: "export",
};

export default config;

이 상태로 next build 를 돌리면 화면마다 HTML 파일이 out 폴더에 생긴다. /about 화면은 out/about.html 이 된다. 여기까지는 클라우드플레어와 아무 상관이 없다.

out 폴더를 정적 자산으로 올리기

클라우드플레어 쪽은 wrangler.jsonc 가 맡는다. assets.directory 를 ./out 으로 지정하면 out 폴더가 통째로 정적 자산이 된다. 아래는 graphite.is 에서 쓰는 설정인데 제외 패턴만 일부 줄였다.

wrangler.jsonc
{
  "name": "graphite",
  "compatibility_date": "2026-10-19",
  "main": "worker/redirect.js",
  "routes": [
    { "pattern": "graphite.is", "custom_domain": true },
    { "pattern": "www.graphite.is", "custom_domain": true }
  ],
  "workers_dev": false,
  "preview_urls": false,
  "assets": {
    "directory": "./out",
    "binding": "ASSETS",
    "not_found_handling": "404-page",
    "html_handling": "auto-trailing-slash",
    "run_worker_first": ["/*", "!/_next/*", "!/*.png", "!/*.xml"]
  }
}

줄마다 하는 일은 이렇다.

이름설명
assets.directory올릴 폴더. Next.js 정적 내보내기 결과인 ./out
assets.not_found_handling404-page 는 없는 주소에 404.html 을 404 상태로 응답
assets.html_handlingauto-trailing-slash 는 /about 요청에 about.html 을 응답
main, assets.binding301 이동이나 API 를 Worker 코드로 처리할 때만 지정. 순수 정적 사이트는 생략
routes 의 custom_domain사용자 지정 도메인 연결. DNS 레코드와 인증서를 Cloudflare 가 만듦
workers_dev, preview_urls*.workers.dev 주소와 미리보기 주소의 사용 여부

파일만 올리는 사이트라면 main 과 assets.binding 은 빼도 된다. 나는 이 둘을 남겨 두었는데 이유는 조금 뒤에 나온다.

npm run deploy

빌드와 업로드는 package.json 스크립트 하나로 묶었다.

package.json
{
  "scripts": {
    "deploy": "next build && wrangler deploy"
  }
}
터미널
npm run deploy

배포가 끝나면 연결된 도메인과 버전 ID 가 찍힌다.

출력
Deployed graphite triggers
  graphite.is (custom domain)
  www.graphite.is (custom domain)
Current Version ID: 44c6a94f-c07c-40ed-83d7-9bb07aa21e00

custom_domain 으로 넣은 두 주소가 나란히 보이면 도메인 연결까지 된 것이다. DNS 레코드와 인증서는 클라우드플레어가 만들어 둔다.

정적 사이트에 남은 301 이동

앞에서 미뤄 둔 redirects 이야기다. 정적 내보내기에서는 next.config 의 redirects 가 동작하지 않는데 내게는 보내야 할 주소가 있었다. www.graphite.is 와 *.workers.dev 로 들어온 요청은 graphite.is 로 보내야 했고 예전 /blog/… 주소는 /posts/… 로 넘겨야 했다. 이 일을 worker/redirect.js 가 맡는다. 그래서 파일만 올리는 사이트인데도 main 을 지정했다.

같은 Worker 에는 /api/visit 도 들어 있다. 방문자 수를 D1 에 날짜별 합계로만 쌓는 작은 API 다. 정적 사이트에 API 하나 붙이자고 서버를 따로 세울 필요는 없었다.

어떤 요청이 Worker 를 거치나

Worker 코드를 붙이고 나면 요청을 세는 방식이 달라진다. 정적 자산 요청은 무료이고 횟수 제한도 없다. Worker 스크립트를 거치는 요청만 Workers 요금 기준으로 센다.

run_worker_first 에 /* 를 넣으면 모든 요청이 Worker 를 먼저 지나간다. 스크립트나 이미지, 데이터 파일은 Worker 가 손댈 일이 없으니 느낌표로 시작하는 제외 패턴으로 빼서 정적 자산이 바로 응답하게 했다. 위 설정의 run_worker_first 에서 첫 항목 뒤에 붙은 것들이 그 제외 패턴이다.

이게 비용만의 문제는 아니었다. 무료 요금제에서 한도를 넘기면 Worker 를 거치는 요청에는 429 가 돌아간다. 정적 자산으로 대신 응답해 주지도 않는다. 제외 패턴으로 뺀 요청은 한도와 상관없이 정적 자산이 응답한다. 과금 기준은 요금과 제한 문서에 있다.

로그인 없이 열리던 주소

비공개 화면은 Cloudflare Access 로 막아 두었다. 그런데 Access 앱의 대상 주소가 graphite.is 였다. 같은 Worker 는 *.workers.dev 주소로도 열리고 그 주소로 들어가면 같은 파일이 로그인 없이 열렸다.

고친 건 두 줄이다. workers_dev 와 preview_urls 를 false 로 두어 두 주소를 껐다. 위 설정에서 두 값이 false 인 이유다.

배포 명령은 npm run deploy 한 줄이다. wrangler.jsonc 의 나머지 줄은 대부분 Worker 를 거치지 않을 경로와 꺼 둘 주소를 정하는 줄이다.