트래픽 무제한인데 월 5달러? Cloudflare로 서버비 종결하기

최근 바이브코딩으로 서비스를 직접 만드는 사람이 많아졌다.
AI 에이전트 덕분에 기능 구현은 훨씬 쉬워졌다.
하지만 개발에 집중하다 보면 서비스 유지 비용은 뒤로 밀리기 쉽다.
트래픽이 늘어난 뒤에야 예상하지 못한 청구서를 받고 구조를 다시 손보는 경우도 적지 않다.

웹사이트 운영 비용은 페이지 수보다 트래픽에 비례한다.
페이지가 1만 개여도 방문이 없으면 비용은 거의 들지 않고,
페이지가 10개뿐이어도 트래픽이 몰리면 요금은 빠르게 늘어난다.
나는 현재 1만 페이지 규모의 계산기 사이트를 월 5달러의 호스팅 비용으로 운영하고 있다.
이 글에서는 트래픽이 늘어도 비용이 함께 늘지 않는 정적 사이트 아키텍처와,
구축 과정에서 확인한 함정을 정리한다.
호스팅 비용 구조
개인 규모 사이트의 비용은 크게 네 가지 항목으로 구성된다.
| 항목 | 과금 기준 | 트래픽 증가 시 |
|---|---|---|
| 서버 | 상시 가동 시간 | 고정 |
| 데이터베이스 | 읽기/쓰기 횟수 | 비례 증가 |
| 전송량 | 전송 용량(GB) | 비례 증가 |
| 외부 API | 호출 횟수 | 비례 증가 |
이 중 트래픽에 비례해 늘어나는 항목은 데이터베이스, 전송량, 외부 API다.
그래서 트래픽이 몰리거나 설계 실수가 하나만 있어도 요금이 순식간에 불어날 수 있다.
과금 사고 사례
구글에 "aws 요금"만 검색해도 요금 폭탄 후기가 줄줄이 나온다.

1,755만 원이 청구됐다는 후기부터,
비용 예상 버그로 "월 70원 쓰는데 3조 원"이 찍힌 일까지 사연도 다양하다.
원인은 제각각이지만 구조는 같다. 사용량 과금에는 지출 상한이 없기 때문이다.
AWS와 Google Cloud의 예산 기능도 기본적으로 알림만 보낼 뿐, 지출을 자동으로 멈추지 않는다.
따라서 비용 최적화의 핵심은 하나로 정리된다.
요청이 과금 지점까지 도달하지 않도록 설계하는 것이다.
주요 클라우드 비교

항목별 과금 구조
같은 서비스라도 어느 클라우드에 올리느냐에 따라 비용이 발생하는 지점이 달라진다.
앞서 본 과금 항목을 기준으로 비교하면 다음과 같다.
| 항목 | Cloudflare | AWS | Google Cloud |
|---|---|---|---|
| 서버 | 상시 서버 없음 (Worker 요청 수) | EC2 가동 시간 | Compute Engine 가동 시간 |
| DB | D1 행 단위 (월 250억 행 읽기 포함) | RDS 가동 시간 DynamoDB 요청 수 | Cloud SQL 가동 시간 Firestore 읽기·쓰기 |
| 전송량 | 정적 자산·R2 무료 | GB당 약 0.09달러 | GB당 약 0.12달러 |
| 정적 호스팅 | 무료·무제한 | S3 + CloudFront 요청·전송 | Cloud Storage 요청·전송 |
AWS와 Google Cloud는 항목마다 과금 미터가 따로 돈다.
Cloudflare는 정적 응답을 과금 대상에서 빼고 나머지에도 넉넉한 기본 제공량을 두어,
트래픽이 늘어도 요금이 거의 변하지 않는다.
전송 요금
트래픽 비례 항목 중 업체별 차이가 가장 큰 것은 전송량(이그레스) 요금이다.
AWS와 Google Cloud는 인터넷으로 나가는 데이터에 GB 단위로 과금하는 반면,
Cloudflare는 정적 파일과 R2 저장소의 전송에 요금을 매기지 않는다.
1MB짜리 이미지가 들어간 페이지가 한 달에 100만 번 열리면 전송량은 1TB가 된다.
이미지 비중이 큰 사이트일수록 방문자 증가가 곧바로 전송 요금 증가로 이어진다.
| 서비스 | 인터넷 전송 요금 | 월 1TB 전송 시 |
|---|---|---|
| AWS | GB당 약 0.09달러 (월 100GB 무료) | 약 81달러 |
| Google Cloud | GB당 약 0.12달러 | 약 120달러 |
| Cloudflare Workers 정적 자산 | 없음 | 0달러 |
| Cloudflare R2 | 없음 | 0달러 |
Cloudflare의 특징

장점과 한계
클라우드플레어의 주요 장점은 아래와 같다.
- 정적 파일 요청 무료: Workers에 배포한 정적 자산은 요청 수와 전송량 모두 과금되지 않는다(공식 문서). 트래픽이 급증해도 정적 응답 비용은 늘지 않는다.
- 이그레스 없는 저장소: R2는 저장 용량과 작업 횟수에만 과금하고, 내려보내는 데이터에는 요금이 없다.
- DDoS 방어 기본 제공: 무료 플랜을 포함한 모든 요금제에 DDoS 방어가 포함되며, 공격 트래픽에 별도 요금을 매기지 않는다.
- 글로벌 엣지 네트워크: 전 세계 데이터센터에서 캐시된 응답을 바로 내려보내, 원본 부하와 응답 지연을 함께 줄인다.

다만 단점도 있다.
Worker 함수는 요청당 CPU 시간에 제한이 있고, Node.js의 모든 기능을 그대로 쓸 수는 없다.
무료 플랜에서는 Worker 요청이 일일 한도를 넘으면 해당 요청이 오류로 응답되므로,
실제 트래픽이 있는 서비스라면 유료 플랜을 기준으로 설계하는 편이 안전하다.
월 5불 정도 되는 유료 플랜에 가입하는 게 마음이 편하다.
Workers 유료 플랜 포함 범위
Cloudflare Workers 유료 플랜(월 5달러)의 기본 제공량은 다음과 같다.
| 항목 | 기본 제공량 | 초과 요금 |
|---|---|---|
| 정적 파일 요청 | 무제한 | 없음 |
| Worker 요청 | 월 1,000만 회 | 100만 회당 0.3달러 |
| 배포 파일 수 | 버전당 10만 개 | - |
월 1,000만 회는 하루 약 33만 회로, 개인 사이트에서 초과하기 어려운 수준이다.
또한 정적 파일 요청은 집계 대상이 아니므로 트래픽이 늘어도 이 수치는 크게 변하지 않는다.
내가 운영 중인 사이트도 구글 Firebase Hosting에서 클라우드플레어로 이전했으며,
이전 전에는 전송량 요금만 한 달에 1만 5천 원 정도 나왔지만,
이전 후에는 전송량 요금 0원이 됐다.
비용 최적화 방법
1. 정적 사이트로 만들기
요청마다 서버가 페이지를 렌더링하는 구조에서는 서버를 상시 가동해야 한다.
반면 빌드 시점에 모든 페이지를 HTML로 생성해 두면(SSG), 요청에는 파일 응답만 남는다.
Next.js는 output: "export", Astro는 기본 설정만으로 정적 빌드를 지원한다.
빌드 결과 폴더를 Workers 정적 자산으로 지정하면 배포 준비가 끝난다.
// wrangler.jsonc
{
"name": "my-site",
"compatibility_date": "2026-11-01",
"assets": { "directory": "./dist" } // Next.js 는 ./out
}npx wrangler deploy 한 번이면 배포되고, 이후 정적 파일 요청은 과금되지 않는다.
단, 배포 파일 수는 무료 플랜 2만 개, 유료 플랜 10만 개까지다.
운영 중인 사이트도 파일이 3만 개를 넘어 유료 플랜을 쓰고 있다.

2. 이미지 저장소 분리하기
이미지는 HTML보다 용량이 커서,
전송량 과금 환경에서는 트래픽이 늘수록 비용 비중이 가장 커진다.
따라서 이미지는 이그레스 요금이 없는 R2에 두고, 업로드 전에 WebP로 변환해 용량을 줄인다.
R2는 저장 용량에 과금하므로, 용량이 줄어드는 만큼 저장 요금도 함께 감소한다.
# WebP 변환 (품질 80)
cwebp -q 80 photo.png -o photo.webp
# R2 업로드
npx wrangler r2 object put my-bucket/images/photo.webp --file photo.webp --remote
3. 정적 데이터와 DB 분리하기
Cloudflare D1은 읽기·쓰기 행 수를 기준으로 과금한다.
그런데 사이트 데이터의 상당수는 세율, 통계, 요금표처럼 변경 빈도가 낮다.
이런 데이터는 DB 대신 빌드 시점에 JSON 파일로 생성해 정적 자산으로 배포한다.
DB에는 평점, 댓글처럼 사용자가 생성하는 데이터만 저장한다.
이렇게 분리하면 페이지 수가 늘어도 DB 읽기량은 거의 변하지 않는다.

4. 브라우저·엣지 캐시 활용하기
캐시는 동일한 리소스의 재전송을 막는 장치로, 두 계층에 적용한다.
- 브라우저 캐시: 클라이언트에 리소스를 저장해 재방문 시 요청 자체를 생략한다
- 엣지 캐시: Cloudflare 엣지 서버가 리소스를 보관해 원본까지 가지 않고 응답한다

Cloudflare에서는 _headers 파일로 경로별 캐시 정책을 지정한다.
여기에는 두 가지 함정이 있다.
첫째, 다른 호스팅에서 쓰던 확장 글롭 문법(@(a|b))은 지원되지 않는다.
오류 없이 규칙만 무시되어 캐시가 적용되지 않는다.
# ❌ 확장 글롭 - Cloudflare 미지원
/*.@(woff2|woff)
Cache-Control: public, max-age=31536000, immutable
# ⭕ 확장자별로 분리
/*.woff2
Cache-Control: public, max-age=31536000, immutable
/*.woff
Cache-Control: public, max-age=31536000, immutable둘째, 여러 규칙이 같은 경로에 겹치면
하나만 적용되는 것이 아니라 헤더가 모두 병합된다.
# ❌ /data/rates.txt 에 두 규칙이 모두 적용된다
/data/*
Cache-Control: public, max-age=3600
/*.txt
Cache-Control: public, max-age=60이 경우 Cache-Control 헤더가 중복 전송되어 브라우저의 캐시 동작을 예측할 수 없다.
규칙의 경로가 서로 겹치지 않도록 범위를 좁혀야 한다.

캐시 적용 여부는 Cloudflare 대시보드의 캐시 적중률로 확인할 수 있다.
운영 중인 사이트는 이전 직후 77% 수준에서 며칠 뒤 91-97%로 안정됐다.
5. 외부 API 호출 최소화하기
요청마다 외부 API를 호출하면 호출 비용과 응답 지연이 함께 늘어난다.
변경 주기가 긴 데이터는 하루 1회처럼 정해진 주기에만 수집해 파일로 저장한다.
요청에서는 저장된 파일만 읽는다.
예약 실행은 Cloudflare Cron Triggers나 GitHub Actions의 schedule로 걸 수 있다.
AI로 생성하는 콘텐츠도 매번 다시 생성하지 않고, 결과를 저장해 재사용한다.

주의: Worker 라우팅 범위
Cloudflare Workers는 run_worker_first 설정으로
특정 경로의 요청을 Worker에서 먼저 처리할 수 있다.
접속 로그 수집이나 리다이렉트에 쓰이는 기능이다.
이 경로에 포함되지 않은 정적 파일은 Worker를 거치지 않아 과금되지 않는다.
문제는 경로를 와일드카드로 넓게 지정할 때다.
해당 폴더의 스크립트와 스타일시트까지 모두 Worker 요청으로 집계된다.
페이지 하나에 리소스가 10개라면 요청 수도 10배가 된다.
// wrangler.jsonc
// ❌ /blog 하위 스크립트·스타일·이미지까지 모두 Worker 요청으로 집계
"run_worker_first": ["/blog/*"]
// ⭕ 정적 파일 폴더는 ! 로 제외
"run_worker_first": ["/blog/*", "!/blog/_assets/*"]실제로 이 설정 한 줄로 일일 Worker 요청이 3배 넘게 늘었고,
사용량 알림을 받기 전까지 인지하지 못했다.

따라서 다음 두 가지를 권장한다.
run_worker_first에는 필요한 경로만 지정하고, 정적 파일 폴더는!로 제외한다- 요청 수가 급증하면 알림을 받도록 설정한다(Cloudflare 사용량 알림 또는 API 기반 일일 점검)
AI 에이전트에게 이렇게 지시하자
Claude Code, Cursor 같은 AI 에이전트로 사이트를 개발하는 경우가 늘고 있다.
에이전트는 기능 구현은 빠르지만, 비용 구조까지 고려해 설계하지는 않는다.
그래서 비용 원칙은 처음부터 알려 주고, 기계적으로 지키게 해야 한다.
핵심 원칙 요약

- 규칙으로 명시: 비용 원칙을 규칙 파일에 적어 둔다.
- 기계로 강제: 검사 스크립트와 도구로 실수를 원천 차단한다.
- 숫자로 확인: 예상 요청량을 묻고, 실제 사용량을 조회한다.
- 배포 전 점검: 체크리스트로 마지막에 한 번 더 확인한다.
먼저 에이전트가 흔히 만드는 비용 함정을 보고, 원칙을 하나씩 살펴본다.
비용을 키우는 구현 패턴
별도 지시가 없으면 다음과 같은 구현이 나오기 쉽다.
- 페이지 요청마다 서버 함수 호출
- 변경되지 않는 데이터의 DB 저장과 반복 조회
- 요청마다 외부 API 호출
/*처럼 범위가 넓은 경로 설정
모두 정상적으로 동작하는 코드라서,
실제 요금이 청구되기 전까지는 이러한 문제를 인지하기가 쉽지 않다.

1. 규칙으로 명시: 규칙 파일 작성
이를 막으려면 프로젝트 초기에 규칙 파일(CLAUDE.md, AGENTS.md, .cursorrules 등)에
비용 원칙을 명시해 두는 것이 효과적이다.
아래 내용은 그대로 복붙해서 에이전트에게 전달해도 좋을듯하다.
## 비용 원칙
- 페이지는 빌드 시점에 정적 HTML로 생성한다. 요청마다 서버에서 렌더링하지 않는다.
- 변경되지 않는 데이터는 JSON 파일로 배포한다. DB에는 사용자가 생성하는 데이터만 저장한다.
- 외부 API는 정해진 주기에만 호출해 파일로 저장한다. 요청 처리 중에 호출하지 않는다.
- 이미지는 WebP로 변환해 업로드한다.
- 서버 코드를 거치는 경로에 와일드카드를 쓸 때는 정적 파일 포함 여부를 확인하고, 정적 폴더는 제외한다.
- 서버 함수, DB 조회, 외부 호출을 새로 추가할 때는 먼저 보고하고 일일 예상 호출 수를 함께 적는다.2. 기계로 강제: 자동 검사와 도구화
다만 규칙을 명시해도 컨텍스트가 길어지면 에이전트가 이를 놓친다.
중요한 원칙은 자동 검사로 기계적으로 강제하거나, 도구로 만들어 두는 것이 재발 방지에 효과가 좋다.
방법은 크게 두 가지다.
- 검사 스크립트: 규칙 위반을 찾는 명령을 스크립트로 묶어 배포 직전에 실행한다. 위반이 있으면 배포가 멈추므로, 사람이나 에이전트가 놓쳐도 걸러진다.
- 도구화: 실수가 생기는 반복 작업을 명령어 하나로 만들어 둔다.
예를 들어 "WebP 변환 + R2 업로드"를 스크립트 하나로 묶으면 원본 PNG가 올라갈 일이 없다.
검사 스크립트에는 아래처럼 앞에서 다룬 함정을 그대로 넣으면 된다.
# Cache-Control 헤더가 한 줄만 응답되는지 확인
curl -sI https://example.com/assets/app.js | grep -i cache-control
# _headers 에 확장 글롭 문법이 없는지 확인
grep -n "@(" public/_headers만든 검사와 도구는 배포 명령에 연결해 두면 빠뜨릴 수 없다.
{
"scripts": {
"check:cost": "node scripts/check-cost.mjs",
"upload:image": "node scripts/upload-image.mjs",
"deploy": "npm run check:cost && wrangler deploy"
}
}규칙 파일에는 "이미지는 npm run upload:image로만 올린다"처럼 도구 이름만 적어 두면,
에이전트도 매번 판단하지 않고 그 명령을 그대로 쓴다.
Claude Code라면 훅(hooks)으로 특정 작업 전후에 검사를 자동으로 끼워 넣을 수도 있다.
3. 숫자로 확인: 요청량 점검
요청량은 두 단계로 점검한다.
기능을 만들 때는 에이전트에게 예상치를 묻고, 배포 후에는 실제 사용량을 조회한다.
A. 개발 단계: 예상 요청량 묻기
기능을 추가한 뒤에는 다음과 같이 요청량을 점검하게 한다.
지금 구조에서 방문자 1명이 이 페이지를 열면 서버 함수 실행, DB 읽기, 외부 API 호출이 각각 몇 번 일어나는지 표로 보여 줘.
0이 아닌 항목이 트래픽에 비례해 늘어나는 비용이다.
B. 운영 단계: 실제 사용량 조회 스크립트
예상과 실제는 다를 수 있다.
Cloudflare는 GraphQL Analytics API로 날짜별 Worker 요청 수를 조회할 수 있다.
조회를 스크립트로 만들어 두면 대시보드를 열지 않고 명령 한 번으로 확인할 수 있다.
// scripts/usage.mjs - 최근 7일 Worker 요청 수
// 필요: CF_API_TOKEN(Account Analytics 읽기 권한), CF_ACCOUNT_ID
const since = new Date(Date.now() - 7 * 864e5).toISOString().slice(0, 10);
const query = `{ viewer { accounts(filter: { accountTag: "${process.env.CF_ACCOUNT_ID}" }) {
workersInvocationsAdaptive(limit: 100, filter: { date_geq: "${since}" }, orderBy: [date_ASC]) {
dimensions { date } sum { requests }
}
} } }`;
const res = await fetch("https://api.cloudflare.com/client/v4/graphql", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.CF_API_TOKEN}`, "Content-Type": "application/json" },
body: JSON.stringify({ query }),
});
const rows = (await res.json()).data.viewer.accounts[0].workersInvocationsAdaptive;
console.table(rows.map((r) => ({ date: r.dimensions.date, requests: r.sum.requests })));package.json에 "usage": "node scripts/usage.mjs"를 추가하면 npm run usage로 바로 볼 수 있다.
같은 방식으로 D1 읽기·쓰기 행 수와 R2 작업 횟수도 조회할 수 있다.
이 스크립트를 하루 한 번 자동으로 돌리고, 전날보다 급증하면 알림을 보내도록 연결해 두자.
앞의 Worker 라우팅 같은 실수를 요금이 청구되기 전에 잡을 수 있다.
4. 배포 전 점검: 체크리스트
앞의 내용을 점검 항목으로 정리했다.
AI 에이전트로 개발 중이라면 아래 블록을 그대로 복사해 붙여 넣고,
"지금 프로젝트가 각 항목을 지키는지 확인해 줘"라고 요청하면 된다.
## 공통 - 페이지·데이터
- [ ] 페이지는 빌드 시점에 HTML로 생성되는가 (요청마다 서버에서 렌더링하지 않는가)
- [ ] 변경되지 않는 데이터는 DB가 아닌 JSON 파일로 제공되는가
- [ ] DB에는 사용자가 생성하는 데이터(평점, 댓글 등)만 저장되는가
- [ ] 외부 API는 정해진 주기에만 호출하고, 결과를 파일로 저장해 사용하는가
## 공통 - 이미지·캐시
- [ ] 이미지는 WebP 등 경량 포맷으로 변환했는가
- [ ] 이미지와 대용량 파일은 외부 전송 요금이 없거나 낮은 저장소에 있는가
- [ ] 스크립트, 스타일, 폰트, 이미지에 장기 캐시가 설정되어 있는가
- [ ] 한 경로에 캐시 규칙이 중복되어 Cache-Control이 두 번 전송되지 않는가
- [ ] 캐시 적중률을 확인했는가
## 공통 - 모니터링
- [ ] 일별 요청량을 명령 한 번으로 조회할 수 있는가
- [ ] 요청 수가 급증하면 알림을 받는가
- [ ] 월 1회 이상 청구 항목을 확인하는가
## Cloudflare 전용
- [ ] 배포 파일 수가 플랜 한도(무료 2만 개, 유료 10만 개) 이내인가
- [ ] _headers에 확장 글롭 문법(@(a|b))을 쓰지 않았는가
- [ ] run_worker_first가 필요한 경로에만 지정되어 있는가
- [ ] 와일드카드 경로에서 정적 파일 폴더를 !로 제외했는가




