서버리스 플랫폼 비교 분석 2025

20 min read
compare serverless lambda vercel cloudflare edge faas cloud

AWS Lambda vs Vercel vs Cloudflare Workers - 서버리스 플랫폼 선택 가이드

⚡ 서버리스 플랫폼 비교 분석 2025

서버리스 아키텍처를 위한 최적의 플랫폼 선택 가이드


📊 개요

비교 대상

  • AWS Lambda: 서버리스 컴퓨팅의 선구자
  • Vercel: 프론트엔드 중심 서버리스
  • Cloudflare Workers: 엣지 컴퓨팅 플랫폼
  • Netlify Functions: JAMstack 친화적
  • Google Cloud Functions: GCP 통합
  • Azure Functions: 엔터프라이즈 서버리스

평가 기준

  • 성능 (콜드 스타트, 응답 시간)
  • 가격
  • 개발 경험
  • 생태계
  • 제한 사항
  • 확장성

📈 상세 비교표

핵심 특성 비교

| 특성 | AWS Lambda | Vercel | Cloudflare Workers | Netlify | |------|------------|---------|-------------------|----------| | 콜드 스타트 | 100-300ms | 50-150ms | <10ms | 100-200ms | | 최대 실행 시간 | 15분 | 10초(무료)/300초 | 30초 | 10초 | | 메모리 | 128MB-10GB | 1GB-3GB | 128MB | 1GB | | 언어 지원 | 다양함 | Node/Python/Go/Ruby | JS/Wasm | JS/Go | | 엣지 배포 | CloudFront | 글로벌 | 280+ 도시 | CDN 통합 | | 가격 | 사용량 기반 | 무료 시작 | 저렴함 | 무료 시작 |

기술적 제한사항

| 항목 | AWS Lambda | Vercel | Cloudflare Workers | Netlify | |------|------------|---------|-------------------|----------| | 패키지 크기 | 250MB | 50MB | 1MB | 50MB | | 환경 변수 | 4KB | 64KB | 1MB | 무제한 | | 동시 실행 | 1000(기본) | 무제한 | 무제한 | 무제한 | | HTTP 페이로드 | 6MB | 4.5MB | 100MB | 6MB | | 파일 시스템 | /tmp 512MB | 읽기 전용 | 없음 | 읽기 전용 |

가격 비교 (월 기준)

| 사용량 | AWS Lambda | Vercel | Cloudflare Workers | Netlify | |--------|------------|---------|-------------------|----------| | 100만 요청 | $0.20 | $0 | $5 | $0 | | 1000만 요청 | $2 | $20 | $50 | $19 | | 1억 요청 | $20 | $200 | $500 | $190 | | 컴퓨팅 시간 | $0.0000166/GB-초 | 포함 | 포함 | 포함 |


💼 사용 사례별 구현

🌐 글로벌 API 서비스

추천: Cloudflare Workers

// Cloudflare Workers - 엣지에서 실행되는 API addEventListener('fetch', event => { event.respondWith(handleRequest(event.request)) }) async function handleRequest(request) { // 지역별 라우팅 const country = request.cf?.country || 'US' const colo = request.cf?.colo || 'Unknown' // KV 스토어에서 캐시 확인 const cache = caches.default const cacheKey = new Request(request.url.toString(), request) const cachedResponse = await cache.match(cacheKey) if (cachedResponse) { return cachedResponse } // API 라우팅 const url = new URL(request.url) switch (url.pathname) { case '/api/users': return handleUsers(request, country) case '/api/products': return handleProducts(request) case '/api/geo': return handleGeoData(request, country, colo) default: return new Response('Not Found', { status: 404 }) } } async function handleUsers(request, country) { // Durable Objects를 사용한 상태 관리 const id = USERS_NAMESPACE.idFromName(country) const stub = USERS_NAMESPACE.get(id) return stub.fetch(request) } // Durable Object 클래스 export class UserCounter { constructor(state, env) { this.state = state this.env = env } async fetch(request) { const url = new URL(request.url) if (request.method === 'GET') { const value = await this.state.storage.get('count') || 0 return new Response(JSON.stringify({ count: value })) } if (request.method === 'POST') { let value = await this.state.storage.get('count') || 0 value++ await this.state.storage.put('count', value) return new Response(JSON.stringify({ count: value })) } } } // WebSocket 지원 async function handleWebSocket(request) { const upgradeHeader = request.headers.get('Upgrade') if (upgradeHeader !== 'websocket') { return new Response('Expected websocket', { status: 400 }) } const [client, server] = Object.values(new WebSocketPair()) server.accept() server.addEventListener('message', event => { server.send(JSON.stringify({ type: 'echo', data: event.data, timestamp: Date.now() })) }) return new Response(null, { status: 101, webSocket: client }) } // R2 스토리지 활용 async function handleFileUpload(request) { const formData = await request.formData() const file = formData.get('file') if (!file) { return new Response('No file uploaded', { status: 400 }) } const key = `uploads/${Date.now()}-${file.name}` await R2_BUCKET.put(key, file.stream(), { httpMetadata: { contentType: file.type } }) return new Response(JSON.stringify({ key, size: file.size, type: file.type }), { headers: { 'Content-Type': 'application/json' } }) } 

🚀 Next.js 풀스택 앱

추천: Vercel

// Vercel Functions - Next.js API Routes // app/api/products/route.ts import { NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { prisma } from '@/lib/prisma' import { revalidatePath } from 'next/cache' // Edge Runtime for better performance export const runtime = 'edge' const productSchema = z.object({ name: z.string().min(1), price: z.number().positive(), description: z.string(), category: z.string() }) export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url) const category = searchParams.get('category') const limit = Number(searchParams.get('limit')) || 10 // Vercel KV for caching const cacheKey = `products:${category || 'all'}:${limit}` const cached = await kv.get(cacheKey) if (cached) { return NextResponse.json(cached) } const products = await prisma.product.findMany({ where: category ? { category } : undefined, take: limit, orderBy: { createdAt: 'desc' } }) // Cache for 5 minutes await kv.setex(cacheKey, 300, products) return NextResponse.json(products) } export async function POST(request: NextRequest) { try { const body = await request.json() const validated = productSchema.parse(body) const product = await prisma.product.create({ data: validated }) // Revalidate cache revalidatePath('/products') // Send to analytics await fetch('https://analytics.vercel.com/api/events', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${process.env.VERCEL_ANALYTICS_TOKEN}` }, body: JSON.stringify({ event: 'product_created', properties: { product_id: product.id, category: product.category, price: product.price } }) }) return NextResponse.json(product, { status: 201 }) } catch (error) { if (error instanceof z.ZodError) { return NextResponse.json( { error: error.errors }, { status: 400 } ) } return NextResponse.json( { error: 'Internal Server Error' }, { status: 500 } ) } } // Vercel Cron Jobs // app/api/cron/cleanup/route.ts export async function GET(request: NextRequest) { // Verify cron secret const authHeader = request.headers.get('authorization') if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) { return new Response('Unauthorized', { status: 401 }) } // Cleanup old data const deleted = await prisma.product.deleteMany({ where: { createdAt: { lt: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) // 30 days } } }) return NextResponse.json({ deleted: deleted.count, timestamp: new Date().toISOString() }) } // vercel.json { "crons": [{ "path": "/api/cron/cleanup", "schedule": "0 2 * * *" }] } 

💼 엔터프라이즈 워크플로우

추천: AWS Lambda

# AWS Lambda - 복잡한 비즈니스 로직 import json import boto3 from aws_lambda_powertools import Logger, Tracer, Metrics from aws_lambda_powertools.metrics import MetricUnit from aws_lambda_powertools.event_handler import APIGatewayRestResolver from aws_lambda_powertools.utilities.typing import LambdaContext logger = Logger() tracer = Tracer() metrics = Metrics() app = APIGatewayRestResolver() dynamodb = boto3.resource('dynamodb') s3 = boto3.client('s3') sns = boto3.client('sns') sqs = boto3.client('sqs') # Step Functions 워크플로우의 일부 @tracer.capture_lambda_handler @logger.inject_lambda_context @metrics.log_metrics def process_order_handler(event: dict, context: LambdaContext) -> dict: """주문 처리 Lambda""" order_id = event['orderId'] logger.info(f"Processing order: {order_id}") try: # 1. DynamoDB에서 주문 조회 table = dynamodb.Table('orders') response = table.get_item(Key={'orderId': order_id}) order = response.get('Item') if not order: raise ValueError(f"Order {order_id} not found") # 2. 재고 확인 (다른 Lambda 호출) lambda_client = boto3.client('lambda') inventory_response = lambda_client.invoke( FunctionName='check-inventory', InvocationType='RequestResponse', Payload=json.dumps({ 'items': order['items'] }) ) inventory_result = json.loads( inventory_response['Payload'].read() ) if not inventory_result['available']: raise ValueError("Insufficient inventory") # 3. 결제 처리 payment_result = process_payment(order) # 4. S3에 영수증 저장 receipt = generate_receipt(order, payment_result) s3.put_object( Bucket='receipts-bucket', Key=f'receipts/{order_id}.pdf', Body=receipt, ContentType='application/pdf' ) # 5. 주문 상태 업데이트 table.update_item( Key={'orderId': order_id}, UpdateExpression='SET #status = :status, paymentId = :paymentId', ExpressionAttributeNames={'#status': 'status'}, ExpressionAttributeValues={ ':status': 'PAID', ':paymentId': payment_result['transactionId'] } ) # 6. SNS 알림 발송 sns.publish( TopicArn='arn:aws:sns:region:account:order-notifications', Message=json.dumps({ 'orderId': order_id, 'status': 'PAID', 'customerId': order['customerId'] }), MessageAttributes={ 'eventType': { 'DataType': 'String', 'StringValue': 'ORDER_PAID' } } ) # 7. 메트릭 기록 metrics.add_metric( name="OrderProcessed", unit=MetricUnit.Count, value=1 ) metrics.add_metric( name="OrderAmount", unit=MetricUnit.Count, value=order['totalAmount'] ) return { 'statusCode': 200, 'body': json.dumps({ 'orderId': order_id, 'status': 'PAID', 'paymentId': payment_result['transactionId'] }) } except Exception as e: logger.exception(f"Error processing order {order_id}") # Dead Letter Queue로 전송 sqs.send_message( QueueUrl='https://sqs.region.amazonaws.com/account/dlq', MessageBody=json.dumps({ 'orderId': order_id, 'error': str(e), 'timestamp': context.aws_request_id }) ) raise # EventBridge 이벤트 처리 @tracer.capture_lambda_handler def event_processor_handler(event: dict, context: LambdaContext) -> dict: """EventBridge 이벤트 처리""" for record in event.get('Records', []): event_detail = json.loads(record['body']) event_type = event_detail.get('detail-type') if event_type == 'ORDER_CREATED': handle_order_created(event_detail['detail']) elif event_type == 'PAYMENT_COMPLETED': handle_payment_completed(event_detail['detail']) elif event_type == 'SHIPMENT_DISPATCHED': handle_shipment_dispatched(event_detail['detail']) return {'statusCode': 200} # Lambda Extensions로 설정 관리 import requests def get_config(): """Lambda Extension에서 설정 가져오기""" response = requests.get( 'http://localhost:2773/systemsmanager/parameters/get', params={'name': '/myapp/config'}, headers={'X-Aws-Parameters-Secrets-Token': os.environ.get('AWS_SESSION_TOKEN')} ) return response.json() 

🎮 실시간 게임 백엔드

추천: Cloudflare Workers + Durable Objects

// Cloudflare Durable Objects - 게임 룸 상태 관리 export class GameRoom { state: DurableObjectState sessions: Map<WebSocket, PlayerInfo> gameState: GameState constructor(state: DurableObjectState) { this.state = state this.sessions = new Map() this.gameState = { players: new Map(), board: createEmptyBoard(), turn: 0, status: 'waiting' } } async fetch(request: Request): Promise<Response> { const url = new URL(request.url) switch (url.pathname) { case '/websocket': return this.handleWebSocket(request) case '/state': return this.getGameState() case '/action': return this.handleAction(request) default: return new Response('Not found', { status: 404 }) } } async handleWebSocket(request: Request): Promise<Response> { const pair = new WebSocketPair() const [client, server] = Object.values(pair) this.state.acceptWebSocket(server) const playerId = new URL(request.url).searchParams.get('playerId') this.sessions.set(server, { playerId, ws: server }) server.addEventListener('message', async (event) => { await this.handleMessage(server, event.data) }) server.addEventListener('close', () => { this.handleDisconnect(server) }) // 현재 게임 상태 전송 server.send(JSON.stringify({ type: 'gameState', data: this.serializeGameState() })) return new Response(null, { status: 101, webSocket: client }) } async handleMessage(ws: WebSocket, message: string) { const data = JSON.parse(message) const player = this.sessions.get(ws) switch (data.type) { case 'move': await this.processMove(player.playerId, data.move) break case 'chat': this.broadcastChat(player.playerId, data.message) break } } async processMove(playerId: string, move: Move) { // 게임 로직 검증 if (!this.isValidMove(playerId, move)) { return } // 상태 업데이트 this.gameState.board[move.x][move.y] = playerId this.gameState.turn++ // 상태 저장 await this.state.storage.put('gameState', this.gameState) // 모든 플레이어에게 브로드캐스트 this.broadcast({ type: 'move', playerId, move, nextTurn: this.getNextPlayer() }) // 승리 조건 확인 if (this.checkWinCondition(playerId)) { this.endGame(playerId) } } broadcast(message: any) { const data = JSON.stringify(message) this.sessions.forEach(({ ws }) => { ws.send(data) }) } } // Workers KV를 사용한 리더보드 export async function updateLeaderboard( playerId: string, score: number, env: Env ) { const key = `leaderboard:${new Date().toISOString().slice(0, 10)}` const current = await env.LEADERBOARD.get(key, 'json') || [] current.push({ playerId, score, timestamp: Date.now() }) // 상위 100명만 유지 const sorted = current .sort((a, b) => b.score - a.score) .slice(0, 100) await env.LEADERBOARD.put(key, JSON.stringify(sorted), { expirationTtl: 86400 * 30 // 30일 보관 }) return sorted } 

🏢 실제 기업 사례

AWS Lambda 사용

  • Netflix: 비디오 인코딩
  • iRobot: IoT 데이터 처리
  • 카카오: 이미지 처리
  • 토스: 일부 마이크로서비스

Vercel 사용

  • TikTok: 웹 프론트엔드
  • McDonald's: 주문 시스템
  • Hulu: 마케팅 사이트
  • 당근마켓: 웹 서비스

Cloudflare Workers 사용

  • Discord: API 라우팅
  • Shopify: 엣지 로직
  • DoorDash: 지역별 처리

🔧 개발 도구 및 프레임워크

프레임워크 지원

| 플랫폼 | 지원 프레임워크 | |--------|----------------| | AWS Lambda | Serverless Framework, SAM, CDK | | Vercel | Next.js, SvelteKit, Nuxt | | Cloudflare | Wrangler, Pages, Remix | | Netlify | Next.js, Gatsby, Nuxt |

로컬 개발

# AWS SAM sam local start-api sam local invoke # Vercel CLI vercel dev # Wrangler (Cloudflare) wrangler dev # Netlify CLI netlify dev 

💰 비용 최적화 전략

비용 절감 팁

  1. 적절한 메모리 할당: 과도한 메모리는 비용 증가
  2. 콜드 스타트 최소화: 워밍 전략 사용
  3. 캐싱 활용: CDN, 메모리 캐시
  4. 배치 처리: 개별 호출보다 효율적

예상 비용 계산

// 월간 비용 예상 const calculateCost = (platform, requests, avgDuration) => { switch(platform) { case 'lambda': const gbSeconds = requests * (avgDuration/1000) * 0.5 // 512MB return (requests * 0.0000002) + (gbSeconds * 0.0000166667) case 'vercel': return requests > 100000 ? (requests - 100000) * 0.000002 : 0 case 'cloudflare': return requests > 100000 ? (requests - 100000) * 0.0000005 : 0 } } 

🎯 선택 가이드

AWS Lambda 선택 시

✅ AWS 서비스 통합 필요
✅ 긴 실행 시간 (최대 15분)
✅ 복잡한 워크플로우
✅ 엔터프라이즈 요구사항
❌ 낮은 레이턴시 필요
❌ 간단한 웹 API

Vercel 선택 시

✅ Next.js 프로젝트
✅ 프론트엔드 중심
✅ 빠른 배포
✅ 프리뷰 배포
❌ 긴 실행 시간
❌ 복잡한 백엔드

Cloudflare Workers 선택 시

✅ 글로벌 저지연
✅ 엣지 컴퓨팅
✅ 높은 동시성
✅ WebSocket 지원
❌ 큰 패키지
❌ 파일 시스템

Netlify Functions 선택 시

✅ JAMstack 프로젝트
✅ 정적 사이트 + API
✅ 간단한 백엔드
✅ Git 기반 배포
❌ 복잡한 로직
❌ 대규모 트래픽


📚 추가 리소스

공식 문서

모범 사례

  • 콜드 스타트 최적화
  • 환경 변수 관리
  • 에러 처리 및 재시도
  • 모니터링 및 로깅

커뮤니티

  • Serverless Stack Discord
  • Vercel Community
  • Cloudflare Developers Discord
  • AWS Serverless Forum

💡 핵심 조언: 서버리스는 "서버가 없다"가 아니라 "서버를 관리하지 않는다"입니다. 간단한 API는 Cloudflare Workers로, Next.js는 Vercel로, 복잡한 워크플로우는 AWS Lambda로 시작하세요. 중요한 것은 벤더 락인을 피하고 필요에 따라 이동할 수 있는 아키텍처를 설계하는 것입니다.

Found this helpful? Share it with others!
Tweet

🔗 Related Content

You might also be interested in these articles

🏢 company

🏢 Vercel 기술 스택 분석

버셀이 프론트엔드 배포의 미래를 만드는 기술 스택 심층 분석 - Next.js, Edge Functions, 그리고 글로벌 엣지 네트워크

31 min read
vercel, nextjs+9
Read more
🏗️ stack

🚀 Modern SaaS MVP Stack

4주 안에 SaaS를 출시하기 위한 검증된 기술 스택 - Next.js, Supabase, Stripe로 빠르게 시작하기

6 min read
nextjs, supabase+13
Read more
🌟 awesome

🌟 Awesome AWS

AWS 개발자와 클라우드 엔지니어를 위한 최고의 도구, 서비스, 리소스 모음

5 min read
awesome, aws+4
Read more
🔁 alternative

🔁 Vercel Alternatives

Vercel을 대체할 수 있는 프론트엔드 및 풀스택 호스팅 플랫폼 비교

5 min read
alternative, vercel+4
Read more

Found this helpful?

Help us improve this content by contributing on GitHub or sharing your feedback with the community.