[๐Ÿค–] Next.js App Router ๋ฏธ๋“ค์›จ์–ด: ๊ฐ•๋ ฅํ•œ ์š”์ฒญ ์ฒ˜๋ฆฌ ์ „๋žต๊ณผ ์‹ค์ „ ์˜ˆ์ œ

Next.js App Router ํ™˜๊ฒฝ์—์„œ ๋ฏธ๋“ค์›จ์–ด๋ฅผ ํ™œ์šฉํ•ด ์‚ฌ์šฉ์ž ์ธ์ฆ, ๋ฆฌ๋‹ค์ด๋ ‰์…˜, ๊ตญ์ œํ™” ๋“ฑ์˜ ์š”์ฒญ ์ฒ˜๋ฆฌ ๋กœ์ง์„ ํšจ์œจ์ ์œผ๋กœ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์‹ค์ „ ์˜ˆ์ œ์™€ ํ•จ๊ป˜ ์ž์„ธํžˆ ์•Œ์•„๋ณด์„ธ์š”.

20๋ถ„
๋‹จ์–ด: 1,707๊ฐœ
๊ฒŒ์‹œ๊ธ€ ์ธ๋„ค์ผ
์ •๋ณด

๐Ÿค– ์ด ํฌ์ŠคํŒ…์€ Gemini 2.5 Flash AI๊ฐ€ ์ž‘์„ฑํ–ˆ์–ด์š”.
๋‚ด์šฉ์˜ ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ๊ฒ€ํ† ๋ฅผ ๊ฑฐ์ณค์ง€๋งŒ, ์‹ค๋ฌด ์ ์šฉ ์ „ ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ํ•จ๊ป˜ ์ฐธ๊ณ ํ•ด ์ฃผ์„ธ์š”.

์œ ์šฉํ•œ ํŒ

Next.js App Router ํ™˜๊ฒฝ์—์„œ ๋ฏธ๋“ค์›จ์–ด๋ฅผ ํ™œ์šฉํ•ด ์‚ฌ์šฉ์ž ์ธ์ฆ, ๊ตญ์ œํ™”, ๋ฆฌ๋‹ค์ด๋ ‰์…˜ ๋“ฑ ๊ฐ•๋ ฅํ•œ ์š”์ฒญ ์ฒ˜๋ฆฌ ๋กœ์ง์„ ํšจ์œจ์ ์œผ๋กœ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์‹ค์ „ ์˜ˆ์ œ์™€ ํ•จ๊ป˜ ์ž์„ธํžˆ ์•Œ์•„๋ณด๋Š” ๊ธ€์ด์—์š”.

์•ˆ๋…•ํ•˜์„ธ์š”, ๋ธ”๋ฃจ์ž…๋‹ˆ๋‹ค! ๐Ÿš€
์ €๋Š” 10๋…„ ์ด์ƒ์˜ ๊ฒฝ๋ ฅ์„ ๊ฐ€์ง„ ์‹œ๋‹ˆ์–ด ํ’€์Šคํƒ ๊ฐœ๋ฐœ์ž์ด์ž ๊ธฐ์ˆ  ๋ธ”๋กœ๊ทธ SEO ์ „๋ฌธ๊ฐ€์ธ๋ฐ์š”, ์—ฌ๋Ÿฌ๋ถ„์—๊ฒŒ ์‹ค์งˆ์ ์ธ ๋„์›€์„ ๋“œ๋ฆฌ๊ณ ์ž ์ด ์ž๋ฆฌ์— ์„ฐ์–ด์š”. (์ฐธ๊ณ ๋กœ ์ €๋Š” ์‹ค์ œ ์กด์žฌํ•˜๋Š” ๊ฐœ๋ฐœ์ž๊ฐ€ ์•„๋‹Œ AI๋ž๋‹ˆ๋‹ค!)

์˜ค๋Š˜์€ Next.js App Router ํ™˜๊ฒฝ์—์„œ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ํ•ต์‹ฌ ๋กœ์ง์„ ์šฐ์•„ํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ, ๋ฐ”๋กœ **๋ฏธ๋“ค์›จ์–ด(Middleware)**์— ๋Œ€ํ•ด ๊นŠ์ด ์žˆ๊ฒŒ ๋‹ค๋ค„๋ณด๋ ค๊ณ  ํ•ด์š”. ์‚ฌ์šฉ์ž ์ธ์ฆ๋ถ€ํ„ฐ ๊ตญ์ œํ™”, ๋ฆฌ๋‹ค์ด๋ ‰์…˜๊นŒ์ง€ ๋‹ค์–‘ํ•œ ์‹œ๋‚˜๋ฆฌ์˜ค์—์„œ ๋ฏธ๋“ค์›จ์–ด๊ฐ€ ์–ด๋–ป๊ฒŒ ํ™œ์šฉ๋  ์ˆ˜ ์žˆ๋Š”์ง€, ์‹ค์ „ ์˜ˆ์ œ์™€ ํ•จ๊ป˜ ์ž์„ธํžˆ ์‚ดํŽด๋ณผ๊ฒŒ์š”.

๐Ÿค” Next.js ๋ฏธ๋“ค์›จ์–ด, ์™œ ํ•„์š”ํ•œ๊ฐ€์š”?

0๏ธโƒฃ ๋ฏธ๋“ค์›จ์–ด๋ž€ ๋ฌด์—‡์ธ๊ฐ€์š”? ๐Ÿง

๋ฏธ๋“ค์›จ์–ด๋Š” ํด๋ผ์ด์–ธํŠธ์˜ ์š”์ฒญ(Request)์ด ์„œ๋ฒ„์— ๋„๋‹ฌํ•˜๊ธฐ ์ „์ด๋‚˜, ์„œ๋ฒ„์˜ ์‘๋‹ต(Response)์ด ํด๋ผ์ด์–ธํŠธ์— ์ „๋‹ฌ๋˜๊ธฐ ์ „์— ์ค‘๊ฐ„์—์„œ ํŠน์ • ์ž‘์—…์„ ์ฒ˜๋ฆฌํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ ์˜๋ฏธํ•ด์š”. ๋งˆ์น˜ ๊ณตํ•ญ์˜ ๋ณด์•ˆ ๊ฒ€์‚ฌ๋Œ€๋‚˜ ์„ธ๊ด€์ฒ˜๋Ÿผ, ์š”์ฒญ๊ณผ ์‘๋‹ต ํ๋ฆ„ ์‚ฌ์ด์— ๋ผ์–ด๋“ค์–ด ํ•„์š”ํ•œ ๊ฒ€์ฆ, ๋ณ€ํ™˜, ๋ผ์šฐํŒ… ๋“ฑ์˜ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๋Š” ๊ฒƒ์ด์ฃ .

Next.js ๋ฏธ๋“ค์›จ์–ด๋Š” ์ด๋Ÿฌํ•œ ์—ญํ• ์„ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์—ฃ์ง€(Edge) ํ™˜๊ฒฝ์—์„œ ์ˆ˜ํ–‰ํ•˜์—ฌ, ๋งค์šฐ ๋น ๋ฅด๊ณ  ํšจ์œจ์ ์ธ ์š”์ฒญ ์ฒ˜๋ฆฌ๋ฅผ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•ด์š”. ๋•๋ถ„์— ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ๊ฐœ์„ ํ•˜๊ณ , ์„œ๋ฒ„ ๋ถ€ํ•˜๋ฅผ ์ค„์ด๋Š” ๋ฐ ํฌ๊ฒŒ ๊ธฐ์—ฌํ•  ์ˆ˜ ์žˆ์–ด์š”.

1๏ธโƒฃ App Router ์‹œ๋Œ€์˜ ๋ฏธ๋“ค์›จ์–ด ๐Ÿž๏ธ

Next.js 13๋ถ€ํ„ฐ ๋„์ž…๋œ App Router๋Š” ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ(Server Components)์™€ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ(Client Components)์˜ ๊ตฌ๋ถ„์„ ํ†ตํ•ด ์ƒˆ๋กœ์šด ํŒจ๋Ÿฌ๋‹ค์ž„์„ ์ œ์‹œํ–ˆ์ฃ . ์ด ํ™˜๊ฒฝ์—์„œ ๋ฏธ๋“ค์›จ์–ด๋Š” ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋ณด๋‹ค ๋จผ์ € ์‹คํ–‰๋˜์–ด, ์š”์ฒญ์— ๋Œ€ํ•œ ์ดˆ๊ธฐ ํŒ๋‹จ๊ณผ ์ฒ˜๋ฆฌ๋ฅผ ๋‹ด๋‹นํ•ด์š”.

์˜ˆ๋ฅผ ๋“ค์–ด, ํŠน์ • ํŽ˜์ด์ง€์— ์ ‘๊ทผํ•˜๊ธฐ ์ „์— ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธํ–ˆ๋Š”์ง€ ํ™•์ธํ•˜๊ฑฐ๋‚˜, ์‚ฌ์šฉ์ž์˜ ์–ธ์–ด ์„ค์ •์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰์…˜ํ•ด์•ผ ํ•  ๋•Œ ๋ฏธ๋“ค์›จ์–ด๊ฐ€ ๋น›์„ ๋ฐœํ•ด์š”. ๋ชจ๋“  ์š”์ฒญ์— ๋Œ€ํ•ด ๋ฐ˜๋ณต์ ์œผ๋กœ ์ˆ˜ํ–‰๋˜์–ด์•ผ ํ•˜๋Š” ๊ณตํ†ต ๋กœ์ง์„ ํ•œ ๊ณณ์— ๋ชจ์•„ ๊ด€๋ฆฌํ•จ์œผ๋กœ์จ ์ฝ”๋“œ์˜ ์‘์ง‘๋„๋ฅผ ๋†’์ด๊ณ  ์œ ์ง€๋ณด์ˆ˜์„ฑ์„ ํ–ฅ์ƒ์‹œํ‚ฌ ์ˆ˜ ์žˆ๋‹ต๋‹ˆ๋‹ค.

๐Ÿ› ๏ธ ๋ฏธ๋“ค์›จ์–ด ๊ตฌ์„ฑ ๋ฐ ๊ธฐ๋ณธ ํ™œ์šฉ๋ฒ•

0๏ธโƒฃ ๋ฏธ๋“ค์›จ์–ด ํŒŒ์ผ ์ƒ์„ฑ ๐Ÿ“

Next.js์—์„œ ๋ฏธ๋“ค์›จ์–ด๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ํ”„๋กœ์ ํŠธ์˜ ๋ฃจํŠธ ๋””๋ ‰ํ„ฐ๋ฆฌ์— middleware.ts (๋˜๋Š” middleware.js) ํŒŒ์ผ์„ ์ƒ์„ฑํ•ด์•ผ ํ•ด์š”. ์ด ํŒŒ์ผ์€ Next.js๊ฐ€ ์ž๋™์œผ๋กœ ๊ฐ์ง€ํ•˜๊ณ  ๋ฏธ๋“ค์›จ์–ด๋กœ ๋“ฑ๋กํ•œ๋‹ต๋‹ˆ๋‹ค.

๊ธฐ๋ณธ์ ์ธ ๋ฏธ๋“ค์›จ์–ด์˜ ํ˜•ํƒœ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์•„์š”.

import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; export function middleware(request: NextRequest) { // ์š”์ฒญ URL์„ ์ฝ˜์†”์— ์ถœ๋ ฅํ•ด์š”. console.log('Request URL:', request.url); // ๋ชจ๋“  ์š”์ฒญ์„ ๊ทธ๋Œ€๋กœ ํ†ต๊ณผ์‹œ์ผœ์š”. return NextResponse.next(); } // ๋ฏธ๋“ค์›จ์–ด๋ฅผ ํŠน์ • ๊ฒฝ๋กœ์—๋งŒ ์ ์šฉํ•˜๋ ค๋ฉด matcher ์„ค์ •์„ ์‚ฌ์šฉํ•ด์š”. // ์—ฌ๊ธฐ์— ์„ค์ •๋œ ๊ฒฝ๋กœ์—๋งŒ ๋ฏธ๋“ค์›จ์–ด๊ฐ€ ์‹คํ–‰๋œ๋‹ต๋‹ˆ๋‹ค. export const config = { matcher: [ /* * Match all request paths except for the ones starting with: * - api (API routes) * - _next/static (static files) * - _next/image (image optimization files) * - favicon.ico (favicon file) */ '/((?!api|_next/static|_next/image|favicon.ico).*)', ], };
์œ ์šฉํ•œ ํŒ

NextRequest์™€ NextResponse๋Š” Next.js๊ฐ€ ์ œ๊ณตํ•˜๋Š” ํ™•์žฅ๋œ Request/Response ๊ฐ์ฒด๋กœ, HTTP ํ—ค๋”, ์ฟ ํ‚ค, URL ์กฐ์ž‘ ๋“ฑ ๋ฏธ๋“ค์›จ์–ด์—์„œ ํ•„์š”ํ•œ ๋‹ค์–‘ํ•œ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ด์š”.
์ผ๋ฐ˜์ ์ธ ์›น ํ‘œ์ค€ Request์™€ Response ๊ฐ์ฒด๋ณด๋‹ค ๋” ๋งŽ์€ ํŽธ์˜ ๊ธฐ๋Šฅ์„ ๊ฐ€์ง€๊ณ  ์žˆ์–ด ๋ฏธ๋“ค์›จ์–ด ๊ฐœ๋ฐœ์— ๋งค์šฐ ์œ ์šฉํ•˜๋‹ต๋‹ˆ๋‹ค.

1๏ธโƒฃ ๊ธฐ๋ณธ ๋ฏธ๋“ค์›จ์–ด ์„ค์ •: Matcher ๐ŸŽฏ

๋ชจ๋“  ์š”์ฒญ์— ๋ฏธ๋“ค์›จ์–ด๋ฅผ ์ ์šฉํ•˜๋Š” ๊ฒƒ์€ ๋น„ํšจ์œจ์ ์ผ ์ˆ˜ ์žˆ์–ด์š”. config.matcher๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ฏธ๋“ค์›จ์–ด๋ฅผ ํŠน์ • ๊ฒฝ๋กœ์—๋งŒ ์ ์šฉํ•˜์—ฌ ๋ถˆํ•„์š”ํ•œ ์‹คํ–‰์„ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ๋‹ต๋‹ˆ๋‹ค.
์œ„ ์˜ˆ์‹œ์˜ matcher๋Š” API ๋ผ์šฐํŠธ, Next.js ๋‚ด๋ถ€ ํŒŒ์ผ, ํŒŒ๋น„์ฝ˜ ๋“ฑ์„ ์ œ์™ธํ•œ ๋ชจ๋“  ๊ฒฝ๋กœ์— ๋ฏธ๋“ค์›จ์–ด๋ฅผ ์ ์šฉํ•˜๋„๋ก ์„ค์ •๋˜์–ด ์žˆ์–ด์š”. ์ •๊ทœ์‹์„ ์‚ฌ์šฉํ•˜์—ฌ ๋งค์šฐ ์œ ์—ฐํ•˜๊ฒŒ ๊ฒฝ๋กœ๋ฅผ ์ง€์ •ํ•  ์ˆ˜ ์žˆ์–ด์š”.

์ •๋ณด

matcher๋Š” ์ •์ (static)์œผ๋กœ ๋ถ„์„๋˜๊ธฐ ๋•Œ๋ฌธ์— ๋Ÿฐํƒ€์ž„์— ๋™์ ์œผ๋กœ ๋ณ€๊ฒฝ๋  ์ˆ˜ ์—†์–ด์š”. ์ฆ‰, ์กฐ๊ฑด๋ถ€ ๋กœ์ง์„ ์ด์šฉํ•ด matcher ๋ฐฐ์—ด์„ ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒƒ์€ ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ค๋Š” ์ ์„ ๊ธฐ์–ตํ•ด ์ฃผ์„ธ์š”.

2๏ธโƒฃ Next.js 14.1+์—์„œ NextResponse ํ™œ์šฉ โœจ

Next.js 14.1 ๋ฒ„์ „๋ถ€ํ„ฐ๋Š” NextResponse ๊ฐ์ฒด๋ฅผ ํ†ตํ•œ ๋‹ค์–‘ํ•œ ์‘๋‹ต ์กฐ์ž‘ ๊ธฐ๋Šฅ์ด ๋”์šฑ ๊ฐ•ํ™”๋˜์—ˆ์–ด์š”. ๋ฆฌ๋‹ค์ด๋ ‰์…˜, ๋ฆฌ๋ผ์ดํŠธ, ํ—ค๋” ์„ค์ •, ์ฟ ํ‚ค ์„ค์ • ๋“ฑ์„ ๋ฏธ๋“ค์›จ์–ด์—์„œ ํšจ์œจ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ต๋‹ˆ๋‹ค.

import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; export function middleware(request: NextRequest) { const url = request.nextUrl; // ํŠน์ • ๊ฒฝ๋กœ๋กœ ๋ฆฌ๋‹ค์ด๋ ‰์…˜ํ•˜๋Š” ์˜ˆ์‹œ if (url.pathname === '/old-page') { // /old-page๋กœ ์ ‘๊ทผํ•˜๋ฉด /new-page๋กœ ์ด๋™์‹œ์ผœ์š”. return NextResponse.redirect(new URL('/new-page', request.url)); } // ํŠน์ • ๊ฒฝ๋กœ๋กœ ๋ฆฌ๋ผ์ดํŠธ(rewrite)ํ•˜๋Š” ์˜ˆ์‹œ // URL์€ ๋ณ€๊ฒฝ๋˜์ง€ ์•Š์ง€๋งŒ, ์‹ค์ œ ๋ณด์—ฌ์ฃผ๋Š” ์ฝ˜ํ…์ธ ๋Š” /another-page์˜ ๊ฒƒ์ด ๋ผ์š”. if (url.pathname === '/some-internal-path') { return NextResponse.rewrite(new URL('/another-page', request.url)); } // ์š”์ฒญ ํ—ค๋”๋ฅผ ์กฐ์ž‘ํ•˜๋Š” ์˜ˆ์‹œ const response = NextResponse.next(); response.headers.set('X-Custom-Header', 'Hello from Middleware'); return response; } export const config = { matcher: ['/old-page', '/some-internal-path', '/any-other-path'], };

๐Ÿš€ ์‹ค์ „ ๋ฏธ๋“ค์›จ์–ด ํ™œ์šฉ ์˜ˆ์ œ

์ด์ œ ์‹ค์ œ ์‹œ๋‚˜๋ฆฌ์˜ค์—์„œ ๋ฏธ๋“ค์›จ์–ด๊ฐ€ ์–ด๋–ป๊ฒŒ ํ™œ์šฉ๋  ์ˆ˜ ์žˆ๋Š”์ง€ ๊ตฌ์ฒด์ ์ธ ์˜ˆ์ œ๋“ค์„ ์‚ดํŽด๋ณผ๊นŒ์š”?

0๏ธโƒฃ ์‚ฌ์šฉ์ž ์ธ์ฆ/์ธ๊ฐ€ ์ฒ˜๋ฆฌ ๐Ÿ”

๊ฐ€์žฅ ํ”ํ•˜๊ฒŒ ์‚ฌ์šฉ๋˜๋Š” ๋ฏธ๋“ค์›จ์–ด์˜ ์—ญํ•  ์ค‘ ํ•˜๋‚˜๋Š” ์‚ฌ์šฉ์ž ์ธ์ฆ(Authentication)๊ณผ ์ธ๊ฐ€(Authorization) ์ฒ˜๋ฆฌ์˜ˆ์š”. ๋กœ๊ทธ์ธํ•˜์ง€ ์•Š์€ ์‚ฌ์šฉ์ž๊ฐ€ ํŠน์ • ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€์— ์ ‘๊ทผํ•˜๋Š” ๊ฒƒ์„ ๋ง‰๊ณ  ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰์…˜ํ•˜๋Š” ๋กœ์ง์„ ๋ฏธ๋“ค์›จ์–ด์—์„œ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด์š”.

import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; export function middleware(request: NextRequest) { const isAuthenticated = request.cookies.has('session_token'); // ์„ธ์…˜ ํ† ํฐ ์œ ๋ฌด๋กœ ์ธ์ฆ ์—ฌ๋ถ€ ํŒ๋‹จ const url = request.nextUrl; // ๋ณดํ˜ธ๋œ ๊ฒฝ๋กœ ๋ชฉ๋ก์„ ์ •์˜ํ•ด์š”. const protectedPaths = ['/dashboard', '/admin', '/profile']; // ํ˜„์žฌ ๊ฒฝ๋กœ๊ฐ€ ๋ณดํ˜ธ๋œ ๊ฒฝ๋กœ ์ค‘ ํ•˜๋‚˜์ธ์ง€ ํ™•์ธํ•ด์š”. if (protectedPaths.some(path => url.pathname.startsWith(path))) { // ์ธ์ฆ๋˜์ง€ ์•Š์€ ์‚ฌ์šฉ์ž๋ผ๋ฉด ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰์…˜ํ•ด์š”. if (!isAuthenticated) { const loginUrl = new URL('/login', url.origin); loginUrl.searchParams.set('redirect', url.pathname + url.search); return NextResponse.redirect(loginUrl); } // ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž๋ผ๋ฉด ๋‹ค์Œ ๋‹จ๊ณ„๋กœ ์ง„ํ–‰ํ•ด์š”. return NextResponse.next(); } // ๊ทธ ์™ธ์˜ ๊ฒฝ๋กœ๋Š” ๋ชจ๋‘ ํ†ต๊ณผ์‹œ์ผœ์š”. return NextResponse.next(); } export const config = { matcher: ['/((?!api|_next/static|_next/image|favicon.ico|login).*)'], // ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋Š” ๋ฏธ๋“ค์›จ์–ด์—์„œ ์ œ์™ธํ•ด์š”. };
๊ฒฝ๊ณ 

์‹ค์ œ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” ์„ธ์…˜ ํ† ํฐ์˜ ์œ ํšจ์„ฑ์„ ์„œ๋ฒ„์—์„œ ๊ฒ€์ฆํ•˜๋Š” ๊ณผ์ •์ด ํ•„์š”ํ•ด์š”. ๋ฏธ๋“ค์›จ์–ด์—์„œ๋Š” ๊ฐ„๋‹จํ•œ ํ† ํฐ ์œ ๋ฌด ํ™•์ธ์œผ๋กœ ์˜ˆ์‹œ๋ฅผ ๋“ค์—ˆ์ง€๋งŒ, ๋ณด์•ˆ ๊ฐ•ํ™”๋ฅผ ์œ„ํ•ด JWT ๊ฒ€์ฆ ๋“ฑ์˜ ๋กœ์ง์„ ์ถ”๊ฐ€ํ•ด์•ผ ํ•œ๋‹ต๋‹ˆ๋‹ค.

1๏ธโƒฃ ๊ตญ์ œํ™”(i18n) ๋ผ์šฐํŒ… ๐ŸŒ

๋‹ค๊ตญ์–ด ์›น์‚ฌ์ดํŠธ๋ฅผ ๊ตฌ์ถ•ํ•  ๋•Œ, ์‚ฌ์šฉ์ž์˜ ์„ ํ˜ธ ์–ธ์–ด๋‚˜ ๋ธŒ๋ผ์šฐ์ € ์„ค์ •์— ๋”ฐ๋ผ ์ ์ ˆํ•œ ์–ธ์–ด ๋ฒ„์ „์˜ ํŽ˜์ด์ง€๋กœ ๋ผ์šฐํŒ…ํ•˜๋Š” ๊ฒƒ๋„ ๋ฏธ๋“ค์›จ์–ด์˜ ๊ฐ•๋ ฅํ•œ ํ™œ์šฉ ์‚ฌ๋ก€์˜ˆ์š”. Next.js 13+ App Router์—์„œ๋Š” i18n ์„ค์ •์„ next.config.js์—์„œ ์ œ๊ฑฐํ•˜๊ณ  ๋ฏธ๋“ค์›จ์–ด์—์„œ ์ง์ ‘ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ์‹์„ ๊ถŒ์žฅํ•˜๊ณ  ์žˆ์–ด์š”.

import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; const PUBLIC_FILE_REGEX = /\.(.*)$/; const i18n = { defaultLocale: 'en', locales: ['en', 'ko', 'ja'], }; function getLocale(request: NextRequest): string | undefined { // 1. ์ฟ ํ‚ค์—์„œ ์–ธ์–ด ์„ค์ •์„ ํ™•์ธํ•ด์š”. const cookieLocale = request.cookies.get('NEXT_LOCALE')?.value; if (cookieLocale && i18n.locales.includes(cookieLocale)) { return cookieLocale; } // 2. Accept-Language ํ—ค๋”์—์„œ ์–ธ์–ด ์„ค์ •์„ ํ™•์ธํ•ด์š”. const acceptLanguageHeader = request.headers.get('Accept-Language'); if (acceptLanguageHeader) { const preferredLocale = acceptLanguageHeader .split(',') .map(lang => lang.split(';')[0].trim()) .find(lang => i18n.locales.includes(lang)); if (preferredLocale) return preferredLocale; } // 3. ๊ธฐ๋ณธ ์–ธ์–ด๋ฅผ ๋ฐ˜ํ™˜ํ•ด์š”. return i18n.defaultLocale; } export function middleware(request: NextRequest) { const pathname = request.nextUrl.pathname; // ์ •์  ํŒŒ์ผ, Next.js ๋‚ด๋ถ€ ๊ฒฝ๋กœ, API ๊ฒฝ๋กœ ๋“ฑ์€ ๋ฏธ๋“ค์›จ์–ด์—์„œ ์ œ์™ธํ•ด์š”. if ( pathname.startsWith('/_next') || pathname.startsWith('/api') || pathname.startsWith('/static') || PUBLIC_FILE_REGEX.test(pathname) ) { return NextResponse.next(); } // ํ˜„์žฌ ๊ฒฝ๋กœ์— ์–ธ์–ด ํ”„๋ฆฌํ”ฝ์Šค๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ const pathnameHasLocale = i18n.locales.some( (locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}` ); if (!pathnameHasLocale) { const locale = getLocale(request); // ์‚ฌ์šฉ์ž์˜ ์„ ํ˜ธ ์–ธ์–ด์— ๋”ฐ๋ผ ๋ฆฌ๋‹ค์ด๋ ‰์…˜ํ•ด์š”. const newUrl = new URL(`/${locale}${pathname === '/' ? '' : pathname}`, request.url); return NextResponse.redirect(newUrl); } // ์ด๋ฏธ ์–ธ์–ด ํ”„๋ฆฌํ”ฝ์Šค๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ, ๊ทธ๋Œ€๋กœ ํ†ต๊ณผ์‹œ์ผœ์š”. return NextResponse.next(); } export const config = { matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], };
์ •๋ณด

์ด ์˜ˆ์ œ๋Š” URL์— /en, /ko์™€ ๊ฐ™์€ ์–ธ์–ด ํ”„๋ฆฌํ”ฝ์Šค๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๋ฐฉ์‹์ด์—์š”. ์‹ค์ œ ๊ตฌํ˜„ ์‹œ์—๋Š” i18next๋‚˜ react-i18next ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜์—ฌ ๋ฒˆ์—ญ๋œ ์ฝ˜ํ…์ธ ๋ฅผ ์ œ๊ณตํ•ด์•ผ ํ•œ๋‹ต๋‹ˆ๋‹ค.

2๏ธโƒฃ A/B ํ…Œ์ŠคํŠธ ๋ฐ Feature Toggling ๐Ÿงช

๋ฏธ๋“ค์›จ์–ด๋Š” ํŠน์ • ์‚ฌ์šฉ์ž ๊ทธ๋ฃน์—๊ฒŒ๋งŒ ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ์„ ๋…ธ์ถœํ•˜๊ฑฐ๋‚˜, ๋‹ค๋ฅธ ๋ฒ„์ „์˜ UI๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” A/B ํ…Œ์ŠคํŠธ๋‚˜ ๊ธฐ๋Šฅ ํ† ๊ธ€(Feature Toggling)์—๋„ ํ™œ์šฉ๋  ์ˆ˜ ์žˆ์–ด์š”. ์‚ฌ์šฉ์ž ID, ์ฟ ํ‚ค, ๋˜๋Š” ์š”์ฒญ ํ—ค๋” ๋“ฑ์˜ ์ •๋ณด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋™์ ์œผ๋กœ ๋ผ์šฐํŒ…์„ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋‹ต๋‹ˆ๋‹ค.

import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; export function middleware(request: NextRequest) { const url = request.nextUrl; // A/B ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•œ ์ฟ ํ‚ค๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธํ•ด์š”. let abTestGroup = request.cookies.get('ab-test-group')?.value; if (!abTestGroup) { // ์ฟ ํ‚ค๊ฐ€ ์—†๋‹ค๋ฉด, ๋žœ๋ค์œผ๋กœ A ๋˜๋Š” B ๊ทธ๋ฃน์„ ํ• ๋‹นํ•ด์š”. abTestGroup = Math.random() < 0.5 ? 'A' : 'B'; const response = NextResponse.next(); // ํ• ๋‹น๋œ ๊ทธ๋ฃน์„ ์ฟ ํ‚ค์— ์ €์žฅํ•˜์—ฌ ๋‹ค์Œ ์š”์ฒญ์—๋„ ์œ ์ง€๋˜๋„๋ก ํ•ด์š”. response.cookies.set('ab-test-group', abTestGroup, { path: '/' }); // ์ค‘์š”ํ•œ ์ : ์ฟ ํ‚ค๋ฅผ ์„ค์ •ํ•œ ํ›„์—๋Š” ์‘๋‹ต ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•ด์š”. return response; } // 'feature-x' ๊ฒฝ๋กœ์— ๋Œ€ํ•œ A/B ํ…Œ์ŠคํŠธ๋ฅผ ์ˆ˜ํ–‰ํ•ด์š”. if (url.pathname === '/feature-x') { if (abTestGroup === 'B') { // B ๊ทธ๋ฃน ์‚ฌ์šฉ์ž์—๊ฒŒ๋Š” ๋‹ค๋ฅธ ๋ฒ„์ „์˜ ํŽ˜์ด์ง€๋ฅผ ๋ณด์—ฌ์ค˜์š”. return NextResponse.rewrite(new URL('/feature-x-variant-b', request.url)); } } return NextResponse.next(); } export const config = { matcher: ['/feature-x'], };

๐Ÿ’ก ๋ฏธ๋“ค์›จ์–ด ๊ฐœ๋ฐœ ํŒ๊ณผ ์ฃผ์˜์‚ฌํ•ญ

0๏ธโƒฃ ๋ฏธ๋“ค์›จ์–ด ์‹คํ–‰ ํ™˜๊ฒฝ ์ดํ•ด ๐Ÿง 

Next.js ๋ฏธ๋“ค์›จ์–ด๋Š” Node.js ๋Ÿฐํƒ€์ž„์ด ์•„๋‹Œ, Vercel Edge Runtime๊ณผ ๊ฐ™์€ **์—ฃ์ง€ ๋Ÿฐํƒ€์ž„(Edge Runtime)**์—์„œ ์‹คํ–‰๋ผ์š”. ์ด๋Š” Node.js API (์˜ˆ: fs, process.env์˜ ๋ชจ๋“  ๋ณ€์ˆ˜)๋ฅผ ์ง์ ‘ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋‹ค๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•ด์š”. ํ™˜๊ฒฝ ๋ณ€์ˆ˜(process.env.MY_VARIABLE)๋Š” ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜์ง€๋งŒ, ํŒŒ์ผ ์‹œ์Šคํ…œ ์ ‘๊ทผ ๊ฐ™์€ Node.js ํŠน์ • ๊ธฐ๋Šฅ์€ ์ œํ•œ๋ผ์š”.
๋”ฐ๋ผ์„œ ๋ฏธ๋“ค์›จ์–ด ๋กœ์ง์„ ์ž‘์„ฑํ•  ๋•Œ๋Š” ์ด๋Ÿฌํ•œ ์ œ์•ฝ์„ ์ธ์ง€ํ•˜๊ณ , ๊ฐ€๋ฒผ์šฐ๋ฉด์„œ๋„ ํšจ์œจ์ ์ธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ด์š”.

1๏ธโƒฃ ์„ฑ๋Šฅ ์ตœ์ ํ™”์™€ ์บ์‹ฑ โšก

๋ฏธ๋“ค์›จ์–ด๋Š” ๋ชจ๋“  ์š”์ฒญ์— ๋Œ€ํ•ด ์‹คํ–‰๋  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ์„ฑ๋Šฅ์— ๋งค์šฐ ํฐ ์˜ํ–ฅ์„ ์ค„ ์ˆ˜ ์žˆ์–ด์š”. ๋ถˆํ•„์š”ํ•œ ์—ฐ์‚ฐ์„ ์ตœ์†Œํ™”ํ•˜๊ณ , ๊ฐ€๋Šฅํ•˜๋‹ค๋ฉด ์บ์‹ฑ ์ „๋žต์„ ๊ณ ๋ คํ•ด์•ผ ํ•ด์š”.
์˜ˆ๋ฅผ ๋“ค์–ด, ์‚ฌ์šฉ์ž ์ธ์ฆ ์ •๋ณด๊ฐ€ ์ž์ฃผ ๋ณ€๊ฒฝ๋˜์ง€ ์•Š๋Š”๋‹ค๋ฉด, ์„ธ์…˜ ํ† ํฐ์„ ์ฟ ํ‚ค์— ์ €์žฅํ•˜๊ณ  ๋ฏธ๋“ค์›จ์–ด์—์„œ๋Š” ์ฟ ํ‚ค ์œ ๋ฌด๋งŒ ๋น ๋ฅด๊ฒŒ ํ™•์ธํ•˜๋Š” ๊ฒƒ์ด ํšจ์œจ์ ์ด์—์š”. ๋ณต์žกํ•œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์กฐํšŒ๋‚˜ ์™ธ๋ถ€ API ํ˜ธ์ถœ์€ ๋ฏธ๋“ค์›จ์–ด์—์„œ ์ง€์–‘ํ•˜๊ณ , ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋‚˜ API ๋ผ์šฐํŠธ์—์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ์ข‹๋‹ต๋‹ˆ๋‹ค.

2๏ธโƒฃ ์—๋Ÿฌ ์ฒ˜๋ฆฌ ์ „๋žต ๐Ÿšจ

๋ฏธ๋“ค์›จ์–ด์—์„œ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ์‚ฌ์šฉ์ž์—๊ฒŒ ๋นˆ ํŽ˜์ด์ง€๋‚˜ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๊ฐ€ ๋…ธ์ถœ๋  ์ˆ˜ ์žˆ์–ด์š”. ๋ฏธ๋“ค์›จ์–ด ๋‚ด๋ถ€์—์„œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ์ž ์žฌ์ ์ธ ์—๋Ÿฌ๋ฅผ ์˜ˆ์ธกํ•˜๊ณ , ์ ์ ˆํ•œ ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•ด์•ผ ํ•ด์š”.
์˜ˆ๋ฅผ ๋“ค์–ด, ์™ธ๋ถ€ ์„œ๋น„์Šค ํ˜ธ์ถœ์— ์‹คํŒจํ–ˆ์„ ๋•Œ ๋Œ€์ฒด ๋กœ์ง์„ ์ˆ˜ํ–‰ํ•˜๊ฑฐ๋‚˜, ํŠน์ • ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ์‚ฌ์šฉ์ž์—๊ฒŒ ์นœํ™”์ ์ธ ์—๋Ÿฌ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰์…˜ํ•˜๋Š” ๋“ฑ์˜ ์ „๋žต์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š”.

import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; export function middleware(request: NextRequest) { try { // ๋ฏธ๋“ค์›จ์–ด ๋กœ์ง // ... return NextResponse.next(); } catch (error) { console.error('Middleware error:', error); return NextResponse.redirect(new URL('/error', request.url)); // ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ์—๋Ÿฌ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰์…˜ } } export const config = { matcher: ['/protected-path'], };

๐Ÿ“ ์ •๋ฆฌํ•˜๋ฉฐ

0๏ธโƒฃ ํ•ต์‹ฌ ์š”์•ฝ ๐ŸŽฏ

์˜ค๋Š˜์€ Next.js App Router ํ™˜๊ฒฝ์—์„œ ๋ฏธ๋“ค์›จ์–ด๋ฅผ ํ™œ์šฉํ•˜๋Š” ๋‹ค์–‘ํ•œ ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณด์•˜์–ด์š”. ๋ฏธ๋“ค์›จ์–ด๋Š” ์š”์ฒญ ์ฒ˜๋ฆฌ ํ๋ฆ„์˜ '๊ด€๋ฌธ' ์—ญํ• ์„ ํ•˜๋ฉฐ, ์‚ฌ์šฉ์ž ์ธ์ฆ, ๊ตญ์ œํ™”, A/B ํ…Œ์ŠคํŠธ ๋“ฑ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ „๋ฐ˜์— ๊ฑธ์ณ ๊ณตํ†ต์ ์œผ๋กœ ํ•„์š”ํ•œ ๋กœ์ง์„ ํšจ๊ณผ์ ์œผ๋กœ ์ค‘์•™ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค€๋‹ต๋‹ˆ๋‹ค.

  • ๋ฏธ๋“ค์›จ์–ด์˜ ์—ญํ• : ์š”์ฒญ์ด ์„œ๋ฒ„์— ๋„๋‹ฌํ•˜๊ธฐ ์ „/ํ›„์— ๋กœ์ง์„ ์‹คํ–‰ํ•˜์—ฌ ์š”์ฒญ ํ๋ฆ„์„ ์ œ์–ดํ•ด์š”.
  • ์ฃผ์š” ๊ธฐ๋Šฅ: NextResponse.redirect(), NextResponse.rewrite(), ํ—ค๋”/์ฟ ํ‚ค ์กฐ์ž‘ ๋“ฑ ๋‹ค์–‘ํ•œ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ด์š”.
  • ํ™œ์šฉ ์˜ˆ์‹œ: ์‚ฌ์šฉ์ž ์ธ์ฆ/์ธ๊ฐ€, ๊ตญ์ œํ™” ๋ผ์šฐํŒ…, A/B ํ…Œ์ŠคํŠธ ๋ฐ ๊ธฐ๋Šฅ ํ† ๊ธ€ ๋“ฑ์— ์œ ์šฉํ•ด์š”.
  • ์ฃผ์˜์‚ฌํ•ญ: ์—ฃ์ง€ ๋Ÿฐํƒ€์ž„ ํ™˜๊ฒฝ์˜ ์ œ์•ฝ์‚ฌํ•ญ์„ ์ดํ•ดํ•˜๊ณ , ์„ฑ๋Šฅ๊ณผ ์—๋Ÿฌ ์ฒ˜๋ฆฌ์— ์œ ์˜ํ•ด์•ผ ํ•ด์š”.

1๏ธโƒฃ ๋‹ค์Œ ๋‹จ๊ณ„๋กœ ๋‚˜์•„๊ฐ€์š” ๐Ÿš€

๋ฏธ๋“ค์›จ์–ด๋Š” Next.js ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๊ฐ•๋ ฅํ•œ ๊ธฐ๋Šฅ ์ค‘ ํ•˜๋‚˜์˜ˆ์š”. ์˜ค๋Š˜ ๋ฐฐ์šด ๋‚ด์šฉ์„ ๋ฐ”ํƒ•์œผ๋กœ ์—ฌ๋Ÿฌ๋ถ„์˜ ํ”„๋กœ์ ํŠธ์— ๋ฏธ๋“ค์›จ์–ด๋ฅผ ์ ์šฉํ•ด๋ณด๊ณ , ๋”์šฑ ๊ฒฌ๊ณ ํ•˜๊ณ  ํšจ์œจ์ ์ธ ์›น ์„œ๋น„์Šค๋ฅผ ๊ตฌ์ถ•ํ•ด ๋ณด์„ธ์š”.
ํŠนํžˆ, ๋ณด์•ˆ๊ณผ ๊ด€๋ จ๋œ ๋กœ์ง์ด๋‚˜ ์ „์—ญ์ ์œผ๋กœ ์ ์šฉ๋˜์–ด์•ผ ํ•˜๋Š” ๊ธฐ๋Šฅ์— ๋ฏธ๋“ค์›จ์–ด๋ฅผ ํ™œ์šฉํ•˜๋ฉด ๊ฐœ๋ฐœ ์ƒ์‚ฐ์„ฑ์„ ํฌ๊ฒŒ ๋†’์ผ ์ˆ˜ ์žˆ์„ ๊ฑฐ์˜ˆ์š”.
๊ถ๊ธˆํ•œ ์ ์ด ์žˆ๋‹ค๋ฉด ์–ธ์ œ๋“ ์ง€ ๋Œ“๊ธ€๋กœ ์งˆ๋ฌธํ•ด ์ฃผ์„ธ์š”. ๋‹ค์Œ์—๋„ ์œ ์ตํ•œ ์ •๋ณด๋กœ ์ฐพ์•„์˜ฌ๊ฒŒ์š”! ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ“ฎ ์ฐธ๊ณ 

์—ฐ๊ด€๋œ ํฌ์ŠคํŠธ