Next.js Middleware: 강력한 요청 처리 활용법
Next.js Middleware를 활용하여 사용자 인증, 국제화, A/B 테스트 등 다양한 요청 처리 로직을 효율적으로 구현하는 방법을 심층적으로 알아봅니다. 실전 코드 예제를 통해 Next.js 애플리케이션의 프론트엔드 기능을 강화하세요.
Next.js Middleware: 강력한 요청 처리 활용법
최신 웹 애플리케이션 개발에서 사용자 경험과 보안은 매우 중요한 요소입니다. Next.js는 React 기반의 프레임워크로서 이러한 요구사항을 충족시키기 위한 다양한 기능을 제공하며, 그중 Middleware는 요청이 완료되기 전에 특정 로직을 실행하여 애플리케이션의 동작을 유연하게 제어할 수 있게 해주는 강력한 도구입니다. 이 글에서는 Next.js Middleware의 개념부터 실제 활용 사례까지 자세히 살펴보고, 여러분의 Next.js 프로젝트에 적용할 수 있는 실전 코드 예제를 제공합니다.
Next.js Middleware란 무엇인가요?
Next.js Middleware는 웹 요청이 페이지 또는 API 라우트로 전달되기 전에 실행되는 함수입니다. 이는 서버리스 환경인 Edge Runtime에서 동작하며, 클라이언트와 서버 사이에서 요청을 가로채고 조작할 수 있는 기회를 제공합니다. 전통적인 백엔드 미들웨어와 유사하지만, Next.js의 Middleware는 프론트엔드 개발자가 친숙하게 사용할 수 있는 JavaScript(또는 TypeScript) 환경에서 동작하며, Edge Runtime의 이점을 활용하여 매우 빠르게 실행될 수 있다는 특징을 가집니다.
Middleware의 주요 목적은 다음과 같습니다.
- 요청 가로채기 및 수정: 특정 조건에 따라 요청 경로를 변경(rewrite)하거나, 다른 페이지로 리다이렉트(redirect)할 수 있습니다.
- 응답 수정: 헤더를 추가하거나 수정하는 등의 작업을 수행할 수 있습니다.
- 인증 및 권한 부여: 사용자의 로그인 상태를 확인하고, 접근 권한이 없는 페이지로의 접근을 차단할 수 있습니다.
- 국제화(i18n): 사용자의 언어 설정을 기반으로 적절한 언어 버전의 페이지로 안내할 수 있습니다.
- A/B 테스트: 특정 사용자 그룹에게 다른 버전의 페이지를 제공하여 테스트할 수 있습니다.
Edge Runtime의 이점
Next.js Middleware는 Vercel Edge Runtime 위에서 실행됩니다. Edge Runtime은 웹 요청에 가장 가까운 지점에서 코드를 실행함으로써 낮은 지연 시간과 높은 성능을 제공합니다. 이는 전 세계 사용자에게 일관되고 빠른 경험을 제공하는 데 필수적입니다. 또한, Node.js 런타임에 비해 더 가볍고 빠르게 시작될 수 있어, 미들웨어와 같이 모든 요청에 대해 실행되어야 하는 로직에 매우 적합합니다.
Middleware 설정 및 기본 사용법
Next.js에서 Middleware를 사용하려면 프로젝트의 루트 디렉터리(예: src 폴더가 있다면 src 폴더 내부)에 middleware.ts 또는 middleware.js 파일을 생성해야 합니다. 이 파일에 정의된 함수는 Next.js 애플리케이션으로 들어오는 모든 요청에 대해 실행됩니다.
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// 요청 URL을 콘솔에 로깅합니다.
console.log(`요청 경로: ${request.nextUrl.pathname}`);
// 특정 경로로의 요청을 감지하여 리다이렉트할 수 있습니다.
if (request.nextUrl.pathname.startsWith('/old-path')) {
return NextResponse.redirect(new URL('/new-path', request.url));
}
// 요청을 계속 진행합니다.
return NextResponse.next();
}
위 코드에서 middleware 함수는 NextRequest 객체를 인자로 받아, NextResponse 객체를 반환합니다.
-
NextRequest: 들어오는 HTTP 요청에 대한 정보를 담고 있습니다. URL, 헤더, 쿠키 등을 포함합니다. -
NextResponse: 나가는 HTTP 응답을 제어하는 데 사용됩니다.redirect(),rewrite(),next()등의 메서드를 제공합니다.
경로 매칭 설정 (config.matcher)
모든 요청에 대해 Middleware를 실행하는 것은 비효율적일 수 있습니다. 특정 경로에만 Middleware를 적용하고 싶다면, middleware.ts 파일에 config.matcher를 정의할 수 있습니다.
// middleware.ts (config.matcher 추가)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// 예를 들어, /admin 경로로의 접근을 제한하는 로직
if (request.nextUrl.pathname.startsWith('/admin')) {
const isAdmin = request.cookies.get('isAdmin')?.value === 'true';
if (!isAdmin) {
console.log('관리자 권한 없음! 홈으로 리다이렉트합니다.');
return NextResponse.redirect(new URL('/', request.url));
}
}
return NextResponse.next();
}
// 특정 경로에만 Middleware를 적용합니다.
export const config = {
matcher: [
/*
* 모든 요청 경로에 대해 Middleware를 적용하지만, 다음 패턴은 제외합니다:
* - API routes (`/api/`로 시작)
* - `_next/`로 시작하는 내부 Next.js 파일 (정적 자산, 런타임 코드 등)
* - `.`(점)을 포함하는 파일 (예: `favicon.ico`)
*/
'/((?!api|_next/static|_next/image|favicon.ico).*)',
'/admin/:path*', // /admin 및 그 하위 경로에 적용
'/dashboard/:path*', // /dashboard 및 그 하위 경로에 적용
],
};
config.matcher는 배열 형태로 여러 패턴을 지정할 수 있으며, 정규 표현식과 유사한 구문을 사용하여 유연하게 경로를 매칭할 수 있습니다. 위 예시에서는 /admin 경로 및 그 하위 경로에만 Middleware가 적용되도록 설정되었습니다.
Next.js Middleware의 주요 활용 사례
Next.js Middleware는 다양한 시나리오에서 애플리케이션의 유연성과 기능을 향상시키는 데 활용될 수 있습니다.
사용자 인증 및 권한 부여
가장 일반적인 Middleware 활용 사례 중 하나는 사용자 인증입니다. 로그인하지 않은 사용자가 특정 보호된 페이지에 접근하려고 할 때, Middleware는 이를 감지하고 로그인 페이지로 리다이렉트할 수 있습니다.
// middleware.ts (사용자 인증 예시)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
const PROTECTED_ROUTES = ['/dashboard', '/profile', '/settings'];
const PUBLIC_ROUTES = ['/', '/login', '/register'];
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const isAuthenticated = request.cookies.has('auth_token'); // 쿠키를 통해 인증 여부 확인
// 보호된 경로에 접근하려 하지만 인증되지 않은 경우
if (PROTECTED_ROUTES.some(route => pathname.startsWith(route)) && !isAuthenticated) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('redirect', pathname); // 로그인 후 원래 페이지로 리다이렉트하기 위한 쿼리 파라미터 추가
return NextResponse.redirect(loginUrl);
}
// 인증된 사용자가 로그인/회원가입 페이지에 접근하려는 경우 대시보드로 리다이렉트
if (PUBLIC_ROUTES.some(route => pathname === route) && isAuthenticated) {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], // 모든 경로에 적용
};
이 예시에서는 auth_token 쿠키의 존재 여부로 사용자의 인증 상태를 확인하고, 보호된 경로에 대한 접근을 제어합니다.
국제화 (Internationalization, i18n)
다국어 웹사이트를 구축할 때 Middleware는 사용자의 언어 설정을 감지하여 적절한 언어 버전의 페이지로 안내하는 데 유용합니다.
// middleware.ts (국제화 예시)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
const LOCALES = ['en', 'ko', 'ja'];
const DEFAULT_LOCALE = 'en';
function getLocale(request: NextRequest) {
// 사용자 요청의 Accept-Language 헤더에서 가장 선호하는 언어를 추출
const acceptLanguage = request.headers.get('Accept-Language');
if (acceptLanguage) {
const preferredLocale = acceptLanguage.split(',')[0].split('-')[0].toLowerCase();
if (LOCALES.includes(preferredLocale)) {
return preferredLocale;
}
}
// 쿠키에 저장된 언어 설정이 있다면 사용
const storedLocale = request.cookies.get('NEXT_LOCALE')?.value;
if (storedLocale && LOCALES.includes(storedLocale)) {
return storedLocale;
}
return DEFAULT_LOCALE;
}
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// 이미 로케일이 URL에 포함되어 있다면, 다음으로 진행
const pathnameHasLocale = LOCALES.some(locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`);
if (pathnameHasLocale) {
return NextResponse.next();
}
// 로케일이 없는 경우, 사용자의 선호 로케일로 리라이트
const locale = getLocale(request);
request.nextUrl.pathname = `/${locale}${pathname}`;
return NextResponse.rewrite(request.nextUrl);
}
export const config = {
matcher: [
// '/:locale/:path*' 패턴을 피하기 위해 모든 경로에 적용하지만,
// 이미 로케일이 포함된 경로는 제외하도록 로직을 작성합니다.
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
};
이 Middleware는 요청 URL에 로케일(예: /en/about)이 없는 경우, Accept-Language 헤더나 쿠키를 기반으로 사용자의 선호 로케일을 감지하여 내부적으로 URL을 리라이트(rewrite)합니다. 이를 통해 사용자에게는 /about으로 보이지만 실제로는 /en/about 페이지가 제공될 수 있습니다.
A/B 테스트 및 Feature Flagging
새로운 기능이나 UI 변경을 특정 사용자 그룹에게만 노출하여 효과를 측정하는 A/B 테스트에도 Middleware를 활용할 수 있습니다.
// middleware.ts (A/B 테스트 예시)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// A/B 테스트 대상 경로인 경우
if (pathname === '/product') {
// 사용자의 쿠키에서 A/B 테스트 그룹을 확인하거나, 새로 할당
let abTestGroup = request.cookies.get('ab-test-group')?.value;
if (!abTestGroup) {
abTestGroup = Math.random() < 0.5 ? 'A' : 'B'; // 50% 확률로 A 또는 B 그룹 할당
const response = NextResponse.next();
response.cookies.set('ab-test-group', abTestGroup, { path: '/' }); // 쿠키 설정
return response;
}
// 그룹에 따라 다른 버전의 페이지로 리라이트
if (abTestGroup === 'B') {
request.nextUrl.pathname = '/product-v2'; // '/product-v2' 페이지로 리라이트
return NextResponse.rewrite(request.nextUrl);
}
}
return NextResponse.next();
}
export const config = {
matcher: ['/product'], // '/product' 경로에만 적용
};
이 Middleware는 /product 페이지에 접근하는 사용자에게 A/B 테스트 그룹을 할당하고, 'B' 그룹에 속한 사용자에게는 /product-v2 페이지를 보여주도록 리라이트합니다.
요청 및 응답 헤더 조작
보안 헤더 추가, 캐싱 정책 설정 등 요청 및 응답 헤더를 조작하는 데에도 Middleware가 활용될 수 있습니다.
// middleware.ts (헤더 조작 예시)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const response = NextResponse.next();
// 보안 헤더 추가
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');
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
// 특정 경로에 대한 캐싱 정책 설정 (예시)
if (request.nextUrl.pathname.startsWith('/api/data')) {
response.headers.set('Cache-Control', 'public, max-age=3600');
}
return response;
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], // 모든 경로에 적용
};
이 예시에서는 모든 응답에 기본적인 보안 헤더를 추가하고, /api/data 경로에 대한 응답에는 캐싱 관련 헤더를 추가합니다.
NextRequest와 NextResponse 객체 심층 분석
Middleware에서 요청과 응답을 다루는 핵심 객체인 NextRequest와 NextResponse에 대해 좀 더 자세히 알아보겠습니다.
NextRequest
NextRequest는 표준 Web Request API를 확장한 객체로, Next.js 환경에 특화된 유용한 속성과 메서드를 제공합니다.
-
url: 현재 요청의 전체 URL (문자열). -
nextUrl:URL객체로, URL의 각 부분을 쉽게 접근하고 조작할 수 있게 합니다. 특히pathname,searchParams,locale등의 속성이 유용합니다. -
cookies: 요청에 포함된 쿠키들을 다루는RequestCookies객체.get(),has()등의 메서드를 제공합니다. -
headers: 요청에 포함된 헤더들을 다루는Headers객체.get(),has(),set()등의 메서드를 제공합니다. -
ip: 클라이언트의 IP 주소 (Edge Runtime에서 사용 가능). -
geo: 클라이언트의 지리적 위치 정보 (Edge Runtime에서 사용 가능).
NextResponse
NextResponse는 표준 Web Response API를 확장한 객체로, Middleware의 핵심적인 동작을 제어하는 메서드들을 제공합니다.
-
NextResponse.next(): 요청을 계속 진행하여 다음 Middleware나 최종 핸들러(페이지/API 라우트)로 전달합니다.response객체를 반환하여 헤더나 쿠키를 추가할 수 있습니다. -
NextResponse.redirect(url, status?): 사용자를 지정된 URL로 리다이렉트합니다. 기본 상태 코드는307 Temporary Redirect입니다.308 Permanent Redirect를 사용하려면status: 308을 지정합니다. -
NextResponse.rewrite(url): 사용자에게는 URL 변경이 보이지 않지만, 내부적으로 요청 URL을 다른 경로로 변경합니다. SEO 친화적인 URL을 유지하면서 다른 페이지 콘텐츠를 보여줄 때 유용합니다. -
NextResponse.json(data, init?): JSON 응답을 생성합니다. API 라우트에서 사용하는Response.json()과 유사합니다. Middleware에서 직접 JSON 응답을 반환할 때 사용할 수 있습니다. -
NextResponse.cookie: 응답에 쿠키를 설정하는ResponseCookies객체.set()메서드를 제공합니다.
Middleware 사용 시 주의사항 및 모범 사례
Next.js Middleware는 강력하지만, 효율적이고 안정적인 애플리케이션을 위해 몇 가지 주의사항과 모범 사례를 따르는 것이 중요합니다.
- 가벼운 로직 유지: Middleware는 모든 요청에 대해 실행될 수 있으므로, 가능한 한 빠르고 가벼운 로직을 유지해야 합니다. 복잡하거나 시간이 많이 소요되는 작업(예: 데이터베이스 쿼리, 외부 API 호출)은 피하는 것이 좋습니다. 이런 작업은 API 라우트나 서버 컴포넌트에서 처리하는 것이 더 적절합니다.
-
config.matcher활용: 불필요한 경로에 Middleware가 실행되지 않도록config.matcher를 사용하여 적용 범위를 명확히 제한합니다. 이는 성능 최적화에 큰 도움이 됩니다. - 환경 변수 사용: 민감한 정보나 환경별 설정은 환경 변수를 통해 관리합니다. Middleware는 Edge Runtime에서 실행되므로,
process.env.NEXT_PUBLIC_VAR와 같은 클라이언트 사이드 환경 변수는 접근할 수 없고,process.env.VAR와 같은 서버 사이드 환경 변수에만 접근 가능합니다. - 테스트 용이성: Middleware 로직은 독립적으로 테스트하기 어려울 수 있으므로, 핵심 로직은 별도의 유틸리티 함수로 분리하여 단위 테스트를 용이하게 만드는 것이 좋습니다.
- 에러 처리: Middleware 내에서 예외가 발생하면 애플리케이션 전체에 영향을 미칠 수 있습니다. 필요한 경우
try-catch블록을 사용하여 안전하게 에러를 처리하고, 적절한 응답(예: 에러 페이지로 리다이렉트)을 반환하도록 합니다. - Edge Runtime API 제한: Middleware는 Node.js 런타임이 아닌 Edge Runtime에서 실행되므로, Node.js 전용 API(예:
fs,path)는 사용할 수 없습니다. Web 표준 API(예:Request,Response,URL,Headers,fetch)는 사용 가능합니다.
마무리
Next.js Middleware는 프론트엔드 개발자에게 서버 측 요청 처리의 강력한 기능을 제공하여, 사용자 인증, 국제화, A/B 테스트 등 다양한 복잡한 시나리오를 효과적으로 구현할 수 있게 합니다. Edge Runtime의 이점을 활용하여 뛰어난 성능과 낮은 지연 시간을 보장하며, NextRequest와 NextResponse 객체를 통해 요청과 응답을 세밀하게 제어할 수 있습니다. 이 글에서 제시된 활용법과 코드 예제를 바탕으로 여러분의 Next.js 애플리케이션을 더욱 견고하고 사용자 친화적으로 만들어 나가시길 바랍니다.
관련 게시글
Vite Build Tool: Fast Frontend Development Guide
Vite는 현대적인 프론트엔드 개발을 위한 빠르고 효율적인 빌드 도구입니다. 이 가이드에서는 Vite의 핵심 기능, React 및 TypeScript 프로젝트 설정, 플러그인 활용법, 그리고 빌드 최적화 전략까지 완벽하게 다룹니다.
React Server Components (RSC) 심층 가이드: Next.js와 함께하는 Full-stack React
React Server Components (RSC)의 개념, 등장 배경, 동작 원리, 그리고 Next.js 13+ App Router에서의 활용법을 심층적으로 다룹니다. 클라이언트/서버 컴포넌트 분리 전략과 실전 코드 예제를 통해 RSC의 강력한 이점을 이해하고 웹 애플리케이션 성능을 최적화하는 방법을 알아봅니다.
Turborepo Monorepo: React, Next.js 프로젝트를 위한 효율적인 구축 가이드
Turborepo를 활용한 Monorepo 구축 방법을 상세히 다루며, React, Next.js 기반의 프론트엔드 프로젝트 관리를 위한 TypeScript, JavaScript, CSS 설정 및 최적화 전략을 소개합니다.