Cloudflare Workers 완전 가이드 | Edge에서 실행되는 서버리스 함수
이 글의 핵심
AWS Lambda보다 빠른 Cloudflare Workers. 전 세계 300개 도시에서 0ms 콜드 스타트로 실행되며, KV·D1·R2·Durable Objects로 풀스택 개발이 가능합니다. 무료 플랜으로 하루 10만 요청까지 지원합니다.
이 글의 핵심
Cloudflare Workers는 전 세계 300개 도시에서 실행되는 서버리스 함수입니다. 0ms 콜드 스타트, V8 Isolates 기반, 무료 10만 요청/일로 AWS Lambda보다 빠르고 저렴하며, KV·D1·R2·Durable Objects로 풀스택 개발이 가능합니다.
목차
Cloudflare Workers란?
Cloudflare Workers는 2017년 출시된 Edge Computing 플랫폼으로, Cloudflare의 글로벌 네트워크에서 서버리스 함수를 실행합니다.
🚀 핵심 특징
1. 0ms 콜드 스타트
AWS Lambda:
- 콜드 스타트: 200-1000ms
- 컨테이너 기반
Cloudflare Workers:
- 콜드 스타트: 0ms
- V8 Isolates 기반
2. Edge에서 실행
- 전 세계 300개 도시에 배포
- 사용자와 가장 가까운 데이터센터에서 실행
- 평균 지연 시간 50ms 이하
3. Web Standards API
// 표준 Web API 사용
export default {
async fetch(request) {
return new Response('Hello World!');
}
}
4. 무료 플랜
- 10만 요청/일
- CPU 시간: 10ms/요청
- 1000개 Workers
- KV·D1·R2 무료 티어 포함
Cloudflare Workers 시작하기
1️⃣ 계정 생성
- dash.cloudflare.com 접속
- 회원가입 (무료)
- Workers & Pages 섹션으로 이동
2️⃣ Wrangler CLI 설치
# Wrangler 설치 (Cloudflare Workers CLI)
npm install -g wrangler
# 로그인
wrangler login
# 버전 확인
wrangler --version
3️⃣ 첫 Worker 생성
# 새 프로젝트 생성
wrangler init my-worker
# 프로젝트로 이동
cd my-worker
생성된 파일:
my-worker/
├── src/
│ └── index.ts # Worker 코드
├── wrangler.toml # 설정 파일
└── package.json
Hello World Worker
Worker 코드 작성
// src/index.ts
export default {
async fetch(request: Request): Promise<Response> {
return new Response('Hello from Cloudflare Workers!', {
headers: {
'content-type': 'text/plain',
},
});
},
};
로컬 개발 서버
# 개발 서버 시작
wrangler dev
# 브라우저에서 접속
# http://localhost:8787
배포
# 프로덕션 배포
wrangler deploy
# 결과:
# Published my-worker (1.2s)
# https://my-worker.YOUR_SUBDOMAIN.workers.dev
REST API 만들기
라우팅 처리
// src/index.ts
export default {
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
// GET /
if (url.pathname === '/' && request.method === 'GET') {
return new Response('Welcome to API!');
}
// GET /users
if (url.pathname === '/users' && request.method === 'GET') {
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
];
return new Response(JSON.stringify(users), {
headers: { 'content-type': 'application/json' },
});
}
// GET /users/:id
const userMatch = url.pathname.match(/^\/users\/(\d+)$/);
if (userMatch && request.method === 'GET') {
const userId = parseInt(userMatch[1]);
const user = { id: userId, name: `User ${userId}` };
return new Response(JSON.stringify(user), {
headers: { 'content-type': 'application/json' },
});
}
// POST /users
if (url.pathname === '/users' && request.method === 'POST') {
const body = await request.json();
const newUser = { id: Date.now(), ...body };
return new Response(JSON.stringify(newUser), {
status: 201,
headers: { 'content-type': 'application/json' },
});
}
// 404
return new Response('Not Found', { status: 404 });
},
};
Workers KV (Key-Value Storage)
KV 네임스페이스 생성
# KV 네임스페이스 생성
wrangler kv:namespace create MY_KV
# 결과:
# id = "abc123def456"
wrangler.toml 설정
name = "my-worker"
main = "src/index.ts"
compatibility_date = "2024-01-01"
[[kv_namespaces]]
binding = "MY_KV"
id = "abc123def456"
KV 사용 예제
// src/index.ts
interface Env {
MY_KV: KVNamespace;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
// GET /cache/:key
if (url.pathname.startsWith('/cache/')) {
const key = url.pathname.split('/')[2];
const value = await env.MY_KV.get(key);
if (value) {
return new Response(value);
}
return new Response('Not found', { status: 404 });
}
// POST /cache/:key
if (url.pathname.startsWith('/cache/') && request.method === 'POST') {
const key = url.pathname.split('/')[2];
const value = await request.text();
// TTL: 60초
await env.MY_KV.put(key, value, { expirationTtl: 60 });
return new Response('Saved!');
}
return new Response('Hello!');
},
};
KV CLI 명령어
# 키 쓰기
wrangler kv:key put --binding=MY_KV "mykey" "myvalue"
# 키 읽기
wrangler kv:key get --binding=MY_KV "mykey"
# 키 삭제
wrangler kv:key delete --binding=MY_KV "mykey"
# 모든 키 조회
wrangler kv:key list --binding=MY_KV
Workers D1 (SQL Database)
D1 데이터베이스 생성
# D1 데이터베이스 생성
wrangler d1 create my-database
# 결과:
# database_id = "xyz789"
wrangler.toml 설정
[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "xyz789"
스키마 생성
-- schema.sql
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO users (name, email) VALUES
('Alice', '[email protected]'),
('Bob', '[email protected]');
# 스키마 적용
wrangler d1 execute my-database --file=schema.sql
D1 사용 예제
// src/index.ts
interface Env {
DB: D1Database;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
// GET /users
if (url.pathname === '/users' && request.method === 'GET') {
const { results } = await env.DB.prepare(
'SELECT * FROM users'
).all();
return Response.json(results);
}
// POST /users
if (url.pathname === '/users' && request.method === 'POST') {
const body = await request.json<{ name: string; email: string }>();
const { success } = await env.DB.prepare(
'INSERT INTO users (name, email) VALUES (?, ?)'
).bind(body.name, body.email).run();
if (success) {
return new Response('Created', { status: 201 });
}
return new Response('Error', { status: 500 });
}
// GET /users/:id
const match = url.pathname.match(/^\/users\/(\d+)$/);
if (match && request.method === 'GET') {
const id = parseInt(match[1]);
const user = await env.DB.prepare(
'SELECT * FROM users WHERE id = ?'
).bind(id).first();
if (user) {
return Response.json(user);
}
return new Response('Not found', { status: 404 });
}
return new Response('Hello!');
},
};
Workers R2 (Object Storage)
R2 버킷 생성
# R2 버킷 생성
wrangler r2 bucket create my-bucket
wrangler.toml 설정
[[r2_buckets]]
binding = "MY_BUCKET"
bucket_name = "my-bucket"
R2 사용 예제
// src/index.ts
interface Env {
MY_BUCKET: R2Bucket;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
// GET /files/:key
if (url.pathname.startsWith('/files/')) {
const key = url.pathname.split('/')[2];
const object = await env.MY_BUCKET.get(key);
if (object) {
return new Response(object.body, {
headers: {
'content-type': object.httpMetadata?.contentType || 'application/octet-stream',
},
});
}
return new Response('Not found', { status: 404 });
}
// POST /files/:key
if (url.pathname.startsWith('/files/') && request.method === 'POST') {
const key = url.pathname.split('/')[2];
const body = await request.arrayBuffer();
await env.MY_BUCKET.put(key, body, {
httpMetadata: {
contentType: request.headers.get('content-type') || 'application/octet-stream',
},
});
return new Response('Uploaded!');
}
return new Response('Hello!');
},
};
실전 프로젝트: URL 단축기
기능 명세
- ✅ 긴 URL → 짧은 코드 생성
- ✅ 짧은 코드 → 원본 URL 리다이렉트
- ✅ 클릭 카운트 추적
- ✅ D1 + KV 활용
데이터베이스 스키마
-- schema.sql
CREATE TABLE urls (
id INTEGER PRIMARY KEY AUTOINCREMENT,
short_code TEXT UNIQUE NOT NULL,
long_url TEXT NOT NULL,
clicks INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_short_code ON urls(short_code);
Worker 구현
// src/index.ts
interface Env {
DB: D1Database;
URL_CACHE: KVNamespace;
}
function generateShortCode(): string {
return Math.random().toString(36).substring(2, 8);
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
// POST /shorten - URL 단축
if (url.pathname === '/shorten' && request.method === 'POST') {
const { longUrl } = await request.json<{ longUrl: string }>();
const shortCode = generateShortCode();
// D1에 저장
await env.DB.prepare(
'INSERT INTO urls (short_code, long_url) VALUES (?, ?)'
).bind(shortCode, longUrl).run();
// KV 캐시에 저장 (빠른 조회)
await env.URL_CACHE.put(shortCode, longUrl, {
expirationTtl: 86400, // 24시간
});
return Response.json({
shortUrl: `${url.origin}/${shortCode}`,
shortCode,
longUrl,
});
}
// GET /:code - 리다이렉트
const code = url.pathname.substring(1);
if (code) {
// 1. KV 캐시에서 먼저 조회
let longUrl = await env.URL_CACHE.get(code);
if (!longUrl) {
// 2. D1에서 조회
const result = await env.DB.prepare(
'SELECT long_url FROM urls WHERE short_code = ?'
).bind(code).first<{ long_url: string }>();
if (result) {
longUrl = result.long_url;
// KV 캐시에 저장
await env.URL_CACHE.put(code, longUrl, {
expirationTtl: 86400,
});
}
}
if (longUrl) {
// 클릭 카운트 증가 (비동기)
env.DB.prepare(
'UPDATE urls SET clicks = clicks + 1 WHERE short_code = ?'
).bind(code).run();
return Response.redirect(longUrl, 301);
}
}
return new Response('URL Shortener\n\nPOST /shorten\n{"longUrl":"https://example.com"}');
},
};
Cloudflare Workers vs AWS Lambda
| 항목 | Workers | Lambda |
|---|---|---|
| 콜드 스타트 | 0ms | 200-1000ms |
| 실행 위치 | Edge (300+ 도시) | 리전 (25개) |
| 격리 | V8 Isolates | 컨테이너 |
| 무료 티어 | 10만 요청/일 | 100만 요청/월 |
| CPU 시간 | 10ms-50ms | 제한 없음 |
| 메모리 | 128MB | 최대 10GB |
| 실행 시간 | 30초 (유료 15분) | 15분 |
| Node.js | Web Standards | 완전 지원 |
성능 최적화
1. KV 캐싱 전략
// 읽기 성능 최적화
async function getWithCache(key: string, kv: KVNamespace, db: D1Database) {
// 1. KV 캐시 확인
let value = await kv.get(key);
if (value) return value;
// 2. DB 조회
const result = await db.prepare('SELECT value FROM data WHERE key = ?')
.bind(key).first();
if (result) {
value = result.value;
// 3. KV 캐시 저장
await kv.put(key, value, { expirationTtl: 3600 });
return value;
}
return null;
}
2. Durable Objects (상태 유지)
// Durable Object 정의
export class Counter {
state: DurableObjectState;
count: number = 0;
constructor(state: DurableObjectState) {
this.state = state;
this.state.blockConcurrencyWhile(async () => {
this.count = (await this.state.storage.get('count')) || 0;
});
}
async fetch(request: Request) {
const url = new URL(request.url);
if (url.pathname === '/increment') {
this.count++;
await this.state.storage.put('count', this.count);
return new Response(String(this.count));
}
return new Response(String(this.count));
}
}
// Worker에서 사용
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const id = env.COUNTER.idFromName('global');
const stub = env.COUNTER.get(id);
return stub.fetch(request);
},
};
핵심 정리
✅ Cloudflare Workers의 장점
- 0ms 콜드 스타트: 즉시 실행
- Edge 실행: 전 세계 300개 도시
- 무료 플랜: 하루 10만 요청
- 풀스택 개발: KV·D1·R2·Durable Objects
- Web Standards: 표준 API 사용
🚀 다음 단계
- Workers 공식 문서에서 심화 학습
- Workers Examples에서 예제 탐색
- Discord에서 커뮤니티 참여
시작하기:
wrangler init로 5분 만에 프로젝트를 시작하고, 전 세계에서 0ms로 실행되는 서버리스 함수를 배포하세요! 🚀