Middleware
๋ฏธ๋ค์จ์ด๋ฅผ ์ฌ์ฉํ๋ฉด ์์ฒญ์ด ์๋ฃ๋๊ธฐ ์ ์ ์ฝ๋๋ฅผ ์คํํ ์ ์์ต๋๋ค. ๊ทธ๋ฐ ๋ค์ ๋ค์ด์ค๋ ์์ฒญ์ ๋ฐ๋ผ ์๋ต์ ์ฌ์์ฑํ๊ฑฐ๋, ๋ฆฌ๋ค์ด๋ ํธํ๊ฑฐ๋, ์์ฒญ ๋๋ ์๋ต ํค๋๋ฅผ ์์ ํ๊ฑฐ๋, ์ง์ ์๋ตํจ์ผ๋ก์จ ์๋ต์ ์์ ํ ์ ์์ต๋๋ค.
์บ์๋ ์ฝํ ์ธ ์ ๊ฒฝ๋ก๊ฐ ์ผ์นํ๊ธฐ ์ ์ ๋ฏธ๋ค์จ์ด๊ฐ ์คํ๋ฉ๋๋ค. ์์ธํ ๋ด์ฉ์ ๊ฒฝ๋ก ๋งค์นญ์ ์ฐธ์กฐํ์ธ์.
1. Convention(๊ท์น)
ํ๋ก์ ํธ์ ๋ฃจํธ์์ middleware.ts(๋๋ .js) ํ์ผ์ ์ฌ์ฉํ์ฌ ๋ฏธ๋ค์จ์ด๋ฅผ ์ ์ํฉ๋๋ค.
์๋ฅผ ๋ค์ด, pages ๋๋ app๊ณผ ๊ฐ์ ๋ ๋ฒจ์ ์๊ฑฐ๋ ํด๋นํ๋ ๊ฒฝ์ฐ src ๋ด๋ถ์ ์ ์ํฉ๋๋ค.
2. ์์
1import { NextResponse } from 'next/server'2import type { NextRequest } from 'next/server'34// ๋ง์ฝ ๋ด๋ถ์์ `await`์ ์ฌ์ฉํ๋ค๋ฉด, ์ด ํจ์๋ `async`๋ก ํ์๋ ์ ์์ต๋๋ค.5export function middleware(request: NextRequest) {6return NextResponse.redirect(new URL('/home', request.url))7}89// ์๋์ "๊ฒฝ๋ก ์ผ์น"๋ฅผ ์ฐธ์กฐํ์ฌ ๋ ์์ธํ ์์๋ณด์ธ์.10export const config = {11matcher: '/about/:path*',12}
3. Matching Paths(๊ฒฝ๋ก ์ผ์น)
ํ๋ก์ ํธ์ ๋ชจ๋ ๊ฒฝ๋ก์ ๋ํด ๋ฏธ๋ค์จ์ด๊ฐ ํธ์ถ๋ฉ๋๋ค. ์คํ ์์๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค:
next.config.js์headersnext.config.js์redirects- ๋ฏธ๋ค์จ์ด (
rewrites,redirects๋ฑ) next.config.js์beforeFiles(rewrites)- ํ์ผ ์์คํ
๊ฒฝ๋ก (
public/,_next/static/,pages/,app/๋ฑ) next.config.js์afterFiles(rewrites)- ๋์ ๊ฒฝ๋ก (
/blog/[slug]) next.config.js์fallback(rewrites)
๋ฏธ๋ค์จ์ด๊ฐ ์คํ๋ ๊ฒฝ๋ก๋ฅผ ์ ์ํ๋ ๋ฐฉ๋ฒ์๋ ๋ ๊ฐ์ง๊ฐ ์์ต๋๋ค:
3.1 Matcher
matcher๋ฅผ ์ฌ์ฉํ๋ฉด ํน์ ๊ฒฝ๋ก์์ ์คํํ ๋ฏธ๋ค์จ์ด๋ฅผ ํํฐ๋งํ ์ ์์ต๋๋ค:
1export const config = {2matcher: '/about/:path*',3}
๋ฐฐ์ด ๋ฌธ๋ฒ์ ์ฌ์ฉํ์ฌ ๋จ์ผ ๊ฒฝ๋ก ๋๋ ์ฌ๋ฌ ๊ฒฝ๋ก๋ฅผ ์ผ์น์ํฌ ์ ์์ต๋๋ค:
1export const config = {2matcher: ['/about/:path*', '/dashboard/:path*'],3}
matcher ๊ตฌ์ฑ์ ๋ถ์ ์ ์ ๋ฐฉํ์(negative lookahead) ๋๋ ๋ฌธ์ ์ผ์น(character matching)์ ๊ฐ์ ์์ ํ ์ ๊ท์์ ์ง์ํฉ๋๋ค.
ํน์ ๊ฒฝ๋ก๋ฅผ ์ ์ธํ ๋ชจ๋ ๊ฒฝ๋ก์ ์ผ์นํ๋ ๋ถ์ ์ ์ ๋ฐฉํ์์ ์๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
1export const config = {2matcher: [3/*4* ๋ค์๊ณผ ๊ฐ์ด ์์ํ๋ ๊ฒฝ๋ก๋ฅผ ์ ์ธํ ๋ชจ๋ ์์ฒญ ๊ฒฝ๋ก์ ์ผ์น์ํต๋๋ค.5* - api (API ๊ฒฝ๋ก)6* - _next/static (์ ์ ํ์ผ)7* - _next/image (์ด๋ฏธ์ง ์ต์ ํ ํ์ผ)8* - favicon.ico (ํ๋น์ฝ ํ์ผ)9*/10'/((?!api|_next/static|_next/image|favicon.ico).*)',11],12}
๋๋ฝ๋ ๋ฐฐ์ด์ ์ฌ์ฉํ์ฌ ๋ฏธ๋ค์จ์ด๋ฅผ ๊ฑฐ์น ํ์๊ฐ ์๋ ํ๋ฆฌํ์น(next/link์์)๋ฅผ ๋ฌด์ํ ์๋ ์์ต๋๋ค:
1export const config = {2matcher: [3/*4* Match all request paths except for the ones starting with:5* - api (API routes)6* - _next/static (static files)7* - _next/image (image optimization files)8* - favicon.ico (favicon file)9*/10{11source: '/((?!api|_next/static|_next/image|favicon.ico).*)',12missing: [13{ type: 'header', key: 'next-router-prefetch' },14{ type: 'header', key: 'purpose', value: 'prefetch' },15],16},17],18}
์์๋๋ฉด ์ข์ต๋๋ค: ๋น๋ ์ ์ ์ ์ผ๋ก ๋ถ์ํ ์ ์๋๋ก
matcher๊ฐ์ ์์์ฌ์ผ ํฉ๋๋ค. ๋ณ์์ ๊ฐ์ ๋์ ๊ฐ์ ๋ฌด์๋ฉ๋๋ค.
matcher ๊ตฌ์ฑ:
-
๋ฐ๋์
/๋ก ์์ํด์ผ ํฉ๋๋ค. -
์ด๋ฆ ์๋ ๋งค๊ฐ๋ณ์๋ฅผ ํฌํจํ ์ ์์ต๋๋ค.
/about/:path๋/about/a์/about/b์ ์ผ์นํ์ง๋ง/about/a/c์๋ ์ผ์นํ์ง ์์ต๋๋ค.
-
์ด๋ฆ ์๋ ๋งค๊ฐ๋ณ์์๋ ์์ ์(์ฝ๋ก ์ผ๋ก ์์ํจ)๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค:
/about/:path*๋*๊ฐ 0๊ฐ ์ด์์ ์๋ฏธํ๊ธฐ ๋๋ฌธ์/about/a/b/c์ ์ผ์นํฉ๋๋ค.?๋ 0๊ฐ ๋๋ 1๊ฐ,+๋ 1๊ฐ ์ด์์ ์๋ฏธํฉ๋๋ค.
-
๊ดํธ๋ก ๋๋ฌ์ธ์ธ ์ ๊ท์์ ์ฌ์ฉํ ์ ์์ต๋๋ค.
/about/(.*)๋/about/:path*์ ๋์ผํ ์๋ฏธ๋ฅผ ๊ฐ์ง๋๋ค.
์์ธํ ๋ด์ฉ์ path-to-regexp ๋ฌธ์๋ฅผ ์ฐธ์กฐํ์ธ์.
์ฐธ๊ณ :
- ์ด์ ๋ฒ์ ๊ณผ์ ํธํ์ฑ์ ์ํด Next.js๋ ํญ์
/public์/public/index๋ก ๊ฐ์ฃผํฉ๋๋ค.- ๋ฐ๋ผ์
/public/:path์ ๊ฐ์ matcher๋ ์ผ์นํฉ๋๋ค.
3.2 Conditional Statements(์กฐ๊ฑด๋ฌธ)
1import { NextResponse } from 'next/server'2import type { NextRequest } from 'next/server'34export function middleware(request: NextRequest) {5if (request.nextUrl.pathname.startsWith('/about')) {6return NextResponse.rewrite(new URL('/about-2', request.url))7}89if (request.nextUrl.pathname.startsWith('/dashboard')) {10return NextResponse.rewrite(new URL('/dashboard/user', request.url))11}12}
4. NextResponse
NextResponse API๋ฅผ ์ฌ์ฉํ๋ฉด ๋ค์ ์์
์ ์ํํ ์ ์์ต๋๋ค.
- ๋ค์ด์ค๋ ์์ฒญ์ ๋ค๋ฅธ URL๋ก
๋ฆฌ๋ค์ด๋ ํธ - ์ฃผ์ด์ง URL์ ํ์ํ์ฌ ์๋ต
์ฌ์์ฑ - API ๊ฒฝ๋ก,
getServerSideProps, ๋ฐ์ฌ์์ฑ๋์์ ๋ํ ์์ฒญ ํค๋ ์ค์ - ์๋ต ์ฟ ํค ์ค์
- ์๋ต ํค๋ ์ค์
๋ฏธ๋ค์จ์ด์์ ์๋ต์ ์์ฑํ๋ ค๋ฉด ๋ค์ ์ค ํ๋๋ฅผ ์ํํ ์ ์์ต๋๋ค.
- ์๋ต์ ์์ฑํ๋ ๊ฒฝ๋ก(Page ๋๋ Edge API Route)๋ก
์ฌ์์ฑํฉ๋๋ค. - ์ง์
NextResponse๋ฅผ ๋ฐํํฉ๋๋ค. ์๋ต ์์ฑํ๊ธฐ๋ฅผ ์ฐธ์กฐํ์ธ์.
5. Cookies ์ฌ์ฉํ๊ธฐ
์ฟ ํค๋ ์ผ๋ฐ์ ์ธ ํค๋์
๋๋ค. Request์์๋ Cookie ํค๋์ ์ ์ฅ๋๊ณ , Response์์๋ Set-Cookie ํค๋์ ์ ์ฅ๋ฉ๋๋ค.
Next.js๋ NextRequest์ NextResponse์ cookies ํ์ฅ์๋ฅผ ํตํด ์ด๋ฌํ ์ฟ ํค์ ์ฝ๊ฒ ์ ๊ทผํ๊ณ ์กฐ์ํ ์ ์๋ ํธ๋ฆฌํ ๋ฐฉ๋ฒ์ ์ ๊ณตํฉ๋๋ค.
- ๋ค์ด์ค๋ ์์ฒญ์ ๊ฒฝ์ฐ
cookies์๋get,getAll,set,delete์ฟ ํค ๋ฉ์๋๊ฐ ์ ๊ณต๋ฉ๋๋ค.has๋ก ์ฟ ํค์ ์กด์ฌ ์ฌ๋ถ๋ฅผ ํ์ธํ๊ฑฐ๋clear๋ก ๋ชจ๋ ์ฟ ํค๋ฅผ ์ ๊ฑฐํ ์ ์์ต๋๋ค.
- ๋๊ฐ๋ ์๋ต์ ๊ฒฝ์ฐ
cookies์๋get,getAll,set,delete๋ฉ์๋๊ฐ ์์ต๋๋ค.
1import { NextResponse } from 'next/server'2import type { NextRequest } from 'next/server'34export function middleware(request: NextRequest) {5// ๋ค์ด์ค๋ ์์ฒญ์ "Cookie: nextjs=fast" ํค๋๊ฐ ์๋ ๊ฒ์ผ๋ก ๊ฐ์ 6// `RequestCookies` API๋ฅผ ์ฌ์ฉํ์ฌ ์์ฒญ์์ ์ฟ ํค ๊ฐ์ ธ์ค๊ธฐ7let cookie = request.cookies.get('nextjs')8console.log(cookie) // => { name: 'nextjs', value: 'fast', Path: '/' }9const allCookies = request.cookies.getAll()10console.log(allCookies) // => [{ name: 'nextjs', value: 'fast' }]1112request.cookies.has('nextjs') // => true13request.cookies.delete('nextjs')14request.cookies.has('nextjs') // => false1516// `ResponseCookies` API๋ฅผ ์ฌ์ฉํ์ฌ ์๋ต์ ์ฟ ํค ์ค์ ํ๊ธฐ17const response = NextResponse.next()18response.cookies.set('vercel', 'fast')19response.cookies.set({20name: 'vercel',21value: 'fast',22path: '/',23})24cookie = response.cookies.get('vercel')25console.log(cookie) // => { name: 'vercel', value: 'fast', Path: '/' }26// ๋๊ฐ๋ ์๋ต์๋ `Set-Cookie: vercel=fast; path=/test` ํค๋๊ฐ ํฌํจ๋จ2728return response29}
6. Headers ์ค์ ํ๊ธฐ
NextResponse API๋ฅผ ์ฌ์ฉํ์ฌ ์์ฒญ๊ณผ ์๋ต ํค๋๋ฅผ ์ค์ ํ ์ ์์ต๋๋ค (์์ฒญ ํค๋ ์ค์ ์ Next.js v13.0.0 ์ด์์์ ์ฌ์ฉ ๊ฐ๋ฅํฉ๋๋ค).
1import { NextResponse } from 'next/server'2import type { NextRequest } from 'next/server'34export function middleware(request: NextRequest) {5// ์์ฒญ ํค๋๋ฅผ ๋ณต์ ํ๊ณ ์๋ก์ด ํค๋ `x-hello-from-middleware1`๋ฅผ ์ค์ 6const requestHeaders = new Headers(request.headers)7requestHeaders.set('x-hello-from-middleware1', 'hello')89// NextResponse.rewrite์์๋ ์์ฒญ ํค๋๋ฅผ ์ค์ ํ ์ ์์10const response = NextResponse.next({11request: {12// ์ ์์ฒญ ํค๋13headers: requestHeaders,14},15})1617// ์ ์๋ต ํค๋ `x-hello-from-middleware2` ์ค์ 18response.headers.set('x-hello-from-middleware2', 'hello')19return response20}
์ฐธ๊ณ : ๋ฐฑ์๋ ์น ์๋ฒ ๊ตฌ์ฑ์ ๋ฐ๋ผ 431 Request Header Fields Too Large ์ค๋ฅ๊ฐ ๋ฐ์ํ ์ ์์ผ๋ฏ๋ก ํฐ ํค๋๋ฅผ ์ค์ ํ๋ ๊ฒ์ ํผํ์ญ์์ค.
7. Producing a Response(์๋ต ์์ฑ)
๋ฏธ๋ค์จ์ด์์๋ Response ๋๋ NextResponse ์ธ์คํด์ค๋ฅผ ์ง์ ๋ฐํํ์ฌ ์๋ตํ ์ ์์ต๋๋ค.
(์ด ๊ธฐ๋ฅ์ Next.js v13.1.0๋ถํฐ ์ฌ์ฉ ๊ฐ๋ฅํฉ๋๋ค)
1import { NextRequest, NextResponse } from 'next/server'2import { isAuthenticated } from '@lib/auth'34// ๋ฏธ๋ค์จ์ด๋ฅผ `/api/`๋ก ์์ํ๋ ๊ฒฝ๋ก๋ก ์ ํ5export const config = {6matcher: '/api/:function*',7}89export function middleware(request: NextRequest) {10// ์์ฒญ์ ํ์ธํ๊ธฐ ์ํด ์ธ์ฆ ํจ์๋ฅผ ํธ์ถ11if (!isAuthenticated(request)) {12// ์ค๋ฅ ๋ฉ์์ง๋ฅผ ๋ํ๋ด๋ JSON์ผ๋ก ์๋ต13return new NextResponse(JSON.stringify({ success: false, message: 'authentication failed' }), {14status: 401,15headers: { 'content-type': 'application/json' },16})17}18}
7.1 waitUntil ๊ณผ NextFetchEvent
NextFetchEvent ๊ฐ์ฒด๋ ๋ค์ดํฐ๋ธ FetchEvent ๊ฐ์ฒด๋ฅผ ํ์ฅํ๋ฉฐ, waitUntil() ๋ฉ์๋๋ฅผ ํฌํจํฉ๋๋ค.
waitUntil() ๋ฉ์๋๋ ํ๋ก๋ฏธ์ค๋ฅผ ์ธ์๋ก ๋ฐ์ ํ๋ก๋ฏธ์ค๊ฐ ํ์ ๋ ๋๊น์ง ๋ฏธ๋ค์จ์ด์ ์๋ช
์ ์ฐ์ฅํฉ๋๋ค.
์ด ๋ฉ์๋๋ ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์์
์ ์ํํ ๋ ์ ์ฉํฉ๋๋ค.
1import { NextResponse } from 'next/server'2import type { NextFetchEvent, NextRequest } from 'next/server'34export function middleware(req: NextRequest, event: NextFetchEvent) {5event.waitUntil(6fetch('https://my-analytics-platform.com', {7method: 'POST',8body: JSON.stringify({ pathname: req.nextUrl.pathname }),9}),10)1112return NextResponse.next()13}
8. ๊ณ ๊ธ ๋ฏธ๋ค์จ์ด ํ๋๊ทธ
Next.js v13.1์์๋ ๊ณ ๊ธ ์ฌ์ฉ ์ฌ๋ก๋ฅผ ์ฒ๋ฆฌํ๊ธฐ ์ํด ๋ฏธ๋ค์จ์ด๋ฅผ ์ํ ๋ ๊ฐ์ ์ถ๊ฐ ํ๋๊ทธ์ธ skipMiddlewareUrlNormalize ๋ฐ skipTrailingSlashRedirect๊ฐ ๋์
๋์์ต๋๋ค.
skipTrailingSlashRedirect๋ ํํ ์ฌ๋์๋ฅผ ์ถ๊ฐํ๊ฑฐ๋ ์ ๊ฑฐํ๊ธฐ ์ํ Next.js ๋ฆฌ๋๋ ์
์ ๋นํ์ฑํํฉ๋๋ค.
์ด๋ ๊ฒ ํ๋ฉด ๋ฏธ๋ค์จ์ด ๋ด๋ถ์์ ์ฌ์ฉ์ ์ ์ ์ฒ๋ฆฌ๋ฅผ ํตํด ์ผ๋ถ ๊ฒฝ๋ก์๋ ํํ ์ฌ๋์๋ฅผ ์ ์งํ์ง๋ง,
๋ค๋ฅธ ๊ฒฝ๋ก์๋ ์ ์งํ์ง ์๋๋ก ํ ์ ์์ผ๋ฏ๋ก ์ฆ๋ถ ๋ง์ด๊ทธ๋ ์ด์
์ด ๋ ์ฌ์์ง๋๋ค.
1module.exports = {2skipTrailingSlashRedirect: true,3}
1const legacyPrefixes = ['/docs', '/blog']23export default async function middleware(req) {4const { pathname } = req.nextUrl56if (legacyPrefixes.some(prefix => pathname.startsWith(prefix))) {7return NextResponse.next()8}910// trailing slash ์ฒ๋ฆฌ ์ ์ฉ11if (!pathname.endsWith('/') && !pathname.match(/((?!\.well-known(?:\/.*)?)(?:[^/]+\/)*[^/]+\.\w+)/)) {12req.nextUrl.pathname += '/'13return NextResponse.redirect(req.nextUrl)14}15}
skipMiddlewareUrlNormalize์ Next.js์์ ์ง์ ๋ฐฉ๋ฌธ๊ณผ ํด๋ผ์ด์ธํธ ์ ํ์ ์ฒ๋ฆฌํ๊ธฐ ์ํด ์ํํ๋ URL ์ ๊ทํ๋ฅผ ๋นํ์ฑํํ๋ ๊ธฐ๋ฅ์
๋๋ค.
์ด๋ฅผ ํตํด ์๋์ URL์ ์ฌ์ฉํ์ฌ ์์ ํ ์ ์ด๊ฐ ํ์ํ ๊ณ ๊ธ ์ํฉ์ด ์๋ ๊ฒฝ์ฐ ์ด๋ฅผ ๊ฐ๋ฅํ๊ฒ ํฉ๋๋ค.
1module.exports = {2skipMiddlewareUrlNormalize: true,3}
1export default async function middleware(req) {2const { pathname } = req.nextUrl34// GET /_next/data/build-id/hello.json56console.log(pathname)7// ํ๋๊ทธ๊ฐ ์์ผ๋ฉด /_next/data/build-id/hello.json8// ํ๋๊ทธ๊ฐ ์์ผ๋ฉด /hello๋ก ์ ๊ทํ๋จ9}
9. Runtime
๋ฏธ๋ค์จ์ด๋ ํ์ฌ Edge ๋ฐํ์๋ง ์ง์ํฉ๋๋ค. Node.js ๋ฐํ์์ ์ฌ์ฉํ ์ ์์ต๋๋ค.
10. Version History
| ๋ฒ์ | ๋ณ๊ฒฝ ๋ด์ฉ |
|---|---|
v13.1.0 | ๊ณ ๊ธ ๋ฏธ๋ค์จ์ด ํ๋๊ทธ ์ถ๊ฐ |
v13.0.0 | ๋ฏธ๋ค์จ์ด์์ ์์ฒญ ํค๋, ์๋ต ํค๋๋ฅผ ์์ ํ๊ณ ์๋ต์ ์ ์กํ ์ ์๋๋ก ํจ |
v12.2.0 | ๋ฏธ๋ค์จ์ด๊ฐ ์์ ํ๋์์ผ๋ฉฐ ์ ๊ทธ๋ ์ด๋ ๊ฐ์ด๋๋ฅผ ์ฐธ์กฐํ์ธ์ |
v12.0.9 | Edge Runtime์์ ์ ๋ URL ๊ฐ์ ์ ์ฉ (PR) |
v12.0.0 | ๋ฏธ๋ค์จ์ด (Beta) ์ถ๊ฐ |