엣지 컴퓨팅 플랫폼 비교 분석 2025
Cloudflare Workers vs AWS Lambda@Edge vs Fastly Compute@Edge - 엣지 컴퓨팅 선택 가이드
🌐 엣지 컴퓨팅 플랫폼 비교 분석 2025
글로벌 저지연 애플리케이션을 위한 엣지 컴퓨팅 플랫폼 선택 가이드
📊 개요
비교 대상
- Cloudflare Workers: 가장 큰 엣지 네트워크
- AWS Lambda@Edge: AWS 통합 엣지 컴퓨팅
- Fastly Compute@Edge: 고성능 엣지 플랫폼
- Vercel Edge Functions: Next.js 최적화
- Deno Deploy: 모던 JavaScript 런타임
- Netlify Edge Functions: JAMstack 친화적
평가 기준
- 글로벌 커버리지
- 성능 (콜드 스타트, 응답 시간)
- 지원 언어/런타임
- 가격
- 기능 제한
- 개발자 경험
📈 상세 비교표
핵심 특성 비교
| 특성 | Cloudflare Workers | Lambda@Edge | Fastly Compute | Vercel Edge | | ------------------ | ------------------ | ----------- | -------------- | ----------- | | PoP 수 | 280+ | 400+ | 60+ | 전 세계 | | 콜드 스타트 | <10ms | 100-300ms | <50ms | <50ms | | 최대 실행 시간 | 30초 | 30초 | 60초 | 25초 | | 메모리 | 128MB | 128MB | 128MB | 128MB | | CPU 시간 | 10ms-50ms | 제한 없음 | 제한 없음 | 제한 있음 | | 언어 지원 | JS/Wasm | JS/Python | Rust/JS/Go | JS/TS |
기술적 제한사항
| 항목 | Cloudflare | Lambda@Edge | Fastly | Vercel | | ------------- | ---------- | ----------- | --------- | ------ | | 번들 크기 | 1MB | 1MB | 100MB | 1MB | | 환경 변수 | 1MB | 4KB | 제한 없음 | 64KB | | 요청 크기 | 100MB | 40KB | 8MB | 4.5MB | | 응답 크기 | 무제한 | 1MB | 무제한 | 4.5MB | | WebSocket | ✅ | ❌ | ✅ | 제한적 | | 스토리지 | KV/R2/D1 | ❌ | KV Store | KV |
가격 비교 (월 기준)
| 사용량 | Cloudflare | Lambda@Edge | Fastly | Vercel | | ------------- | ---------- | ----------- | ------ | ------ | | 10M 요청 | $5 | $6 | $50 | $20 | | 100M 요청 | $50 | $60 | $500 | $200 | | 1B 요청 | $500 | $600 | $5,000 | $2,000 | | 무료 티어 | 100K/일 | 제한적 | 없음 | 제한적 |
💼 사용 사례별 구현
🌍 글로벌 A/B 테스팅
추천: Cloudflare Workers
// Cloudflare Workers - A/B 테스팅 구현
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
// 사용자 정보 추출
const country = request.cf?.country || "US";
const city = request.cf?.city || "Unknown";
const colo = request.cf?.colo || "Unknown";
// 사용자 ID 또는 세션 기반 일관된 할당
const userId =
request.headers.get("x-user-id") || (await generateUserId(request));
// A/B 그룹 결정 (일관성 보장)
const abGroup = await getABGroup(userId, env.AB_TEST);
// KV에서 테스트 설정 로드
const testConfig = await env.CONFIG_KV.get("ab-test-config", "json");
const variant = testConfig.variants[abGroup];
// 요청 라우팅
let response;
if (variant.type === "redirect") {
// 다른 오리진으로 리다이렉트
response = await fetch(variant.origin + url.pathname, request);
} else if (variant.type === "rewrite") {
// HTML 수정
response = await fetch(request);
response = await modifyHTML(response, variant.modifications);
} else {
// 프록시
response = await fetch(request);
}
// 메트릭 수집 (비동기)
ctx.waitUntil(
collectMetrics(
{
testId: testConfig.id,
variant: abGroup,
userId,
country,
city,
timestamp: Date.now(),
responseTime: Date.now() - startTime,
},
env.ANALYTICS,
),
);
// 응답에 헤더 추가
response = new Response(response.body, response);
response.headers.set("x-ab-variant", abGroup);
response.headers.set("x-edge-location", colo);
return response;
},
};
// HTML 수정 함수
async function modifyHTML(response, modifications) {
const rewriter = new HTMLRewriter();
modifications.forEach((mod) => {
rewriter.on(mod.selector, {
element(element) {
if (mod.action === "replace") {
element.setInnerContent(mod.content);
} else if (mod.action === "setAttribute") {
element.setAttribute(mod.attribute, mod.value);
} else if (mod.action === "addClass") {
element.setAttribute(
"class",
element.getAttribute("class") + " " + mod.className,
);
}
},
});
});
return rewriter.transform(response);
}
// 메트릭 수집
async function collectMetrics(data, analytics) {
// Analytics Engine으로 전송
await analytics.writeDataPoint({
indexes: [data.testId, data.variant],
blobs: [data.country, data.city],
doubles: [data.responseTime],
timestamp: data.timestamp,
});
// 실시간 집계를 위한 Durable Object 업데이트
const id = analytics.idFromName(`test-${data.testId}`);
const stub = analytics.get(id);
await stub.fetch("https://internal/increment", {
method: "POST",
body: JSON.stringify(data),
});
}
// Durable Object for 실시간 집계
export class ABTestAnalytics {
constructor(state, env) {
this.state = state;
this.env = env;
}
async fetch(request) {
const url = new URL(request.url);
if (url.pathname === "/increment") {
const data = await request.json();
await this.increment(data);
return new Response("ok");
}
if (url.pathname === "/stats") {
const stats = await this.getStats();
return new Response(JSON.stringify(stats));
}
}
async increment(data) {
const key = `${data.variant}:${data.country}`;
const current = (await this.state.storage.get(key)) || {
impressions: 0,
totalTime: 0,
};
current.impressions++;
current.totalTime += data.responseTime;
await this.state.storage.put(key, current);
}
}
🔒 보안 게이트웨이
추천: AWS Lambda@Edge
// Lambda@Edge - 보안 검증 및 필터링
exports.handler = async (event, context) => {
const request = event.Records[0].cf.request;
const headers = request.headers;
// 1. IP 기반 지역 차단
const clientIp = headers["cloudfront-viewer-address"][0].value;
const country = headers["cloudfront-viewer-country"][0].value;
if (await isBlockedCountry(country)) {
return {
status: "403",
statusDescription: "Forbidden",
body: "Access denied from your region",
};
}
// 2. DDoS 방어 - Rate Limiting
const rateLimitKey = `ratelimit:${clientIp}`;
const requests = await incrementAndGetCount(rateLimitKey);
if (requests > 100) {
// 분당 100 요청 제한
return {
status: "429",
statusDescription: "Too Many Requests",
headers: {
"retry-after": [{ key: "Retry-After", value: "60" }],
},
};
}
// 3. WAF 규칙 실행
const wafResult = await runWAFRules(request);
if (wafResult.blocked) {
await logSecurityEvent({
type: "waf_block",
rule: wafResult.rule,
ip: clientIp,
uri: request.uri,
headers: headers,
});
return {
status: "403",
statusDescription: "Forbidden",
body: "Request blocked by security rules",
};
}
// 4. JWT 토큰 검증 (API 보호)
if (request.uri.startsWith("/api/")) {
const authHeader = headers.authorization?.[0]?.value;
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return {
status: "401",
statusDescription: "Unauthorized",
headers: {
"www-authenticate": [
{
key: "WWW-Authenticate",
value: 'Bearer realm="api"',
},
],
},
};
}
const token = authHeader.substring(7);
try {
const payload = await verifyJWT(token);
// 토큰 정보를 헤더에 추가하여 오리진에 전달
request.headers["x-user-id"] = [
{
key: "X-User-Id",
value: payload.sub,
},
];
request.headers["x-user-role"] = [
{
key: "X-User-Role",
value: payload.role,
},
];
} catch (error) {
return {
status: "401",
statusDescription: "Unauthorized",
body: "Invalid token",
};
}
}
// 5. 요청 변환 및 보안 헤더 추가
request.headers["x-real-ip"] = [
{
key: "X-Real-IP",
value: clientIp,
},
];
request.headers["x-forwarded-country"] = [
{
key: "X-Forwarded-Country",
value: country,
},
];
// 민감한 헤더 제거
delete request.headers["x-amzn-trace-id"];
delete request.headers["x-amz-cf-id"];
return request;
};
// WAF 규칙 실행
async function runWAFRules(request) {
const rules = [
// SQL Injection 탐지
{
name: "sql_injection",
pattern:
/((union|select|insert|update|delete|drop|create)\s+.*\s+(from|into|where|table))|(-{2})|(\/\*|\*\/)/i,
target: ["uri", "querystring", "body"],
},
// XSS 탐지
{
name: "xss",
pattern:
/(<script[^>]*>|javascript:|onerror=|onload=|alert\(|document\.|window\.|eval\()/i,
target: ["querystring", "body"],
},
// Path Traversal
{
name: "path_traversal",
pattern: /(\.\.\/|\.\.\\/i,
target: ["uri", "querystring"],
},
];
for (const rule of rules) {
for (const target of rule.target) {
let value = "";
if (target === "uri") {
value = request.uri;
} else if (target === "querystring") {
value = request.querystring;
} else if (target === "body" && request.body) {
value = Buffer.from(request.body.data, "base64").toString();
}
if (rule.pattern.test(value)) {
return { blocked: true, rule: rule.name };
}
}
}
return { blocked: false };
}
⚡ 실시간 개인화
추천: Fastly Compute@Edge
// Fastly Compute@Edge - Rust로 구현한 실시간 개인화
use fastly::{Error, Request, Response};
use fastly::http::{header, Method, StatusCode};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Serialize, Deserialize)]
struct UserProfile {
id: String,
preferences: HashMap<String, String>,
history: Vec<String>,
segment: String,
}
#[derive(Serialize, Deserialize)]
struct PersonalizationRule {
segment: String,
modifications: Vec<Modification>,
}
#[derive(Serialize, Deserialize)]
struct Modification {
selector: String,
action: String,
value: String,
}
#[fastly::main]
fn main(req: Request) -> Result<Response, Error> {
// 사용자 식별
let user_id = get_user_id(&req);
// 사용자 프로필 로드 (엣지 KV에서)
let profile = match load_user_profile(&user_id) {
Some(p) => p,
None => create_default_profile(&user_id),
};
// 개인화 규칙 로드
let rules = load_personalization_rules();
let applicable_rules: Vec<_> = rules.iter()
.filter(|r| r.segment == profile.segment)
.collect();
// 오리진 요청
let mut beresp = req.clone_without_body()
.send("origin_0")?;
// HTML 응답인 경우 개인화 적용
if is_html_response(&beresp) {
let body = beresp.take_body_str();
let personalized_body = apply_personalizations(
&body,
&applicable_rules,
&profile
);
beresp.set_body(personalized_body);
}
// 추천 헤더 추가
if req.get_path().starts_with("/api/recommendations") {
let recommendations = generate_recommendations(&profile);
beresp.set_header(
"X-Recommendations",
serde_json::to_string(&recommendations)?
);
}
// 개인화 메트릭 수집 (비동기)
log_personalization_event(PersonalizationEvent {
user_id: user_id.clone(),
segment: profile.segment.clone(),
path: req.get_path().to_string(),
timestamp: std::time::SystemTime::now(),
applied_rules: applicable_rules.len(),
});
Ok(beresp)
}
fn apply_personalizations(
html: &str,
rules: &[&PersonalizationRule],
profile: &UserProfile
) -> String {
let mut result = html.to_string();
for rule in rules {
for modification in &rule.modifications {
result = match modification.action.as_str() {
"replace" => {
// CSS 선택자 기반 교체
replace_by_selector(
&result,
&modification.selector,
&modification.value
)
},
"inject" => {
// 동적 콘텐츠 주입
inject_content(
&result,
&modification.selector,
&personalize_content(&modification.value, profile)
)
},
"remove" => {
// 요소 제거
remove_by_selector(&result, &modification.selector)
},
_ => result,
};
}
}
result
}
fn generate_recommendations(profile: &UserProfile) -> Vec<String> {
// 사용자 히스토리 기반 추천 생성
let mut recommendations = Vec::new();
// 협업 필터링 로직
let similar_users = find_similar_users(&profile.id);
for user in similar_users {
let user_profile = load_user_profile(&user).unwrap();
for item in user_profile.history {
if !profile.history.contains(&item) {
recommendations.push(item);
}
}
}
// 콘텐츠 기반 필터링
for item in &profile.history {
let similar_items = find_similar_items(item);
recommendations.extend(similar_items);
}
// 중복 제거 및 상위 N개 반환
recommendations.dedup();
recommendations.truncate(10);
recommendations
}
// WebAssembly 모듈 호출 (복잡한 ML 모델)
#[link(wasm_import_module = "ml_model")]
extern "C" {
fn predict_user_segment(features: *const f32, len: usize) -> i32;
}
fn update_user_segment(profile: &mut UserProfile) {
// 특징 추출
let features = extract_features(profile);
// WASM ML 모델 호출
let segment_id = unsafe {
predict_user_segment(features.as_ptr(), features.len())
};
profile.segment = match segment_id {
0 => "casual".to_string(),
1 => "power_user".to_string(),
2 => "enterprise".to_string(),
_ => "default".to_string(),
};
}
🎯 Next.js 앱 최적화
추천: Vercel Edge Functions
// Vercel Edge Functions - Next.js 미들웨어
import { NextRequest, NextResponse } from "next/server";
import { geolocation, ipAddress } from "@vercel/edge";
export const config = {
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};
export async function middleware(request: NextRequest) {
// 지역 정보 추출
const { country, city, region } = geolocation(request);
const ip = ipAddress(request);
// 1. 지역별 리다이렉트
if (request.nextUrl.pathname === "/") {
const locale = getLocaleByCountry(country);
if (locale && locale !== "en") {
return NextResponse.redirect(new URL(`/${locale}`, request.url));
}
}
// 2. 기능 플래그 (엣지에서 결정)
const features = await getFeatureFlags({
userId: request.cookies.get("userId")?.value,
country,
ip,
});
// 3. 실험 그룹 할당
const experiments = assignExperiments(request, features);
// 4. 성능 최적화 - 이미지 포맷 협상
if (request.nextUrl.pathname.match(/\.(jpg|jpeg|png|gif)$/)) {
const accept = request.headers.get("accept") || "";
if (accept.includes("image/avif")) {
return NextResponse.rewrite(
new URL(
request.nextUrl.pathname.replace(/\.[^.]+$/, ".avif"),
request.url,
),
);
} else if (accept.includes("image/webp")) {
return NextResponse.rewrite(
new URL(
request.nextUrl.pathname.replace(/\.[^.]+$/, ".webp"),
request.url,
),
);
}
}
// 5. 봇 감지 및 처리
const isBot = detectBot(request.headers.get("user-agent") || "");
if (isBot) {
// 봇용 정적 버전 제공
return NextResponse.rewrite(new URL("/api/static-render", request.url));
}
// 6. 보안 헤더 추가
const response = NextResponse.next();
response.headers.set("X-Frame-Options", "DENY");
response.headers.set("X-Content-Type-Options", "nosniff");
response.headers.set("X-XSS-Protection", "1; mode=block");
response.headers.set("Content-Security-Policy", getCSPHeader(features));
// 7. 쿠키 설정 (실험 및 기능 플래그)
response.cookies.set("features", JSON.stringify(features), {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
maxAge: 60 * 60 * 24 * 7, // 1주일
});
response.cookies.set("experiments", JSON.stringify(experiments), {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
maxAge: 60 * 60 * 24 * 30, // 30일
});
// 8. 커스텀 헤더 추가 (디버깅용)
response.headers.set("X-Edge-Location", city || "unknown");
response.headers.set("X-Edge-Country", country || "unknown");
response.headers.set("X-Edge-IP", ip || "unknown");
return response;
}
// 기능 플래그 로드 (Edge Config)
async function getFeatureFlags(context: {
userId?: string;
country?: string;
ip?: string;
}) {
const flags = await fetch(`https://edge-config.vercel.com/flags`, {
headers: {
Authorization: `Bearer ${process.env.EDGE_CONFIG_TOKEN}`,
},
}).then((res) => res.json());
// 조건부 플래그 평가
const evaluatedFlags: Record<string, boolean> = {};
for (const [key, config] of Object.entries(flags)) {
if (typeof config === "boolean") {
evaluatedFlags[key] = config;
} else if (typeof config === "object") {
// 복잡한 규칙 평가
evaluatedFlags[key] = evaluateRule(config, context);
}
}
return evaluatedFlags;
}
// 실험 할당
function assignExperiments(
request: NextRequest,
features: Record<string, boolean>,
): Record<string, string> {
const experiments: Record<string, string> = {};
const userId = request.cookies.get("userId")?.value || generateUserId();
// 각 실험에 대해 일관된 할당
if (features.newCheckoutFlow) {
experiments.checkoutVariant = hashToVariant(
userId,
["control", "variant-a", "variant-b"],
"checkout-experiment",
);
}
if (features.personalizedHomepage) {
experiments.homepageVariant = hashToVariant(
userId,
["default", "personalized", "ai-powered"],
"homepage-experiment",
);
}
return experiments;
}
// 일관된 해시 기반 할당
function hashToVariant(
userId: string,
variants: string[],
experimentName: string,
): string {
const hash = require("crypto")
.createHash("md5")
.update(userId + experimentName)
.digest("hex");
const hashInt = parseInt(hash.substring(0, 8), 16);
const index = hashInt % variants.length;
return variants[index];
}
🏢 실제 기업 사례
Cloudflare Workers 사용
- Discord: API 라우팅 및 보안
- Shopify: 동적 콘텐츠 캐싱
- DoorDash: 지역별 라우팅
Lambda@Edge 사용
- Amazon Prime Video: 비디오 스트리밍 최적화
- Slack: 정적 자산 최적화
- 많은 미디어 기업: 콘텐츠 보호
Fastly Compute@Edge 사용
- The New York Times: 콘텐츠 전달
- Reddit: 트래픽 관리
- Stripe: API 가속
Vercel Edge Functions 사용
- TikTok: 웹 프론트엔드
- Hulu: 스트리밍 서비스
- 많은 Next.js 앱: SSR 최적화
🔧 개발 도구 및 테스팅
로컬 개발 환경
# Cloudflare Workers
wrangler dev
# Vercel Edge
vercel dev
# Fastly
fastly compute serve
# Deno Deploy
deno task dev
테스팅 전략
| 플랫폼 | 단위 테스트 | 통합 테스트 | E2E 테스트 | | --------------- | ----------- | ------------ | ---------- | | Cloudflare | Vitest | Miniflare | Wrangler | | Lambda@Edge | Jest | SAM Local | AWS SDK | | Fastly | Rust tests | Local server | CLI | | Vercel | Jest | Edge Runtime | Playwright |
💰 비용 최적화 전략
비용 절감 팁
- 캐싱 극대화: 엣지에서 최대한 캐시
- 조건부 요청: 필요한 경우만 오리진 호출
- 압축: Brotli/Gzip 적극 활용
- 이미지 최적화: 실시간 리사이징
ROI 계산
// 엣지 컴퓨팅 ROI 계산기
function calculateROI({
monthlyRequests,
avgLatencyReduction, // ms
conversionImpact, // %
avgOrderValue,
currentConversionRate,
}) {
// 지연시간 감소로 인한 수익 증가
const latencyImpact = (avgLatencyReduction / 100) * 0.01; // 100ms = 1% 전환율
const newConversionRate = currentConversionRate * (1 + latencyImpact);
// 월간 추가 수익
const additionalRevenue =
monthlyRequests *
(newConversionRate - currentConversionRate) *
avgOrderValue;
// 엣지 컴퓨팅 비용
const edgeCost = calculateEdgeCost(monthlyRequests);
return {
additionalRevenue,
edgeCost,
netBenefit: additionalRevenue - edgeCost,
roi: ((additionalRevenue - edgeCost) / edgeCost) * 100,
};
}
🎯 선택 가이드
Cloudflare Workers 선택 시
✅ 최대 글로벌 커버리지
✅ 낮은 지연시간 중요
✅ KV/R2/D1 필요
✅ WebSocket 지원
❌ 복잡한 계산 작업
❌ 긴 실행 시간
Lambda@Edge 선택 시
✅ AWS 에코시스템
✅ CloudFront 사용 중
✅ 보안 기능 중요
✅ Python 지원 필요
❌ 빠른 콜드 스타트
❌ 실시간 양방향 통신
Fastly Compute@Edge 선택 시
✅ 고성능 필요
✅ Rust/Go 선호
✅ 대용량 처리
✅ 미디어 스트리밍
❌ 작은 규모
❌ 저렴한 비용
Vercel Edge Functions 선택 시
✅ Next.js 사용
✅ 프론트엔드 최적화
✅ 간단한 설정
✅ TypeScript 중심
❌ 복잡한 백엔드
❌ Non-JS 워크로드
📚 추가 리소스
공식 문서
성능 벤치마크
- Edge Computing Report 2024
- CDN Performance Benchmark
- Real User Monitoring (RUM) 데이터
커뮤니티
- Cloudflare Developers Discord
- AWS Edge Services Forum
- Vercel Community
- Edge Computing Subreddit
💡 핵심 조언: 엣지 컴퓨팅은 "사용자와의 거리"를 줄이는 기술입니다. 단순히 "빠르다"가 아니라 "어디서나 빠르다"가 중요합니다. 글로벌 서비스라면 Cloudflare Workers, AWS 중심이라면 Lambda@Edge, Next.js 앱이라면 Vercel Edge Functions를 선택하세요. 중요한 것은 사용자 경험 개선이지, 기술 자체가 아닙니다.