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์ด ๋ง๊ฐ์ง ๊ฒ ์๋๋ผ ๊ทธ๋ฅ ๋ค๋ฅธ ๊ฑฐ์์. ๊ทธ ์ฐจ์ด ์๋ฉด ํจ์ ์ ๋ฐ๊ณ ์ ์ธ ์ ์์ต๋๋ค.