본문으로 건너뛰기
Previous
Next
Cloudflare Workers 완전 가이드 | Edge에서 실행되는 서버리스 함수

Cloudflare Workers 완전 가이드 | Edge에서 실행되는 서버리스 함수

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️⃣ 계정 생성

  1. dash.cloudflare.com 접속
  2. 회원가입 (무료)
  3. 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

항목WorkersLambda
콜드 스타트0ms200-1000ms
실행 위치Edge (300+ 도시)리전 (25개)
격리V8 Isolates컨테이너
무료 티어10만 요청/일100만 요청/월
CPU 시간10ms-50ms제한 없음
메모리128MB최대 10GB
실행 시간30초 (유료 15분)15분
Node.jsWeb 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의 장점

  1. 0ms 콜드 스타트: 즉시 실행
  2. Edge 실행: 전 세계 300개 도시
  3. 무료 플랜: 하루 10만 요청
  4. 풀스택 개발: KV·D1·R2·Durable Objects
  5. Web Standards: 표준 API 사용

🚀 다음 단계


시작하기: wrangler init로 5분 만에 프로젝트를 시작하고, 전 세계에서 0ms로 실행되는 서버리스 함수를 배포하세요! 🚀