미들웨어는 요청이 완료되기 전에 코드를 실행할 수 있도록 해준다. 이를 통해 들어오는 요청을 기반으로 응답을 수정할 수 있으며, Rewriting, Redirecting, 응답 헤더 수정, 직접 응답 반환과 같은 작업을 수행할 수 있다. 미들웨어는 캐시된 콘텐츠나 라우트가 매칭되기 전에 실행된다. 미들웨어는 Edge 런타임만 지원한다. Node.js 런타임에서는 사용할 수 없지만, 미들웨어가 실행될 때, Next.js는 자동으로 Edge Runtime에서 실행되도록 설정된다.
주요 활용 사례
인증 및 권한 관리: 사용자를 확인하고 세션 쿠키를 검사하여 특정 페이지나 API 경로에 대한 접근을 제어한다.
서버 사이드 리디렉션: 사용자의 로케일, 역할, 로그인 상태 등에 따라 서버에서 직접 redirect 처리한다.
경로 재작성: A/B 테스트, 기능 롤아웃, 레거시 URL 지원을 위해 요청 경로를 동적으로 변경한다.
봇 탐지 및 차단: 악의적인 봇 트래픽을 감지하고 특정 조건에 따라 차단하여 보안을 강화한다.
로그 및 분석: 사용자의 요청 데이터를 분석하여 트래픽 패턴을 파악하고 성능 최적화에 활용한다.
미들웨어를 사용하지 않는 것이 더 적절한 경우
복잡한 데이터 가져오기 및 조작: 미들웨어는 직접적인 데이터 가져오기나 조작을 위해 설계되지 않았다.
무거운 계산 작업: 미들웨어는 가볍고 빠르게 반응해야 하며 그렇지 않으면 페이지 로드가 지연될 수 있다.
광범위한 세션 관리: 광범위한 세션 관리는 전담 인증 서비스나 Route Handler 내에서 관리해야 한다.
직접 데이터베이스 작업: 데이터베이스 상호 작용은 Route Handler 또는 서버 측 유틸리티 내에서 수행해야 한다.
Convention
프로젝트 루트에 middleware.ts (또는 middleware.js) 파일을 생성해야 한다. pages, app과 같은 최상위 폴더와 같은 레벨에 위치하거나, src 폴더 내부에 둘 수도 있다.
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
return NextResponse.redirect(new URL('/home', request.url))
}
export const config = {
matcher: '/about/:path*',
}
NextJS에서는 프로젝트당 하나의 middleware.ts 파일만 지원한다. 하지만 미들웨어 로직을 여러 개의 파일로 나누고, 주요 middleware.ts에서 필요한 로직을 가져와서(import) 사용할 수 있다.
project-root
├─ src
│ ├─ middleware
│ │ ├─ auth.ts // 인증 관련 미들웨어
│ │ ├─ logging.ts // 로그 기록 미들웨어
│ │ └─ rewrite.ts // 경로 재작성 미들웨어
│ └─ middleware.ts // 모든 미들웨어를 불러오는 메인 파일
└─ ...
// src/middleware/auth.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function authMiddleware(request: NextRequest) {
const token = request.cookies.get('auth-token');
if (!token) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
// src/middleware/logging.ts
export function loggingMiddleware(request: Request) {
console.log(`Request received: ${request.url}`);
}
// src/middleware.ts
import { authMiddleware } from './middleware/auth';
import { loggingMiddleware } from './middleware/logging';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
loggingMiddleware(request);
const authResponse = authMiddleware(request);
if (authResponse) return authResponse; // 인증 실패 시 즉시 응답
return Response.next();
}
미들웨어 실행 순서
A. next.config.js의 headers 설정. 뒤에서 미들웨어가 'X-Custom-Header'를 다른 값으로 덮어씌울 수 있다.
module.exports = {
async headers() {
return [
{
source: '/secure-page', // 특정 페이지 요청 시
headers: [
{
key: 'X-Custom-Header',
value: 'MyHeaderValue', // 헤더 추가
},
],
},
];
},
};
B. next.config.js의 redirects 설정. 이 단계에서 redirects가 실행되면 새 요청으로 처음부터 다시 진행된다.
module.exports = {
async redirects() {
return [
{
source: '/old-route',
destination: '/new-route',
permanent: true, // 301 리디렉트
},
];
},
};
C. middleware.js 실행. next.config.js의 headers 설정을 덮어씌운다. middleware.js의 redirects가 실행되면 새 요청으로 처음부터 다시 진행된다. middleware.js에서 rewrites가 실행됐다면 정적 파일을 탐색하고 만약 경로를 찾지 못하면 404.tsx를 보여준다.
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// 특정 경로를 다른 경로로 리디렉트
if (request.nextUrl.pathname === '/middleware-redirect') {
return NextResponse.redirect(new URL('/redirected-page', request.url));
}
// 특정 경로를 내부적으로 재작성 (rewrite)
if (request.nextUrl.pathname === '/middleware-rewrite') {
return NextResponse.rewrite(new URL('/new-path', request.url));
}
// 응답 헤더 수정
const response = NextResponse.next();
response.headers.set('X-Middleware-Header', 'Middleware Applied');
return response;
}
export const config = {
matcher: ['/middleware-redirect', '/middleware-rewrite'], // 특정 경로에서만 실행
};
D. next.config.js의 beforeFiles rewrites 설정. 앞서 rewrites가 실행됐다면 이 과정은 스킵된다.
module.exports = {
async rewrites() {
return {
beforeFiles: [
{
source: '/before-files-rewrite',
destination: '/before-files-page',
},
]
}
},
};
E. 정적 파일 탐색. 현재 단계까지 결정된 destination을 정적파일에서 탐색한다. 앞서 rewrite가 실행되지 않았을 경우, 모든 단계를 진행하고도 맞는 경로를 찾지못하면 404.tsx를 보여준다.
pages
├─ index.tsx (→ /)
├─ about.tsx (→ /about)
├─ actual-page.tsx (→ /actual-page)
└─ 404.tsx (→ 없는 페이지 요청 시)
F. next.config.js의 afterFiles rewrites 설정. 정적 파일에서 destination을 찾지 못하면 afterFiles rewrites을 실행한다. 앞서 rewrites가 실행됐다면 이 과정은 스킵된다.
module.exports = {
async rewrites() {
return {
afterFiles: [
{
source: '/after-files-rewrite',
destination: '/after-files-page'
},
]
}
},
};
G. /blog/[slug] 같은 Dynamic Routes를 탐색. 앞서 rewrite가 실행되었다면 이 과정은 스킵된다.
import { useRouter } from 'next/router';
export default function BlogPost() {
const { slug } = useRouter().query;
return <h1>블로그 게시글: {slug}</h1>;
}
H. next.config.js의 fallback rewrites 설정. 모든 매칭 실패 시 최후의 대체 경로로 fallback rewrites을 실행한다. 앞서 rewrites가 실행됐다면 이 과정은 스킵된다. 여기서도 경로를 찾지 못하게 되면 정적 파일 탐색의 404.tsx를 보여준다.
module.exports = {
async rewrites() {
return {
fallback: [
{
source: '/b',
destination: '/fallback-page'
},
],
};
},
};
Matcher
matcher는 Middleware를 특정 경로에서만 실행되도록 필터링하는 기능이다. 배열을 사용해서 여러 경로를 동시에 지정 가능하고, 정규식을 활용하여 특정 경로를 제외하거나 포함 가능하다. matcher 값은 빌드 타임에 정적으로 분석되어야 하므로 변수 값을 사용할 수 없다. 경로는 반드시 '/'로 시작해야한다.
':path'는 동적인 값을 의미하므로 '/about/a', '/about/b' 등 한 개의 경로 세그먼트만 허용된다. '/about/c/d' 처럼 슬래시(/)가 포함된 경우는 매칭되지 않는다.
':path*'는 0개 이상의 세그먼트를 허용하며, '/about', '/about/a', '/about/a/b/c' 등 모든 하위 경로와 매칭된다. ':path?'는 세그먼트가 0개 또는 1개 이상일 때만 매칭된다. '/about' 또는 '/about/a'는 매칭되지만 '/about/a/b'는 매칭되지 않는다. ':path+'는 매개변수가 1개 이상일 때만 매칭된다. '/about'은 매칭되지 않지만, '/about/a', '/about/a/b/c' 등은 매칭된다.
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 배열 또눈 두 가지를 섞어서 일부 요청을 건너뛰기할 수 있다.
// 특정 헤더가 없는 요청에서만 미들웨어 실행
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' },
],
},
],
}
// 특정 헤더가 없고 특정 헤더가 있는 요청에서만 미들웨어 실행
export const config = {
matcher: [
{
source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
has: [
{ type: 'header', key: 'x-present' },
],
missing: [
{ type: 'header', key: 'x-missing', value: 'prefetch' },
],
},
],
}
미들웨어에서 조건문
미들웨어에서 조건문을 사용하면 특정 경로에 따라 다른 동작을 수행할 수 있다. 특정 요청이 들어왔을 때 리라이트(rewrite) 또는 리디렉트(redirect) 가능하며, 조건문을 활용하면 여러 개의 경로를 한 번에 처리 가능하다.
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// "/about" 페이지 요청이 들어오면 "/about-2"로 리라이트
if (request.nextUrl.pathname.startsWith('/about')) {
return NextResponse.rewrite(new URL('/about-2', request.url))
}
// "/dashboard" 페이지 요청이 들어오면 "/dashboard/user"로 리라이트
if (request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.rewrite(new URL('/dashboard/user', request.url))
}
}
NextResponse API 사용 예시
특정 URL로 사용자를 이동시키는 Redirect를 사용할 수 있다.
import { NextResponse } from "next/server";
export function middleware(req) {
return NextResponse.redirect("https://example.com");
}
사용자가 다른 URL을 요청해도 실제로는 다른 경로의 데이터를 반환하는 rewrite를 사용할 수 있다.
import { NextResponse } from "next/server";
export function middleware(req) {
return NextResponse.rewrite(new URL("/new-path", req.url));
}
모든 요청에 헤더를 설정할 수 있다.
import { NextResponse } from "next/server";
export function middleware(req) {
const requestHeaders = new Headers(req.headers);
requestHeaders.set("x-custom-header", "my-value");
const response = NextResponse.next({
request: {
headers: requestHeaders,
},
});
return response;
}
모든 응답에 헤더를 설정할 수 있다.
import { NextResponse } from "next/server";
export function middleware(req) {
const res = NextResponse.next();
res.headers.set("x-powered-by", "Next.js");
return res;
}
모든 응답에 쿠키를 설정할 수 있다.
import { NextResponse } from "next/server";
export function middleware(req) {
const res = NextResponse.next();
res.cookies.set("user-token", "123456", { httpOnly: true });
return res;
}
Middleware에서 직접 응답을 생성 및 수정할 수 있다.
import { NextResponse } from "next/server";
export function middleware(req) {
return new NextResponse("Forbidden", { status: 403 });
}
import { NextRequest } from "next/server";
import { isAuthenticated } from "@lib/auth"; // 가정된 인증 함수
// 특정 경로(`/api/*`)에만 Middleware 적용
export const config = {
matcher: "/api/:function*",
};
export function middleware(request: NextRequest) {
// 인증 체크
if (!isAuthenticated(request)) {
return Response.json(
{ success: false, message: "authentication failed" },
{ status: 401 }
);
}
return NextResponse.next();
}
Using Cookies
NextJS에서는 NextRequest와 NextResponse 객체를 활용해 쿠키를 쉽게 다룰 수 있다.
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// "nextjs=fast" 쿠키가 있는 요청을 가정
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' }]
console.log(request.cookies.has('nextjs')) // true
request.cookies.delete('nextjs') // 쿠키 삭제
console.log(request.cookies.has('nextjs')) // false
const response = NextResponse.next()
// 쿠키 설정
response.cookies.set('vercel', 'fast')
response.cookies.set({
name: 'vercel',
value: 'fast',
path: '/dashboard', // /dashboard, /dasboard/a, /dashbaord/a/b
})
const cookie = response.cookies.get('vercel')
console.log(cookie) // { name: 'vercel', value: 'fast', Path: '/' }
return response
}
Setting Headers
NextResponse.next()를 사용하여 요청과 응답 헤더를 모두 설정 가능하다. 너무 큰 헤더를 설정하면 "431 Request Header Fields Too Large" 오류 발생 가능하다.
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-middleware1', 'hello')
// 요청 헤더를 포함하여 NextResponse 생성
const response = NextResponse.next({
request: {
headers: requestHeaders, // 새로운 요청 헤더 적용
},
})
// 응답 헤더 설정
response.headers.set('x-hello-from-middleware2', 'hello')
return response
}