Nextjs Middleware 읽어보기 - 1

cansweep·2025년 2월 13일

📌 AppRouter, v.15.1.6
https://nextjs.org/docs/app/building-your-application/routing/middleware

Middleware

미들웨어는 요청이 완료되기 전에 코드를 실행시킬 수 있습니다. 그리고 요청에 대해 rewrite, redirect, 요청/응답 헤더 수정 등으로 응답을 수정하거나 직접 응답할 수 있습니다.

미들웨어는 캐시된 콘텐츠와 경로가 매칭되기 전에 실행됩니다. 자세한 내용은 Matching Paths에서 확인할 수 있습니다.

Use Cases

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

  • 인증 및 권한 부여: 특정 페이지나 API 경로에 대해 접근을 승인하기 전에 사용자 신원, 세션 쿠키를 확인할 수 있습니다.
  • 서버사이드 리다이렉션: 특정 조건(지역, 유저 권한 등)에 따라 서버에서 사용자를 리다이렉트시킬 수 있습니다.
  • 경로 재작성: 요청 속성을 기반으로 API 경로나 페이지 경로를 동적으로 재작성해 AB 테스트, 기능 출시, 레거시 경로 등을 지원할 수 있습니다.
  • 봇 감지: 봇 트래픽을 감지하고 차단해 자원을 보호할수 있습니다.
  • 로깅 및 분석: 페이지나 API에서 처리하기 전에 요청 데이터를 캡처하고 분석해 인사이트를 얻을 수 있습니다.
  • 기능 플래그: 원활한 기능 출시 또는 테스트를 위해 동적으로 기능을 끄고 켤 수 있습니다.

반대로 미들웨어가 최적의 접근 방식이 아닌 것을 인지하는 것도 중요합니다. 주의해야 할 시나리오는 다음과 같습니다.

  • 복잡한 데이터 가져오기 및 조작: 미들웨어는 직접적인 데이터 가져오기나 조작을 하기위해 설계되지 않았습니다. 이러한 작업은 라우트 핸들러나 서버 사이드 유틸리티에서 실행되어야 합니다.
  • 무거운 연산 작업: 미들웨어는 페이지 로드를 지연시킬 수 있기 때문에 가볍고 빠르게 응답해야 합니다. 무거운 연산 작업이나 오래 걸리는 프로세스는 전용 라우트 핸들러에서 실행되어야 합니다.
  • 광범위한 세션 관리: 미들웨어에서 간단한 세션 작업은 가능하지만 광범위한 세션 관리는 전용 인증 서비스나 라우터 핸들러에서 관리되어야 합니다.
  • 직접적인 데이터베이스 작업: 미들웨어 내에서의 직접적인 데이터베이스 작업은 권장하지 않습니다. 데이터베이스 상호작용은 라우트 핸들러나 서버사이드 유틸리티에서 실행되어야 합니다.

Convention

프로젝트의 루트 디렉터리에 middleware.ts(또는 .js) 파일을 생성해 미들웨어를 정의할 수 있습니다. 예를 들어, pages, app 과 같은 레벨에 있거나 src 디렉터리 안에 있을 수 있습니다.

참고로, 프로젝트 당 하나의 middleware.ts 파일만 지원되지만 미들웨어 로직을 모듈식으로 구성할 수 있습니다. 미들웨어 기능들을 .ts 또는 .js 파일로 나누고 middleware.ts 파일에서 이들을 가져와 사용할 수 있습니다. 이를 통해 경로별 미들웨어를 효율적으로 관리할 수 있으며 middleware.ts에서 중앙 집중적으로 통합하여 제어할 수 있습니다. 단일 미들웨어 파일을 사용하도록 함으로써 구성이 단순해지고 잠재적인 충돌을 방지하며 성능을 최적화할 수 있습니다.

Example

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))
}
 
export const config = {
  matcher: '/about/:path*',
}

Matching Paths

미들웨어는 프로젝트의 모든 경로에서 호출됩니다. 이를 감안하면, matcher를 사용해 타겟을 정확히 지정하거나 특정 경로를 제외시키는 것이 매우 중요합니다. 실행 순서는 다음과 같습니다.

  1. next.config.js > headers
  2. next.config.js > redirects
  3. Middleware
  4. next.config.js > rewrites > beforeFiles
  5. Filesystem routes (public/, _next/static/, pages/, app/ 등)
  6. next.config.js > rewrites > afterFiles
  7. Dynamic Routes
  8. next.config.js > rewrites > fallback

미들웨어가 실행되는 경로를 지정하는 방법은 두 가지가 있습니다.

Matcher

matcher 는 미들웨어가 특정 경로에서 실행되도록 필터링할 수 있습니다.

export const config = {
	matcher: '/about/:path*'
}

단일 경로를 지정하거나 배열으로 여러 개의 경로를 지정할 수 있습니다.

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

matcher 는 정규식을 허용하므로 부정 전방 탐색이나 문자 매칭을 사용할 수 있습니다. 특정 경로를 제외한 모든 경로를 매칭하는 부정 전방 탐색 예제는 다음과 같습니다.

export const config = {
  matcher: [
    /*
     * 다음으로 시작하는 요청 경로를 제외한 모든 경로와 매칭됨.
     * - api (API routes)
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico, sitemap.xml, robots.txt (metadata files)
     */
    '/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
  ],
}

missing 또는 has 배열을 사용하거나 둘을 조합하여 특정 요청에 대해 미들웨어를 우회할 수도 있습니다.

export const config = {
  matcher: [
    /*
     * 다음으로 시작하는 요청 경로를 제외한 모든 경로와 매칭됨.
     * - api (API routes)
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico, sitemap.xml, robots.txt (metadata files)
     */
    {
      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' },
      ],
    },
 
    {
      source:
        '/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
      has: [{ type: 'header', key: 'x-present' }],
      missing: [{ type: 'header', key: 'x-missing', value: 'prefetch' }],
    },
  ],
}

matcher 값은 빌드 시 정적 분석이 가능하도록 상수여야 합니다. 변수와 같은 동적인 값은 무시됩니다.

matcher 구성

  1. 반드시 / 로 시작해야 합니다.
  2. 이름이 있는 파라미터를 사용할 수 있습니다.
    1. /about/:path/about/a 또는 /about/b 와 매칭되지만 /about/a/b 와는 매칭되지 않습니다.
  3. 이름이 있는 파라미터에 수정자(: 로 시작하는)를 사용할 수 있습니다.
    1. /about/:path*/about/a/b/c 와 매칭됩니다.

      *0회 이상 반복
      ?0회 또는 1회 반복
      +1회 이상 반복
  4. 괄호로 묶인 정규표현식을 사용할 수 있습니다.
    1. /about/(.*)/about/:path* 와 같습니다.

❗ 이전 버전과의 호환성을 위해 Next.js는 /public/public/index로 간주합니다.
따라서 matcher가 /public/:path 일 경우 /public 도 매칭됩니다.

조건문

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))
  }
}
profile
하고 싶은 건 다 해보자! 를 달고 사는 프론트엔드 개발자입니다.

0개의 댓글