Middleware

Middleware를 사용하면 요청이 완료되기 전에 코드를 실행할 수 있습니다. 그런 다음, 들어오는 요청에 따라 응답을 재작성, 리디렉션, 요청 또는 응답 헤더 수정, 또는 직접 응답할 수 있습니다.

Middleware는 캐시된 콘텐츠와 라우트가 일치하기 전에 실행됩니다. 자세한 내용은 Matching Paths를 참조하세요.

Use Cases

애플리케이션에 Middleware를 통합하면 성능, 보안 및 사용자 경험이 크게 향상될 수 있습니다. Middleware가 특히 효과적인 일반적인 시나리오는 다음과 같습니다:

  • 인증 및 권한 부여: 사용자 신원을 확인하고 특정 페이지나 API 라우트에 접근하기 전에 세션 쿠키를 확인합니다.
  • 서버 측 리디렉션: 특정 조건(예: 로케일, 사용자 역할)에 따라 서버 수준에서 사용자를 리디렉션합니다.
  • 경로 재작성: 요청 속성에 따라 경로를 동적으로 재작성하여 A/B 테스트, 기능 출시 또는 레거시 경로를 지원합니다.
  • 봇 감지: 리소스를 보호하기 위해 봇 트래픽을 감지하고 차단합니다.
  • 로깅 및 분석: 페이지나 API가 처리되기 전에 요청 데이터를 캡처하고 분석합니다.
  • 기능 플래깅: 기능을 동적으로 활성화 또는 비활성화하여 원활한 기능 출시 또는 테스트를 진행합니다.

Middleware가 최적의 접근 방식이 아닌 상황을 인식하는 것도 중요합니다. 주의해야 할 몇 가지 시나리오는 다음과 같습니다:

  • 복잡한 데이터 가져오기 및 조작: Middleware는 직접적인 데이터 가져오기나 조작을 위해 설계되지 않았으며, 이는 Route Handlers 또는 서버 측 유틸리티 내에서 수행해야 합니다.
  • 무거운 계산 작업: Middleware는 가볍고 빠르게 응답해야 하며, 무거운 계산 작업이나 장기 실행 프로세스는 전용 Route Handlers 내에서 수행해야 합니다.
  • 광범위한 세션 관리: Middleware는 기본적인 세션 작업을 관리할 수 있지만, 광범위한 세션 관리는 전용 인증 서비스 또는 Route Handlers 내에서 관리해야 합니다.
  • 직접적인 데이터베이스 작업: Middleware 내에서 직접적인 데이터베이스 작업을 수행하는 것은 권장되지 않습니다. 데이터베이스 상호작용은 Route Handlers 또는 서버 측 유틸리티 내에서 수행해야 합니다.

Convention

프로젝트의 루트에 middleware.ts(또는 .js) 파일을 사용하여 Middleware를 정의합니다. 예를 들어, pages 또는 app과 같은 수준에 있거나 src 내에 있을 수 있습니다.

Note: 프로젝트당 하나의 middleware.ts 파일만 지원되지만, 여전히 모듈화된 방식으로 미들웨어 로직을 구성할 수 있습니다. 미들웨어 기능을 별도의 .ts 또는 .js 파일로 분리하고 주요 middleware.ts 파일에 가져올 수 있습니다. 이렇게 하면 경로별 미들웨어를 보다 깔끔하게 관리할 수 있으며, 중앙 집중식 제어를 위해 middleware.ts에 집계할 수 있습니다. 단일 미들웨어 파일을 강제함으로써 구성의 간소화, 잠재적 충돌 방지 및 여러 미들웨어 계층을 피하여 성능 최적화를 달성할 수 있습니다.

Example

middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
// 이 함수는 내부에서 `await`를 사용하는 경우 `async`로 표시될 수 있습니다
export function middleware(request: NextRequest) {
  return NextResponse.redirect(new URL('/home', request.url))
}
 
// 아래 "Matching Paths"를 참조하여 자세히 알아보세요
export const config = {
  matcher: '/about/:path*',
}
middleware.js
import { NextResponse } from 'next/server'
 
// 이 함수는 내부에서 `await`를 사용하는 경우 `async`로 표시될 수 있습니다
export function middleware(request) {
  return NextResponse.redirect(new URL('/home', request.url))
}
 
// 아래 "Matching Paths"를 참조하여 자세히 알아보세요
export const config = {
  matcher: '/about/:path*',
}

Matching Paths

Middleware는 프로젝트의 모든 라우트에 대해 호출됩니다. 따라서, 특정 라우트를 정확히 타겟팅하거나 제외하기 위해 매처를 사용하는 것이 중요합니다. 실행 순서는 다음과 같습니다:

  1. next.config.jsheaders
  2. next.config.jsredirects
  3. Middleware (rewrites, redirects 등)
  4. next.config.jsbeforeFiles (rewrites)
  5. 파일 시스템 라우트 (public/, _next/static/, pages/, app/ 등)
  6. next.config.jsafterFiles (rewrites)
  7. 동적 라우트 (/blog/[slug])
  8. next.config.jsfallback (rewrites)

Middleware가 실행될 경로를 정의하는 두 가지 방법이 있습니다:

  1. Custom matcher config
  2. Conditional statements

Matcher

matcher를 사용하여 특정 경로에서만 Middleware를 실행하도록 필터링할 수 있습니다.

middleware.js
export const config = {
  matcher: '/about/:path*',
}

단일 경로나 여러 경로를 배열 구문으로 매칭할 수 있습니다:

middleware.js
export const config = {
  matcher: ['/about/:path*', '/dashboard/:path*'],
}

matcher 구성은 전체 정규식을 허용하므로, 부정형 전방탐색이나 문자 매칭과 같은 정규식 매칭이 지원됩니다. 특정 경로를 제외하고 모든 경로를 매칭하는 부정형 전방탐색의 예는 다음과 같습니다:

middleware.js
export const config = {
  matcher: [
    /*
     * 다음으로 시작하는 경로를 제외한 모든 요청 경로를 매칭합니다:
     * - api (API 라우트)
     * - _next/static (정적 파일)
     * - _next/image (이미지 최적화 파일)
     * - favicon.ico (파비콘 파일)
     */
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
  ],
}

또한 missing 또는 has 배열 또는 두 가지 조합을 사용하여 특정 요청을 위해 Middleware를 우회할 수 있습니다:

middleware.js
export const config = {
  matcher: [
    /*
     * 다음으로 시작하는 경로를 제외한 모든 요청 경로를 매칭합니다:
     * - api (API 라우트)
     * - _next/static (정적 파일)
     * - _next/image (이미지 최적화 파일)
     * - favicon.ico (파비콘 파일)
     */
    {
      source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
      missing: [
        { type: 'header', key: 'next-router-prefetch' },
        { type: 'header', key: 'purpose', value: 'prefetch' },
      ],
    },
 
    {
      source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
      has: [
        { type: 'header', key: 'next-router-prefetch' },
        { type: 'header', key: 'purpose', value: 'prefetch' },
      ],
    },
 
    {
      source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
      has: [{ type: 'header', key: 'x-present' }],
      missing: [{ type: 'header', key: 'x-missing', value: 'prefetch' }],
    },
  ],
}

알아두면 좋은 점: matcher 값은 빌드 시 정적으로 분석될 수 있도록 상수여야 합니다. 변수와 같은 동적 값은 무시됩니다.

구성된 매처:

  1. /로 시작해야 합니다
  2. 명명된 매개변수를 포함할 수 있습니다: /about/:path/about/a/about/b와 일치하지만 /about/a/c와는 일치하지 않습니다
  3. 명명된 매개변수( :로 시작)에 수정자를 가질 수 있습니다: /about/:path**0개 이상이므로 /about/a/b/c와 일치합니다. ?0개 또는 1개, +1개 이상
  4. 괄호로 둘러싸인 정규식을 사용할 수 있습니다: /about/(.*)/about/:path*와 동일합니다

자세한 내용은 [path-to-regexp](https://github.com/pillarjs/path-to-regexp#path-to-regexp- (opens in a new tab)

  1. 문서를 참조하세요.

알아두면 좋은 점: 이전 버전과의 호환성을 위해 Next.js는 항상 /public/public/index로 간주합니다. 따라서 /public/:path의 매처는 일치합니다.

Conditional Statements

middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith('/about')) {
    return NextResponse.rewrite(new URL('/about-2', request.url))
  }
 
  if (request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.rewrite(new URL('/dashboard/user', request.url))
  }
}
middleware.js
import { NextResponse } from 'next/server'
 
export function middleware(request) {
  if (request.nextUrl.pathname.startsWith('/about')) {
    return NextResponse.rewrite(new URL('/about-2', request.url))
  }
 
  if (request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.rewrite(new URL('/dashboard/user', request.url))
  }
}

NextResponse

NextResponse API를 사용하여 다음 작업을 수행할 수 있습니다:

  • 들어오는 요청을 다른 URL로 redirect
  • 주어진 URL을 표시하여 응답을 rewrite
  • API Routes, getServerSideProps, 및 rewrite 대상에 대해 요청 헤더 설정
  • 응답 쿠키 설정
  • 응답 헤더 설정

Middleware에서 응답을 생성하려면 다음을 수행할 수 있습니다:

  1. 응답을 생성하는 라우트 (Page 또는 Edge API Route)로 rewrite
  2. NextResponse를 직접 반환합니다. Producing a Response를 참조하세요

Using Cookies

쿠키는 일반 헤더입니다. Request에서는 Cookie 헤더에 저장됩니다. Response에서는 Set-Cookie 헤더에 저장됩니다. Next.js는 NextRequestNextResponse에서 쿠키에 쉽게 접근하고 조작할 수 있는 편리한 방법을 제공합니다.

  1. 들어오는 요청의 경우, cookies는 다음 메서드를 제공합니다: get, getAll, set, 및 delete 쿠키. has를 사용하여 쿠키의 존재 여부를 확인하거나 clear를 사용하여 모든 쿠키를 제거할 수 있습니다.
  2. 나가는 응답의 경우, cookies는 다음 메서드를 제공합니다: get, getAll, set, 및 delete.
middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
export function middleware(request: NextRequest) {
  // 들어오는 요청에 "Cookie:nextjs=fast" 헤더가 있다고 가정합니다
  // `RequestCookies` API를 사용하여 요청에서 쿠키를 가져옵니다
  let cookie = request.cookies.get('nextjs')
  console.log(cookie) // => { name: 'nextjs', value: 'fast', Path: '/' }
  const allCookies = request.cookies.getAll()
  console.log(allCookies) // => [{ name: 'nextjs', value: 'fast' }]
 
  request.cookies.has('nextjs') // => true
  request.cookies.delete('nextjs')
  request.cookies.has('nextjs') // => false
 
  // `ResponseCookies` API를 사용하여 응답에 쿠키를 설정합니다
  const response = NextResponse.next()
  response.cookies.set('vercel', 'fast')
  response.cookies.set({
    name: 'vercel',
    value: 'fast',
    path: '/',
  })
  cookie = response.cookies.get('vercel')
  console.log(cookie) // => { name: 'vercel', value: 'fast', Path: '/' }
  // 나가는 응답에는 `Set-Cookie:vercel=fast;path=/` 헤더가 포함됩니다.
 
  return response
}
middleware.js
import { NextResponse } from 'next/server'
 
export function middleware(request) {
  // 들어오는 요청에 "Cookie:nextjs=fast" 헤더가 있다고 가정합니다
  // `RequestCookies` API를 사용하여 요청에서 쿠키를 가져옵니다
  let cookie = request.cookies.get('nextjs')
  console.log(cookie) // => { name: 'nextjs', value: 'fast', Path: '/' }
  const allCookies = request.cookies.getAll()
  console.log(allCookies) // => [{ name: 'nextjs', value: 'fast' }]
 
  request.cookies.has('nextjs') // => true
  request.cookies.delete('nextjs')
  request.cookies.has('nextjs') // => false
 
  // `ResponseCookies` API를 사용하여 응답에 쿠키를 설정합니다
  const response = NextResponse.next()
  response.cookies.set('vercel', 'fast')
  response.cookies.set({
    name: 'vercel',
    value: 'fast',
    path: '/',
  })
  cookie = response.cookies.get('vercel')
  console.log(cookie) // => { name: 'vercel', value: 'fast', Path: '/' }
  // 나가는 응답에는 `Set-Cookie:vercel=fast;path=/test` 헤더가 포함됩니다.
 
  return response
}

Setting Headers

NextResponse API를 사용하여 요청 및 응답 헤더를 설정할 수 있습니다 (요청 헤더 설정은 Next.js v13.0.0부터 사용할 수 있습니다).

middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
export function middleware(request: NextRequest) {
  // 요청 헤더를 복제하고 새로운 헤더 `x-hello-from-middleware1`을 설정합니다
  const requestHeaders = new Headers(request.headers)
  requestHeaders.set('x-hello-from-middleware1', 'hello')
 
  // NextResponse.next에서 요청 헤더를 설정할 수도 있습니다
  const response = NextResponse.next({
    request: {
      // 새로운 요청 헤더
      headers: requestHeaders,
    },
  })
 
  // 새로운 응답 헤더 `x-hello-from-middleware2`를 설정합니다
  response.headers.set('x-hello-from-middleware2', 'hello')
  return response
}
middleware.js
import { NextResponse } from 'next/server'
 
export function middleware(request) {
  // 요청 헤더를 복제하고 새로운 헤더 `x-hello-from-middleware1`을 설정합니다
  const requestHeaders = new Headers(request.headers)
  requestHeaders.set('x-hello-from-middleware1', 'hello')
 
  // NextResponse.next에서 요청 헤더를 설정할 수도 있습니다
  const response = NextResponse.next({
    request: {
      // 새로운 요청 헤더
      headers: requestHeaders,
    },
  })
 
  // 새로운 응답 헤더 `x-hello-from-middleware2`를 설정합니다
  response.headers.set('x-hello-from-middleware2', 'hello')
  return response
}

알아두면 좋은 점: 큰 헤더를 설정하면 백엔드 웹 서버 구성에 따라 431 Request Header Fields Too Large (opens in a new tab) 오류가 발생할 수 있으므로 피해야 합니다.

CORS

Middleware에서 CORS 헤더를 설정하여 교차 출처 요청을 허용할 수 있으며, 여기에는 단순 요청 (opens in a new tab)사전 요청 (opens in a new tab)이 포함됩니다.

middleware.ts
import { NextRequest, NextResponse } from 'next/server'
 
const allowedOrigins = ['https://acme.com', 'https://my-app.org']
 
const corsOptions = {
  'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
  'Access-Control-Allow-Headers': 'Content-Type, Authorization',
}
 
export function middleware(request: NextRequest) {
  // 요청의 origin을 확인합니다
  const origin = request.headers.get('origin') ?? ''
  const isAllowedOrigin = allowedOrigins.includes(origin)
 
  // 사전 요청을 처리합니다
  const isPreflight = request.method === 'OPTIONS'
 
  if (isPreflight) {
    const preflightHeaders = {
      ...(isAllowedOrigin && { 'Access-Control-Allow-Origin': origin }),
      ...corsOptions,
    }
    return NextResponse.json({}, { headers: preflightHeaders })
  }
 
  // 단순 요청을 처리합니다
  const response = NextResponse.next()
 
  if (isAllowedOrigin) {
    response.headers.set('Access-Control-Allow-Origin', origin)
  }
 
  Object.entries(corsOptions).forEach(([key, value]) => {
    response.headers.set(key, value)
  })
 
  return response
}
 
export const config = {
  matcher: '/api/:path*',
}
middleware.js
import { NextResponse } from 'next/server'
 
const allowedOrigins = ['https://acme.com', 'https://my-app.org']
 
const corsOptions = {
  'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
  'Access-Control-Allow-Headers': 'Content-Type, Authorization',
}
 
export function middleware(request) {
  // 요청의 origin을 확인합니다
  const origin = request.headers.get('origin') ?? ''
  const isAllowedOrigin = allowedOrigins.includes(origin)
 
  // 사전 요청을 처리합니다
  const isPreflight = request.method === 'OPTIONS'
 
  if (isPreflight) {
    const preflightHeaders = {
      ...(isAllowedOrigin && { 'Access-Control-Allow-Origin': origin }),
      ...corsOptions,
    }
    return NextResponse.json({}, { headers: preflightHeaders })
  }
 
  // 단순 요청을 처리합니다
  const response = NextResponse.next()
 
  if (isAllowedOrigin) {
    response.headers.set('Access-Control-Allow-Origin', origin)
  }
 
  Object.entries(corsOptions).forEach(([key, value]) => {
    response.headers.set(key, value)
  })
 
  return response
}
 
export const config = {
  matcher: '/api/:path*',
}

Producing a Response

Middleware에서 Response 또는 NextResponse 인스턴스를 반환하여 직접 응답할 수 있습니다. (이는 Next.js v13.1.0 (opens in a new tab)부터 사용 가능합니다)

middleware.ts
import type { NextRequest } from 'next/server'
import { isAuthenticated } from '@lib/auth'
 
// Middleware를 `/api/`로 시작하는 경로로 제한합니다
export const config = {
  matcher: '/api/:function*',
}
 
export function middleware(request: NextRequest) {
  // 요청을 확인하기 위해 인증 함수를 호출합니다
  if (!isAuthenticated(request)) {
    // 오류 메시지를 나타내는 JSON으로 응답합니다
    return Response.json(
      { success: false, message: 'authentication failed' },
      { status: 401 },
    )
  }
}
middleware.js
import { isAuthenticated } from '@lib/auth'
 
// Middleware를 `/api/`로 시작하는 경로로 제한합니다
export const config = {
  matcher: '/api/:function*',
}
 
export function middleware(request) {
  // 요청을 확인하기 위해 인증 함수를 호출합니다
  if (!isAuthenticated(request)) {
    // 오류 메시지를 나타내는 JSON으로 응답합니다
    return Response.json(
      { success: false, message: 'authentication failed' },
      { status: 401 },
    )
  }
}

waitUntil and NextFetchEvent

NextFetchEvent 객체는 기본 FetchEvent (opens in a new tab) 객체를 확장하며 waitUntil() (opens in a new tab) 메서드를 포함합니다.

waitUntil() 메서드는 프라미스를 인수로 받아서 프라미스가 해결될 때까지 Middleware의 수명을 연장합니다. 이는 백그라운드에서 작업을 수행하는 데 유용합니다.

middleware.ts
import { NextResponse } from 'next/server'
import type { NextFetchEvent, NextRequest } from 'next/server'
 
export function middleware(req: NextRequest, event: NextFetchEvent) {
  event.waitUntil(
    fetch('https://my-analytics-platform.com', {
      method: 'POST',
      body: JSON.stringify({ pathname: req.nextUrl.pathname }),
    }),
  )
 
  return NextResponse.next()
}

Advanced Middleware Flags

Next.js v13.1에서 두 가지 추가 플래그, skipMiddlewareUrlNormalizeskipTrailingSlashRedirect가 도입되어 고급 사용 사례를 처리할 수 있습니다.

skipTrailingSlashRedirect는 후행 슬래시를 추가하거나 제거하기 위한 Next.js 리디렉션을 비활성화합니다. 이를 통해 일부 경로에서는 후행 슬래시를 유지하고 다른 경로에서는 유지하지 않도록 커스텀 처리를 수행할 수 있어 점진적인 마이그레이션을 용이하게 합니다.

next.config.js
module.exports = {
  skipTrailingSlashRedirect: true,
}
middleware.js
const legacyPrefixes = ['/docs', '/blog']
 
export default async function middleware(req) {
  const { pathname } = req.nextUrl
 
  if (legacyPrefixes.some((prefix) => pathname.startsWith(prefix))) {
    return NextResponse.next()
  }
 
  // 후행 슬래시 처리 적용
  if (
    !pathname.endsWith('/') &&
    !pathname.match(/((?!\.well-known(?:\/.*)?)(?:[^/]+\/)*[^/]+\.\w+)/)
  ) {
    return NextResponse.redirect(
      new URL(`${req.nextUrl.pathname}/`, req.nextUrl),
    )
  }
}

skipMiddlewareUrlNormalize는 Next.js에서 URL 정규화를 비활성화하여 직접 방문과 클라이언트 전환을 동일하게 처리할 수 있게 합니다. 일부 고급 사례에서는 이 옵션을 통해 원래 URL을 사용하여 완전한 제어를 제공합니다.

next.config.js
module.exports = {
  skipMiddlewareUrlNormalize: true,
}
middleware.js
export default async function middleware(req) {
  const { pathname } = req.nextUrl
 
  // GET /_next/data/build-id/hello.json
 
  console.log(pathname)
  // 플래그 사용 시 /_next/data/build-id/hello.json
  // 플래그 사용 안 할 경우 /hello로 정규화됩니다
}

Runtime

Middleware는 현재 Edge runtime만 지원합니다. Node.js 런타임은 사용할 수 없습니다.

Version History

VersionChanges
v13.1.0Advanced Middleware flags added
v13.0.0Middleware can modify request headers, response headers, and send responses
v12.2.0Middleware is stable, please see the upgrade guide
v12.0.9Enforce absolute URLs in Edge Runtime (PR (opens in a new tab))
v12.0.0Middleware (Beta) added