미들웨어
미들웨어를 사용하면 요청이 완료되기 전에 코드를 실행할 수 있습니다. 그런 다음 들어오는 요청에 따라 응답을 재작성, 리디렉션, 요청 또는 응답 헤더 수정, 또는 직접 응답함으로써 응답을 수정할 수 있습니다.
미들웨어는 캐시된 콘텐츠와 경로가 일치하기 전에 실행됩니다. 자세한 내용은 경로 매칭을 참조하세요.
사용 사례
애플리케이션에 미들웨어를 통합하면 성능, 보안 및 사용자 경험을 크게 향상시킬 수 있습니다. 미들웨어가 특히 효과적인 일반적인 시나리오는 다음과 같습니다:
- 인증 및 권한 부여: 특정 페이지나 API 경로에 대한 접근 권한을 부여하기 전에 사용자 신원을 확인하고 세션 쿠키를 확인합니다.
- 서버 측 리디렉션: 특정 조건(예: 로케일, 사용자 역할)에 따라 서버 수준에서 사용자를 리디렉션합니다.
- 경로 재작성: 요청 속성에 따라 API 경로나 페이지로 경로를 동적으로 재작성하여 A/B 테스팅, 기능 출시 또는 레거시 경로를 지원합니다.
- 봇 탐지: 봇 트래픽을 탐지하고 차단하여 리소스를 보호합니다.
- 로깅 및 분석: 페이지나 API에서 처리하기 전에 요청 데이터를 캡처하고 분석하여 인사이트를 얻습니다.
- 기능 플래깅: 기능을 동적으로 활성화 또는 비활성화하여 원활한 기능 출시나 테스팅을 수행합니다.
미들웨어가 최적의 접근 방식이 아닐 수 있는 상황을 인식하는 것도 중요합니다. 다음은 주의해야 할 몇 가지 시나리오입니다:
- 복잡한 데이터 가져오기 및 조작: 미들웨어는 직접적인 데이터 가져오기나 조작을 위해 설계되지 않았습니다. 이는 대신 Route Handlers나 서버 측 유틸리티 내에서 수행해야 합니다.
- 무거운 계산 작업: 미들웨어는 가볍고 빠르게 응답해야 하며, 그렇지 않으면 페이지 로드 지연을 일으킬 수 있습니다. 무거운 계산 작업이나 장기 실행 프로세스는 전용 Route Handlers 내에서 수행해야 합니다.
- 광범위한 세션 관리: 미들웨어가 기본적인 세션 작업을 관리할 수 있지만, 광범위한 세션 관리는 전용 인증 서비스나 Route Handlers 내에서 관리해야 합니다.
- 직접적인 데이터베이스 작업: 미들웨어 내에서 직접적인 데이터베이스 작업을 수행하는 것은 권장되지 않습니다. 데이터베이스 상호 작용은 Route Handlers나 서버 측 유틸리티 내에서 수행해야 합니다.
규칙
미들웨어를 정의하려면 프로젝트의 루트에 있는 middleware.ts
(또는 .js
) 파일을 사용하세요. 예를 들어, pages
나 app
과 같은 수준이나 해당되는 경우 src
내부에 위치시킵니다.
참고: 프로젝트당 하나의
middleware.ts
파일만 지원되지만, 여전히 미들웨어 로직을 모듈식으로 구성할 수 있습니다. 미들웨어 기능을 별도의.ts
또는.js
파일로 분리하고 이를 주middleware.ts
파일로 가져올 수 있습니다. 이를 통해 경로별 미들웨어를 더 깔끔하게 관리할 수 있으며, 중앙 집중식 제어를 위해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));
}
// 아래의 "경로 매칭"에서 자세히 알아보세요
export const config = {
matcher: "/about/:path*",
};
import { NextResponse } from "next/server";
// 이 함수는 내부에서 `await`를 사용하는 경우 `async`로 표시될 수 있습니다
export function middleware(request) {
return NextResponse.redirect(new URL("/home", request.url));
}
// 아래의 "경로 매칭"에서 자세히 알아보세요
export const config = {
matcher: "/about/:path*",
};
경로 매칭
미들웨어는 프로젝트의 모든 경로에 대해 호출됩니다. 이러한 이유로, 특정 경로를 정확하게 대상으로 하거나 제외하기 위해 매처를 사용하는 것이 중요합니다. 다음은 실행 순서입니다:
next.config.js
의headers
next.config.js
의redirects
- 미들웨어 (
rewrites
,redirects
등) next.config.js
의beforeFiles
(rewrites
)- 파일 시스템 경로 (
public/
,_next/static/
,pages/
,app/
등) next.config.js
의afterFiles
(rewrites
)- 동적 경로 (
/blog/[slug]
) next.config.js
의fallback
(rewrites
)
미들웨어가 실행될 경로를 정의하는 방법에는 두 가지가 있습니다:
매처 (Matcher)
matcher
를 사용하면 특정 경로에서 미들웨어를 실행하도록 필터링할 수 있습니다.
export const config = {
matcher: "/about/:path*",
};
배열 구문을 사용하여 단일 경로 또는 여러 경로를 일치시킬 수 있습니다:
export const config = {
matcher: ["/about/:path*", "/dashboard/:path*"],
};
matcher
구성은 완전한 정규식을 허용하므로 부정적 선행 확인(negative lookahead)나 문자 매칭과 같은 일치가 지원됩니다. 특정 경로를 제외한 모든 경로와 일치하는 부정적 선행 확인(negative lookahead)의 예는 다음과 같습니다:
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: [
/*
* 다음으로 시작하는 경로를 제외한 모든 요청 경로와 일치:
* - 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" }],
},
],
};
알아두면 좋은 점:
matcher
값은 빌드 시 정적으로 분석될 수 있도록 상수여야 합니다. 변수와 같은 동적 값은 무시됩니다.
구성된 매처:
- 반드시
/
로 시작해야 합니다 - 명명된 매개변수를 포함할 수 있습니다:
/about/:path
는/about/a
와/about/b
와 일치하지만/about/a/c
와는 일치하지 않습니다 - 명명된 매개변수에 수정자를 가질 수 있습니다(
:
로 시작):/about/:path*
는/about/a/b/c
와 일치합니다.*
는 0개 이상입니다.?
는 0개 또는 1개이고+
는 1개 이상입니다 - 괄호로 묶인 정규식을 사용할 수 있습니다:
/about/(.*)
는/about/:path*
와 동일합니다
자세한 내용은 path-to-regexp 문서를 참조하세요.
알아두면 좋은 점: 하위 호환성을 위해 Next.js는 항상
/public
을/public/index
로 간주합니다. 따라서/public/:path
매처가 일치할 것입니다.
조건문
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));
}
}
import { NextResponse } from "next/server";
export function middleware(request) {
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));
}
}
NextResponse
NextResponse
API를 사용하면 다음을 수행할 수 있습니다:
- 들어오는 요청을 다른 URL로
redirect
- 주어진 URL을 표시하여 응답을
rewrite
- API 경로,
getServerSideProps
및rewrite
대상에 대한 요청 헤더 설정 - 응답 쿠키 설정
- 응답 헤더 설정
미들웨어에서 응답을 생성하려면 다음과 같이 할 수 있습니다:
쿠키(Cookie) 사용
쿠키는 일반적인 헤더입니다. Request
에서는 Cookie
헤더에 저장되고, Response
에서는 Set-Cookie
헤더에 저장됩니다. Next.js는 NextRequest
와 NextResponse
의 cookies
확장을 통해 이러한 쿠키에 접근하고 조작하는 편리한 방법을 제공합니다.
- 들어오는 요청(request)의 경우,
cookies
는 다음과 같은 메서드를 제공합니다:get
,getAll
,set
, 그리고delete
쿠키.has
로 쿠키의 존재 여부를 확인하거나clear
로 모든 쿠키를 제거할 수 있습니다. - 나가는 응답(response)의 경우,
cookies
는 다음과 같은 메서드를 가집니다:get
,getAll
,set
, 그리고delete
.
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;
}
import { NextResponse } from "next/server";
export function middleware(request) {
// 들어오는 요청에 "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=/test` 헤더가 있을 것입니다.
return response;
}
헤더(Header) 설정
NextResponse
API를 사용하여 요청 및 응답 헤더를 설정할 수 있습니다 (요청 헤더 설정은 Next.js v13.0.0부터 사용 가능).
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;
}
import { NextResponse } from "next/server";
export function middleware(request) {
// 요청 헤더를 복제하고 새 헤더 `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;
}
알아두면 좋은 점: 백엔드 웹 서버 구성에 따라 큰 헤더를 설정하면 431 Request Header Fields Too Large 오류가 발생할 수 있으므로 주의하세요.
CORS
미들웨어에서 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) {
// 요청의 오리진을 확인합니다
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*",
};
import { 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) {
// 요청의 오리진을 확인합니다
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*",
};
알아두면 좋은 점: 라우트 핸들러에서 개별 라우트에 대한 CORS 헤더를 구성할 수 있습니다.
응답(Response) 생성
미들웨어에서 직접 Response
또는 NextResponse
인스턴스를 반환하여 응답할 수 있습니다. (이는 Next.js v13.1.0부터 사용 가능합니다)
import type { NextRequest } from "next/server";
import { isAuthenticated } from "@lib/auth";
// 미들웨어를 `/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 }
);
}
}
import { isAuthenticated } from "@lib/auth";
// 미들웨어를 `/api/`로 시작하는 경로로 제한합니다
export const config = {
matcher: "/api/:function*",
};
export function middleware(request) {
// 요청을 확인하기 위해 인증 함수를 호출합니다
if (!isAuthenticated(request)) {
// 오류 메시지를 나타내는 JSON으로 응답합니다
return Response.json(
{ success: false, message: "authentication failed" },
{ status: 401 }
);
}
}
waitUntil
과 NextFetchEvent
NextFetchEvent
객체는 네이티브 FetchEvent
객체를 확장하며, waitUntil()
메서드를 포함합니다.
waitUntil()
메서드는 프로미스를 인자로 받아, 프로미스가 해결될 때까지 미들웨어의 수명을 연장합니다. 이는 백그라운드에서 작업을 수행하는 데 유용합니다.
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();
}
고급 미들웨어 플래그
Next.js의 v13.1
에서는 고급 사용 사례를 처리하기 위해 skipMiddlewareUrlNormalize
와 skipTrailingSlashRedirect
라는 두 가지 추가 플래그가 도입되었습니다.
skipTrailingSlashRedirect
는 후행 슬래시를 추가하거나 제거하는 Next.js의 리디렉션을 비활성화합니다. 이를 통해 미들웨어 내에서 일부 경로에 대해서는 후행 슬래시를 유지하고 다른 경로에 대해서는 그렇지 않도록 사용자 정의 처리를 할 수 있어, 점진적인 마이그레이션을 더 쉽게 만들 수 있습니다.
module.exports = {
skipTrailingSlashRedirect: true,
};
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)
);
}
}
skipMiddlewareUrlNormalize
는 Next.js의 URL 정규화를 비활성화하여 직접 방문과 클라이언트 전환을 동일하게 처리할 수 있게 합니다. 일부 고급 사례에서 이 옵션은 원본 URL을 사용하여 완전한 제어를 제공합니다.
module.exports = {
skipMiddlewareUrlNormalize: true,
};
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로 정규화되었을 것입니다
}
런타임
미들웨어는 현재 Edge 런타임만 지원합니다. Node.js 런타임은 사용할 수 없습니다.
버전 히스토리
버전 | 변경 사항 |
---|---|
v13.1.0 | 고급 미들웨어 플래그 추가 |
v13.0.0 | 미들웨어가 요청 헤더, 응답 헤더를 수정하고 응답을 보낼 수 있음 |
v12.2.0 | 미들웨어가 안정화됨, 업그레이드 가이드를 참조하세요 |
v12.0.9 | Edge Runtime에서 절대 URL 강제 적용 (PR) |
v12.0.0 | 미들웨어(베타) 추가 |