[Cloudflare 완전 정복: 입문부터 2026 AI 에이전트까지] 9/16화: Cloudflare Workers 서버리스 엣지 컴퓨팅 입문 가이드
이 글은 「Cloudflare 완전 정복」 시리즈 9회입니다. 지난 8회까지 DNS·CDN·SSL(1~3회), Pages 정적 배포(4회), Tunnel·Zero Trust·WARP(5~7회), R2 스토리지(8회)를 거치며 Cloudflare의 ‘인프라 계층’을 완성했습니다. 오늘부터는 Phase 3 — 개발자 플랫폼 영역으로 넘어갑니다. 첫 주제는 Cloudflare Workers, 서버 한 대 없이 전 세계 330개 이상의 엣지 로케이션에 코드를 배포하는 서버리스 컴퓨팅입니다.
Cloudflare Workers란 무엇인가 — 서버리스 엣지 컴퓨팅의 핵심
Cloudflare Workers는 Cloudflare의 글로벌 네트워크 엣지에서 JavaScript·TypeScript·Rust·Python 코드를 실행하는 서버리스 플랫폼입니다. AWS Lambda나 Google Cloud Functions와 비슷한 개념이지만, 결정적 차이가 있습니다. Lambda는 특정 리전(예: us-east-1)에서 실행되지만, Workers는 요청이 도착한 그 도시의 데이터센터에서 즉시 실행됩니다. 서울에서 접속하면 서울 엣지에서, 뉴욕에서 접속하면 뉴욕 엣지에서 — 별도 설정 없이 자동으로.
이것이 가능한 이유는 Workers의 런타임 아키텍처에 있습니다. 전통적인 서버리스가 컨테이너를 띄우고 언어 런타임을 초기화하는 데 수백 밀리초의 콜드 스타트가 필요한 반면, Workers는 Chrome V8 엔진의 Isolate 기술을 사용합니다. 하나의 프로세스 안에서 수천 개의 격리된 실행 환경을 만들어내므로, 콜드 스타트가 사실상 0ms에 수렴합니다.
V8 Isolate가 바꾼 서버리스의 경제학
전통적인 서버리스 아키텍처를 떠올려 보세요. 요청이 들어오면 VM을 프로비저닝하고, 컨테이너를 시작하고, Node.js/Python 런타임을 올린 뒤에야 실제 코드가 실행됩니다. 이 과정이 짧게는 100ms, 길게는 수 초가 걸립니다. 그래서 Lambda는 ‘웜 컨테이너’를 유지하는 데 비용을 쓰고, 개발자는 프로비저닝된 동시성(Provisioned Concurrency)을 구매합니다.
Workers의 V8 Isolate는 이 문제를 근본적으로 해결합니다. Isolate는 같은 프로세스 메모리 공간 안에서 별도의 힙을 할당받은 경량 실행 컨텍스트입니다. 새 Isolate를 생성하는 데 드는 시간은 5ms 미만이며, 메모리 오버헤드도 컨테이너의 1/100 수준입니다. 이 경량성 덕분에 Cloudflare는 모든 엣지 노드에 Workers 런타임을 상주시킬 수 있고, 어떤 위치에서든 첫 요청부터 빠르게 응답할 수 있습니다.

Workers가 적합한 유스케이스
- API Gateway / BFF(Backend For Frontend): 여러 백엔드 API를 조합해 클라이언트에 최적화된 응답을 만드는 중간 계층
- A/B 테스트·피처 플래그: 요청 헤더·쿠키를 기반으로 엣지에서 즉시 라우팅 분기
- 이미지/HTML 변환: 디바이스별 이미지 리사이즈, HTML 리라이트, 개인화 콘텐츠 삽입
- 인증·권한 검증: JWT 검증을 오리진 서버 앞단에서 처리해 불필요한 트래픽 차단
- Webhook 수신·이벤트 처리: GitHub, Stripe, Slack 등의 Webhook을 엣지에서 즉시 처리
- 정적 사이트 + 동적 API: Pages(4회에서 다룬)와 결합해 풀스택 애플리케이션 구축
- R2 프록시: 8회에서 설정한 R2 버킷에 인증·변환 로직을 추가한 커스텀 CDN
첫 번째 Worker 만들기 — 환경 준비부터 배포까지
Workers 개발의 핵심 도구는 Wrangler CLI입니다. 11회에서 Wrangler를 심층적으로 다루겠지만, 오늘은 첫 Worker를 배포하는 데 필요한 최소한의 명령어를 익힙니다.
Step 1: Node.js와 Wrangler 설치
Workers 개발에는 Node.js 18+ 환경이 필요합니다. Mac Studio 홈랩 기준으로 진행합니다.
# Mac Studio (macOS) — Node.js 설치 확인
node --version
# v20.x 이상이면 OK. 없으면:
brew install node@20
# Wrangler CLI 글로벌 설치
npm install -g wrangler@latest
# 버전 확인
wrangler --version
# ⚠️ 3.x 이상 필수 (2026-06 기준 최신: 3.95+)
Synology NAS DS+925에서 개발하려면 Docker 컨테이너 내 Node.js 환경을 사용하는 것이 깔끔합니다.
# Synology NAS — Docker로 Node.js 개발 환경 실행
docker run -it --rm \
-v /volume1/projects/workers:/app \
-w /app \
node:20-slim bash
# 컨테이너 내에서 Wrangler 설치
npm install -g wrangler@latest
Step 2: Cloudflare 계정 인증
# 브라우저 기반 OAuth 인증 (로컬 머신에서)
wrangler login
# 인증 성공 확인
wrangler whoami
# ┌─────────────────────────────────────┐
# │ Account Name │ Account ID │
# ├─────────────────────────────────────┤
# │ My Account │ abc123def456... │
# └─────────────────────────────────────┘
SSH를 통해 NAS에서 작업하는 경우처럼 브라우저를 열 수 없는 환경에서는 API 토큰을 사용합니다.
# 환경변수로 API 토큰 설정 (headless 환경)
export CLOUDFLARE_API_TOKEN="your-api-token-here"
# 토큰 생성: Cloudflare Dashboard → My Profile → API Tokens
# 템플릿: "Edit Cloudflare Workers" 선택
Step 3: 프로젝트 스캐폴딩
# 새 Workers 프로젝트 생성
wrangler init my-first-worker
# 대화형 프롬프트:
# ? Would you like to use git? › Yes
# ? Would you like to use TypeScript? › Yes
# ? Would you like to create a Worker at src/index.ts? › Fetch handler
# ? Would you like us to write your first test? › Yes
생성된 프로젝트 구조를 살펴보겠습니다.
my-first-worker/
├── src/
│ └── index.ts # Worker 메인 코드
├── test/
│ └── index.spec.ts # Vitest 테스트
├── wrangler.toml # 배포 설정
├── package.json
└── tsconfig.json

Step 4: 기본 Worker 코드 이해
생성된 src/index.ts를 열어봅시다. Workers의 핵심은 fetch 이벤트 핸들러입니다.
// src/index.ts — Workers의 가장 기본적인 형태
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
return new Response('Hello World!');
},
};
이 세 줄짜리 코드가 전 세계 330개 엣지에서 동시에 실행됩니다. fetch 핸들러는 브라우저의 Fetch API와 동일한 Request/Response 인터페이스를 사용합니다. 웹 표준(Web Standards)을 그대로 따르기 때문에, 프론트엔드 개발 경험이 있다면 별도 학습 없이 시작할 수 있습니다.
각 매개변수의 역할:
- request: 들어온 HTTP 요청. URL, 헤더, 바디, 메서드 등 모든 요청 정보 포함
- env: 환경 변수와 바인딩(KV, R2, D1, Durable Objects 등)에 접근하는 객체
- ctx: 실행 컨텍스트.
waitUntil()로 응답 후 백그라운드 작업 예약 가능
Step 5: 로컬 개발 서버 실행
# 로컬에서 Worker 실행 (Miniflare 기반)
cd my-first-worker
wrangler dev
# 출력:
# ⎔ Starting local server...
# [mf:inf] Ready on http://localhost:8787
# [mf:inf] - http://127.0.0.1:8787
# 다른 터미널에서 테스트
curl http://localhost:8787
# Hello World!
wrangler dev는 Cloudflare의 오픈소스 로컬 런타임인 Miniflare를 내장하고 있어, 실제 Workers 런타임과 거의 동일한 환경에서 로컬 테스트가 가능합니다. 코드를 수정하면 핫 리로드로 즉시 반영됩니다.
Step 6: 글로벌 배포
# 전 세계 엣지에 배포 — 단 한 줄
wrangler deploy
# 출력:
# Uploaded my-first-worker (1.23 sec)
# Published my-first-worker (0.45 sec)
# https://my-first-worker.<your-subdomain>.workers.dev
# Current Deployment ID: abc123-def456-...
이것으로 끝입니다. wrangler deploy 한 번이면 전 세계 330개 이상의 데이터센터에 코드가 배포됩니다. 서버 프로비저닝도, 로드밸런서 설정도, 리전 선택도 필요 없습니다. 배포된 Worker는 *.workers.dev 도메인으로 즉시 접근 가능하며, 2회에서 설정한 커스텀 도메인에 연결하는 것도 간단합니다.
실전 예제: JSON API Worker 만들기
Hello World를 넘어, 실제로 유용한 Worker를 만들어 봅시다. URL 경로에 따라 다른 JSON 응답을 반환하는 간단한 API 서버입니다.
// src/index.ts — 라우팅이 포함된 실전 API Worker
interface Env {
// 10회에서 KV/D1 바인딩을 여기에 추가합니다
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const url = new URL(request.url);
const path = url.pathname;
// CORS 헤더 (프론트엔드에서 호출 시 필요)
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
};
// OPTIONS preflight 처리
if (request.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders });
}
// 라우팅
if (path === '/api/hello') {
return Response.json(
{
message: 'Hello from the edge!',
timestamp: new Date().toISOString(),
colo: request.cf?.colo ?? 'unknown', // 요청 처리 데이터센터 코드
country: request.cf?.country ?? 'unknown',
},
{ headers: corsHeaders }
);
}
if (path === '/api/headers') {
// 요청 헤더를 그대로 반환 (디버깅용)
const headers: Record<string, string> = {};
request.headers.forEach((value, key) => {
headers[key] = value;
});
return Response.json({ headers }, { headers: corsHeaders });
}
if (path === '/api/geo') {
// Cloudflare가 자동으로 채워주는 지리 정보
return Response.json(
{
ip: request.headers.get('CF-Connecting-IP'),
country: request.cf?.country,
city: request.cf?.city,
region: request.cf?.region,
timezone: request.cf?.timezone,
latitude: request.cf?.latitude,
longitude: request.cf?.longitude,
asn: request.cf?.asn,
asOrganization: request.cf?.asOrganization,
},
{ headers: corsHeaders }
);
}
// 404 처리
return Response.json(
{
error: 'Not Found',
available_endpoints: ['/api/hello', '/api/headers', '/api/geo'],
},
{ status: 404, headers: corsHeaders }
);
},
};
이 Worker를 배포하고 호출해 봅시다.
# 배포
wrangler deploy
# 테스트 — 어디서 접속했는지에 따라 colo 값이 달라짐
curl https://my-first-worker.<subdomain>.workers.dev/api/hello
# {
# "message": "Hello from the edge!",
# "timestamp": "2026-06-08T09:30:00.000Z",
# "colo": "ICN", ← 서울 엣지에서 처리됨
# "country": "KR"
# }
# 지리 정보 확인
curl https://my-first-worker.<subdomain>.workers.dev/api/geo
# {
# "ip": "xxx.xxx.xxx.xxx",
# "country": "KR",
# "city": "Seoul",
# "timezone": "Asia/Seoul",
# ...
# }
request.cf 객체는 Workers만의 강력한 기능입니다. Cloudflare 네트워크가 자동으로 감지한 요청자의 지리 정보, ASN, 봇 스코어 등을 별도 API 호출 없이 즉시 사용할 수 있습니다. 지역별 콘텐츠 분기, 접근 제어, 분석 등에 매우 유용합니다.
Workers의 실행 모델 — 알아야 할 제약과 특성
CPU 시간 vs 벽시계 시간
Workers의 과금과 제한은 CPU 시간 기준입니다. 이것은 ‘벽시계 시간(wall-clock time)’과 다릅니다. 외부 API를 호출하며 응답을 기다리는 동안은 CPU를 사용하지 않으므로 과금되지 않습니다.
// 이 Worker의 CPU 시간은 매우 짧음 (수 ms)
// 하지만 벽시계 시간은 외부 API 응답에 따라 수 초일 수 있음
export default {
async fetch(request: Request): Promise<Response> {
// fetch()로 외부 호출 — 대기 중엔 CPU 시간 안 쌓임
const apiResponse = await fetch('https://api.example.com/data');
const data = await apiResponse.json();
// JSON 가공 — 이 부분만 CPU 시간으로 계산
const processed = transform(data);
return Response.json(processed);
},
};
무료 플랜은 요청당 CPU 시간 10ms, 유료 플랜(Workers Paid)은 30ms가 기본이며 Cron Trigger와 Queue Consumer는 최대 15분까지 허용됩니다.
메모리와 글로벌 상태
각 Worker Isolate는 최대 128MB 메모리를 사용할 수 있습니다. 중요한 점은 글로벌 스코프의 변수가 같은 Isolate 내에서 여러 요청 간에 공유될 수 있다는 것입니다.
// ⚠️ 글로벌 변수 — 같은 Isolate의 요청 간 공유 가능
let requestCount = 0;
export default {
async fetch(request: Request): Promise<Response> {
requestCount++;
// 주의: 이 값은 특정 엣지 노드의 특정 Isolate에서만 유효
// 전 세계적으로 일관된 카운터가 아님!
return new Response(`This isolate has handled ${requestCount} requests`);
},
};
이 특성은 캐시나 커넥션 풀링에 활용할 수 있지만, 영속적이거나 글로벌하게 일관된 상태가 필요하면 KV, D1, Durable Objects를 사용해야 합니다(10회에서 다룹니다).
사용 가능한 Web API
Workers는 브라우저의 Web API 대부분을 지원합니다. 다만 서버리스 특성상 일부 제약이 있습니다.
- 사용 가능: fetch, Request/Response, URL, Headers, TextEncoder/Decoder, crypto(SubtleCrypto), Streams API, WebSocket, Cache API, HTMLRewriter
- 제한적: setTimeout/setInterval (Cron Trigger로 대체), localStorage (KV로 대체)
- 불가: 파일시스템 접근(fs), 네이티브 모듈(node: 일부 polyfill 제공), TCP/UDP 소켓 직접 생성(connect()로 대체)
2026년 현재 Workers는 node: 호환 모듈을 상당수 지원합니다. node:buffer, node:crypto, node:util, node:streams 등이 네이티브로 동작하며, wrangler.toml에서 node_compat = true를 설정하면 추가 polyfill이 활성화됩니다.
커스텀 도메인 연결 — workers.dev를 넘어서
배포된 Worker는 기본적으로 *.workers.dev 서브도메인을 받지만, 2회에서 설정한 자신의 도메인에 연결하는 것이 실전입니다.
방법 1: Custom Domains (권장)
# wrangler.toml에 커스텀 도메인 추가
name = "my-api"
main = "src/index.ts"
compatibility_date = "2026-06-01"
# 커스텀 도메인 — Cloudflare DNS에 등록된 도메인만 가능
# 자동으로 DNS 레코드 + SSL 인증서 생성
routes = [
{ pattern = "api.yourdomain.com", custom_domain = true }
]
# 배포하면 자동으로 DNS 레코드가 생성됨
wrangler deploy
# 이제 커스텀 도메인으로 접근 가능
curl https://api.yourdomain.com/api/hello
방법 2: Route Pattern
기존 도메인의 특정 경로에만 Worker를 적용하고 싶을 때 사용합니다. 예를 들어 블로그는 그대로 두고 /api/* 경로만 Worker가 처리하게 할 수 있습니다.
# wrangler.toml — route 패턴 방식
name = "my-api"
main = "src/index.ts"
compatibility_date = "2026-06-01"
routes = [
{ pattern = "yourdomain.com/api/*", zone_name = "yourdomain.com" }
]
이 설정은 5회의 Tunnel이나 4회의 Pages와 조합하면 강력합니다. Pages로 정적 사이트를 서빙하면서, /api/* 경로만 Workers가 처리하는 구성이 대표적입니다.
실전 패턴: R2 연동 이미지 프록시 Worker
8회에서 설정한 R2 버킷을 Workers와 연결해 봅시다. 단순 공개 URL이 아닌, 접근 제어와 이미지 변환이 포함된 커스텀 CDN을 만듭니다.
R2 바인딩 설정
# wrangler.toml
name = "image-proxy"
main = "src/index.ts"
compatibility_date = "2026-06-01"
# R2 버킷 바인딩 — 8회에서 만든 버킷 이름
[[r2_buckets]]
binding = "MEDIA_BUCKET"
bucket_name = "my-media-bucket"
이미지 프록시 Worker 코드
// src/index.ts — R2 이미지 프록시 + 간단한 접근 제어
interface Env {
MEDIA_BUCKET: R2Bucket;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const path = url.pathname.slice(1); // 선행 / 제거
if (!path) {
return new Response('Not Found', { status: 404 });
}
// Referer 기반 간단한 핫링크 방지
const referer = request.headers.get('Referer');
if (referer) {
const refererHost = new URL(referer).hostname;
if (!refererHost.endsWith('yourdomain.com')) {
return new Response('Forbidden', { status: 403 });
}
}
// R2에서 객체 조회
const object = await env.MEDIA_BUCKET.get(path);
if (!object) {
return new Response('Not Found', { status: 404 });
}
// 캐시 헤더 설정
const headers = new Headers();
object.writeHttpMetadata(headers);
headers.set('Cache-Control', 'public, max-age=86400, s-maxage=604800');
headers.set('ETag', object.httpEtag);
// 조건부 요청 처리 (304 Not Modified)
const ifNoneMatch = request.headers.get('If-None-Match');
if (ifNoneMatch === object.httpEtag) {
return new Response(null, { status: 304, headers });
}
return new Response(object.body, { headers });
},
};
# 배포 및 테스트
wrangler deploy
# R2에 업로드한 이미지에 Worker를 통해 접근
curl -I https://image-proxy.<subdomain>.workers.dev/photos/hero.webp
# HTTP/2 200
# content-type: image/webp
# cache-control: public, max-age=86400, s-maxage=604800
# etag: "abc123..."
이 패턴은 8회의 R2 공개 버킷과 달리, 접근 제어·캐시 정책·URL 구조를 자유롭게 커스터마이징할 수 있습니다. Cloudflare Image Resizing과 결합하면 요청 시점에 리사이즈/포맷 변환까지 가능합니다.

Cron Triggers — 스케줄 작업
Workers는 HTTP 요청뿐 아니라 정해진 시간에 자동 실행되는 Cron Trigger도 지원합니다. 외부 스케줄러 없이 주기적 작업을 엣지에서 처리할 수 있습니다.
# wrangler.toml에 Cron 스케줄 추가
name = "health-checker"
main = "src/index.ts"
compatibility_date = "2026-06-01"
# 매 5분마다 실행
[triggers]
crons = ["*/5 * * * *"]
// src/index.ts — 정기 헬스체크 Worker
interface Env {
// KV나 D1로 결과 저장 가능 (10회에서 다룸)
}
export default {
// HTTP 요청 핸들러
async fetch(request: Request, env: Env): Promise<Response> {
return new Response('Health Checker Worker');
},
// Cron 트리거 핸들러
async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> {
const targets = [
'https://yourdomain.com',
'https://api.yourdomain.com/api/hello',
];
const results = await Promise.allSettled(
targets.map(async (url) => {
const start = Date.now();
const response = await fetch(url, {
method: 'HEAD',
signal: AbortSignal.timeout(10000), // 10초 타임아웃
});
return {
url,
status: response.status,
latency: Date.now() - start,
ok: response.ok,
};
})
);
// 로그 출력 (Workers Logs에서 확인 가능)
console.log(JSON.stringify({
trigger: event.cron,
scheduledTime: new Date(event.scheduledTime).toISOString(),
results: results.map((r) =>
r.status === 'fulfilled' ? r.value : { error: r.reason?.message }
),
}));
},
};
# Cron 로컬 테스트 (wrangler dev에서 수동 트리거)
curl "http://localhost:8787/__scheduled?cron=*/5+*+*+*+*"
wrangler.toml 핵심 설정 해부
프로젝트의 모든 배포 설정은 wrangler.toml에 선언적으로 관리됩니다. 핵심 필드를 정리합니다.
# wrangler.toml — 주석 포함 전체 예시
name = "my-worker" # Worker 이름 (URL에 사용됨)
main = "src/index.ts" # 엔트리 포인트
compatibility_date = "2026-06-01" # Workers 런타임 호환성 날짜
compatibility_flags = ["nodejs_compat_v2"] # Node.js 호환 플래그
# 환경 변수 (평문 — 민감하지 않은 값만)
[vars]
ENVIRONMENT = "production"
API_VERSION = "v1"
# 시크릿은 CLI로만 설정 (toml에 절대 쓰지 않음)
# wrangler secret put API_SECRET
# 커스텀 도메인 + 경로 라우트
routes = [
{ pattern = "api.yourdomain.com", custom_domain = true },
{ pattern = "yourdomain.com/api/*", zone_name = "yourdomain.com" }
]
# R2 바인딩
[[r2_buckets]]
binding = "MEDIA"
bucket_name = "my-media"
# KV 바인딩 (10회에서 상세 다룸)
[[kv_namespaces]]
binding = "CACHE"
id = "abc123def456"
# Cron 스케줄
[triggers]
crons = ["0 */6 * * *"] # 6시간마다
# 로그 푸시 (선택)
[observability]
enabled = true
시크릿 관리
API 키나 토큰 같은 민감 정보는 절대 wrangler.toml이나 코드에 넣지 않습니다. Wrangler CLI로 암호화된 시크릿을 설정합니다.
# 시크릿 설정 (암호화되어 저장, 대시보드에서도 평문 조회 불가)
wrangler secret put DATABASE_URL
# Enter a secret value: › (입력)
# 🌀 Creating the secret for the Worker "my-worker"
# ✨ Success!
# 코드에서 env.DATABASE_URL로 접근
디버깅과 로그
실시간 로그 스트리밍
# 배포된 Worker의 실시간 로그 확인
wrangler tail
# 필터링 — 에러만 보기
wrangler tail --format=json | jq 'select(.level == "error")'
# 특정 IP에서의 요청만 보기
wrangler tail --ip-address=203.0.113.1
로컬 디버깅
# Chrome DevTools 연결로 디버깅
wrangler dev --inspector-port=9229
# Chrome에서 chrome://inspect 접속 → Remote Target 클릭
# 브레이크포인트, 스텝 실행, 메모리 프로파일 모두 가능
Workers 배포 전략 — 점진적 롤아웃
프로덕션 환경에서는 새 버전을 한 번에 전체 배포하는 것이 위험할 수 있습니다. Workers는 Gradual Deployments(점진적 배포)를 지원합니다.
# 현재 배포 상태 확인
wrangler deployments list
# 새 버전을 10% 트래픽에만 먼저 적용
wrangler versions upload
wrangler versions deploy <version-id> --percentage=10
# 문제 없으면 점진적으로 확대
wrangler versions deploy <version-id> --percentage=50
wrangler versions deploy <version-id> --percentage=100
# 문제 발생 시 즉시 롤백
wrangler rollback
이 기능은 유료 플랜(Workers Paid, $5/월)에서 사용 가능합니다. 무료 플랜에서는 wrangler deploy가 즉시 100% 적용됩니다.
Workers Free vs Paid — 무엇을 선택할 것인가
무료 플랜으로도 상당히 많은 것을 할 수 있습니다. 하지만 프로덕션 서비스를 운영한다면 유료 플랜의 차이점을 알아야 합니다.
월 비용 명세표
| 항목 | Free | Workers Paid ($5/월) | Enterprise |
|---|---|---|---|
| 요청 수 | 10만 건/일 | 1,000만 건/월 포함 +$0.30/백만건 초과 |
무제한(협의) |
| CPU 시간/요청 | 10ms | 30ms (기본) Cron: 15분까지 |
커스텀 |
| Worker 스크립트 수 | 100개 | 500개 | 무제한 |
| 스크립트 크기 | 1MB (압축 후) | 10MB (압축 후) | 커스텀 |
| Cron Triggers | 5개/Worker | 5개/Worker | 커스텀 |
| KV 읽기 | 10만/일 | 1,000만/월 포함 | 무제한 |
| KV 쓰기 | 1,000/일 | 100만/월 포함 | 무제한 |
| R2 연동 | 가능 | 가능 | 가능 |
| Durable Objects | 불가 | 가능 (+사용량 과금) | 가능 |
| 점진적 배포 | 불가 | 가능 | 가능 |
| Workers Analytics Engine | 불가 | 가능 | 가능 |
| 월 기본 비용 | $0 | $5 | 협의 |
1인 개발자·사이드 프로젝트 추천: 무료 플랜으로 시작해서, 일일 10만 건을 넘기거나 Durable Objects/점진적 배포가 필요해지면 $5/월 유료로 전환. 일 10만 건은 월 300만 건에 해당하므로, 대부분의 초기 프로젝트에 충분합니다.
Workers vs 다른 서버리스 — 왜 엣지인가
레이턴시 비교
서울에서 각 서버리스 플랫폼으로 간단한 JSON API를 호출했을 때의 대략적인 레이턴시:
- Cloudflare Workers (ICN 엣지): ~5–15ms — 같은 도시에서 처리
- AWS Lambda (ap-northeast-2): ~30–80ms — 같은 리전이지만 VPC/NAT 오버헤드
- AWS Lambda (us-east-1): ~150–250ms — 태평양 횡단 RTT
- Vercel Serverless (iad1): ~140–220ms — 미동부 리전 (기본)
물론 이 비교는 단순 API 응답 기준이며, 데이터베이스 접근이 필요한 경우 DB 위치에 따라 달라집니다. 그래서 Cloudflare는 D1(글로벌 분산 SQLite)과 Hyperdrive(DB 커넥션 풀러)를 함께 제공합니다.
언제 Workers가 아닌 다른 선택이 나은가
- 무거운 연산(ML 추론, 영상 인코딩): CPU 10–30ms 제한이 있으므로 부적합. AWS Lambda/GCP Cloud Run이 나음
- 긴 실행 시간이 필요한 배치: 일반 요청은 30ms 제한. Cron Worker(15분)나 Queues Consumer(15분)로 가능하지만, 시간 단위 작업은 전통 서버가 나음
- 기존 Node.js 앱 마이그레이션: fs, net 등 네이티브 모듈에 깊이 의존한다면 호환성 이슈 가능
Mac Studio 홈랩과 Workers의 조합
홈랩 운영자에게 Workers는 “엣지 레이어”로 매우 유용합니다. 5회의 Tunnel로 홈 서버를 공개하면서, Workers를 앞단에 두면:
- 레이트 리밋: 오리진(홈 서버)에 과도한 요청이 도달하기 전에 엣지에서 차단
- 캐싱: 동적 API 응답도 Cache API로 엣지 캐싱하여 홈 서버 부하 감소
- 인증 게이트: 6회의 Zero Trust Access와 별개로, API 레벨 인증을 Worker에서 처리
- 지리 기반 분기: 해외 접속은 Workers가 직접 응답(캐시), 국내만 오리진으로 프록시
// 홈랩 오리진 앞단의 엣지 캐시 Worker 예시
interface Env {
ORIGIN_URL: string; // 예: https://home.yourdomain.com (Tunnel 경유)
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const cacheKey = new Request(request.url, request);
const cache = caches.default;
// 캐시 히트 확인
let response = await cache.match(cacheKey);
if (response) {
return response;
}
// 오리진으로 프록시
const originUrl = new URL(request.url);
originUrl.hostname = new URL(env.ORIGIN_URL).hostname;
response = await fetch(originUrl.toString(), {
method: request.method,
headers: request.headers,
body: request.body,
});
// 200 응답만 캐시 (5분 TTL)
if (response.ok) {
const cached = new Response(response.body, response);
cached.headers.set('Cache-Control', 'public, max-age=300');
ctx.waitUntil(cache.put(cacheKey, cached.clone()));
return cached;
}
return response;
},
};
흔한 실수와 트러블슈팅
1. “Error 1101: Worker threw an exception”
가장 흔한 에러입니다. Worker 코드에서 잡히지 않은 예외가 발생했을 때 나타납니다.
// ❌ 잡히지 않은 예외
export default {
async fetch(request: Request): Promise<Response> {
const data = await fetch('https://api.example.com/data');
const json = await data.json(); // API가 HTML을 반환하면 여기서 터짐
return Response.json(json);
},
};
// ✅ 방어적 코드
export default {
async fetch(request: Request): Promise<Response> {
try {
const data = await fetch('https://api.example.com/data');
if (!data.ok) {
return Response.json(
{ error: `Upstream error: ${data.status}` },
{ status: 502 }
);
}
const json = await data.json();
return Response.json(json);
} catch (err) {
return Response.json(
{ error: 'Internal error', message: (err as Error).message },
{ status: 500 }
);
}
},
};
2. “Error 1102: Worker exceeded CPU time limit”
무료 플랜의 10ms CPU 제한을 초과했습니다. 복잡한 JSON 파싱이나 암호화 연산에서 발생할 수 있습니다.
// ❌ CPU 집약적 작업
function heavyComputation(data: any[]) {
// 대용량 배열 정렬 + 변환 → CPU 시간 초과 가능
return data
.sort((a, b) => complexCompare(a, b))
.map((item) => expensiveTransform(item));
}
// ✅ 해결 방법:
// 1) 유료 플랜으로 업그레이드 (30ms)
// 2) 작업을 분할하여 여러 요청으로 나눔
// 3) 사전 계산 결과를 KV에 캐시
3. “Error 1042: Worker tried to fetch itself”
Worker가 자기 자신의 URL을 fetch하면 무한 루프 방지를 위해 차단됩니다.
// ❌ 자기 자신 호출 (차단됨)
export default {
async fetch(request: Request): Promise<Response> {
const res = await fetch(request.url); // 자기 자신!
return res;
},
};
// ✅ 오리진 URL을 명시적으로 다르게 지정
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const originUrl = `https://origin.yourdomain.com${new URL(request.url).pathname}`;
const res = await fetch(originUrl);
return res;
},
};
4. Subrequest 제한
하나의 Worker 실행에서 외부로 나갈 수 있는 fetch 호출(subrequest)은 무료 플랜 50건, 유료 플랜 1,000건으로 제한됩니다. 팬아웃 패턴을 쓸 때 주의하세요.
TypeScript 개발 팁
타입 정의 활용
# Workers 타입 정의 패키지 (프로젝트 생성 시 자동 포함)
npm install --save-dev @cloudflare/workers-types
# tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"lib": ["ES2022"],
"types": ["@cloudflare/workers-types"],
"strict": true,
"moduleResolution": "bundler"
}
}
Env 인터페이스 패턴
// src/env.d.ts — 환경 바인딩 타입을 한 곳에서 관리
interface Env {
// 환경 변수
ENVIRONMENT: string;
API_VERSION: string;
// 시크릿
API_SECRET: string;
// KV Namespace
CACHE: KVNamespace;
// R2 Bucket
MEDIA: R2Bucket;
// D1 Database (10회에서 다룸)
DB: D1Database;
// Service Binding (다른 Worker 호출)
AUTH_SERVICE: Fetcher;
}
테스트 작성
Workers 프로젝트는 Vitest와 @cloudflare/vitest-pool-workers로 단위 테스트를 작성합니다. 실제 Workers 런타임과 동일한 환경에서 테스트가 실행됩니다.
// test/index.spec.ts
import { env, createExecutionContext, waitOnExecutionContext } from 'cloudflare:test';
import { describe, it, expect } from 'vitest';
import worker from '../src/index';
describe('API Worker', () => {
it('returns hello from /api/hello', async () => {
const request = new Request('http://localhost/api/hello');
const ctx = createExecutionContext();
const response = await worker.fetch(request, env, ctx);
await waitOnExecutionContext(ctx);
expect(response.status).toBe(200);
const body = await response.json();
expect(body.message).toBe('Hello from the edge!');
});
it('returns 404 for unknown paths', async () => {
const request = new Request('http://localhost/unknown');
const ctx = createExecutionContext();
const response = await worker.fetch(request, env, ctx);
await waitOnExecutionContext(ctx);
expect(response.status).toBe(404);
});
});
# 테스트 실행
npx vitest run
# 워치 모드
npx vitest

보안 체크리스트
Workers를 프로덕션에 배포하기 전 확인해야 할 보안 사항:
- 시크릿 관리: API 키·토큰은 반드시
wrangler secret put으로만 설정.wrangler.toml이나 코드에 하드코딩 금지 - 입력 검증: URL 파라미터, 요청 바디를 그대로 신뢰하지 않기. 특히 외부 fetch URL을 사용자 입력에서 조립하면 SSRF 위험
- 에러 노출: 프로덕션에서 스택 트레이스나 내부 정보를 응답에 포함하지 않기
- CORS:
Access-Control-Allow-Origin: *은 공개 API에만 사용. 민감한 API는 오리진을 명시적으로 지정 - Rate Limiting: Workers의 내장
cf-rate-limit바인딩이나 KV 기반 카운터로 남용 방지
다음 단계로의 확장
오늘 만든 Worker는 상태가 없는(stateless) 함수입니다. 하지만 실제 애플리케이션은 데이터를 저장하고 읽어야 합니다. Cloudflare는 Workers와 네이티브로 통합되는 세 가지 스토리지를 제공합니다:
- Workers KV: 전역적으로 분산된 키-값 저장소. 읽기 최적화, 최종 일관성
- D1: 엣지에서 실행되는 SQLite 데이터베이스. 관계형 쿼리가 필요할 때
- Durable Objects: 강한 일관성이 필요한 상태(채팅방, 실시간 카운터, 게임 세션)
10회에서 이 세 가지 스토리지를 Worker에 연결해 “상태 있는 엣지 애플리케이션”을 만드는 방법을 다룹니다.
정리 — 오늘 배운 것
- Cloudflare Workers는 V8 Isolate 기반 서버리스로, 전 세계 330개 엣지에서 0ms 콜드 스타트로 코드를 실행
wrangler init→wrangler dev→wrangler deploy세 명령으로 로컬 개발부터 글로벌 배포까지 완료- Web Standards(Fetch API) 기반이라 프론트엔드 개발 경험이 그대로 통용
- 무료 플랜으로 일 10만 건(월 300만 건) 처리 가능, 유료는 $5/월부터
- R2·KV·D1 등 Cloudflare 생태계와 네이티브 바인딩으로 결합
- 커스텀 도메인, Cron Trigger, 점진적 배포까지 프로덕션 운영 패턴 지원
다음 10회에서는 Workers KV, D1 SQLite, Durable Objects를 연결해 “상태 있는 엣지 풀스택”을 완성합니다. 데이터베이스 없는 서버리스의 한계를 깨는 시간이 될 것입니다.
이미지는 Leonardo AI 로 생성되었습니다.
이미지는 Claude AI 로 생성되었습니다.
◀ 이전 8화 (다음 차수는 아직 게시되지 않았습니다)



[Cloudflare 완전 정복: 입문부터 2026 AI 에이전트까지] 10/16화: Workers KV vs D1 vs Durable Objects 선택 가이드 - 일상의 소소함
[…] Cloudflare 완전 정복: 입문부터 2026 AI 에이전트까지 (총 16화 중 10화)◀ 이전 9화 (다음 차수는 아직 게시되지 않았습니다) 카테고리: IT기술 […]