FE/NextJS

NextJS 공식문서 파헤치기 (Middleware)

-Daniel- 2024. 12. 17. 17:05

0️⃣ INTRO

  • Middleware를 사용하면 요청이 완료되기 전에 코드 실행 가능
  • 그 후에 들어오는 요청에 따라 응답을 재작성, 리디렉션, 요청 또는 응답 헤더 수정 또는 직접 응답할 수 있다.
  • Middleware는 캐시된 콘텐츠와 라우트가 일치하기 전에 실행된다.

1️⃣ Use Cases

  • 일반적인 시나리오
    • 인증 및 권한 부여: 사용자 신원을 확인하고 특정 페이지나 API 라우트에 접근하기 전에 세션 쿠키 확인
    • 서버 측 리디렉션: 특정 조건에 따라 서버 수ㅜㄴ에서 사용자를 리디렉션
    • 경로 재작성: 요청 속성에 따라 경로를 동적으로 재작성하여 A/B 테스트, 기능 출시 또는 레거시 경로를 지원
    • 봇 감지: 리소스를 보호하기 위해 봇 트래픽을 감지하고 차단
    • 로깅 및 분석: 페이지나 API가 처리되기 전에 요청 데이터를 캡처하고 분석
    • 기능 플래깅: 기능을 동적으로 활성화 또는 비활성화하여 원활한 기능 출시 또는 테스트를 진행
  • 주의해야 할 시나리오(아래의 시나리오는 Route Handler에서 수행해야 한다)
    • 복잡한 데이터 가져오기 및 조작: 직접적인 데이터 가져오기나 조작을 하면 안된다
    • 무거운 계산 작업: Middleware는 가볍고 빠르게 응답해야 한다
    • 광범위한 세션 관리: 기본적인 세션 작업만 Middleware에서 해야 한다
    • 직접적인 데이터베이스 작업: Middleware 내에서 직접적인 데이터베이스 작업을 수행하는 것은 권장되지 않음

2️⃣ Convention

  • 프로젝트의 루트에 middleware.ts(또는 .js) 파일을 사용하여 Middleware를 정의
  • pages 또는 app과 같은 수준에 있거나 src 내에 있을 수 있다.
🚀 TIP

1. 프로젝트당 하나의 middleware.ts 파일만 지원하지만 모듈화하여 미들웨어 로직을 구성할 수 있다.
2. 미들웨어 기능을 별도의 파일로 분리하고 주요 middleware.ts 파일에 import 할 수 있다.

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*',
}

4️⃣ Matching Paths

  • Middleware는 프로젝트의 모든 라우트에 대해 호출된다.
  • 특정 라우트를 정확히 타겟팅하거나 제외하기 위해 Matcher를 사용하는 것이 중요함
  • 실행순서
    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를 실행하도록 필터링 가능
  • matcher 값은 빌드 시 정적으로 분석될 수 있도록 상수여야 한다. (변수와 같은 동적 값은 무시됨)
📌 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*와 동일합니다

자세한 내용: https://github.com/pillarjs/path-to-regexp#path-to-regexp
// 특정 경로에서만 실행
export const config = {
  matcher: '/about/:path*',
}

// 단일 경로나 여러 경로를 배열 구문으로 매칭
export const config = {
  matcher: ['/about/:path*', '/dashboard/:path*'],
}

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

// missing 또는 has 배열 또는 두 가지 조합을 사용하여 특정 요청을 위해 Middleware를 우회
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' }],
    },
  ],
}

Conditional Statements

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  // 요청 경로(pathname)가 /about으로 시작하는 경우, 요청 경로를 /about-2로 재작성
  // ex) /about -> /about-2
  // ex) /about/team -> /about-2
  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))
  }
}

5️⃣ NextResponse

  • NextResponse API를 사용하여 다음의 작업을 수행 가능
    • 들어오는 요청을 다른 URL로 redirect
    • 주어진 URL을 표시하여 응답을 rewrite
    • API Routes, getServerSideProps, rewrite 대상에 대해 요청 헤더 설정
    • 응답 쿠키 설정
    • 응답 헤더 설정
  • Middleware에서 응답을 생성하는 방법
    1. 응답을 생성하는 라우트(Page 또는 Route Handler)로 rewrite
    2. NextResponse를 직접 반환

6️⃣ Using Cookies

  • Nextjs는 NextRequestNextResponse 에서 쿠키에 쉽게 접근하고 조작할 수 있는 방법을 제공한다.
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
}

7️⃣ Setting Headers

  • NextResponse API를 사용하여 요청 및 응답 헤더 설정 가능 (요청 헤더 설정은 Nextjs v13.0.0 부터 사용 가능)
  • 큰 헤더를 설정하면 백엔드 웹 서버 구성에 따라 431 Request Header Fields Too Large(opens in a new tab) 오류가 발생할 수 있으므로 피해야 한다.
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
}

8️⃣ CORS

  • Middleware에서 CORS 헤더를 설정 하여 교차 출처 요청을 허용할 수 있다.
  • Route Handlers 에서는 개별 라우트에 대한 CORS 헤더를 구성할 수 있다.
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*',
}

9️⃣ Producing a Response

  • Middleware 에서 Response 또는 NextResponse 인스턴스를 반환하여 직접 응답 (Nextjs v13.1.0 부터 가능)
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 },
    )
  }
}

 

🔟 ETC

waitUntil()

  • promise를 인수로 받아 promise가 해결될 때까지 Middleware의 수명을 연장한다.
  • 백그라운드에서 작업을 수행하는 데 유용하다.
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

  • skipMiddlewareUrlNormalize: 후행 슬래시를 추가하거나 제거하기 위한 Nextjs 리디렉션을 비활성화한다.
// 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),
    )
  }
}
  • skipTrailingSlashRedirect: Nextjs에서 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만 지원한다. Nodejs 런타임은 사용 불가

📚 REFERENCE