Edge Runtime vs Node.js Runtime: 서버리스 함수가 갑자기 안 돌아가는 이유
Edge Runtime vs Node.js Runtime: 서버리스 함수가 갑자기 안 돌아가는 이유
다들 한 번쯤 들어봤을 겁니다. Edge 함수는 빠르고, 저렴하고, 유저 가까이에서 돌아간다고. 콜드 스타트도 초 단위가 아니라 밀리초 단위고, 글로벌 배포가 기본이라고. 그래서 Next.js API 라우트에 export const runtime = 'edge' 딱 한 줄 넣고 배포했는데...
다 터졌습니다.
Edge Runtime 디버깅 지옥에 오신 걸 환영합니다.
에러 메시지도 암호 같죠. "Dynamic code evaluation not supported." "Module not found." "This API is not available." 분명 로컬에서 잘 돌아갔는데. Node.js에서도 문제없었는데. Edge만 붙이면 왜 이러는 걸까요?
Edge Runtime 욕하려는 건 아닙니다—진짜 좋은 기술이에요. 근데 "Edge 빠름" 마케팅이랑 실제로 프로덕션 코드 돌리는 현실 사이에 엄청난 갭이 있습니다. 오늘 그 갭을 메워봅시다.
일단 개념부터: Edge Runtime이 뭔데?
디버깅 전에 뭘 다루는지부터 알아야 합니다. Edge Runtime이랑 Node.js Runtime은 단순히 코드 돌아가는 위치만 다른 게 아닙니다. 완전히 다른 실행 환경이에요. API도 다르고, 제약도 다르고, 머릿속 그림 자체가 달라야 해요.
Node.js Runtime: 있을 건 다 있음
Node.js 런타임은 우리가 아는 그거. 서버에서 돌아가는 풀옵션 Node.js 환경입니다:
// Node.js 런타임에선 이거 다 됨 import fs from 'fs'; import path from 'path'; import crypto from 'crypto'; import { spawn } from 'child_process'; export async function POST(request) { // 파일 읽기? 됨 const config = JSON.parse(fs.readFileSync('./config.json', 'utf8')); // 네이티브 crypto? 됨 const hash = crypto.createHash('sha256').update('secret').digest('hex'); // 프로세스 띄우기? 됨 const child = spawn('ls', ['-la']); // npm 패키지 아무거나? 됨 const pdf = await generatePDF(data); return Response.json({ success: true }); }
Node.js 런타임으로 얻는 것:
- Node.js 코어 모듈 전부 (
fs,path,crypto,child_process등) - 네이티브 모듈 (C++ 애드온, NAPI로 Rust 바인딩)
- 사실상 코드 크기 무제한
- 긴 타임아웃 (웬만하면 5분까지)
- 네이티브 드라이버로 DB 연결
- 스택 트레이스 제대로 나오는 디버깅
대신? 콜드 스타트 250ms~1초+, 리전별 배포라 글로벌 아님, 스케일 커지면 비용도 커짐.
Edge Runtime: 가볍고 빠름, 대신...
Edge Runtime은 완전 다른 녀석입니다. Web API랑 V8 isolates 기반—Cloudflare Workers 돌리는 기술이랑 같아요:
// Edge Runtime에선 이게 전부 export const runtime = 'edge'; export async function POST(request) { // Web Fetch API? 됨 const response = await fetch('https://api.example.com/data'); // Web Crypto API? 됨 (근데 Node crypto랑 다름!) const hashBuffer = await crypto.subtle.digest('SHA-256', new TextEncoder().encode('secret') ); // Headers, Request, Response? 됨 const headers = new Headers(); headers.set('Cache-Control', 'max-age=3600'); return new Response(JSON.stringify({ success: true }), { headers }); }
Edge Runtime으로 얻는 것:
- Web Platform API만 (Fetch, Streams, Crypto, URL 등)
- 10ms도 안 걸리는 콜드 스타트
- 글로벌 배포 (300군데+에서 실행)
- 스케일 커져도 비용 낮음
- 1~4MB 코드 크기 제한
- 짧은 타임아웃 (보통 30초 한계)
대신? fs 없음, 네이티브 모듈 없음, Node.js 코어 모듈 없음, npm 패키지 절반은 안 돌아감.
왜 터지는가: 실패 유형별 정리
터지는 이유를 유형별로 정리해봅시다. 뭐가 문제인지 알아야 고치죠.
유형 1: Node.js 코어 모듈이 없음
가장 흔합니다. Edge에 없는 걸 import 하는 경우:
// ❌ Edge에서 전부 안 됨 import fs from 'fs'; // 파일시스템 없음 import path from 'path'; // path 모듈 없음 import crypto from 'crypto'; // crypto API 다름 import { Buffer } from 'buffer'; // Buffer 일부만 지원 import stream from 'stream'; // Node streams 없음 import http from 'http'; // http 모듈 없음 import https from 'https'; // https 모듈 없음 import net from 'net'; // TCP 소켓 없음 import dns from 'dns'; // DNS 조회 없음 import child_process from 'child_process'; // 프로세스 없음 import os from 'os'; // OS 정보 없음 import worker_threads from 'worker_threads'; // 워커 스레드 없음
해결: Web API로 대체하거나 폴리필 쓰기:
// ✅ Edge에서 쓸 수 있는 대체제 // crypto.createHash() 대신 async function sha256(message) { const msgBuffer = new TextEncoder().encode(message); const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer); const hashArray = Array.from(new Uint8Array(hashBuffer)); return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); } // Buffer.from() 대신 function base64Encode(str) { return btoa(String.fromCharCode(...new TextEncoder().encode(str))); } function base64Decode(base64) { return new TextDecoder().decode( Uint8Array.from(atob(base64), c => c.charCodeAt(0)) ); } // path.join() 대신 function joinPath(...segments) { return segments.join('/').replace(/\/+/g, '/'); } // querystring 대신 URL 파싱 function parseQuery(url) { return Object.fromEntries(new URL(url).searchParams); }
유형 2: 동적 코드 실행 금지
Edge Runtime은 보안상 eval()이랑 new Function() 못 씁니다. 생각보다 많은 패키지가 이거 때문에 터져요:
// ❌ Edge에서 전부 안 됨 eval('console.log("hello")'); new Function('return 1 + 1')(); require('vm').runInNewContext('1 + 1'); // vm 모듈도 없음 // 이런 패키지들이 내부에서 씀: // - 일부 템플릿 엔진 (Handlebars, 특정 모드의 EJS) // - 일부 스키마 검증 라이브러리 // - 일부 직렬화 라이브러리 // - 소스맵 처리 라이브러리
에러 메시지:
Dynamic code evaluation (e.g., 'eval', 'new Function', 'WebAssembly.compile')
not allowed in Edge Runtime
해결: 대체 패키지 찾기:
// lodash template 쓰면 안 됨 (내부에서 new Function 씀) // ❌ import template from 'lodash/template'; const compiled = template('Hello <%= name %>'); // ✅ 정적 템플릿 라이브러리로 교체 import Mustache from 'mustache'; const output = Mustache.render('Hello {{name}}', { name: 'World' }); // 아니면 그냥 템플릿 리터럴 쓰기 function html(strings, ...values) { return strings.reduce((result, str, i) => result + str + (values[i] ?? ''), '' ); } const name = 'World'; const output = html`Hello ${name}`;
유형 3: 네이티브 모듈
C++/Rust 바인딩 쓰는 패키지는 Edge에서 안 돌아갑니다:
// ❌ Edge에서 안 되는 패키지들 import sharp from 'sharp'; // 이미지 처리 (네이티브) import bcrypt from 'bcrypt'; // 비밀번호 해싱 (네이티브) import canvas from 'canvas'; // 캔버스 렌더링 (네이티브) import sqlite3 from 'sqlite3'; // SQLite (네이티브) import puppeteer from 'puppeteer'; // 브라우저 자동화 (네이티브) import prisma from '@prisma/client'; // Prisma (쿼리 엔진이 네이티브)
에러 메시지:
Error: Cannot find module 'sharp'
Module build failed: Native modules are not supported in Edge Runtime
해결: Edge에서 돌아가는 대체제 쓰기:
// ✅ Edge 호환 대체제 // bcrypt 대신 (순수 JS 구현) import { hash, compare } from 'bcryptjs'; // sharp 대신 // 클라우드 이미지 서비스나 WASM 기반 솔루션 쓰기 async function resizeImage(imageUrl, width, height) { const response = await fetch( `https://images.example.com/resize?url=${encodeURIComponent(imageUrl)}&w=${width}&h=${height}` ); return response; } // Prisma 대신 // Edge 호환 드라이버 어댑터 쓰기 import { PrismaClient } from '@prisma/client'; import { PrismaNeon } from '@prisma/adapter-neon'; // 또는 서버리스 친화적 ORM import { drizzle } from 'drizzle-orm/neon-http'; // SQLite 대신 // D1 (Cloudflare), Turso, PlanetScale 쓰기
유형 4: 블로킹 연산
Edge Runtime은 빠른 논블로킹 작업용입니다. 이벤트 루프 막는 건 문제됨:
// ❌ Edge에서 문제되는 패턴 import { readFileSync } from 'fs'; // 동기 파일 읽기 (+ fs도 없음) sleep(1000); // 블로킹 sleep // 무거운 동기 연산 function fibonacciSync(n) { if (n <= 1) return n; return fibonacciSync(n - 1) + fibonacciSync(n - 2); } const result = fibonacciSync(45); // 몇 초간 블로킹 // 거대한 JSON 동기 파싱 const hugeData = JSON.parse(hugeJsonString); // 타임아웃 위험
해결: 스트리밍이랑 청크 처리:
// ✅ Edge 친화적 패턴 // 큰 응답은 스트리밍 export async function GET() { const stream = new ReadableStream({ async start(controller) { for (let i = 0; i < 1000; i++) { const chunk = await fetchChunk(i); controller.enqueue(new TextEncoder().encode(JSON.stringify(chunk) + '\n')); // 다른 작업 돌아갈 틈 주기 await new Promise(resolve => setTimeout(resolve, 0)); } controller.close(); } }); return new Response(stream, { headers: { 'Content-Type': 'application/x-ndjson' } }); } // Web Streams로 파싱 async function* parseJsonStream(response) { const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop(); for (const line of lines) { if (line.trim()) { yield JSON.parse(line); } } } }
유형 5: 크기 초과
Edge 함수는 크기 제한이 타이트합니다. Vercel: 14MB, Cloudflare Workers: 110MB:
// ❌ 번들 사이즈 폭탄 import _ from 'lodash'; // 70KB+ minified import moment from 'moment'; // 300KB+ 로케일 포함 import * as AWS from 'aws-sdk'; // 엄청 큼 import 'core-js/stable'; // 150KB+ 폴리필 // 아이콘 라이브러리 통째로 import * as Icons from '@heroicons/react/24/solid'; // 번들에 ML 모델이나 큰 데이터 포함 import model from './large-ml-model.json'; // 2MB 모델
해결: 트리쉐이킹 빡세게 + 레이지 로드:
// ✅ 사이즈 최적화 // lodash 전체 대신 import groupBy from 'lodash/groupBy'; import debounce from 'lodash/debounce'; // moment 대신 import { format, parseISO } from 'date-fns'; // aws-sdk v2 대신 import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3'; // 아이콘 개별 import import { HomeIcon } from '@heroicons/react/24/solid'; // 큰 데이터는 런타임에 fetch export async function GET() { const model = await fetch('https://cdn.example.com/model.json').then(r => r.json()); // ... }
실전 디버깅 케이스
흔히 겪는 Edge Runtime 실패 케이스 봅시다.
케이스 1: 갑자기 "Module Not Found"
상황: 로컬에서 잘 됨, 빌드도 ㅇㅋ, 배포하면 런타임에서 터짐.
Error: Cannot find module 'util'
at EdgeRuntime (edge-runtime.js:1:1)
원인 분석:
// 1. 뭐가 'util' 쓰는지 찾기 // 의존성 트리 까봐야 함 // package.json은 멀쩡해 보임 { "dependencies": { "next": "14.0.0", "jsonwebtoken": "9.0.0", // <-- 범인! "next-auth": "4.24.0" } } // 2. 의존성 트리 확인 // npm ls util 치면 // 출력: // └─┬ [email protected] // └── [email protected] // jsonwebtoken이 내부에서 Node util 씀!
해결:
// 방법 1: Edge 호환 JWT 라이브러리로 교체 import { SignJWT, jwtVerify } from 'jose'; // Edge 호환! export const runtime = 'edge'; export async function POST(request) { const secret = new TextEncoder().encode(process.env.JWT_SECRET); // 서명 const token = await new SignJWT({ userId: '123' }) .setProtectedHeader({ alg: 'HS256' }) .setExpirationTime('1h') .sign(secret); // 검증 const { payload } = await jwtVerify(token, secret); return Response.json({ token, payload }); } // 방법 2: 그냥 이 라우트는 Edge 안 쓰기 // export const runtime = 'edge'; 빼버리기 // Node.js 런타임에서 돌리기
케이스 2: NextAuth.js 세션 폭발
상황: 로컬에선 로그인 잘 됨, Edge 프로덕션에서 터짐.
Error: next-auth requires a secret to be set in production
또는
Error: [next-auth]: `useSession` must be wrapped in a <SessionProvider />
// (분명 감쌌는데? 어제까지 됐는데?)
원인:
// next-auth 라우트 핸들러가 Edge 완전 호환 아님 // app/api/auth/[...nextauth]/route.js import NextAuth from 'next-auth'; import GitHub from 'next-auth/providers/github'; export const runtime = 'edge'; // ❌ 이게 문제! export const { handlers, auth, signIn, signOut } = NextAuth({ providers: [GitHub], // DB 어댑터도 Edge에서 안 됨 adapter: PrismaAdapter(prisma), // ❌ 네이티브 의존성! });
해결:
// app/api/auth/[...nextauth]/route.js import NextAuth from 'next-auth'; import GitHub from 'next-auth/providers/github'; // 방법 1: 인증은 Node.js로 (지금 기준 권장) export const runtime = 'nodejs'; // ✅ 명시적 Node.js // 방법 2: Edge 호환 어댑터 쓰기 import { DrizzleAdapter } from '@auth/drizzle-adapter'; import { drizzle } from 'drizzle-orm/neon-http'; export const { handlers, auth } = NextAuth({ providers: [GitHub], adapter: DrizzleAdapter(drizzle(process.env.DATABASE_URL)), // ✅ Edge 호환 }); // 미들웨어에선 Edge 써도 됨 (읽기만, 쓰기 X) // middleware.js export { auth as middleware } from './auth'; export const config = { matcher: ['/dashboard/:path*'] };
케이스 3: DB 연결 지옥
상황: 로컬에서 쿼리 잘 됨, Edge 프로덕션에서 터짐.
Error: Connection pool exhausted
Error: prepared statement "s0" already exists
Error: Cannot establish database connection
원인:
// 일반적인 DB 연결은 Edge에서 안 됨 // Edge 함수는 상태가 없고 전세계에 흩어져 있음 import { Pool } from 'pg'; // ❌ 커넥션 풀 Edge에서 안 됨 const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: 20, // Edge에선 의미 없음 }); // Edge 호출마다 격리됨 - 공유 커넥션 풀 없음! // 300군데+에서 동시에 단일 DB로 연결 시도 = 망
해결:
// ✅ Edge 호환 DB 패턴 // 방법 1: HTTP 기반 DB 연결 (Edge에 최적) import { neon } from '@neondatabase/serverless'; export const runtime = 'edge'; export async function GET() { const sql = neon(process.env.DATABASE_URL); // 쿼리마다 별도 HTTP 요청 - 연결 관리 필요 없음 const users = await sql`SELECT * FROM users LIMIT 10`; return Response.json({ users }); } // 방법 2: Prisma Data Proxy import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient({ datasources: { db: { url: process.env.PRISMA_DATA_PROXY_URL, // 직접 연결 X, data proxy 경유 }, }, }); // 방법 3: 무거운 DB 작업은 그냥 Edge 안 쓰기 export const runtime = 'nodejs'; // 콜드 스타트 감수 import { db } from '@/lib/database'; export async function GET() { const users = await db.user.findMany(); return Response.json({ users }); }
판단 기준: 언제 뭘 쓸까
디버깅 고통 겪고 나면 궁금해지죠. 도대체 Edge는 언제 쓰는 거야?
Edge 쓸 때:
1. 레이턴시 중요 + 캐시 가능한 데이터:
// ✅ Edge 최적: 지역 기반 라우팅, 캐시된 콘텐츠 export const runtime = 'edge'; export async function GET(request) { const country = request.geo?.country || 'US'; const content = await fetch(`https://cdn.example.com/content/${country}.json`, { next: { revalidate: 3600 } }); return Response.json(await content.json()); }
2. 인증 체크 (읽기만):
// ✅ Edge 최적: JWT 검증, 권한 확인 export const runtime = 'edge'; export async function middleware(request) { const token = request.cookies.get('session'); if (!token) { return Response.redirect(new URL('/login', request.url)); } try { await jwtVerify(token.value, secret); return NextResponse.next(); } catch { return Response.redirect(new URL('/login', request.url)); } }
3. A/B 테스트, 피처 플래그:
// ✅ Edge 최적: 즉시 판단, 백엔드 호출 불필요 export const runtime = 'edge'; export async function middleware(request) { const bucket = Math.random(); const variant = bucket < 0.5 ? 'control' : 'treatment'; const response = NextResponse.next(); response.cookies.set('experiment_variant', variant); return response; }
4. 간단한 리다이렉트, 헤더 조작:
// ✅ Edge 최적: URL 리라이팅, 보안 헤더 export const runtime = 'edge'; export async function middleware(request) { const url = request.nextUrl.clone(); if (url.pathname.startsWith('/old-blog/')) { url.pathname = url.pathname.replace('/old-blog/', '/blog/'); return Response.redirect(url); } const response = NextResponse.next(); response.headers.set('X-Frame-Options', 'DENY'); response.headers.set('X-Content-Type-Options', 'nosniff'); return response; }
Node.js 쓸 때:
1. 커넥션 풀 필요한 DB 작업:
// ✅ Node.js: 전통적 DB, 커넥션 유지 필요 export const runtime = 'nodejs'; import { prisma } from '@/lib/prisma'; export async function GET() { const users = await prisma.user.findMany({ include: { posts: true }, take: 50, }); return Response.json({ users }); }
2. 파일 처리, 바이너리:
// ✅ Node.js: 파일시스템, 네이티브 바이너리 export const runtime = 'nodejs'; import sharp from 'sharp'; import { writeFile } from 'fs/promises'; export async function POST(request) { const formData = await request.formData(); const file = formData.get('image'); const buffer = Buffer.from(await file.arrayBuffer()); const processed = await sharp(buffer) .resize(800, 600) .webp({ quality: 80 }) .toBuffer(); await writeFile(`./uploads/${file.name}.webp`, processed); return Response.json({ success: true }); }
3. 복잡한 비즈니스 로직:
// ✅ Node.js: npm 생태계 풀로 쓸 때 export const runtime = 'nodejs'; import Stripe from 'stripe'; import { sendEmail } from '@/lib/email'; import { generatePDF } from '@/lib/pdf'; import { prisma } from '@/lib/prisma'; export async function POST(request) { const order = await request.json(); const stripe = new Stripe(process.env.STRIPE_SECRET); const payment = await stripe.paymentIntents.create({...}); await prisma.order.update({...}); const pdf = await generatePDF(order); await sendEmail({ to: order.email, subject: '주문 확인', attachments: [{ filename: 'invoice.pdf', content: pdf }], }); return Response.json({ success: true }); }
4. 오래 걸리는 작업:
// ✅ Node.js: 30초 넘게 걸릴 때 export const runtime = 'nodejs'; export const maxDuration = 300; // 5분 export async function POST(request) { const { videoUrl } = await request.json(); const processed = await processVideo(videoUrl); // 2~3분 걸림 return Response.json({ downloadUrl: processed.url, duration: processed.duration }); }
하이브리드: 둘 다 쓰기
정답은 "Edge냐 Node.js냐"가 아니라 둘 다 전략적으로 쓰는 겁니다:
src/
├── app/
│ ├── api/
│ │ ├── auth/ # Node.js - DB 세션
│ │ ├── payments/ # Node.js - Stripe, 복잡한 로직
│ │ ├── upload/ # Node.js - 파일 처리
│ │ ├── geo/ # Edge - 위치 기반
│ │ ├── flags/ # Edge - 피처 플래그
│ │ └── health/ # Edge - 헬스체크
│ └── middleware.ts # Edge - 인증, 리다이렉트
// middleware.ts - 전역 Edge import { auth } from './auth'; export default auth((request) => { if (!request.auth && request.nextUrl.pathname.startsWith('/dashboard')) { return Response.redirect(new URL('/login', request.url)); } }); export const config = { matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], };
// app/api/geo/route.ts - 빠른 지역 조회는 Edge export const runtime = 'edge'; export async function GET(request) { const { country, city, latitude, longitude } = request.geo || {}; const nearestStore = await fetch( `https://api.stores.com/nearest?lat=${latitude}&lng=${longitude}`, { next: { revalidate: 3600 } } ).then(r => r.json()); return Response.json({ nearestStore, userLocation: { country, city } }); }
// app/api/orders/route.ts - 복잡한 건 Node.js export const runtime = 'nodejs'; export async function POST(request) { // 주문 처리 풀파워 }
정리
Edge Runtime은 좋은 기술이지만 만능은 아닙니다. Node.js 풀 기능을 글로벌 배포 + 10ms 콜드 스타트와 맞바꾼 제약된 환경이에요. 이 제약—모듈 없음, 동적 평가 금지, 네이티브 안 됨, 크기 제한, 상태 없음—을 알아야 Edge 성공적으로 씁니다.
핵심:
-
Edge는 빠르고 단순한 작업용 — 인증 체크, 리다이렉트, A/B 테스트, 캐시 콘텐츠. 복잡한 비즈니스 로직 아님.
-
애매하면 Node.js — 콜드 스타트 250ms~1초 감수하고 API 풀 호환성 확보.
-
DB 전략 중요 — Edge엔 HTTP 연결 (Neon, PlanetScale HTTP, Prisma Data Proxy), Node.js엔 커넥션 풀.
-
의존성 까보기 —
npm ls돌려서 Node 코어 모듈 쓰는 패키지 찾기. Edge 호환으로 교체. -
사이즈 신경쓰기 — 트리쉐이킹 열심히, 데이터 레이지 로드, 배럴 파일 대신 구체적 import.
-
하이브리드가 답 — 미들웨어랑 레이턴시 민감한 라우트는 Edge, 나머진 Node.js.
다음에 export const runtime = 'edge' 붙이고 함수 터지면, 이제 어디 봐야 할지 아실 겁니다. Edge Runtime이 망가진 게 아니라 그냥 다른 거예요. 그 차이 알면 함정 안 밟고 잘 쓸 수 있습니다.
관련 도구 둘러보기
Pockit의 무료 개발자 도구를 사용해 보세요