[Next.js] Middleware

 

01. Middleware

middleware는 페이지를 렌더링하기 전에 서버 측에서 실행되는 함수이다.

즉, 요청이 완료되기 전에 코드를 실행할 수 있게 해주며, 그런 다음 요청에 따라 응답을 수정할 수 있다.

미들웨어는 캐시된 콘텐츠와 라우트가 일치하기 전에 실행되며, 자세한 내용은 아래 Matching Paths와 Matcher에서 다룬다.

 

미들웨어에서는 Request 객체와 Response 객체에 접근할 수 있으며, 이를 활용해 요청 정보를 받아와 부가적인 처리를 하고 응답 객체에 무언가를 추가하거나 응답을 변경할 수 있다. 미들웨어는 다음과 같은 상황에서 사용할 수 있다.

  • 페이지 렌더링 전에 인증을 확인하거나 요청을 확인할 때
  • 요청 데이터를 사전에 처리하거나 특정 API 요청을 수행하거나 캐시를 관리할 때
  • 요청에 대한 응답을 변환하거나 에러를 처리할 때

미들웨어는 프로젝트의 루트에 있는 middleware.ts(middleware.js) 파일을 사용하여 정의한다.

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
// This function can be marked `async` if using `await` inside
export function middleware(request: NextRequest) {
  return NextResponse.redirect(new URL('/home', request.url))
}
 
// See "Matching Paths" below to learn more
export const config = {
  matcher: '/about/:path*',
}

 

위의 코드는 Next.js 공식 문서의 middleware 사용 예시이다.

middleware의 파라미터로 NextResponse와 NextRequest를 사용할 수 있어 요청과 응답을 핸들링할 수 있다.

 

먼저 위의 middleware 코드는 다음과 같다.

◼︎ middleware 함수

request는 NextRequest 타입의 객체로, 요청에 관련된 정보(URL, 헤더, 쿠키 등)을 포함한다.

이 객체를 사용해 클라이언트 요청을 검사하거나 커스터마이징된 로직을 적용할 수 있다.

 

◼︎ NextResponse

NextResponse는 응답을 생성하거나 요청을 변경하기 위해 사용된다.

위 예시에서는 NextResponse.redirect(url)을 통해 요청을 다른 URL로 리다이렉션하도록 한다.

new URL을 통해 요청 URL을 기준으로 /home을 상대 경로로 계산한 절대 URL을 생성한다.

 

◼︎ config

config 객체에서는 matcher를 통해 미들웨어가 실행될 경로를 지정한다. 이때 경로는 특정 패턴을 포함하거나 특정 URL에만 적용되도록 제한할 수 있다. 이를 위해 필요한 Matching paths와 matcher에 대해서는 아래에서 더 자세히 알아보고자 한다.

 

 

 

 

02. Matching Paths & Matcher

Matching Paths는 요청된 URL에 따라 미들웨어의 실행 여부를 결정하는 규칙을 정의하는 것이다.

미들웨어는 기본적으로 모든 경로에서 실행된다. 그러나 모든 요청에 대해 실행되면 불필요한 작업이 늘어나서 성능이 저하될 수 있다.

따라서 matcher 설정이나 조건문을 사용해 특정 경로만 실행되도록 제어할 수 있다.

 

우선 미들웨어는 기본적으로 모든 라우트에 대해 다음과 같이 순차적으로 동작한다.

1️⃣ next.config.js의 'headers', 'redirects'

next.config.js에서 정의한 응답 헤더와 리다이렉션 규칙이 적용

 

2️⃣ Middleware

요청을 가로채고 필요한 작업(리다이렉션, 재작성, 사용자 인증 등)을 실행

 

3️⃣ next.config.js의 'beforeFiles'

파일 시스템 라우트 전에 실행되는 rewrites가 적용

 

4️⃣ Filesystem routes

Next.js의 기본 파일 시스템 라우트가 처리

 

5️⃣ next.config.js의 'afterFiles'

파일 시스템 라우트 이후에 실행되는 rewrites가 적용

 

6️⃣ Dynamic Routes

/blog/[slug]와 같은 동적 라우트가 처리

 

7️⃣ next.config.js의 'fallback'

마지막으로 라우트가 처리되지 않은 경우 fallback 재작성 규칙이 적용

 

미들웨어는 기본적으로 모든 파일 시스템의 정적인 콘텐츠들에 대한 요청에서도 동작한다.

즉, 앞서 언급했듯이 기본적으로 모든 경로에서 실행된다. 따라서 다음과 같은 방법으로 특정 경로에만 미들웨어를 적용할 수 있다.

  • custom matcher를 config에 설정
  • 미들웨어 내에서의 Condition으로 분기

 

 

Matcher

mathcer는 단순 경로 뿐만 아니라 정규식이나 특정 조건을 활용해 좀 더 세밀하게 미들웨어를 적용할 수 있다.

 

◼︎ custom matcher config

// 단일 경로 매칭
export const config = {
  matcher: '/about/:path*', // "/about"와 그 하위 경로에서만 Middleware 실행
};

// 다중 경로 매칭
export const config = {
  matcher: ['/about/:path*', '/dashboard/:path*'], // "/about"와 "/dashboard" 하위 경로에서 실행
};

 

 

◼︎ 정규식을 활용한 Matcher

Matcher는 정규식을 완벽히 지원하므로, 특정 패턴에 맞는 경로나 특정 경로를 제외하는 Negative Lookahead와 같은 고급 매칭도 가능하다. 특정 경로를 제외(Negative Lookahead)하는 예시는 다음과 같다.

export const config = {
  matcher: [
    /*
     * 다음 경로를 제외하고 모든 요청 경로에서 Middleware 실행:
     * - api (API 경로)
     * - _next/static (정적 파일)
     * - _next/image (이미지 최적화 파일)
     * - favicon.ico, sitemap.xml, robots.txt (메타데이터 파일)
     */
    '/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
  ],
};

위 예시는 api나 정적 파일 경로를 제외한 모든 요청 경로에서 Middleware를 실행한다.

 

 

◼︎ 조건을 활용한 Matcher

Matcher는 missing 또는 has 배열을 사용하여 특정 헤더나 조건에 따라 경로를 매칭하거나 제외할 수 있다.

예를 들어, 다음과 같이 요청 헤더가 있는지 없는지에 따라 미들웨어 실행 여부를 결정하도록 설정할 수 있다.

export const config = {
  matcher: [
    {
      source: '/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
      missing: [
        { type: 'header', key: 'next-router-prefetch' }, // 특정 헤더가 없는 요청만 매칭
        { type: 'header', key: 'purpose', value: 'prefetch' },
      ],
    },
    {
      source: '/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
      has: [
        { type: 'header', key: 'next-router-prefetch' }, // 특정 헤더가 있는 요청만 매칭
        { type: 'header', key: 'purpose', value: 'prefetch' },
      ],
    },
  ],
};

 

 

💡 matcher 작성시 주의사항

- matcher는 반드시 / 로 시작해야 한다.

- Named Parameters를 사용하여 경로의 특정 부분을 변수처럼 매칭할 수 있다.

matcher: '/about/:path'

다음과 같이 matcher를 작성하면 /about/a, /about/b는 매칭되지만 /about/a/c는 매칭되지 않는다.

 

- Named Parameters에 Modifier를 추가하여 매칭할 수 있다.

  • * (zero or more) : 0개 이상의 경로 매칭
  • ? (zero or one) : 0개 또는 1개의 경로 매칭
  • + (one or more) : 1개 이상의 경로 매칭

- 정규식을 활용 가능하다.

Named Parameters 대신 정규식을 사용할 수 있으며, 이를 괄호 안에 작성해야 한다.

matcher: '/about/(.*)'

 

 

 

Conditional Statements

특정 경로에만 미들웨어를 적용하기 위해서는 matcher를 설정하는 방법 대신 미들웨어 내 조건문으로 분기하여 설정할 수 있다.

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))
  }
}

 

 

 

 

03. NextResponse & NextRequest

NextResponse와 NextRequest는 Next.js에서 미들웨어를 작성할 때 사용되는 두 개의 중요한 객체이다.

각각은 요청과 응답을 다루며 자세한 차이점은 다음과 같다.

 

◼︎ NextRequest

NextRequest는 HTTP 요청을 다루는 객체이다.

클라이언트가 서버로 보낸 요청에 대한 다양한 정보를 제공한다.

주로 요청에 포함된 데이터를 확인하거나 수정하는데 사용되며, 요청 URL, 헤더, 쿠키 등에 접근할 수 있다.

 

◼︎ NextResponse

NextResponse는 HTTP 응답을 다루는 객체이다.

서버가 클라이언트로 보내는 응답을 수정하거나 리다이렉션을 설정하는 등의 작업을 할 때 사용된다.

 

 

 

 

04. Cookies 사용하기

Next.js는 요청과 응답에서 쿠키를 쉽게 접근하고 조작할 수 있도록 cookies API를 제공한다.

이를 통해 미들웨어에서 쿠키를 읽고 설정하거나 삭제할 수 있다.

 

◼︎ 요청에서 Cookies 다루기 (NextRequest)

요청에서 쿠키는 Cookie 헤더에 저장되며, NextRequest 객체의 cookies를 통해 접근 가능하다.

요청의 경우 cookies는 get, getAll, set, delete 메서드를 제공하여 쿠키에 접근할 수 있도록 제공한다.

특정 쿠키의 존재 여부를 확인하기 위해서는 'has'를 사용하거나 모든 쿠키를 삭제하려면 'clear'를 사용할 수 있다.

 

◼︎ 응답에서 Cookies 다루기 (NextResponse)

응답에서 쿠키는 Set-Cookie 헤더에 저장된다.

응답의 경우 NextResponse의 cookies를 통해 쿠키를 설정하거나 삭제할 수 있으며, get, getAll, set, delete 메서드를 활용할 수 있다.

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
export function middleware(request: NextRequest) {
  // Assume a "Cookie:nextjs=fast" header to be present on the incoming request
  // Getting cookies from the request using the `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
 
  // Setting cookies on the response using the `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: '/' }
  // The outgoing response will have a `Set-Cookie:vercel=fast;path=/` header.
 
  return response
}

 

 

 


References

https://nextjs.org/docs/app/building-your-application/routing/middleware

 

Routing: Middleware | Next.js

Learn how to use Middleware to run code before a request is completed.

nextjs.org

 

 

반응형