Edge Computing 실전 가이드 | Cloudflare Workers, Vercel Edge, Deno Deploy
이 글의 핵심
Edge Computing의 핵심 개념과 실전 활용법. Cloudflare Workers, Vercel Edge Functions, Deno Deploy 비교, 구현 예제, 제약사항, 성능 최적화, 실무 사례를 다룹니다.
들어가며
Edge Computing은 코드를 전 세계 CDN 노드에서 실행하여 사용자와 가장 가까운 위치에서 응답을 생성하는 기술입니다. 서울 사용자는 서울 노드에서, 뉴욕 사용자는 뉴욕 노드에서 실행되므로 지연 시간이 50ms 이하로 낮아집니다.
이 글은 Edge Computing의 핵심 개념, 주요 플랫폼 비교 (Cloudflare Workers, Vercel Edge, Deno Deploy), 실전 구현, 제약사항, 최적화 기법을 단계별로 설명합니다.
목차
- Edge Computing이란?
- 플랫폼 비교
- Cloudflare Workers
- Vercel Edge Functions
- Deno Deploy
- Edge 데이터베이스
- 제약사항 및 해결
- 성능 최적화
- 실무 사례
- 트러블슈팅
- 마무리
Edge Computing이란?
아키텍처 비교
전통적인 서버:
사용자 (서울) → 서버 (미국 버지니아) → 응답
지연 시간: 200-300ms
CDN (정적 콘텐츠):
사용자 (서울) → CDN 노드 (서울) → 캐시된 파일
지연 시간: 10-20ms
Edge Computing (동적 콘텐츠):
사용자 (서울) → Edge 노드 (서울) → 코드 실행 → 응답
지연 시간: 30-50ms
핵심 특징
1. 글로벌 분산
- 전 세계 200+ 노드에서 동일 코드 실행
- 사용자와 가장 가까운 노드 자동 선택
- 지역별 트래픽 폭증에 자동 대응
2. 서버리스 실행
- 서버 관리 불필요
- 자동 스케일링
- 사용량 기반 과금
3. 낮은 콜드 스타트
- V8 Isolate 기반 (Cloudflare)
- 콜드 스타트: ~5ms (일반 Lambda: ~100-500ms)
4. 제약사항
- 실행 시간: 10-30초
- 메모리: 128MB
- CPU 제한
- Node.js API 일부만 지원
플랫폼 비교
| 항목 | Cloudflare Workers | Vercel Edge | Deno Deploy |
|---|---|---|---|
| 노드 수 | 300+ | 20+ | 35+ |
| 런타임 | V8 Isolate | V8 Isolate | Deno Runtime |
| 언어 | JS, TS, Rust, C++ (WASM) | JS, TS | JS, TS |
| 실행 시간 | 30초 (무료: 10ms) | 30초 | 30초 |
| 메모리 | 128MB | 128MB | 512MB |
| 무료 요청 | 100K/일 | 100K/월 | 100K/일 |
| 가격 | $5/1000만 요청 | $20/100만 요청 | $10/100만 요청 |
| DB 통합 | D1, KV, Durable Objects | Vercel Postgres, KV | Deno KV |
| WebSocket | Durable Objects | 제한적 | 지원 |
| 특징 | 가장 빠름, 저렴 | Next.js 통합 우수 | 표준 API, TypeScript 네이티브 |
선택 가이드
Cloudflare Workers 선택:
- 최저 지연 시간 필요
- 대규모 트래픽 (수백만 요청/일)
- 비용 최적화 중요
Vercel Edge 선택:
- Next.js 프로젝트
- 빠른 배포 및 개발 경험
- Vercel 생태계 활용
Deno Deploy 선택:
- TypeScript 네이티브 개발
- 표준 Web API 선호
- Deno 생태계 활용
Cloudflare Workers
기본 예제
// worker.js
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
if (url.pathname === '/api/hello') {
return new Response(JSON.stringify({
message: 'Hello from Edge!',
location: request.cf.city, // 사용자 위치
timestamp: Date.now()
}), {
headers: { 'Content-Type': 'application/json' }
});
}
return new Response('Not Found', { status: 404 });
}
};
KV 스토리지 사용
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
const key = url.pathname.slice(1); // /key → key
if (request.method === 'GET') {
// KV에서 읽기
const value = await env.MY_KV.get(key);
if (value === null) {
return new Response('Not found', { status: 404 });
}
return new Response(value);
}
if (request.method === 'PUT') {
// KV에 쓰기
const value = await request.text();
await env.MY_KV.put(key, value, {
expirationTtl: 3600 // 1시간 후 만료
});
return new Response('Stored');
}
return new Response('Method not allowed', { status: 405 });
}
};
D1 데이터베이스
export default {
async fetch(request, env, ctx) {
if (request.method === 'GET') {
// 사용자 목록 조회
const { results } = await env.DB.prepare(
'SELECT * FROM users LIMIT 10'
).all();
return Response.json(results);
}
if (request.method === 'POST') {
// 사용자 생성
const { name, email } = await request.json();
await env.DB.prepare(
'INSERT INTO users (name, email) VALUES (?, ?)'
).bind(name, email).run();
return Response.json({ success: true });
}
}
};
캐싱 전략
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
const cacheKey = new Request(url.toString(), request);
const cache = caches.default;
// 캐시 확인
let response = await cache.match(cacheKey);
if (!response) {
// 캐시 미스: 원본 서버에서 가져오기
response = await fetch(request);
// 캐시 저장 (1시간)
response = new Response(response.body, response);
response.headers.set('Cache-Control', 'max-age=3600');
ctx.waitUntil(cache.put(cacheKey, response.clone()));
}
return response;
}
};
Vercel Edge Functions
기본 예제
// app/api/hello/route.ts
import { NextRequest, NextResponse } from 'next/server';
export const runtime = 'edge'; // Edge Runtime 사용
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const name = searchParams.get('name') || 'World';
return NextResponse.json({
message: `Hello, ${name}!`,
location: request.geo?.city,
country: request.geo?.country
});
}
Middleware (Edge에서 실행)
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// A/B 테스트
const bucket = Math.random() < 0.5 ? 'a' : 'b';
const response = NextResponse.next();
response.cookies.set('bucket', bucket);
// 지역별 리다이렉트
const country = request.geo?.country;
if (country === 'KR' && !request.nextUrl.pathname.startsWith('/ko')) {
return NextResponse.redirect(new URL('/ko', request.url));
}
// 인증 확인
const token = request.cookies.get('auth_token');
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
return response;
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)']
};
Vercel KV (Redis)
import { kv } from '@vercel/kv';
export const runtime = 'edge';
export async function GET(request: Request) {
const url = new URL(request.url);
const key = url.searchParams.get('key');
if (!key) {
return new Response('Key required', { status: 400 });
}
// KV에서 읽기
const value = await kv.get(key);
if (value === null) {
return new Response('Not found', { status: 404 });
}
return Response.json({ key, value });
}
export async function POST(request: Request) {
const { key, value, ttl } = await request.json();
// KV에 쓰기
if (ttl) {
await kv.setex(key, ttl, value);
} else {
await kv.set(key, value);
}
return Response.json({ success: true });
}
Deno Deploy
기본 예제
// main.ts
Deno.serve(async (req) => {
const url = new URL(req.url);
if (url.pathname === '/api/hello') {
return new Response(JSON.stringify({
message: 'Hello from Deno Deploy!',
timestamp: new Date().toISOString()
}), {
headers: { 'Content-Type': 'application/json' }
});
}
return new Response('Not Found', { status: 404 });
});
Deno KV 사용
const kv = await Deno.openKv();
Deno.serve(async (req) => {
const url = new URL(req.url);
if (req.method === 'GET') {
const key = url.searchParams.get('key');
if (!key) {
return new Response('Key required', { status: 400 });
}
// KV에서 읽기
const entry = await kv.get([key]);
if (entry.value === null) {
return new Response('Not found', { status: 404 });
}
return Response.json({ key, value: entry.value });
}
if (req.method === 'POST') {
const { key, value } = await req.json();
// KV에 쓰기
await kv.set([key], value);
return Response.json({ success: true });
}
});
표준 Web API 활용
// Fetch API
const response = await fetch('https://api.example.com/data');
const data = await response.json();
// Web Crypto API
const encoder = new TextEncoder();
const data = encoder.encode('hello');
const hash = await crypto.subtle.digest('SHA-256', data);
// Streams API
Deno.serve(async (req) => {
const stream = new ReadableStream({
start(controller) {
controller.enqueue('chunk 1\n');
controller.enqueue('chunk 2\n');
controller.close();
}
});
return new Response(stream, {
headers: { 'Content-Type': 'text/plain' }
});
});
Edge 데이터베이스
Cloudflare D1 (SQLite)
export default {
async fetch(request, env, ctx) {
// 쿼리 실행
const { results } = await env.DB.prepare(
'SELECT * FROM posts WHERE published = ? ORDER BY created_at DESC LIMIT 10'
).bind(true).all();
return Response.json(results);
}
};
// 트랜잭션
async function createUser(db, name, email) {
const batch = [
db.prepare('INSERT INTO users (name, email) VALUES (?, ?)').bind(name, email),
db.prepare('INSERT INTO audit_log (action, timestamp) VALUES (?, ?)').bind('user_created', Date.now())
];
await db.batch(batch);
}
PlanetScale (MySQL)
// Vercel Edge Function
import { connect } from '@planetscale/database';
export const runtime = 'edge';
export async function GET() {
const conn = connect({
url: process.env.DATABASE_URL
});
const results = await conn.execute(
'SELECT * FROM posts WHERE published = true LIMIT 10'
);
return Response.json(results.rows);
}
Upstash Redis
import { Redis } from '@upstash/redis';
export const runtime = 'edge';
const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL,
token: process.env.UPSTASH_REDIS_REST_TOKEN
});
export async function GET(request: Request) {
const url = new URL(request.url);
const key = url.searchParams.get('key');
// 캐시 확인
const cached = await redis.get(key);
if (cached) {
return Response.json({ value: cached, cached: true });
}
// 원본 데이터 가져오기
const data = await fetchFromOrigin(key);
// 캐시 저장 (1시간)
await redis.setex(key, 3600, data);
return Response.json({ value: data, cached: false });
}
제약사항 및 해결
1. Node.js API 제한
문제: fs, path, crypto (Node.js) 사용 불가
해결:
// ❌ Node.js API
import fs from 'fs';
import crypto from 'crypto';
// ✅ Web API
const hash = await crypto.subtle.digest('SHA-256', data);
// ✅ Cloudflare Workers API
const file = await env.BUCKET.get('file.txt');
2. 실행 시간 제한
문제: 30초 초과 시 타임아웃
해결:
// ❌ 긴 연산
export default {
async fetch(request) {
const result = await longComputation(); // 1분 소요
return Response.json(result);
}
};
// ✅ 백그라운드 작업으로 분리
export default {
async fetch(request, env, ctx) {
// 즉시 응답
const jobId = crypto.randomUUID();
// 백그라운드 작업 (ctx.waitUntil)
ctx.waitUntil(
env.QUEUE.send({ jobId, data: await request.json() })
);
return Response.json({ jobId, status: 'processing' });
}
};
3. 상태 유지 불가
문제: 요청 간 메모리 공유 불가
해결:
// ❌ 전역 변수 (요청 간 공유 안 됨)
let counter = 0;
export default {
async fetch(request) {
counter++; // 매번 0에서 시작
return Response.json({ counter });
}
};
// ✅ KV 스토리지 사용
export default {
async fetch(request, env, ctx) {
const counter = await env.KV.get('counter') || 0;
await env.KV.put('counter', counter + 1);
return Response.json({ counter: counter + 1 });
}
};
4. 패키지 크기 제한
문제: 번들 크기 1MB 제한
해결:
// ❌ 큰 라이브러리
import moment from 'moment'; // 200KB+
// ✅ 작은 대안
import { format } from 'date-fns'; // 10KB (트리 셰이킹)
// ✅ 네이티브 API
const date = new Date().toISOString();
성능 최적화
1. 캐싱 전략
Edge 캐시 + KV 조합:
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
const cacheKey = new Request(url.toString());
const cache = caches.default;
// 1. Edge 캐시 확인 (가장 빠름)
let response = await cache.match(cacheKey);
if (response) {
return response;
}
// 2. KV 확인 (중간)
const cached = await env.KV.get(url.pathname);
if (cached) {
response = new Response(cached, {
headers: { 'Cache-Control': 'max-age=3600' }
});
ctx.waitUntil(cache.put(cacheKey, response.clone()));
return response;
}
// 3. 원본 서버 (가장 느림)
const data = await fetchFromOrigin(url.pathname);
response = Response.json(data, {
headers: { 'Cache-Control': 'max-age=3600' }
});
// 비동기로 캐시 저장
ctx.waitUntil(Promise.all([
cache.put(cacheKey, response.clone()),
env.KV.put(url.pathname, JSON.stringify(data), { expirationTtl: 3600 })
]));
return response;
}
};
2. 조건부 요청
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
const key = url.pathname;
// ETag 생성
const data = await env.KV.get(key);
const etag = `"${await hashData(data)}"`;
// 클라이언트 ETag 확인
const clientETag = request.headers.get('If-None-Match');
if (clientETag === etag) {
return new Response(null, { status: 304 }); // Not Modified
}
return new Response(data, {
headers: {
'ETag': etag,
'Cache-Control': 'max-age=3600'
}
});
}
};
async function hashData(data: string): Promise<string> {
const encoder = new TextEncoder();
const hash = await crypto.subtle.digest('SHA-256', encoder.encode(data));
return Array.from(new Uint8Array(hash))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
3. 스트리밍 응답
export default {
async fetch(request) {
const stream = new ReadableStream({
async start(controller) {
// 대용량 데이터를 청크로 전송
for (let i = 0; i < 100; i++) {
const chunk = await fetchChunk(i);
controller.enqueue(new TextEncoder().encode(chunk + '\n'));
// 백프레셔 처리
if (controller.desiredSize <= 0) {
await new Promise(resolve => setTimeout(resolve, 100));
}
}
controller.close();
}
});
return new Response(stream, {
headers: { 'Content-Type': 'text/plain' }
});
}
};
실무 사례
1. API 게이트웨이
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
// 인증 확인
const token = request.headers.get('Authorization');
if (!token) {
return new Response('Unauthorized', { status: 401 });
}
// Rate limiting
const clientIP = request.headers.get('CF-Connecting-IP');
const rateLimitKey = `rate:${clientIP}`;
const count = await env.KV.get(rateLimitKey) || 0;
if (count > 100) {
return new Response('Too Many Requests', { status: 429 });
}
await env.KV.put(rateLimitKey, count + 1, { expirationTtl: 60 });
// 백엔드로 프록시
const backendUrl = `https://api.backend.com${url.pathname}`;
const response = await fetch(backendUrl, {
method: request.method,
headers: request.headers,
body: request.body
});
return response;
}
};
2. 이미지 최적화
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
const imageUrl = url.searchParams.get('url');
if (!imageUrl) {
return new Response('URL required', { status: 400 });
}
// Accept 헤더로 포맷 결정
const accept = request.headers.get('Accept') || '';
const format = accept.includes('image/webp') ? 'webp' :
accept.includes('image/avif') ? 'avif' : 'jpeg';
// Cloudflare Image Resizing
const imageRequest = new Request(imageUrl, {
cf: {
image: {
width: 800,
quality: 85,
format: format
}
}
});
const response = await fetch(imageRequest);
return new Response(response.body, {
headers: {
'Content-Type': `image/${format}`,
'Cache-Control': 'max-age=86400'
}
});
}
};
3. 개인화 콘텐츠
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
// 사용자 정보 (쿠키 또는 헤더)
const userId = request.headers.get('X-User-ID');
const country = request.cf.country;
// 개인화 캐시 키
const cacheKey = `content:${url.pathname}:${userId}:${country}`;
// KV에서 개인화 콘텐츠 확인
const cached = await env.KV.get(cacheKey);
if (cached) {
return new Response(cached, {
headers: { 'Content-Type': 'text/html' }
});
}
// 개인화 콘텐츠 생성
const content = await generatePersonalizedContent(userId, country);
// 캐시 저장 (10분)
ctx.waitUntil(
env.KV.put(cacheKey, content, { expirationTtl: 600 })
);
return new Response(content, {
headers: { 'Content-Type': 'text/html' }
});
}
};
4. 서버사이드 A/B 테스트
export default {
async fetch(request, env, ctx) {
// 사용자 ID로 일관된 버킷 할당
const userId = request.headers.get('X-User-ID') ||
request.headers.get('CF-Connecting-IP');
const hash = await hashString(userId);
const bucket = hash % 100 < 50 ? 'A' : 'B';
// 버킷별 다른 응답
const content = bucket === 'A'
? await fetchVariantA()
: await fetchVariantB();
// 분석 이벤트 전송
ctx.waitUntil(
env.ANALYTICS.writeDataPoint({
userId,
bucket,
timestamp: Date.now()
})
);
return new Response(content, {
headers: { 'X-AB-Bucket': bucket }
});
}
};
async function hashString(str: string): Promise<number> {
const encoder = new TextEncoder();
const data = encoder.encode(str);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.reduce((acc, byte) => acc + byte, 0);
}
트러블슈팅
문제 1: CPU 시간 초과
증상:
Error: CPU time limit exceeded
해결:
// ❌ CPU 집약적 작업
function heavyComputation(n: number) {
let sum = 0;
for (let i = 0; i < n * 1000000; i++) {
sum += Math.sqrt(i);
}
return sum;
}
// ✅ 작업 분할 또는 백엔드로 이동
export default {
async fetch(request, env, ctx) {
// Edge에서는 간단한 작업만
const params = await request.json();
// 무거운 작업은 백엔드로
const response = await fetch('https://backend.com/compute', {
method: 'POST',
body: JSON.stringify(params)
});
return response;
}
};
문제 2: 패키지 호환성
증상:
Error: Module "fs" is not available in Workers
해결:
// ❌ Node.js 전용 패키지
import fs from 'fs';
// ✅ Edge 호환 패키지 찾기
// 또는 필요한 기능만 직접 구현
// ✅ 조건부 import
let parser;
if (typeof Deno !== 'undefined') {
parser = await import('./deno-parser.ts');
} else {
parser = await import('./edge-parser.ts');
}
문제 3: 콜드 스타트 느림
증상: 첫 요청이 느림
해결:
// 1. 번들 크기 최소화
// ❌ 전체 라이브러리 import
import _ from 'lodash';
// ✅ 필요한 함수만
import { debounce } from 'lodash-es';
// 2. 동적 import 사용
export default {
async fetch(request) {
const url = new URL(request.url);
if (url.pathname === '/heavy') {
// 필요할 때만 로드
const { processHeavy } = await import('./heavy.js');
return await processHeavy(request);
}
return new Response('OK');
}
};
// 3. 워밍업 요청
// 정기적으로 더미 요청 전송하여 콜드 스타트 방지
비용 최적화
요청 수 절감
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
// 1. 정적 파일은 CDN 캐시 활용
if (url.pathname.match(/\.(js|css|png|jpg)$/)) {
return fetch(request); // 오리진으로 (CDN 캐시됨)
}
// 2. API 응답 캐싱
const cacheKey = new Request(url.toString());
const cache = caches.default;
let response = await cache.match(cacheKey);
if (response) {
return response; // Edge 함수 실행 안 함 (무료)
}
// 3. 실제 처리
response = await processRequest(request, env);
// 4. 캐시 저장
response = new Response(response.body, response);
response.headers.set('Cache-Control', 'max-age=3600');
ctx.waitUntil(cache.put(cacheKey, response.clone()));
return response;
}
};
마무리
Edge Computing은 글로벌 저지연 서비스를 구축하는 핵심 기술입니다:
핵심 장점:
- 낮은 지연: 30-50ms (전통적 서버: 200-300ms)
- 자동 스케일링: 트래픽 급증에 자동 대응
- 글로벌 분산: 전 세계 동일한 성능
- 비용 효율: 사용량 기반 과금
적합한 사용 사례:
- API 게이트웨이, 인증/인가
- 개인화 콘텐츠, A/B 테스트
- 이미지 최적화, 리사이징
- 지역별 리다이렉트, 라우팅
부적합한 사용 사례:
- 긴 연산 (30초 이상)
- 대용량 파일 처리
- 레거시 Node.js 패키지 의존성
- 상태 유지 연결 (WebSocket 제한적)
시작 가이드:
- 프로토타입: Vercel Edge (Next.js 통합)
- 프로덕션: Cloudflare Workers (비용, 성능)
- TypeScript 중심: Deno Deploy (표준 API)
다음 학습:
- WebAssembly 가이드로 성능 극대화
- Node.js 시리즈에서 백엔드 기초
- RAG 가이드로 AI 통합
참고 자료: