Next.js Middleware Routing Deep Dive
Next.js Middleware를 활용하여 강력한 라우팅 제어, 인증, 국제화 및 요청/응답 조작 방법을 심층적으로 알아봅니다. 실전 코드 예제로 Next.js 애플리케이션의 유연성을 극대화하세요.
Next.js Middleware Routing Deep Dive
현대 웹 애플리케이션은 단순한 정적 페이지를 넘어 사용자 경험과 비즈니스 로직이 밀접하게 연결된 동적인 환경을 요구합니다. 이러한 요구사항을 충족시키기 위해 Next.js는 강력한 기능을 제공하며, 그중에서도 Middleware는 애플리케이션의 요청 처리 흐름을 유연하게 제어할 수 있는 핵심 도구입니다. 이 글에서는 Next.js Middleware가 무엇인지부터 시작하여, 실제 프로젝트에서 라우팅 제어, 인증, 국제화 등 다양한 시나리오에 어떻게 활용할 수 있는지 실전 코드 예제와 함께 자세히 살펴보겠습니다.
Next.js Middleware란 무엇인가요?
Next.js Middleware는 사용자의 요청이 완료되기 전에 실행되는 함수입니다. 서버 사이드 렌더링(SSR)이나 정적 사이트 생성(SSG)이 이루어지기 전에 요청을 가로채서 특정 로직을 수행할 수 있도록 해주는 Next.js의 특별한 기능입니다. 이 미들웨어는 Vercel의 Edge Functions 환경에서 실행되므로, 전 세계 사용자에게 낮은 지연 시간으로 빠른 응답을 제공할 수 있다는 장점을 가집니다.
미들웨어는 모든 요청에 대해 실행될 수 있으며, 특정 경로에 대해서만 실행되도록 matcher 설정을 통해 제어할 수도 있습니다. NextRequest 객체를 통해 들어오는 요청에 대한 정보를 얻고, NextResponse 객체를 통해 응답을 조작할 수 있습니다. 이를 통해 다음과 같은 다양한 기능을 구현할 수 있습니다.
- Rewrite: URL을 마스킹하여 다른 내부 경로로 다시 작성합니다. (예:
/blog/1을/posts/1로) - Redirect: 사용자를 다른 URL로 리다이렉션합니다. (예: 로그인되지 않은 사용자를
/login으로) - Header 조작: 요청 또는 응답 헤더를 추가, 수정, 삭제합니다.
- Cookie 조작: 쿠키를 설정하거나 읽습니다.
- A/B 테스트: 사용자 그룹에 따라 다른 페이지를 제공합니다.
- 인증 및 권한 부여: 특정 경로에 대한 접근을 제한합니다.
- 국제화(i18n): 사용자의 언어 설정에 따라 적절한 로케일 페이지로 라우팅합니다.
Middleware 설정 및 기본 사용법
Next.js에서 미들웨어를 사용하려면 프로젝트의 루트 디렉터리에 middleware.ts (또는 middleware.js) 파일을 생성해야 합니다. 이 파일에 작성된 함수는 애플리케이션의 모든 경로에 대한 요청 이전에 실행됩니다.
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// 모든 요청에 대해 실행됩니다.
console.log('Middleware executed for:', request.nextUrl.pathname);
// 특정 조건을 만족할 경우 리다이렉트 예시
// if (request.nextUrl.pathname.startsWith('/admin')) {
// return NextResponse.redirect(new URL('/login', request.url));
// }
// 요청을 그대로 통과시킵니다.
return NextResponse.next();
}
// 미들웨어를 실행할 경로를 지정하는 matcher 설정
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* - api (API routes)
* - any files in the public folder
*/
'/((?!api|_next/static|_next/image|favicon.ico|images|svgs).*)',
],
};
middleware 함수는 NextRequest 객체를 인자로 받으며, NextResponse 객체를 반환합니다. NextResponse.next()는 요청을 다음 미들웨어 또는 페이지로 계속 진행시키라는 의미입니다. 만약 NextResponse.redirect()나 NextResponse.rewrite()를 사용하면 요청의 흐름을 변경하게 됩니다.
config 객체의 matcher 속성은 미들웨어가 실행될 경로를 정규식 패턴으로 정의합니다. 위 예시에서는 api 경로, _next/static, _next/image, favicon.ico, images, svgs 등 정적 파일 관련 경로는 제외하고 모든 경로에 대해 미들웨어를 실행하도록 설정하고 있습니다. 이는 미들웨어의 불필요한 실행을 막아 성능을 최적화하는 데 매우 중요합니다.
라우팅 제어: Rewrite와 Redirect
Next.js Middleware의 강력한 기능 중 하나는 요청의 라우팅 흐름을 제어하는 rewrite와 redirect입니다. 이 둘은 비슷해 보이지만 중요한 차이점이 있습니다.
Rewrite (URL 재작성)
rewrite는 브라우저의 URL은 변경하지 않고, 내부적으로 다른 경로의 콘텐츠를 렌더링하도록 요청을 재작성합니다. 이는 마치 URL 마스킹과 같습니다. 사용자는 현재 URL이 /blog/my-post이지만, 실제로는 /posts/my-post-id 경로의 컴포넌트가 렌더링되는 식입니다.
주요 활용 사례:
- A/B 테스트: 특정 사용자 그룹에게 다른 버전의 페이지를 보여줄 때.
- Feature Flag: 특정 기능의 활성화 여부에 따라 다른 페이지를 보여줄 때.
- SEO 친화적인 URL: 내부적으로 복잡한 경로를 가지고 있지만, 사용자에게는 간단하고 의미 있는 URL을 노출하고 싶을 때.
// middleware.ts (Rewrite 예제)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// '/old-product' 경로로 접근하면, 실제로는 '/products/legacy-product' 페이지를 보여줍니다.
// 브라우저 주소창에는 여전히 '/old-product'가 표시됩니다.
if (pathname === '/old-product') {
return NextResponse.rewrite(new URL('/products/legacy-product', request.url));
}
// '/blog/:slug' 패턴의 URL을 '/posts/:slug'로 재작성
// 예: /blog/hello-world -> /posts/hello-world
if (pathname.startsWith('/blog/')) {
const newPath = pathname.replace('/blog/', '/posts/');
return NextResponse.rewrite(new URL(newPath, request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/old-product', '/blog/:path*'],
};
Redirect (URL 리다이렉션)
redirect는 사용자를 완전히 다른 URL로 이동시킵니다. 브라우저의 URL이 변경되며, HTTP 상태 코드(301 영구 이동, 302 임시 이동)를 통해 검색 엔진에도 변경 사항이 전달됩니다.
주요 활용 사례:
- 인증: 로그인되지 않은 사용자가 보호된 페이지에 접근하려고 할 때 로그인 페이지로 이동시킬 때.
- URL 변경: 페이지의 URL 구조를 변경했을 때, 이전 URL로 접근하는 사용자를 새 URL로 안내할 때.
- 권한 부여: 특정 권한이 없는 사용자를 접근 불가능 페이지로 이동시킬 때.
// middleware.ts (Redirect 예제)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// '/legacy-url' 경로로 접근하면, '/new-url'로 영구적으로 리다이렉션합니다.
// 브라우저 주소창도 '/new-url'로 변경됩니다.
if (pathname === '/legacy-url') {
return NextResponse.redirect(new URL('/new-url', request.url), 301); // 301: 영구 이동
}
// '/dashboard' 경로에 접근 시, 특정 조건에 따라 리다이렉트
const isAuthenticated = request.cookies.has('session_token'); // 예시: 세션 토큰 확인
if (pathname.startsWith('/dashboard') && !isAuthenticated) {
return NextResponse.redirect(new URL('/login', request.url)); // 302: 임시 이동 (기본값)
}
return NextResponse.next();
}
export const config = {
matcher: ['/legacy-url', '/dashboard/:path*'],
};
인증(Authentication) 미들웨어 구현
Next.js Middleware는 사용자 인증 로직을 중앙에서 관리하는 데 매우 효과적입니다. 특정 페이지나 경로 그룹에 접근하기 전에 사용자의 인증 상태를 확인하고, 인증되지 않았다면 로그인 페이지로 리다이렉션할 수 있습니다.
// middleware.ts (인증 예제)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
const protectedRoutes = ['/dashboard', '/profile', '/settings'];
const publicRoutes = ['/login', '/signup', '/'];
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// 예시: 'session_token' 쿠키로 사용자 인증 상태 확인
const isAuthenticated = request.cookies.has('session_token');
// 보호된 경로에 접근하려 하지만 인증되지 않은 경우
if (protectedRoutes.some(route => pathname.startsWith(route)) && !isAuthenticated) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('redirect', pathname); // 로그인 후 원래 페이지로 리다이렉트하기 위해 쿼리 파라미터 추가
return NextResponse.redirect(loginUrl);
}
// 이미 인증된 사용자가 로그인/회원가입 페이지에 접근하려는 경우 대시보드로 리다이렉트
if (publicRoutes.some(route => pathname === route) && isAuthenticated && !pathname.startsWith('/')) {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* - api (API routes)
* - public folder files (e.g., /images, /svgs)
*/
'/((?!api|_next/static|_next/image|favicon.ico|images|svgs).*)',
],
};위 예제에서는 session_token이라는 쿠키의 존재 여부로 인증 상태를 판단합니다. 실제 애플리케이션에서는 JWT(JSON Web Token) 검증, 세션 ID 확인 등 더 복잡한 로직이 필요할 수 있습니다. 중요한 점은 클라이언트 측 JavaScript가 로드되기 전에 서버의 Edge 환경에서 이 로직이 실행되어, 인증되지 않은 사용자에게는 보호된 페이지의 콘텐츠가 아예 전달되지 않는다는 것입니다.
국제화(Internationalization) 미들웨어 활용
다국어 웹사이트는 사용자 경험을 향상시키는 중요한 요소입니다. Next.js Middleware는 사용자의 선호 언어에 따라 적절한 로케일(locale) 페이지로 라우팅하는 데 유용하게 사용될 수 있습니다.
// middleware.ts (국제화 예제)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
const PUBLIC_FILE = /\.(.*)$/; // public 폴더의 파일을 제외하기 위한 정규식
const locales = ['en', 'ko', 'ja'];
const defaultLocale = 'en';
function getLocale(request: NextRequest) {
// 1. 쿠키에서 언어 설정 확인
const localeFromCookie = request.cookies.get('NEXT_LOCALE')?.value;
if (localeFromCookie && locales.includes(localeFromCookie)) {
return localeFromCookie;
}
// 2. Accept-Language 헤더에서 언어 설정 확인
const acceptLanguageHeader = request.headers.get('Accept-Language');
if (acceptLanguageHeader) {
const preferredLocales = acceptLanguageHeader.split(',').map(lang => lang.split(';')[0].trim());
for (const preferred of preferredLocales) {
const match = locales.find(locale => preferred.startsWith(locale));
if (match) {
return match;
}
}
}
// 3. 기본 언어 반환
return defaultLocale;
}
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// 정적 파일, API 경로, Next.js 내부 경로 등은 미들웨어 처리를 건너뜀
if (
PUBLIC_FILE.test(pathname) ||
pathname.startsWith('/api') ||
pathname.startsWith('/_next')
) {
return NextResponse.next();
}
// 이미 로케일이 URL에 포함되어 있는 경우 (예: /en/about)
const pathnameHasLocale = locales.some(
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
);
if (pathnameHasLocale) {
return NextResponse.next();
}
// URL에 로케일이 없는 경우, 적절한 로케일을 감지하여 리라이트
const locale = getLocale(request);
request.nextUrl.pathname = `/${locale}${pathname}`;
// 쿠키에 로케일 설정 (다음 요청을 위해)
const response = NextResponse.rewrite(request.nextUrl);
response.cookies.set('NEXT_LOCALE', locale, {
maxAge: 60 * 60 * 24 * 30, // 30 days
path: '/',
});
return response;
}
export const config = {
matcher: [
// '/:path*'는 모든 경로를 의미하며, 이미 제외된 정적 파일 등은 제외됩니다.
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
};
이 미들웨어는 다음과 같은 방식으로 작동합니다.
- 로케일 감지:
getLocale함수는 먼저NEXT_LOCALE쿠키를 확인하고, 없으면Accept-Language헤더를 분석하여 사용자의 선호 언어를 감지합니다. - 로케일 경로 리라이트: 감지된 로케일을 기반으로
/about과 같은 경로를/{locale}/about(예:/ko/about)으로 재작성합니다. 브라우저의 URL은 변경되지 않지만, 실제로는 로케일이 적용된 페이지가 렌더링됩니다. - 쿠키 설정: 다음 요청을 위해 감지된 로케일을
NEXT_LOCALE쿠키에 저장합니다.
이를 통해 사용자는 별도의 설정 없이 자신의 선호 언어에 맞는 콘텐츠를 자동으로 볼 수 있게 됩니다.
헤더 및 쿠키 조작
Middleware를 사용하면 요청(Request) 및 응답(Response)의 헤더와 쿠키를 자유롭게 조작할 수 있습니다. 이는 A/B 테스트, 캐싱 전략, 사용자 세션 관리 등 다양한 고급 시나리오에 활용됩니다.
// middleware.ts (헤더 및 쿠키 조작 예제)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const response = NextResponse.next();
// 1. 요청 헤더에 사용자 정의 정보 추가 (예: A/B 테스트 그룹)
// 이 헤더는 서버 컴포넌트나 API 라우트에서 request.headers.get('X-AB-Test-Group')으로 접근 가능합니다.
const userAgent = request.headers.get('user-agent');
if (userAgent && userAgent.includes('Mobile')) {
response.headers.set('X-Device-Type', 'Mobile');
} else {
response.headers.set('X-Device-Type', 'Desktop');
}
// 2. 응답 헤더 설정 (예: 보안 헤더)
response.headers.set('X-Content-Type-Options', 'nosniff');
response.headers.set('X-Frame-Options', 'DENY');
response.headers.set('Strict-Transport-Security', 'max-age=63072000; includeSubDomains; preload');
// 3. 쿠키 설정 또는 삭제
// 'theme' 쿠키가 없으면 'light'로 설정
if (!request.cookies.has('theme')) {
response.cookies.set('theme', 'light', {
maxAge: 60 * 60 * 24 * 365, // 1년
path: '/',
httpOnly: true, // JavaScript에서 접근 불가
secure: process.env.NODE_ENV === 'production', // HTTPS에서만 전송
});
}
// 'old_cookie' 쿠키 삭제
// response.cookies.delete('old_cookie');
return response;
}
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico|images|svgs).*)',
],
};
이 예시에서는 다음과 같은 조작을 수행합니다.
- 요청 헤더 추가:
User-Agent를 분석하여X-Device-Type헤더를 추가합니다. 이 정보는 이후 서버 컴포넌트나 API 라우트에서 활용될 수 있습니다. - 응답 헤더 설정: 웹 보안을 강화하기 위한
X-Content-Type-Options,X-Frame-Options,Strict-Transport-Security등의 헤더를 설정합니다. - 쿠키 설정: 사용자의 테마 설정을 저장하는
theme쿠키가 없는 경우light테마를 기본으로 설정합니다.httpOnly와secure옵션을 통해 보안을 강화할 수 있습니다.
성능 고려사항 및 주의점
Next.js Middleware는 강력하지만, 효율적으로 사용하지 않으면 애플리케이션의 성능에 부정적인 영향을 미칠 수 있습니다.
- Edge Runtime의 특성 이해: 미들웨어는 Vercel Edge Runtime에서 실행됩니다. 이는 빠르고 전역적으로 분산되어 있지만, Node.js 환경과 달리 특정 API(예: 파일 시스템 접근, 복잡한 데이터베이스 쿼리)에 제한이 있을 수 있습니다. 가볍고 빠른 로직을 작성하는 것이 중요합니다.
-
matcher최적화: 미들웨어가 불필요한 경로에서 실행되지 않도록config.matcher를 최대한 구체적으로 설정하세요. 모든 요청에 대해 미들웨어를 실행하는 것은 오버헤드를 증가시킬 수 있습니다. - 비동기 작업 주의: 미들웨어 내에서
fetch와 같은 비동기 작업을 수행할 수 있지만, 이러한 작업이 많아지면 요청 처리 시간이 길어질 수 있습니다. 가능한 한 동기적으로 처리하거나, 필수적인 비동기 작업만 포함하는 것이 좋습니다. - 로깅 및 디버깅: Edge Runtime의 특성상 로컬 개발 환경과 프로덕션 환경에서의 로깅 및 디버깅 방식이 다를 수 있습니다. Vercel 배포 시 대시보드에서 Edge Function 로그를 확인하는 방법을 숙지해야 합니다.
- 에러 핸들링: 미들웨어 내부에서 발생하는 예상치 못한 에러는 애플리케이션 전체에 영향을 미칠 수 있습니다. 견고한 에러 핸들링 로직을 포함하여 안정성을 확보하세요.
마무리
Next.js Middleware는 요청-응답 주기를 가로채어 애플리케이션의 동작을 유연하게 제어할 수 있는 매우 강력한 도구입니다. 이 글에서 다룬 라우팅 제어, 인증, 국제화, 헤더/쿠키 조작 외에도 A/B 테스트, Feature Flag, 데이터 로깅 등 무궁무진한 활용 가능성을 가지고 있습니다. 성능 고려사항과 주의점을 염두에 두고 Next.js Middleware를 적절히 활용한다면, 더욱 견고하고 사용자 친화적인 웹 애플리케이션을 구축할 수 있을 것입니다.
관련 게시글
React Server Components (RSC) 심층 가이드: Next.js App Router 활용
React Server Components(RSC)의 개념, 동작 원리, 장점, 그리고 Next.js App Router에서 RSC를 활용하는 방법을 심층적으로 탐구합니다. 프론트엔드 개발의 새로운 패러다임을 이해하고 실전 코드 예제를 통해 RSC를 마스터하세요.
Progressive Web App (PWA) 구축 실전: Next.js와 React를 활용한 PWA 개발 가이드
Next.js와 React 기반 PWA 구축 실전 가이드. Service Worker, Web App Manifest 설정 및 next-pwa 활용법을 통해 사용자 경험을 향상시키세요.
CSS Container Queries: 반응형 웹 디자인의 새로운 지평
CSS Container Queries를 활용하여 컴포넌트 기반 반응형 웹 디자인을 구현하는 방법을 심층적으로 알아봅니다. React, Next.js 환경에서 실전 예제를 통해 강력한 기능을 경험하세요.