๐Ÿš€ Next.js 13์žฅ: Middleware & Edge Runtime โ€” ์š”์ฒญ์„ ๊ฐ€๋กœ์ฑ„๋Š” Edge ๊ฒฝ๋น„์›

๐Ÿ“‹ ๊ฐœ์š”

Middleware์™€ Edge Runtime์œผ๋กœ ์š”์ฒญ์„ ๊ฐ€๋กœ์ฑ„๊ณ  ์ธ์ฆ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ, A/B ํ…Œ์ŠคํŠธ๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ“‹ ๋ชฉ์ฐจ


๐Ÿ“Œ ์ด ๋ฌธ์„œ๋ฅผ ์ฝ๊ธฐ ์ „์—

โฑ๏ธ ์˜ˆ์ƒ ์ฝ๊ธฐ ์‹œ๊ฐ„: 15๋ถ„ (์ „์ฒด) / ํ•ต์‹ฌ ํŒŒํŠธ๋งŒ: 7๋ถ„

๐Ÿ—บ๏ธ ์ด ๋ฌธ์„œ์˜ ๋ฐฐ๊ฒฝ ์„ธ๊ณ„๊ด€: '์˜์ˆ˜๋„ค ์ปค๋ฎค๋‹ˆํ‹ฐ'

  • ์˜์ฒ (์‹ ์ž…): "๋กœ๊ทธ์ธ ์•ˆ ํ•œ ์‚ฌ์šฉ์ž๊ฐ€ /dashboard ํŽ˜์ด์ง€์— ์ ‘๊ทผํ•˜๋ ค ํ•˜๋ฉด /login์œผ๋กœ ๋ณด๋‚ด์•ผ ํ•˜๋Š”๋ฐ... ๊ฐ ํŽ˜์ด์ง€ ์ปดํฌ๋„ŒํŠธ ์ƒ๋‹จ๋งˆ๋‹ค ์„ธ์…˜ ์ฒดํฌ ์ฝ”๋“œ๋ฅผ ๋ณต๋ถ™ํ•ด์•ผ ํ•˜๋‚˜์š”? ํŽ˜์ด์ง€๊ฐ€ 100๊ฐœ๋ฉด 100๊ตฐ๋ฐ ๋‹ค ์จ์•ผ ํ•ด์š”?"
  • ์˜ํ˜ธ(๋ฆฌ๋“œ): "์ ˆ๋Œ€ ๊ทธ๋Ÿฌ๋ฉด ์•ˆ ๋˜์ฃ ! middleware.ts ํŒŒ์ผ ํ•˜๋‚˜๋ฅผ ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ์— ๋‘๋ฉด ๋ผ์š”. ๋ชจ๋“  ์š”์ฒญ์ด ํŽ˜์ด์ง€์— ๋„์ฐฉํ•˜๊ธฐ ์ „์— ์ด ํŒŒ์ผ์„ ๋ฌด์กฐ๊ฑด ๊ฑฐ์น˜๊ฑฐ๋“ ์š”. ๊ฒฝ๋น„์›์ฒ˜๋Ÿผ ๋”ฑ ํ•œ ๊ณณ์—์„œ ๋ชจ๋“  ์ถœ์ž…์„ ํ†ต์ œํ•˜๋Š” ๊ฑฐ์˜ˆ์š”."

๐Ÿ—บ๏ธ ์ด ๋ฌธ์„œ์˜ ํ๋ฆ„
Middleware ์‹คํ–‰ ์œ„์น˜ โ†’ ์ธ์ฆ ๋ฆฌ๋””๋ ‰ํŠธ โ†’ Edge Runtime ๊ฐœ๋… โ†’ ๋ณด์•ˆ ํ—ค๋” ์„ค์ •

๐ŸŽฏ ์ด ๋ฌธ์„œ๋ฅผ ๋‹ค ์ฝ์œผ๋ฉด ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ

  • middleware.ts๋กœ ๋กœ๊ทธ์ธ ์—ฌ๋ถ€์— ๋”ฐ๋ผ ํŽ˜์ด์ง€ ์ ‘๊ทผ์„ ์ œ์–ดํ•  ์ˆ˜ ์žˆ๋‹ค
  • matcher๋กœ Middleware๊ฐ€ ์‹คํ–‰๋  ๊ฒฝ๋กœ๋ฅผ ์ •๋ฐ€ํ•˜๊ฒŒ ์ œ์–ดํ•  ์ˆ˜ ์žˆ๋‹ค
  • Edge Runtime์ด Node.js์™€ ์–ด๋–ป๊ฒŒ ๋‹ค๋ฅธ์ง€ ์„ค๋ช…ํ•  ์ˆ˜ ์žˆ๋‹ค

๐Ÿค” ์™œ ์•Œ์•„์•ผ ํ•˜๋Š”๊ฐ€

์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค๋‹ค ๋ณด๋ฉด ๋ฐ˜๋“œ์‹œ ์ด๋Ÿฐ ์š”๊ตฌ์‚ฌํ•ญ์ด ์ƒ๊ฒจ:

  • ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž๋งŒ /dashboard, /my, /settings ์ ‘๊ทผ ๊ฐ€๋Šฅ
  • ํŠน์ • ๊ตญ๊ฐ€(์ง€์—ญ)์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ํŽ˜์ด์ง€๋กœ ๋ผ์šฐํŒ… (A/B ํ…Œ์ŠคํŠธ, ๋‹ค๊ตญ์–ด)
  • ๋ชจ๋“  ์‘๋‹ต์— ๋ณด์•ˆ ํ—ค๋”(X-Frame-Options, CSP) ์ž๋™ ์ถ”๊ฐ€
  • ํŠน์ • ๋ด‡ ๋˜๋Š” IP ์ฐจ๋‹จ

์ด๊ฑธ ๊ฐ ํŽ˜์ด์ง€๋งˆ๋‹ค ๊ตฌํ˜„ํ•˜๋ฉด ์ฝ”๋“œ ์ค‘๋ณต, ๋น ๋œจ๋ฆฌ๋Š” ํŽ˜์ด์ง€ ๋ฐœ์ƒ, ์œ ์ง€๋ณด์ˆ˜ ์ง€์˜ฅ์ด ํŽผ์ณ์ ธ.

Middleware๋Š” ๋ชจ๋“  ์š”์ฒญ์˜ ๊ณตํ†ต ๊ด€๋ฌธ์ด์•ผ. CDN ์—ฃ์ง€ ๋…ธ๋“œ์—์„œ ์‹คํ–‰๋˜๊ธฐ ๋•Œ๋ฌธ์— ์„œ๋ฒ„๋‚˜ DB์— ๋„๋‹ฌํ•˜๊ธฐ๋„ ์ „์—, ์‹ฌ์ง€์–ด Next.js ์•ฑ ์ž์ฒด์— ์š”์ฒญ์ด ๋‹ฟ๊ธฐ ์ „์— ๋จผ์ € ์‹คํ–‰๋ผ. ์ด๊ฒŒ ํ•ต์‹ฌ์ด์•ผ.


๐Ÿ—๏ธ ๋น„์œ ๋กœ ๋จผ์ € ์ดํ•ดํ•˜๊ธฐ

๐Ÿง’ 5์‚ด์—๊ฒŒ ์„ค๋ช…ํ•œ๋‹ค๋ฉด?
์•„ํŒŒํŠธ ์ž…๊ตฌ์— ๊ฒฝ๋น„์›์ด ์žˆ์–ด. ๋ˆ„๊ฐ€ ๋“ค์–ด์˜ค๋“  ๋จผ์ € ๊ฒฝ๋น„์›์„ ๋งŒ๋‚˜์•ผ ํ•ด.
"์ž…์ฃผ๋ฏผ ์นด๋“œ ์žˆ์–ด์š”?" โ†’ ์—†์œผ๋ฉด ๋ชป ๋“ค์–ด๊ฐ€๊ฒŒ ๋ง‰์•„.
"๋ช‡ ๋™ ๊ฐ€์„ธ์š”?" โ†’ 1๋™์€ ์™ผ์ชฝ, 2๋™์€ ์˜ค๋ฅธ์ชฝ์œผ๋กœ ์•ˆ๋‚ดํ•ด์ค˜.

Middleware๊ฐ€ ๊ทธ ๊ฒฝ๋น„์›์ด์•ผ. ๋ชจ๋“  ์š”์ฒญ์ด ์‹ค์ œ ํŽ˜์ด์ง€(์•„ํŒŒํŠธ ๊ฐ ์„ธ๋Œ€)์— ๋‹ฟ๊ธฐ ์ „์— ๋ฐ˜๋“œ์‹œ ๊ฒฝ๋น„์›์„ ๊ฑฐ์ณ์•ผ ํ•ด.

Middleware์˜ ์‹คํ–‰ ์œ„์น˜:

์‚ฌ์šฉ์ž ๋ธŒ๋ผ์šฐ์ € ์š”์ฒญ
    โ†“
๐Ÿ›ก๏ธ middleware.ts (Edge์—์„œ ์‹คํ–‰, ๊ฐ€์žฅ ๋จผ์ €!)
    โ†“
Next.js ์•ฑ ์„œ๋ฒ„
    โ†“
layout.tsx โ†’ page.tsx

๐Ÿงฉ middleware.ts ๊ธฐ๋ณธ ๊ตฌ์กฐ โ€” ๋ชจ๋“  ์š”์ฒญ์˜ ์ฒซ ๊ด€๋ฌธ ๐ŸŸข

๐ŸŽฏ ์ด ์„น์…˜์„ ์ฝ๊ณ  ๋‚˜๋ฉด:

  • middleware.ts ํŒŒ์ผ์„ ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ์— ๋งŒ๋“ค์–ด ๋ชจ๋“  ์š”์ฒญ์„ ๊ฐ€๋กœ์ฑŒ ์ˆ˜ ์žˆ๋‹ค
  • NextResponse.redirect()์™€ NextResponse.next()์˜ ์ฐจ์ด๋ฅผ ์•ˆ๋‹ค
  • matcher๋กœ Middleware ์‹คํ–‰ ๊ฒฝ๋กœ๋ฅผ ์ œ์–ดํ•  ์ˆ˜ ์žˆ๋‹ค
// middleware.ts (ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ โ€” app ํด๋”์™€ ๊ฐ™์€ ๋ ˆ๋ฒจ)
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl
 
  console.log(`[Middleware] ์š”์ฒญ ๊ฒฝ๋กœ: ${pathname}`)
 
  // ํŠน์ • ์กฐ๊ฑด์—์„œ ๋ฆฌ๋””๋ ‰ํŠธ
  if (pathname === '/old-posts') {
    // NextResponse.redirect: ๋‹ค๋ฅธ URL๋กœ ์˜๊ตฌ ๋ณด๋‚ด๊ธฐ
    return NextResponse.redirect(new URL('/posts', request.url))
  }
 
  // ์š”์ฒญ ํ—ค๋”์— ์ •๋ณด ์ถ”๊ฐ€ (ํŽ˜์ด์ง€ ์ปดํฌ๋„ŒํŠธ์—์„œ ์ฝ์„ ์ˆ˜ ์žˆ์Œ)
  const requestHeaders = new Headers(request.headers)
  requestHeaders.set('x-pathname', pathname)
 
  // NextResponse.next(): "ํ†ต๊ณผ, ๋‹ค์Œ์œผ๋กœ ์ง„ํ–‰ํ•ด"
  return NextResponse.next({
    request: { headers: requestHeaders },
  })
}
 
// โš™๏ธ matcher: Middleware๋ฅผ ์‹คํ–‰ํ•  ๊ฒฝ๋กœ ํŒจํ„ด ์„ค์ •
export const config = {
  matcher: [
    // ์•„๋ž˜ ๊ฒฝ๋กœ๋“ค์„ ์ œ์™ธํ•œ ๋ชจ๋“  ๊ฒฝ๋กœ์—์„œ ์‹คํ–‰
    // ์ด๋ฏธ์ง€, ์ •์  ํŒŒ์ผ, API ๋“ฑ์€ ๋ณดํ†ต ์ œ์™ธํ•˜๋Š” ๊ฒŒ ์„ฑ๋Šฅ์— ์ข‹์Œ
    '/((?!_next/static|_next/image|favicon.ico|api/webhooks).*)',
  ],
}

matcher ํŒจํ„ด ์ž‘์„ฑ๋ฒ•:

ํŒจํ„ด์˜๋ฏธ
'/dashboard'/dashboard ์ •ํ™•ํžˆ ์ผ์น˜
'/dashboard/:path*'/dashboard ํ•˜์œ„ ๋ชจ๋“  ๊ฒฝ๋กœ
`'/((?!api_next).*)' `
['/dashboard/:path*', '/my/:path*']๋ฐฐ์—ด๋กœ ์—ฌ๋Ÿฌ ๊ฒฝ๋กœ ์ง€์ •

๐Ÿ’ก ํ•œ ์ค„๋กœ ๊ธฐ์–ตํ•˜๊ธฐ
middleware.ts๋Š” ์•„ํŒŒํŠธ ๊ฒฝ๋น„์›. matcher๋Š” ๊ฒฝ๋น„์› ๊ทผ๋ฌด ๊ตฌ์—ญ ์ง€๋„์•ผ. ์ง€๋„์— ์—†๋Š” ๊ณณ์€ ๊ทธ๋ƒฅ ํ†ต๊ณผ์‹œ์ผœ.


๐Ÿ” ์ธ์ฆ ๋ฆฌ๋””๋ ‰ํŠธ ํŒจํ„ด โ€” ๋กœ๊ทธ์ธ ์•ˆ ํ•œ ์‚ฌ๋žŒ ์ฐจ๋‹จํ•˜๊ธฐ ๐ŸŸก

๐ŸŽฏ ์ด ์„น์…˜์„ ์ฝ๊ณ  ๋‚˜๋ฉด:

  • ์ฟ ํ‚ค์˜ ์„ธ์…˜ ํ† ํฐ์„ ์ฝ์–ด ๋กœ๊ทธ์ธ ์—ฌ๋ถ€๋ฅผ ํŒ๋‹จํ•  ์ˆ˜ ์žˆ๋‹ค
  • ๋กœ๊ทธ์ธ ์•ˆ ํ•œ ์‚ฌ์šฉ์ž๋ฅผ /login์œผ๋กœ ๋ฆฌ๋””๋ ‰ํŠธํ•˜๋Š” ํŒจํ„ด์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค

๐Ÿค” ์ž ๊น, ๋จผ์ € ์ƒ๊ฐํ•ด๋ด
์„ธ์…˜ ํ† ํฐ์€ ์ฟ ํ‚ค์— ์žˆ์–ด. Middleware์—์„œ ์ฟ ํ‚ค๋ฅผ ์ฝ์–ด์„œ ์œ ํšจํ•œ ์„ธ์…˜์ธ์ง€ ์–ด๋–ป๊ฒŒ ํ™•์ธํ•  ์ˆ˜ ์žˆ์„๊นŒ?

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
// ๋กœ๊ทธ์ธ ์—†์ด ์ ‘๊ทผ ๊ฐ€๋Šฅํ•œ ๊ณต๊ฐœ ๊ฒฝ๋กœ ๋ชฉ๋ก
const PUBLIC_PATHS = ['/', '/login', '/signup', '/posts']
 
export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl
 
  // ๊ณต๊ฐœ ๊ฒฝ๋กœ๋Š” ๋ฐ”๋กœ ํ†ต๊ณผ
  const isPublic = PUBLIC_PATHS.some(
    (path) => pathname === path || pathname.startsWith(`${path}/`)
  )
  if (isPublic) return NextResponse.next()
 
  // ์ฟ ํ‚ค์—์„œ ์„ธ์…˜ ํ† ํฐ ์ฝ๊ธฐ
  const sessionToken = request.cookies.get('session')?.value
 
  // ์„ธ์…˜์ด ์—†์œผ๋ฉด ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋””๋ ‰ํŠธ
  if (!sessionToken) {
    const loginUrl = new URL('/login', request.url)
    // ๋กœ๊ทธ์ธ ํ›„ ์›๋ž˜ ๊ฐ€๋ ค๋˜ ํŽ˜์ด์ง€๋กœ ๋Œ์•„์˜ค๊ธฐ ์œ„ํ•ด callbackUrl ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€
    loginUrl.searchParams.set('callbackUrl', pathname)
    return NextResponse.redirect(loginUrl)
  }
 
  // โš ๏ธ ์ฃผ์˜: Middleware๋Š” Edge Runtime์—์„œ ๋Œ์•„๊ฐ
  // DB ์กฐํšŒ๋‚˜ ๋ฌด๊ฑฐ์šด JWT ๊ฒ€์ฆ์€ ์—ฌ๊ธฐ์„œ ํ•˜๋ฉด ์•ˆ ๋ผ
  // ๊ฐ„๋‹จํ•œ ํ† ํฐ ์กด์žฌ ์—ฌ๋ถ€ ํ™•์ธ๋งŒ ํ•˜๊ณ , ์‹ค์ œ ๊ฒ€์ฆ์€ ํŽ˜์ด์ง€/API์—์„œ ํ•ด
  return NextResponse.next()
}
 
export const config = {
  // ์ •์  ํŒŒ์ผ, ์ด๋ฏธ์ง€, API๋Š” ์ œ์™ธ
  matcher: ['/((?!_next/static|_next/image|favicon.ico|api).*)'],
}

์ด ํŒจํ„ด์˜ ํ•œ๊ณ„์™€ ์˜ฌ๋ฐ”๋ฅธ ์—ญํ•  ๋ถ„๋ฆฌ:

Middleware (Edge)    โ†’ "ํ† ํฐ์ด ์žˆ๋Š”๊ฐ€?" ๋งŒ ํ™•์ธ (๋น ๋ฅธ 1์ฐจ ์ฒดํฌ)
    โ†“ ํ†ต๊ณผ
ํŽ˜์ด์ง€ / Server Action โ†’ "ํ† ํฐ์ด ์œ ํšจํ•œ๊ฐ€? ๊ถŒํ•œ์ด ์žˆ๋Š”๊ฐ€?" ์‹ฌ์ธต ๊ฒ€์ฆ (๋А๋ฆฌ์ง€๋งŒ ์ •ํ™•)

โš ๏ธ ์ฃผ์˜: Middleware์—์„œ DB ์ฟผ๋ฆฌ๋‚˜ ๋ณต์žกํ•œ ์•”ํ˜ธํ™” ์—ฐ์‚ฐ์„ ํ•˜๋ฉด ์•ˆ ๋ผ. Edge Runtime์€ Node.js API ์ผ๋ถ€๊ฐ€ ์ œํ•œ๋˜์–ด ์žˆ๊ณ , ๋ชจ๋“  ์š”์ฒญ๋งˆ๋‹ค ์‹คํ–‰๋˜๋ฏ€๋กœ ์„ฑ๋Šฅ์— ์ง๊ฒฐ๋ผ.

๐Ÿ”— ์—ฐ๊ฒฐ ๊ณ ๋ฆฌ
์„ธ์…˜ ๊ฒ€์ฆ์˜ ์‹ฌ์ธต ํŒจํ„ด (DAL, verifySession())์€ ์‹ฌํ™” 7์žฅ Authentication Architecture์—์„œ ์ž์„ธํžˆ ๋‹ค๋ค„. ์ง€๊ธˆ์€ Middleware์˜ 1์ฐจ ๊ด€๋ฌธ ์—ญํ• ๋งŒ ํŒŒ์•…ํ•˜๋ฉด ์ถฉ๋ถ„ํ•ด.


๐ŸŒ Edge Runtime์ด๋ž€ ๋ฌด์—‡์ธ๊ฐ€ ๐ŸŸก

๐ŸŽฏ ์ด ์„น์…˜์„ ์ฝ๊ณ  ๋‚˜๋ฉด:

  • Edge Runtime๊ณผ Node.js Runtime์˜ ์ฐจ์ด๋ฅผ ์„ค๋ช…ํ•  ์ˆ˜ ์žˆ๋‹ค
  • Middleware์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋Š” Node.js API๊ฐ€ ๋ฌด์—‡์ธ์ง€ ์•Œ๊ณ  ๋Œ€์•ˆ์„ ์ฐพ์„ ์ˆ˜ ์žˆ๋‹ค

๐Ÿ“– ์šฉ์–ด: Edge Runtime โ€” ์‚ฌ์šฉ์ž์—๊ฒŒ ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด CDN ์—ฃ์ง€ ์„œ๋ฒ„์—์„œ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•˜๋Š” ํ™˜๊ฒฝ์ด์•ผ. ์ผ๋ฐ˜ ์„œ๋ฒ„(Origin)๊นŒ์ง€ ์š”์ฒญ์ด ๋‹ฟ์ง€ ์•Š์•„๋„ ๋˜๋‹ˆ๊นŒ ๋ ˆ์ดํ„ด์‹œ๊ฐ€ ํ›จ์”ฌ ๋‚ฎ์•„.

์ผ๋ฐ˜ Node.js ์„œ๋ฒ„
์‚ฌ์šฉ์ž(์„œ์šธ) โ†’ ์„œ๋ฒ„(๋ฏธ๊ตญ) โ†’ ์‘๋‹ต ๋ฐ˜ํ™˜  [๋ ˆ์ดํ„ด์‹œ: 200ms+]

Edge Runtime
์‚ฌ์šฉ์ž(์„œ์šธ) โ†’ ์„œ์šธ ์—ฃ์ง€ ๋…ธ๋“œ โ†’ ์‘๋‹ต ๋ฐ˜ํ™˜  [๋ ˆ์ดํ„ด์‹œ: 10ms ์ดํ•˜]

Edge Runtime์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋Š” ๊ฒƒ๋“ค:

์‚ฌ์šฉ ๋ถˆ๊ฐ€์ด์œ ๋Œ€์•ˆ
fs (ํŒŒ์ผ ์‹œ์Šคํ…œ)Edge ํ™˜๊ฒฝ์€ ์„œ๋ฒ„ ํŒŒ์ผ ์‹œ์Šคํ…œ ์—†์ŒS3 ๋“ฑ ์™ธ๋ถ€ ์Šคํ† ๋ฆฌ์ง€ ์‚ฌ์šฉ
๋Œ€๋ถ€๋ถ„์˜ npm ํŒจํ‚ค์ง€Node.js ์ „์šฉ API ์˜์กดEdge ํ˜ธํ™˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํ™•์ธ
๋ฌด๊ฑฐ์šด ์•”ํ˜ธํ™” (bcrypt)CPU ์ง‘์ค‘ ์ž‘์—… ๊ธˆ์ง€jose ๊ฐ™์€ ๊ฐ€๋ฒผ์šด ๋Œ€์•ˆ ์‚ฌ์šฉ
Prisma, Drizzle ๋“ฑ DB ํด๋ผ์ด์–ธํŠธTCP ์†Œ์ผ“ ์˜์กดHTTP ๊ธฐ๋ฐ˜ DB ๋˜๋Š” ํŽ˜์ด์ง€์—์„œ ์ฒ˜๋ฆฌ
// โŒ Middleware์—์„œ ์“ฐ๋ฉด ์•ˆ ๋˜๋Š” ํŒจํ„ด
import bcrypt from 'bcrypt'     // Node.js ์ „์šฉ โ€” Edge์—์„œ ์—๋Ÿฌ!
import { PrismaClient } from '@prisma/client'  // TCP ์†Œ์ผ“ โ€” Edge์—์„œ ์—๋Ÿฌ!
 
// โœ… Middleware์—์„œ ์“ธ ์ˆ˜ ์žˆ๋Š” ํŒจํ„ด
import { jwtVerify } from 'jose'   // Web Crypto API ๊ธฐ๋ฐ˜ โ€” Edge ํ˜ธํ™˜!
const token = request.cookies.get('session')?.value  // Web API โ€” Edge ํ˜ธํ™˜!

๐Ÿ’ก ํ•œ ์ค„๋กœ ๊ธฐ์–ตํ•˜๊ธฐ
Edge Runtime์€ "์ดˆ๊ฒฝ๋Ÿ‰ ์‹คํ–‰ ํ™˜๊ฒฝ"์ด์•ผ. ๋น ๋ฅธ ๋Œ€์‹  Node.js์˜ ๋ฌด๊ฑฐ์šด ๊ธฐ๋Šฅ์„ ๋ชป ์จ. Middleware๋Š” ์—ฌ๊ธฐ์„œ ๋Œ์•„๊ฐ€๋‹ˆ ๊ฐ€๋ณ๊ฒŒ ์จ์•ผ ํ•ด.


๐Ÿ”’ ๋ณด์•ˆ ํ—ค๋” ์„ค์ • โ€” CSP์™€ HSTS ๐Ÿ”ด

๐ŸŽฏ ์ด ์„น์…˜์„ ์ฝ๊ณ  ๋‚˜๋ฉด:

  • Middleware์—์„œ ๋ชจ๋“  ์‘๋‹ต์— ๋ณด์•ˆ ํ—ค๋”๋ฅผ ์ž๋™์œผ๋กœ ์ถ”๊ฐ€ํ•˜๋Š” ํŒจํ„ด์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค

Middleware๋Š” ๋ชจ๋“  ์‘๋‹ต์— ํ—ค๋”๋ฅผ ์ถ”๊ฐ€ํ•˜๊ธฐ์— ์™„๋ฒฝํ•œ ์œ„์น˜์•ผ.

// middleware.ts โ€” ๋ณด์•ˆ ํ—ค๋” ์„ค์ • ํŒจํ„ด
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
export function middleware(request: NextRequest) {
  const response = NextResponse.next()
 
  // XSS ๋ฐฉ์–ด: ๋ธŒ๋ผ์šฐ์ €๊ฐ€ Content-Type์„ ๋ฌด์‹œํ•˜์ง€ ๋ชปํ•˜๊ฒŒ ๊ฐ•์ œ
  response.headers.set('X-Content-Type-Options', 'nosniff')
 
  // ํด๋ฆญ์žฌํ‚น ๋ฐฉ์–ด: ๋‹ค๋ฅธ ์‚ฌ์ดํŠธ์˜ iframe์—์„œ ์šฐ๋ฆฌ ํŽ˜์ด์ง€ ์ž„๋ฒ ๋“œ ๊ธˆ์ง€
  response.headers.set('X-Frame-Options', 'DENY')
 
  // HTTPS ๊ฐ•์ œ: 1๋…„๊ฐ„ HTTPS๋งŒ ํ—ˆ์šฉ (HSTS)
  response.headers.set(
    'Strict-Transport-Security',
    'max-age=31536000; includeSubDomains'
  )
 
  // CSP: ์Šคํฌ๋ฆฝํŠธยท์ด๋ฏธ์ง€ยทํฐํŠธ ์†Œ์Šค ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ
  response.headers.set(
    'Content-Security-Policy',
    [
      "default-src 'self'",
      "script-src 'self' 'unsafe-inline'",   // Next.js inline script ํ—ˆ์šฉ
      "img-src 'self' data: https:",
      "font-src 'self' https://fonts.gstatic.com",
    ].join('; ')
  )
 
  return response
}

๐Ÿ’ฅ ์—๋Ÿฌ ํ•ด๊ฒฐ ์นดํƒˆ๋กœ๊ทธ


โŒ require() of ES modules is not supported

์–ธ์ œ ๋‚˜์˜ค๋Š”๊ฐ€?

Error: require() of ES Module ... not supported.

์›์ธ: Middleware(Edge Runtime)์—์„œ CommonJS ๋ชจ๋“ˆ์„ importํ–ˆ์„ ๋•Œ. bcrypt, jsonwebtoken ๊ฐ™์€ ํŒจํ‚ค์ง€๊ฐ€ ์ฃผ๋ฒ”.

ํ•ด๊ฒฐ์ฑ…:

// โŒ ์—๋Ÿฌ ๋ฐœ์ƒ
import jwt from 'jsonwebtoken'  // CJS ํŒจํ‚ค์ง€
 
// โœ… Edge ํ˜ธํ™˜ ๋Œ€์•ˆ
import { jwtVerify, SignJWT } from 'jose'  // Web Crypto API ๊ธฐ๋ฐ˜

โŒ Middleware๊ฐ€ ์ •์  ํŒŒ์ผ ์š”์ฒญ์—๋„ ์‹คํ–‰๋˜์–ด ๋А๋ ค์ง

์›์ธ: matcher๋ฅผ ์„ค์ •ํ•˜์ง€ ์•Š์•„ CSS, JS, ์ด๋ฏธ์ง€ ํŒŒ์ผ ์š”์ฒญ์—๋„ Middleware๊ฐ€ ์‹คํ–‰๋จ.

ํ•ด๊ฒฐ์ฑ…:

export const config = {
  matcher: [
    // _next ํ•˜์œ„ ์ •์  ํŒŒ์ผ๊ณผ favicon์€ ์ œ์™ธ
    '/((?!_next/static|_next/image|favicon.ico).*)',
  ],
}

๐Ÿ ์ด๋ฒˆ์— ๋ฐฐ์šด ๋‚ด์šฉ ์ด์ •๋ฆฌ

๐Ÿ“‹ ํ•ต์‹ฌ ๋ช…๋ น ํŒจํ„ด

์ƒํ™ฉ์ฝ”๋“œ
๋‹ค๋ฅธ URL๋กœ ๋ณด๋‚ด๊ธฐNextResponse.redirect(new URL('/login', request.url))
๊ทธ๋ƒฅ ํ†ต๊ณผNextResponse.next()
์‘๋‹ต ํ—ค๋” ์ถ”๊ฐ€response.headers.set('X-Frame-Options', 'DENY')
์ฟ ํ‚ค ์ฝ๊ธฐrequest.cookies.get('session')?.value
URL ๊ฒฝ๋กœ ์ฝ๊ธฐrequest.nextUrl.pathname

โš ๏ธ ์ ˆ๋Œ€ ํ•˜์ง€ ๋ง ๊ฒƒ

์ƒํ™ฉโŒ ๋‚˜์œ ์˜ˆโœ… ์ข‹์€ ์˜ˆ
DB ์กฐํšŒMiddleware์—์„œ Prisma ์‚ฌ์šฉํŽ˜์ด์ง€/API Route์—์„œ ์ฒ˜๋ฆฌ
๋ฌด๊ฑฐ์šด ์•”ํ˜ธํ™”bcrypt.compare() in middlewarejose์˜ jwtVerify()
matcher ์—†์Œ๋ชจ๋“  ๊ฒฝ๋กœ์—์„œ ์‹คํ–‰์ •์  ํŒŒ์ผ ๊ฒฝ๋กœ ์ œ์™ธ

๐Ÿ“ ๋งˆ๋ฌด๋ฆฌ ํ€ด์ฆˆ

Q1. Middleware์—์„œ ์ ˆ๋Œ€๋กœ ํ•˜๋ฉด ์•ˆ ๋˜๋Š” ๊ฒƒ์€?

  • A) ์ฟ ํ‚ค์—์„œ ์„ธ์…˜ ํ† ํฐ ๊ฐ’ ์ฝ๊ธฐ
  • B) ํŠน์ • ๊ฒฝ๋กœ๋กœ ๋ฆฌ๋””๋ ‰ํŠธํ•˜๊ธฐ
  • C) Prisma๋กœ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์กฐํšŒํ•˜๊ธฐ
  • D) ์‘๋‹ต ํ—ค๋”์— ๋ณด์•ˆ ํ—ค๋” ์ถ”๊ฐ€ํ•˜๊ธฐ

โœ… ์ •๋‹ต: C

์˜ค๋‹ต ํ•ด์„ค:

  • A, D โ€” ์ฟ ํ‚ค ์ฝ๊ธฐ, ํ—ค๋” ์ถ”๊ฐ€๋Š” Edge ํ˜ธํ™˜ ์ž‘์—…
  • B โ€” ๋ฆฌ๋””๋ ‰ํŠธ๋Š” Middleware์˜ ํ•ต์‹ฌ ๊ธฐ๋Šฅ ์ค‘ ํ•˜๋‚˜
  • C โ€” Prisma๋Š” TCP ์†Œ์ผ“ ์˜์กด. Edge Runtime์—์„œ ๋ถˆ๊ฐ€

๐Ÿ“Œ ํ•ต์‹ฌ ๊ธฐ์–ต๋ฒ•: Middleware๋Š” "๊ฐ€๋ณ๊ฒŒ, ๋น ๋ฅด๊ฒŒ, DB ์—†์ด."


Q2. ์•„๋ž˜ ๋นˆ์นธ์„ ์ฑ„์›Œ๋ณด์ž.

๋กœ๊ทธ์ธ ํŽ˜์ด์ง€(/login)๋Š” ์„ธ์…˜ ์—†์ด๋„ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•ด์•ผ ํ•œ๋‹ค. ๋นˆ์นธ์„ ์ฑ„์›Œ ์˜ฌ๋ฐ”๋ฅธ matcher๋ฅผ ์™„์„ฑํ•ด.

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico|______).*)'],
}

โœ… ์ •๋‹ต: login (๋˜๋Š” login|signup|api)

ํ•ด์„ค: ๊ณต๊ฐœ ๊ฒฝ๋กœ๋“ค์€ matcher์—์„œ ์ œ์™ธํ•ด Middleware๊ฐ€ ์‹คํ–‰๋˜์ง€ ์•Š๊ฒŒ ํ•˜๊ฑฐ๋‚˜, Middleware ๋‚ด๋ถ€์—์„œ PUBLIC_PATHS ๋ฐฐ์—ด๋กœ ํ†ต๊ณผ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•ด.


Q3. ์นœ๊ตฌ์—๊ฒŒ ์„ค๋ช…ํ•œ๋‹ค๋ฉด?

Middleware์™€ ๊ฐ ํŽ˜์ด์ง€ ์ปดํฌ๋„ŒํŠธ ์•ˆ์—์„œ ์ธ์ฆ ์ฒดํฌ๋ฅผ ํ•˜๋Š” ๊ฒƒ์˜ ์ฐจ์ด๋ฅผ ๋น„์œ ๋กœ ์„ค๋ช…ํ•ด๋ด.

์˜ˆ์‹œ ๋‹ต๋ณ€:

"Middleware๋Š” ๊ฑด๋ฌผ ์ž…๊ตฌ ๊ฒฝ๋น„์›์ด์•ผ. ๋“ค์–ด์˜ค๋Š” ๋ชจ๋“  ์‚ฌ๋žŒ์„ ํ•œ ๋ฒˆ์— ์ฒดํฌํ•ด. ๋ฐ˜๋ฉด ๊ฐ ํŽ˜์ด์ง€์—์„œ ์ฒดํฌํ•˜๋Š” ๊ฑด ๊ฐ ๋ฐฉ๋งˆ๋‹ค ๊ฒฝ๋น„์›์„ ์„ธ์šฐ๋Š” ๊ฑฐ์•ผ. 100๊ฐœ ๋ฐฉ์ด๋ฉด 100๋ช…. Middleware ํ•˜๋‚˜๋กœ ์ „๋ถ€ ๋ง‰๋Š” ๊ฒŒ ํ›จ์”ฌ ํšจ์œจ์ ์ด์ง€."


๐Ÿฃ ์˜์ฒ ์ด์˜ ํ‡ด๊ทผ ์ผ๊ธฐ

์˜ค๋Š˜์€ ์ •๋ง ๋“ ๋“ ํ•œ '์š”์ƒˆ ๊ฒฝ๋น„์›(Middleware)'์„ ๊ณ ์šฉํ•œ ๋‚ ์ด์•ผ! ํŽ˜์ด์ง€๊ฐ€ ๋Š˜์–ด๋‚  ๋•Œ๋งˆ๋‹ค "๋กœ๊ทธ์ธ ์ฒดํฌ ์ฝ”๋“œ๋ฅผ ๋˜ ์–ด๋””์— ๋ณต๋ถ™ํ•ด์•ผ ํ•˜์ง€?" ๊ณ ๋ฏผํ•˜๋ฉฐ ๋จธ๋ฆฌ๊ฐ€ ์•„ํŒ ๋Š”๋ฐ, ํ•œ ๊ณณ์—์„œ ๋ชจ๋“  ์ถœ์ž…์„ ํ†ต์ œํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒŒ ์–ผ๋งˆ๋‚˜ ํฐ ์ถ•๋ณต์ธ์ง€ ๊นจ๋‹ฌ์•˜์–ด.

๐Ÿ’ก ์˜ค๋Š˜์˜ ๊ตํ›ˆ: "๋ชจ๋“  ๋ฐฉ๋งˆ๋‹ค ๊ฒฝ๋น„์›์„ ์„ธ์šฐ์ง€ ๋ง๊ณ  ๊ฑด๋ฌผ ์ž…๊ตฌ(Middleware)๋ฅผ ์ง€ํ‚ค์ž. Edge ํ™˜๊ฒฝ์€ ๊ฐ€๋ณ๊ณ  ๋น ๋ฅด๊ฒŒ ์œ ์ง€ํ•˜๋Š” ๊ฒƒ์ด ์ƒ๋ช…์ด๋‹ค!"

Edge Runtime์ด๋ผ๋Š” ์ƒ์†Œํ•œ ํ™˜๊ฒฝ์—์„œ Prisma ๊ฐ™์€ ๋ฌด๊ฑฐ์šด ๋„๊ตฌ๋ฅผ ๋ชป ์จ์„œ ์กฐ๊ธˆ ๋‹นํ™ฉํ•˜๊ธด ํ–ˆ์ง€๋งŒ, ์˜คํžˆ๋ ค ๊ทธ ๋•๋ถ„์— "์–ด๋–ค ์ฝ”๋“œ๊ฐ€ ๊ฐ€๋ณ๊ณ  ๋น ๋ฅธ ์ฝ”๋“œ์ธ๊ฐ€"์— ๋Œ€ํ•ด ๊นŠ๊ฒŒ ์ƒ๊ฐํ•ด๋ณด๋Š” ๊ณ„๊ธฐ๊ฐ€ ๋์–ด. ์˜ค๋Š˜ ํ•˜๋ฃจ ์ฐธ ๋ณด๋žŒ์ฐผ๋‹ค! ์ง‘์— ๊ฐ€์„œ ์‹œ์›ํ•œ ์บ”๋งฅ์ฃผ ํ•˜๋‚˜ ๋”ฐ๊ณ  ์˜ํ™”๋‚˜ ํ•œ ํŽธ ๋ณด๋ฉด์„œ ๊ธˆ์š”์ผ ๋ถ„์œ„๊ธฐ ๋‚ด์•ผ์ง€. ๋‚ด์ผ์€ ๋” '๊ฐ€๋ณ๊ณ  ๊ฐ•๋ ฅํ•œ' ๋กœ์ง์„ ์งœ๋Š” ๊ฐœ๋ฐœ์ž๊ฐ€ ๋˜์–ด์•ผ๊ฒ ์–ด. ๐Ÿฃ


๐Ÿ”— ๋” ์•Œ์•„๋ณด๊ธฐ