Edge Runtime × 서버리스: Next.js로 시작하는 하이브리드 아키텍처 실전 가이드 (2025)
솔직히 말하면, 저도 처음엔 "엣지 런타임이요? 그냥 CDN 아닌가요?"라고 생각했습니다. 그런데 2024년 말부터 Vercel이 기본 배포 타겟을 엣지 PoP(Points of Presence, 전 세계 분산 접속 지점)으로 전환하고, 엣지 함수 채택이 눈에 띄게 늘어나는 걸 보면서 이건 그냥 지나칠 수 있는 트렌드가 아니라는 걸 실감했습니다.
서울 사용자가 요청을 보낸 순간, 무슨 일이 벌어지는지 상상해 보세요. 기존 구조라면 그 요청은 미국 버지니아 리전까지 갔다가 돌아옵니다. 엣지 런타임이라면 서울 근처 PoP에서 바로 처리됩니다. 단순히 "빠르다"는 얘기가 아니라, 아키텍처를 설계하는 방식 자체가 달라지는 겁니다. Next.js 미들웨어 하나 잘못 짰다가 API 호출이 전부 막혀버린 경험이 있다면, 이 글이 특히 도움이 될 겁니다.
핵심은 "엣지 vs 서버리스"가 아니라, 두 모델을 어떻게 조합하느냐가 2025년 이후 프론트엔드 아키텍처의 핵심이라는 것입니다. 인증 미들웨어, 지역별 콘텐츠 분기, 이미지 리사이징 같은 실전 예시를 코드와 함께 살펴봅니다.
핵심 개념
Edge Runtime이란 무엇인가
Edge Runtime은 사용자와 가장 가까운 CDN 엣지 노드에서 서버 코드를 실행하는 환경입니다. 컨테이너나 VM 없이 V8 Isolate 기반으로 경량 JS 컨텍스트를 순식간에 부팅합니다. 다만 제약이 있습니다. Web Standard API(Fetch, Web Crypto, Streams)만 지원하고, fs, child_process, net 같은 Node.js 네이티브 모듈은 사용할 수 없습니다. 메모리도 128MB, CPU 시간도 30~50ms로 제한됩니다.
처음 이 제약을 만났을 때 "그럼 뭘 할 수 있지?"라고 당황했는데, 오히려 이 제약이 아키텍처를 명확하게 만들어줍니다. "이 작업이 엣지에서 처리될 수 있는가?"라는 질문이 설계의 기준이 되거든요.
V8 Isolate: Chrome 브라우저에서 쓰는 JS 엔진(V8)의 격리된 실행 컨텍스트입니다. 컨테이너처럼 OS 수준 격리가 아니라 프로세스 내 메모리 격리이기 때문에 시작 속도가 극도로 빠릅니다. 단, 이 격리 컨텍스트 초기화 자체가 <1ms인 것이고, 실제 응답 지연(네트워크 왕복 + 코드 실행)인 TTFB(Time To First Byte)는 별개입니다.
서버리스(Lambda형)와의 결정적 차이
서버리스는 AWS Lambda처럼 컨테이너 기반으로 함수 단위 코드를 실행하는 모델입니다. 풀 Node.js를 지원하고, 수 GB 메모리와 수분 단위 실행 시간이 가능합니다. 그 대신 콜드 스타트가 100ms~1초 이상 걸리고, 특정 리전에 배포되기 때문에 원거리 사용자에겐 지연이 생깁니다.
두 모델을 나란히 놓으면 이렇습니다:
| 구분 | Edge Runtime | 서버리스(Lambda형) |
|---|---|---|
| 실행 위치 | 전 세계 300+ PoP | 특정 리전 |
| 격리 컨텍스트 초기화 | <1ms (V8 Isolate) | 100ms~1초+ (컨테이너) |
| 메모리 한도 | 128MB | 수 GB |
| 실행 시간 | 30~50ms CPU | 수분 |
| API 지원 | Web Standard만 | 풀 Node.js |
| 가격 | CPU 시간 기준 | 요청 + 실행 시간 |
Fluid Compute: Vercel이 도입한 개념으로, 서버리스 함수의 실행 컨텍스트를 재사용해 콜드 스타트를 거의 제거하면서도 서버리스 과금 방식을 유지하는 모델입니다. 엣지와 서버리스의 간극을 좁히는 방향으로 플랫폼이 진화하고 있다는 신호이기도 합니다.
실전 적용
예시 1: Next.js 미들웨어로 JWT 인증 처리하기
실무에서 자주 맞닥뜨리는 상황입니다. 로그인하지 않은 사용자가 /dashboard에 접근할 때, 오리진 서버까지 요청이 도달하기 전에 엣지에서 차단하는 패턴입니다. 서버 부하도 줄이고, 응답 속도도 빠릅니다.
한 가지 주의할 점이 있는데, atob 기반 base64url 디코딩은 일부 엣지 런타임에서 패딩 처리 방식이 미묘하게 다를 수 있습니다. 아래 코드에서는 직접 구현한 base64urlDecode 유틸을 사용하는 것이 더 안전합니다.
// middleware.ts — 프로젝트 루트에 위치
import { NextRequest, NextResponse } from 'next/server';
export const config = {
matcher: ['/dashboard/:path*', '/api/protected/:path*'],
runtime: 'edge', // 엣지 런타임 명시
};
export async function middleware(req: NextRequest) {
const token = req.cookies.get('auth-token')?.value;
if (!token) {
return NextResponse.redirect(new URL('/login', req.url));
}
// Web Crypto API로 JWT 서명 검증 (Node.js crypto 모듈 불가)
const isValid = await verifyJWT(token);
if (!isValid) {
const response = NextResponse.redirect(new URL('/login', req.url));
response.cookies.delete('auth-token');
return response;
}
return NextResponse.next();
}
function base64urlDecode(str: string): Uint8Array {
// base64url → base64 변환 후 디코딩 (엣지 런타임 패딩 차이 대응)
const base64 = str.replace(/-/g, '+').replace(/_/g, '/')
+ '='.repeat((4 - str.length % 4) % 4);
const binary = atob(base64);
return Uint8Array.from(binary, c => c.charCodeAt(0));
}
async function verifyJWT(token: string): Promise<boolean> {
try {
const [header, payload, signature] = token.split('.');
if (!header || !payload || !signature) return false;
// exp 클레임(만료 시간) 검증
const claims = JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/')));
if (typeof claims.exp === 'number' && claims.exp * 1000 < Date.now()) {
return false; // 토큰 만료
}
const secret = new TextEncoder().encode(process.env.JWT_SECRET!);
const key = await crypto.subtle.importKey(
'raw',
secret,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['verify']
);
const data = new TextEncoder().encode(`${header}.${payload}`);
const sig = base64urlDecode(signature);
return await crypto.subtle.verify('HMAC', key, sig, data);
} catch {
return false;
}
}| 코드 포인트 | 설명 |
|---|---|
runtime: 'edge' |
이 미들웨어를 엣지 런타임에서 실행하도록 지시 |
crypto.subtle |
Node.js crypto 대신 Web Crypto API 사용 (엣지 호환) |
claims.exp 검증 |
JWT 만료 시간 확인 — 이 검증이 없으면 만료된 토큰도 통과됨 |
base64urlDecode |
엣지 런타임 간 패딩 처리 차이를 안전하게 흡수하는 유틸 |
matcher |
미들웨어가 동작할 경로를 제한해 불필요한 실행 방지 |
예시 2: Cloudflare Workers로 지역별 콘텐츠 분기 + A/B 테스트
서울 사용자에겐 한국어 랜딩 페이지, 도쿄 사용자에겐 일본어 페이지를 엣지에서 즉시 분기하는 예시입니다. 오리진 서버 왕복 없이 처리되기 때문에 지연이 거의 없습니다.
// Cloudflare Workers — wrangler.toml 설정 후 배포
// 최소 wrangler.toml 예시:
// name = "my-worker"
// main = "src/index.ts"
// compatibility_date = "2025-01-01"
// [[kv_namespaces]]
// binding = "REGION_KV"
// id = "YOUR_KV_ID"
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const country = request.headers.get('cf-ipcountry') ?? 'US';
const lang = request.headers.get('accept-language')?.split(',')[0] ?? 'en';
// KV 스토어에서 지역별 설정 조회 (엣지 상태 저장소)
const regionConfig = await env.REGION_KV.get(country, 'json') as RegionConfig | null;
const targetUrl = new URL(request.url);
if (country === 'KR' || lang.startsWith('ko')) {
targetUrl.pathname = `/ko${targetUrl.pathname}`;
} else if (country === 'JP' || lang.startsWith('ja')) {
targetUrl.pathname = `/ja${targetUrl.pathname}`;
}
// A/B 테스트: 쿠키 기반 사용자 그룹 분기
const existingGroup = request.headers.get('cookie')?.match(/ab-group=(\w+)/)?.[1];
const abGroup = existingGroup ?? (Math.random() < 0.5 ? 'control' : 'variant');
const response = await fetch(targetUrl.toString(), request);
const newResponse = new Response(response.body, response);
// set 대신 append로 기존 쿠키를 보존하면서 추가
newResponse.headers.append(
'Set-Cookie',
`ab-group=${abGroup}; Path=/; Max-Age=86400; SameSite=Lax; Secure`
);
newResponse.headers.set('X-Region', country);
return newResponse;
},
} satisfies ExportedHandler<Env>;
interface RegionConfig {
currency: string;
defaultLang: string;
}
interface Env {
REGION_KV: KVNamespace;
}| 코드 포인트 | 설명 |
|---|---|
cf-ipcountry |
Cloudflare가 자동으로 주입하는 국가 코드 헤더 |
env.REGION_KV |
Cloudflare KV — 엣지 친화적 글로벌 분산 키-값 스토어 |
headers.append |
set 대신 append를 사용해 기존 Set-Cookie 헤더를 덮어쓰지 않음 |
SameSite=Lax; Secure |
쿠키 보안 속성 — 프로덕션 환경에서 반드시 포함해야 함 |
예시 3: 무거운 작업은 서버리스(리전)로 위임
이미지 리사이징이나 DB 트랜잭션처럼 엣지 제약(128MB, 50ms CPU)을 넘는 작업은 리전 서버리스 함수로 위임하는 패턴입니다. 엣지는 라우팅만 담당하고, 실제 처리는 Lambda에서 수행합니다.
// app/api/resize/route.ts — 리전 서버리스 함수 (runtime 미지정 = Node.js)
import { NextRequest, NextResponse } from 'next/server';
import sharp from 'sharp'; // Node.js 전용 — 엣지에서 사용 불가
// Vercel 기본값은 10초, 최대 300초까지 설정 가능. 이미지 처리용으로 30초 지정
export const maxDuration = 30;
export async function POST(req: NextRequest) {
const formData = await req.formData();
const file = formData.get('image') as File;
if (!file) {
return NextResponse.json({ error: 'No image provided' }, { status: 400 });
}
const buffer = Buffer.from(await file.arrayBuffer());
const resized = await sharp(buffer)
.resize(800, 600, { fit: 'inside', withoutEnlargement: true })
.webp({ quality: 85 })
.toBuffer();
return new NextResponse(resized, {
headers: {
'Content-Type': 'image/webp',
'Cache-Control': 'public, max-age=31536000, immutable',
},
});
}sharp: Node.js 네이티브 바인딩을 사용하는 고성능 이미지 처리 라이브러리입니다. 엣지 런타임에서는 실행 자체가 불가능하므로 반드시 리전 서버리스 함수에서 처리해야 합니다.
maxDuration을 명시하지 않으면 플랫폼 기본값(Vercel 기준 10초)이 적용되므로, 이미지 처리처럼 시간이 걸리는 작업에서는 반드시 명시하는 것이 좋습니다.
장단점 분석
장점과 단점 비교
실무에서 가장 자주 밟는 함정은 "TCP 연결 불가" 제약입니다. 엣지 런타임은 HTTP 기반 샌드박스 구조라 TCP 소켓을 직접 열 수 없거든요. 그래서 일반적인 DB 클라이언트(pg, mysql2)가 동작하지 않습니다. 이 제약을 모르고 엣지 미들웨어에서 DB 쿼리를 날리려다 막힌 경험, 저만 있는 게 아닐 거라 생각합니다.
| 항목 | Edge Runtime | 서버리스(Lambda형) |
|---|---|---|
| 응답 속도 | 5~30ms (전 세계 PoP 분산) | 리전 의존, 원거리 고지연 가능 |
| 격리 컨텍스트 초기화 | <1ms, 워밍업 불필요 | 100ms~1초+ |
| 스케일링 | 오토스케일링 기본 | 오토스케일링 지원 |
| API 지원 | Web Standard만 | 풀 Node.js + npm |
| 비용 효율 | CPU 시간 기준, 에그레스(Egress) 비용 없음(CF) | 요청 + 실행 시간 과금 |
| DB 연결 | HTTP API 기반만 가능 (TCP 소켓 불가) | RDB 포함 모든 연결 지원 |
| 디버깅 | 툴링 아직 성숙 중 | 모니터링·로깅 도구 풍부 |
에그레스(Egress) 비용: 클라우드 서버에서 인터넷으로 데이터가 나갈 때 발생하는 전송 비용입니다. AWS Lambda는 리전 외부 트래픽에 GB당 과금이 발생하지만, Cloudflare Workers 유료 플랜은 에그레스 비용이 없어 대용량 응답에 유리합니다.
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| Node.js 모듈 불가 | fs, sharp, pg 등 사용 불가 |
해당 작업은 리전 서버리스로 위임 |
| TCP 소켓 연결 불가 | 엣지가 HTTP 기반 샌드박스 구조이기 때문 | HTTP API 기반 엣지 DB(Turso, Neon) 사용 |
| 번들 크기 제한 | Vercel 엣지 함수 4MB 상한 | tree-shaking 적극 활용, 무거운 의존성 제거 |
| 실행 시간 제한 | CPU 50ms 초과 작업 불가 | 장시간 작업은 서버리스 + 큐로 처리 |
| Observability | 분산 로그 수집·트레이싱 미성숙 | Cloudflare Logpush, OpenTelemetry 활용 |
흔한 실수 3가지
-
엣지 미들웨어에서 DB를 직접 조회하려다 TCP 연결 오류 —
prisma.$connect()나pg.Pool같은 TCP 기반 DB 클라이언트는 엣지에서 동작하지 않습니다. 저도 이 오류를 처음 만났을 때 30분을 날렸습니다. Neon이나 Turso처럼 HTTP API를 제공하는 엣지 친화적 DB를 사용하거나, DB 조회 자체를 리전 함수로 이동하는 것이 좋습니다. (참고로 PlanetScale은 2024년 서버리스 플랜을 종료했으니 새 프로젝트에서는 Neon 또는 Turso가 대안입니다.) -
runtime: 'edge'를 전체 API 라우트에 일괄 적용 — 처음엔 "엣지가 빠르니까 다 엣지로 돌리자"고 생각하기 쉽습니다. 실제로는 Node.js 의존 라이브러리가 하나라도 포함된 순간 빌드 타임에 조용히 실패하거나 런타임 에러가 납니다. 라우트별로 필요한 런타임을 명시적으로 지정하는 습관이 중요합니다. -
엣지 함수의 번들 크기를 신경 쓰지 않음 — Zod나 date-fns처럼 일반적인 유틸 라이브러리도 번들에 포함되면 4MB 제한을 빠르게 채울 수 있습니다.
@edge-runtime/jest-environment로 로컬에서 번들 크기를 미리 확인해보는 것이 좋습니다.
2025~2026 핵심 흐름: 하이브리드가 표준이 됐다
"엣지 vs 서버리스" 이분법은 이미 과거의 논쟁입니다. 인증·라우팅·캐싱은 엣지, 무거운 연산·DB 쓰기·배치는 리전 서버리스로 분리하는 하이브리드 구조가 사실상 표준으로 자리 잡았습니다.
여기에 두 가지 변화가 더해지고 있습니다. 하나는 WebAssembly(Wasm)의 엣지 통합입니다. 콜드 스타트 0ms에 Node.js 대비 1/10 메모리로 동작하면서, Cloudflare Workers·Fastly·Deno Deploy 모두 Wasm 네이티브 지원을 강화 중입니다. 다른 하나는 엣지 AI 추론으로, Cloudflare Workers AI를 필두로 Llama 같은 모델을 엣지에서 직접 실행하는 흐름이 가속화되고 있습니다. 지금 당장 쓰지 않더라도, 향후 아키텍처 결정에서 이 맥락을 알고 있으면 선택의 폭이 달라집니다.
마치며
결국 Edge Runtime과 서버리스는 경쟁 관계가 아니라, 각자의 강점을 살려 조합할 때 진가를 발휘하는 상호 보완적 모델입니다. 인증·라우팅처럼 빠르고 자주, 전 세계에서 처리해야 하는 작업은 엣지로, DB 쓰기·파일 처리·복잡한 비즈니스 로직은 리전 서버리스로 분리하는 것이 오늘날의 사실상 표준입니다. 이 글을 읽고 나서 "그래서 내 프로젝트에서 오늘 뭘 바꿔볼까"라는 질문이 생겼다면, 아래 세 가지부터 시작해보시면 좋겠습니다.
-
기존 Next.js 프로젝트에
middleware.ts를 추가해보세요 — 프로젝트 루트에middleware.ts를 만들고runtime: 'edge'를 지정한 뒤, 인증 토큰 확인 로직 하나만 옮겨보는 것으로 충분합니다.next dev로 로컬에서 바로 동작을 확인할 수 있습니다. -
Cloudflare Workers 무료 플랜으로 간단한 A/B 테스트 함수를 배포해보세요 —
npm create cloudflare@latest로 프로젝트를 생성하고,wrangler deploy로 실제 전 세계 PoP에 배포하는 전 과정을 30분 안에 경험할 수 있습니다. -
팀의 API 라우트를 "엣지 가능 / 리전 필요"로 분류해보세요 — Node.js 모듈 의존성, 실행 시간, DB 접근 여부를 기준으로 스프레드시트 하나에 정리해보면 아키텍처 개선 포인트가 명확하게 드러납니다.
시리즈 다음 편: Cloudflare Workers + Hono로 엣지 API 서버를 처음부터 구축하는 실전 튜토리얼 — 라우팅, 미들웨어, KV 연동까지 한 번에 다룰 예정입니다.
참고 자료
- Serverless and Edge Are Eating the Backend in 2025 | DEV Community
- Edge Functions vs Serverless: The 2025 Performance Battle | byteiota
- Edge Computing for Frontend Developers: Cloudflare Workers, Deno Deploy, and Vercel Edge | daily.dev
- Edge Computing Frontend 2026 | Serverless Edge Functions Guide
- Vercel Edge Runtime 공식 문서
- Cloudflare Workers: The Complete Serverless Edge Computing Platform | Medium
- WebAssembly 2026: Server-Side Runtimes, WASI, and the Universal Binary Revolution
- Unlocking the Next Wave of Edge Computing with Serverless WebAssembly | Akamai
- The Edge Effect: Serverless & Deployment Redefined in 2026 | Apex Logic