처음 만난 Next.js 문서


6.2

Next.js의 Middleware

그렇다면 Next.js에서 Middleware는 어떤 역할을 할까요? 지금부터는 Next.js의 Middleware에 대해서 살펴보도록 하겠습니다.

Next.js의 Middleware는 서버에서 요청에 대해 응답하기 전에 실행되는 코드 또는 프로그램을 의미합니다. 여기서 서버는 우리가 일반적으로 얘기하는 백엔드 서버가 아니라 Next.js 애플리케이션이 실행되는 서버를 의미합니다. 그래서 Middleware를 사용하면 요청이 완료되기 전에 원하는 코드를 실행할 수 있습니다. 예를 들면, Middleware에서 Request 또는 Response Header를 다시 작성할 수도 있고, 특정 경로로 Redirection을 시키거나 Response를 직접 수정할 수도 있습니다.

6.2.1 Middleware 사용 사례

Next.js에서 Middleware를 사용하는 일반적인 경우로는 어떤 것들이 있을까요?

가장 대표적으로 인증이나 권한과 관련된 경우가 있습니다. 요청에 응답하기 전에 사용자의 로그인 여부나 권한을 먼저 확인해서 각 상황에 맞게 처리할 수 있는 것이죠. 그리고 서버 측에서 Redirection을 하고 싶은 경우에도 Middleware를 사용할 수 있습니다. Middleware를 사용하면 사용자나 권한여부에 따라서 특정 페이지로 Redirection 시킬 수 있습니다. 비슷한 사용 사례로 Path Rewriting이 있습니다. Redirection은 특정 페이지로 보내는 것인 반면 Path Rewriting은 경로는 유지하면서 다른 페이지를 보여줄 수 있는 기능입니다. 이러한 기능을 활용하면 요청에 따라 다른 페이지를 보여줌으로써 A/B 테스트를 하거나 각종 기능들을 테스트 할 수 있습니다. 마지막으로 봇을 탐지하거나 로깅 및 분석 데이터를 쌓는 경우에도 Middleware를 활용할 수 있습니다.

이처럼 Middleware를 사용하면 다양한 경우에 대해 유연하게 대처할 수 있지만, Middleware를 사용하면 안 좋은 사례도 있습니다. 대표적으로 Middleware에서 크고 복잡한 데이터를 가져오거나 조작하는 동작을 수행하면 안 됩니다. 왜냐하면 애초에 Middleware가 그러한 작업을 위해서 설계된 것이 아니기 때문입니다. 그래서 이러한 작업은 Route Handlers 또는 실제 서버 측에서 수행되어야 합니다. 이와 비슷하게 계산 비용(Computation Cost)이 많이 드는 작업 또한 Middleware에서 수행하면 안 됩니다. 이러한 작업을 Middleware에서 수행하게 되면 지연 시간이 늘어나서 페이지 로딩이 느려질 수 있기 때문입니다. 그리고 전체적인 세션 관리를 모두 Middleware를 통해서 하거나, 직접 데이터베이스와 관련된 작업을 수행하는 것도 좋지 않은 Middleware 사용 사례라고 할 수 있습니다. 한 마디로 정리하면 Middleware에서는 시간이 오래 걸리는 작업은 하지 않는 것이 좋습니다.

이처럼 Middleware를 사용하면 좋은 경우와 좋지 않은 경우를 잘 기억하고 용도에 맞게 사용하는 것이 중요합니다.

6.2.2 Middleware 사용 방법

Next.js에서 Middleware를 사용 하는 방법은 아래 그림과 같이 프로젝트의 루트 경로에 middleware.ts파일을 작성하면 됩니다.

만약 src 폴더를 사용한다면, src 폴더 안에 middleware.ts파일을 작성하면 됩니다.

Middleware example

아래 코드는 Next.js의 Middleware 예시 코드입니다. 이 Middleware는 /v1으로 시작하는 모든 경로 요청을 /v2라는 경로로 Redirection 시켜줍니다. 이렇게 하면 웹사이트를 전체 리뉴얼했을 때 쉽게 새로운 버전의 페이지로 사용자들을 Redirection 시킬 수 있습니다.

// middleware.ts

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

export function middleware(request: NextRequest) {
    return NextResponse.redirect(new URL('/v2', request.url));
}

export const config = {
    matcher: '/v1/:path*',
};

참고로 Next.js에서 Middleware는 모든 route path에 대해 실행됩니다. 그래서 특정 경로에서만 Middleware를 실행하거나, 반대로 일부 경로를 제외한 나머지 모든 경로에서 Middleware를 실행하고 싶을 경우에 경로를 매칭해주는 Matcher를 잘 사용하는 것이 중요합니다.

Matcher를 사용하는 방법은 아래와 같이 middleware.ts 파일의 config옵션에 matcher를 규칙에 맞게 작성하면 됩니다. 특정 경로에 대해서만 작성하거나, 여러 개의 경로에 대해서 작성하거나, 정규 표현식을 사용해서 작성할 수 있습니다. 그리고 missing 또는 has 옵션을 사용해서 작성할 수도 있습니다.

// 특정 경로에 대해서만 작성한 경우
export const config = {
    matcher: '/blog/:path*',
}
// 여러 개의 경로에 대해서 작성한 경우
export const config = {
    matcher: ['/blog/:path*', '/user/:path*'],
};
// 정규 표현식(Regular Expression)을 사용해서 작성한 경우
export const config = {
    matcher: [
        '/((?!api|_next/static|_next/image|favicon.ico).*)',
    ],
};
// missing 또는 has 옵션을 사용해서 작성한 경우
export const config = {
    matcher: [
        {
            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' },
            ],
        },
    ],
};

matcher의 값을 작성할 때 지켜야 할 규칙은 아래와 같습니다.

  • 무조건 /로 시작해야 함
  • named parameters를 포함할 수 있음
    • 예시: /posts/:postId/posts/1, /posts/2 등과 모두 매칭됨
  • modifiers를 포함할 수 있음
    • *: zero or more
    • ?: zero or one
    • +: one or more
  • 괄호 안에 정규 표현식을 사용할 수 있음
    • /blog/(.*)/blog/:path*와 동일

참고로 matcher의 값은 빌드 타임에 정적으로 분석되기 때문에, 동적인 값을 넣으면 안되고 상수 형태의 값만을 사용해야 한다는 점에 유의하기 바랍니다.

6.2.3 Middleware 사용 예시

지금부터는 Next.js에서 Middleware를 사용하는 다양한 예시들을 하나씩 살펴보도록 하겠습니다.

Middleware를 사용하면 아래와 같이 요청에 포함된 Cookie의 값을 조작하거나, 응답에 포함시킬 Cookie를 쉽게 넣을 수 있습니다.

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

// 요청에 "Cookie:author=soaple" header가 존재한다고 가정
export function middleware(request: NextRequest) {
    // Cookie 값 가져오기
    let cookie = request.cookies.get('author');
    console.log(cookie);
    // 출력 결과: { name: 'author', value: 'soaple', Path: '/' }

    const allCookies = request.cookies.getAll();
    console.log(allCookies);
    // 출력 결과: [{ name: 'author', value: 'soaple' }]

    request.cookies.has('author'); // 결과: true
    request.cookies.delete('author');
    request.cookies.has('author'); // 결과: false

    // 응답 Cookie 추가
    const response = NextResponse.next();
    response.cookies.set('framework', 'nextjs');
    response.cookies.set({
        name: 'framework',
        value: 'nextjs',
        path: '/',
    });
    cookie = response.cookies.get('framework');
    console.log(cookie);
    // 출력 결과: { name: 'framework', value: 'nextjs', Path: '/' }

    // 응답에 `Set-Cookie:framework=nextjs;path=/` header가 포함됨
    return response;
}

요청 및 응답 Header 조작하기

Middleware에서는 아래와 같이 NextResponse API를 사용하여 요청 및 응답 헤더를 조작할 수 있습니다.

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

export function middleware(request: NextRequest) {
    // 요청 헤더를 복제해서 새로운 헤더 추가
    const requestHeaders = new Headers(request.headers);
    requestHeaders.set('x-hello-from-middleware-1', 'hello');

    // NextResponse.next()를 사용하여 요청 헤더 설정
    const response = NextResponse.next({
        request: {
            // 새로운 요청 헤더
            headers: requestHeaders,
        },
    });

    // 새로운 응답 헤더 설정
    response.headers.set('x-hello-from-middleware-2', 'hello');

    return response;
}

Response 직접 반환

Middleware에서는 Response 또는 NextResponse 인스턴스를 직접 반환함으로써 곧바로 응답할 수도 있습니다. 아래 코드는 인증된 사용자만 API 경로에 접근할 수 있도록 하는 예시 코드입니다.

import { NextRequest } from 'next/server';
// isAuthenticated()는 요청에 대해 인증여부를 확인해주는 함수라고 가정
import { isAuthenticated } from '@/lib/auth';

export const config = {
    matcher: '/api/:path*',
};

export function middleware(request: NextRequest) {
    if (!isAuthenticated(request)) {
        return Response.json(
            { success: false, message: 'Unauthorized' },
            { status: 401 }
        );
    }
}

마지막 업데이트: 2025년 10월 24일 02시 21분

이 문서의 저작권은 이인제(소플)에 있습니다. 무단 전재와 무단 복제를 금합니다.

On this page