Next.js Middleware 활용: Edge Runtime 기반의 강력한 요청 처리
Next.js Middleware를 사용하여 요청을 가로채고 수정하는 방법을 알아봅니다. 인증, 권한, 리디렉션, 국제화 등 다양한 활용 사례와 실전 코드를 통해 Next.js 애플리케이션의 유연성을 극대화하는 방법을 제시합니다.
Next.js Middleware 활용: Edge Runtime 기반의 강력한 요청 처리
최신 웹 애플리케이션 개발에서 사용자 경험과 보안은 매우 중요한 요소입니다. 특히 Next.js와 같은 프레임워크는 강력한 서버 사이드 렌더링(SSR) 및 정적 사이트 생성(SSG) 기능을 제공하며, 여기에 Middleware라는 강력한 기능을 더해 요청 처리의 유연성을 한층 강화했습니다. Next.js Middleware는 클라이언트의 요청이 서버 컴포넌트나 API 라우트로 도달하기 전에 특정 로직을 실행할 수 있게 해주어, 인증, 권한 부여, 국제화(i18n), A/B 테스트 등 다양한 기능을 효율적으로 구현할 수 있도록 돕습니다.
이 글에서는 Next.js Middleware가 무엇인지, 어떻게 동작하는지, 그리고 실전 프로젝트에서 어떻게 활용할 수 있는지 다양한 코드 예제와 함께 심층적으로 다루어보겠습니다. Edge Runtime의 강점을 활용하여 여러분의 Next.js 애플리케이션을 더욱 강력하고 유연하게 만드는 방법을 함께 살펴보시죠.
Next.js Middleware란 무엇인가요?
Next.js 12부터 도입된 Middleware는 클라이언트의 요청이 Next.js 애플리케이션의 페이지나 API 라우트에 도달하기 전에 실행되는 코드입니다. 이는 마치 HTTP 요청 파이프라인의 중간에 위치하여, 요청과 응답 객체를 조작할 수 있는 기회를 제공합니다. Middleware는 애플리케이션의 모든 요청에 대해 전역적으로 적용될 수도 있고, 특정 경로에만 선택적으로 적용될 수도 있습니다.
Middleware의 가장 큰 특징 중 하나는 Vercel Edge Runtime에서 실행된다는 점입니다. Edge Runtime은 웹 표준 API(Request, Response 등)를 기반으로 하며, Node.js 런타임보다 훨씬 가볍고 빠릅니다. 이는 Middleware가 전 세계 Edge Location에서 실행되어 사용자에게 가장 가까운 곳에서 요청을 처리하므로, 인증이나 리디렉션과 같은 작업에서 매우 낮은 지연 시간을 보장하고 뛰어난 성능을 제공할 수 있음을 의미합니다.
Middleware는 프로젝트의 루트 디렉토리나 src 디렉토리 내에 middleware.ts (또는 .js, .tsx, .jsx) 파일을 생성하여 정의합니다. 이 파일은 반드시 middleware라는 이름의 함수를 export 해야 합니다.
Middleware의 기본 동작 원리
Next.js Middleware는 표준 Web API의 Request 및 Response 객체와 유사한 NextRequest 및 NextResponse 객체를 사용합니다. NextRequest는 들어오는 요청에 대한 정보를 담고 있으며, NextResponse는 나가는 응답을 제어하는 데 사용됩니다.
middleware 함수 내에서는 다음과 같은 작업을 수행할 수 있습니다.
- 요청/응답 헤더 조작:
request.headers.set()또는response.headers.set()등을 사용하여 헤더를 추가, 수정, 삭제할 수 있습니다. - 쿠키 조작:
request.cookies.get(),response.cookies.set()등을 통해 쿠키를 읽거나 설정할 수 있습니다. - 리디렉션 (Redirect):
NextResponse.redirect()를 사용하여 다른 URL로 사용자를 보낼 수 있습니다. 이는 주로 인증되지 않은 사용자를 로그인 페이지로 보내는 데 사용됩니다. - 리라이트 (Rewrite):
NextResponse.rewrite()를 사용하여 URL을 변경하지만, 사용자에게는 변경된 URL이 노출되지 않도록 내부적으로 다른 경로의 콘텐츠를 제공할 수 있습니다. 이는 주로 국제화나 A/B 테스트에 활용됩니다. - 다음 Middleware 또는 페이지로 진행:
NextResponse.next()를 호출하여 요청 처리를 다음 Middleware나 최종 페이지/API 라우트로 넘깁니다.
Middleware를 특정 경로에만 적용하려면 config 객체에 matcher 속성을 정의해야 합니다. matcher는 배열 형태로 여러 경로 패턴을 지정할 수 있으며, Next.js는 이 패턴과 일치하는 요청에 대해서만 Middleware를 실행합니다.
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
// Middleware 함수 정의
export function middleware(request: NextRequest) {
// 요청 URL의 pathname을 콘솔에 기록합니다.
console.log('Middleware executed for:', request.nextUrl.pathname);
// 요청 헤더에 커스텀 헤더를 추가하는 예시
const response = NextResponse.next();
response.headers.set('X-Custom-Header', 'Hello from Middleware!');
response.headers.set('X-Current-Path', request.nextUrl.pathname);
// 다음 요청 처리 단계로 넘어갑니다.
return response;
}
// Middleware를 적용할 경로를 설정합니다.
// '/dashboard'로 시작하는 모든 경로와 '/api'로 시작하는 모든 경로에 적용됩니다.
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
};
위 예제는 /dashboard 또는 /api 경로로 들어오는 모든 요청에 대해 Middleware를 실행하고, 요청 정보를 로깅하며, 응답 헤더에 커스텀 값을 추가합니다. matcher의 강력한 패턴 매칭 기능을 활용하면 매우 정교하게 Middleware의 적용 범위를 제어할 수 있습니다.
Middleware 활용 사례 1: 사용자 인증 및 권한 부여
Next.js Middleware의 가장 흔하고 강력한 활용 사례는 사용자 인증(Authentication) 및 권한 부여(Authorization)입니다. 특정 페이지나 API 라우트에 접근하기 전에 사용자가 로그인되어 있는지 확인하거나, 특정 권한을 가지고 있는지 검사하여 접근을 제어할 수 있습니다.
일반적으로, 사용자의 로그인 상태는 쿠키(예: JWT 토큰)에 저장되며, Middleware는 이 쿠키의 존재 여부나 유효성을 검사하여 접근을 허용하거나 로그인 페이지로 리디렉션합니다.
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// 1. 보호된 경로 정의
const protectedPaths = ['/dashboard', '/profile', '/settings'];
const isAdminPath = pathname.startsWith('/admin');
// 2. 인증 토큰 확인 (예: 쿠키에서 토큰을 가져옴)
// 실제 프로덕션 환경에서는 토큰의 유효성을 서버에서 검증하는 API 호출이 필요할 수 있습니다.
const accessToken = request.cookies.get('accessToken')?.value;
// 3. 보호된 경로에 접근하려 하지만 토큰이 없는 경우
if (protectedPaths.some(path => pathname.startsWith(path)) && !accessToken) {
// 로그인 페이지로 리디렉션
const url = new URL('/login', request.url);
url.searchParams.set('redirect', pathname); // 로그인 후 원래 페이지로 돌아가기 위한 쿼리 파라미터
return NextResponse.redirect(url);
}
// 4. 관리자 경로에 접근하려 하지만 관리자 권한이 없는 경우 (예: role 쿠키 확인)
// 이 예제에서는 간단히 'admin' 역할을 확인하지만, 실제로는 더 복잡한 권한 로직이 필요합니다.
const userRole = request.cookies.get('userRole')?.value;
if (isAdminPath && userRole !== 'admin') {
// 권한 없음 페이지로 리디렉션
return NextResponse.redirect(new URL('/unauthorized', request.url));
}
// 5. 모든 검사를 통과하면 다음 요청 처리 단계로 진행
return NextResponse.next();
}
export const config = {
// 모든 경로에 Middleware를 적용하여 인증/권한 검사를 수행할 수 있습니다.
// 단, 정적 파일이나 Next.js 내부 경로, API 경로는 제외하는 것이 일반적입니다.
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico|login|unauthorized).*)',
],
};
이 코드는 /dashboard, /profile, /settings와 같은 보호된 경로에 접근할 때 accessToken 쿠키의 존재 여부를 확인하고, 없으면 /login 페이지로 리디렉션합니다. 또한 /admin 경로에 대해서는 userRole 쿠키가 'admin'인지 확인하여 권한을 부여합니다. 이러한 로직을 Middleware에서 처리함으로써, 각 페이지 컴포넌트나 API 라우트에서 중복되는 인증 코드를 제거하고, 애플리케이션의 보안 및 유지보수성을 향상시킬 수 있습니다.
Middleware 활용 사례 2: 국제화 (i18n) 처리
글로벌 서비스를 제공하는 애플리케이션에서는 국제화(Internationalization, i18n)가 필수적입니다. Next.js Middleware는 사용자의 언어 설정을 감지하고, 이에 따라 적절한 언어 버전의 페이지로 리라이트하거나 리디렉션하는 데 매우 유용합니다. 이를 통해 사용자에게 맞춤형 콘텐츠를 제공하고, URL 구조를 깔끔하게 유지할 수 있습니다.
가장 일반적인 접근 방식은 사용자의 Accept-Language 헤더를 읽어 선호하는 언어를 파악하고, URL에 언어 접두사(예: /ko/about, /en/about)가 없는 경우 적절한 언어 접두사를 추가하여 내부적으로 리라이트하는 것입니다.
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
const locales = ['en', 'ko', 'ja']; // 지원하는 언어 목록
const defaultLocale = 'en'; // 기본 언어
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// 1. 요청 경로에 이미 지원하는 언어 접두사가 포함되어 있는지 확인합니다.
// 예: /en/about, /ko/products
const pathnameHasLocale = locales.some(
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
);
// 2. 이미 언어 접두사가 있다면, Middleware를 통과시킵니다.
if (pathnameHasLocale) {
return NextResponse.next();
}
// 3. 언어 접두사가 없다면, 사용자의 Accept-Language 헤더를 기반으로 선호 언어를 감지합니다.
let locale = defaultLocale;
const acceptLanguage = request.headers.get('accept-language');
if (acceptLanguage) {
const preferredLocale = acceptLanguage.split(',')[0].split('-')[0];
if (locales.includes(preferredLocale)) {
locale = preferredLocale;
}
}
// 4. 감지된 언어를 URL에 포함하여 내부적으로 리라이트합니다.
// 예: '/about' -> '/en/about' (사용자에게는 여전히 '/about'으로 보임)
request.nextUrl.pathname = `/${locale}${pathname}`;
return NextResponse.rewrite(request.nextUrl);
}
export const config = {
// API 라우트, Next.js 내부 경로, 정적 파일 등은 제외합니다.
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};
이 Middleware는 사용자가 /about 페이지에 접근했을 때, Accept-Language 헤더가 ko-KR이라면 내부적으로 /ko/about 페이지를 렌더링하도록 URL을 리라이트합니다. 사용자는 브라우저 주소창에서 여전히 /about을 보게 되므로, 깔끔한 URL을 유지하면서도 언어별 콘텐츠를 제공할 수 있습니다. 이는 SEO에도 긍정적인 영향을 미 미칩니다.
Middleware 활용 사례 3: A/B 테스트 및 Feature Flagging
Next.js Middleware는 A/B 테스트나 Feature Flagging과 같은 동적인 콘텐츠 제공 전략에도 효과적으로 활용될 수 있습니다. 특정 사용자 그룹에게만 새로운 기능이나 다른 UI를 보여주고 싶을 때, Middleware에서 요청을 가로채어 적절한 페이지로 리라이트하거나, 응답 헤더/쿠키를 조작하여 클라이언트 측에서 로직을 분기할 수 있습니다.
아래 예제는 홈페이지('/')에 접근하는 사용자들을 대상으로 A/B 테스트를 수행하는 Middleware입니다. 사용자에게 'A' 또는 'B' variant 중 하나를 무작위로 할당하고, 해당 variant에 맞는 페이지로 리라이트합니다.
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// 홈페이지에 대해서만 A/B 테스트를 적용합니다.
if (pathname === '/') {
// 1. 'ab-test-variant' 쿠키가 있는지 확인합니다.
let variant = request.cookies.get('ab-test-variant')?.value;
if (!variant) {
// 2. 쿠키가 없다면, 무작위로 'A' 또는 'B' variant를 할당합니다 (예: 50% 확률).
variant = Math.random() < 0.5 ? 'A' : 'B';
// 3. 사용자에게 variant 쿠키를 설정하고, 해당 variant 페이지로 리라이트합니다.
// 예를 들어, /page-a 또는 /page-b와 같은 페이지가 존재한다고 가정합니다.
const response = NextResponse.rewrite(new URL(`/variant-${variant}`, request.url));
response.cookies.set('ab-test-variant', variant, {
path: '/',
maxAge: 60 * 60 * 24 * 30, // 30일 동안 유지
});
return response;
} else {
// 4. 이미 variant 쿠키가 있다면, 해당 variant 페이지로 리라이트합니다.
return NextResponse.rewrite(new URL(`/variant-${variant}`, request.url));
}
}
// 5. A/B 테스트 대상이 아닌 경로는 그대로 진행합니다.
return NextResponse.next();
}
export const config = {
// 홈페이지('/')에만 Middleware를 적용합니다.
matcher: ['/'],
};
이 Middleware는 사용자가 처음 홈페이지에 방문하면 무작위로 A 또는 B 그룹에 할당하고, 이 정보를 쿠키에 저장합니다. 이후 방문 시에는 저장된 쿠키 값을 기반으로 항상 동일한 버전의 페이지를 보게 됩니다. 개발자는 /variant-A와 /variant-B 페이지를 각각 구현하여 A/B 테스트를 진행할 수 있습니다. 이는 사용자 경험을 최적화하고, 새로운 기능의 도입 효과를 측정하는 데 매우 효과적인 방법입니다.
Middleware의 장점과 고려사항
Next.js Middleware는 애플리케이션의 유연성과 성능을 크게 향상시킬 수 있는 강력한 도구이지만, 효과적으로 사용하기 위해서는 그 장점과 함께 몇 가지 고려사항을 이해하는 것이 중요합니다.
장점
- Edge Runtime 기반의 뛰어난 성능: Middleware는 Edge Runtime에서 실행되므로, Node.js 런타임보다 훨씬 가볍고 빠르게 동작합니다. 이는 전 세계 Edge Location에서 요청을 처리하여 사용자에게 낮은 지연 시간과 빠른 응답 속도를 제공합니다.
- 중앙 집중식 요청 처리: 인증, 국제화, 로깅 등 애플리케이션 전반에 걸쳐 필요한 횡단 관심사(cross-cutting concerns) 로직을 한 곳에서 관리할 수 있습니다. 이는 코드 중복을 줄이고 유지보수성을 높입니다.
- 강력한 URL 제어:
NextResponse.redirect()와NextResponse.rewrite()를 통해 요청의 흐름과 URL을 완벽하게 제어할 수 있습니다. 이는 SEO 친화적인 URL 구조를 만들거나, 복잡한 라우팅 규칙을 구현하는 데 유용합니다. - 애플리케이션 로직 분리: 페이지 컴포넌트나 API 라우트에서 인증, 권한, 리디렉션과 같은 비즈니스 로직이 아닌 부분을 분리하여, 각 컴포넌트의 책임을 명확히 할 수 있습니다.
- 유연한 확장성: 쿠키, 헤더 조작 등 웹 표준 API를 기반으로 다양한 커스텀 로직을 구현할 수 있어, A/B 테스트, Feature Flagging 등 고급 기능 구현에 용이합니다.
고려사항
- Edge Runtime 제약 사항: Middleware는 Edge Runtime에서 실행되므로, Node.js 환경에서 사용 가능한 파일 시스템 접근(
fs), 프로세스 환경 변수 직접 접근(process.env의 일부), 일부 Node.js 모듈(http,https등)을 직접 사용할 수 없습니다. 외부 API 호출은fetch를 통해 가능합니다. - 디버깅의 복잡성: Middleware는 서버리스 함수와 유사하게 Edge 환경에서 실행되므로, 브라우저 개발자 도구나 일반적인 Node.js 디버거로 직접 디버깅하기 어려울 수 있습니다.
console.log를 활용하거나 Vercel 대시보드에서 로그를 확인하는 방식으로 디버깅해야 합니다. - 성능 최적화: Middleware는 모든 요청에 대해 실행될 수 있으므로, 과도하게 복잡하거나 지연이 발생하는 로직은 전체 애플리케이션의 성능에 부정적인 영향을 미칠 수 있습니다. 필요한 최소한의 로직만 포함하고, 무거운 작업은 Server Component나 API 라우트에서 처리하는 것이 좋습니다.
- 상태 관리: Middleware는 기본적으로 Stateless하게 동작합니다. 요청 간에 상태를 유지해야 하는 경우, 쿠키나 헤더를 활용하거나 외부 저장소(Redis 등)와 연동하는 방식을 고려해야 합니다.
-
matcher의 중요성:config.matcher를 사용하여 Middleware가 실행될 경로를 정확하게 지정하는 것이 중요합니다. 불필요한 경로에 Middleware가 실행되지 않도록 하여 성능 저하를 방지해야 합니다.
마무리
Next.js Middleware는 현대적인 웹 애플리케이션 개발에서 필수적인 기능을 Edge Runtime의 강력한 성능과 유연성을 통해 제공합니다. 사용자 인증 및 권한 부여, 국제화 처리, A/B 테스트 등 다양한 시나리오에서 Middleware를 활용하여 애플리케이션의 복잡성을 줄이고, 사용자 경험을 향상시키며, 개발 효율성을 높일 수 있습니다.
이 글에서 제시된 코드 예제와 활용법을 바탕으로 여러분의 Next.js 프로젝트에 Middleware를 적용해보세요. Edge Runtime 기반의 강력한 요청 처리 메커니즘을 이해하고 적절히 활용한다면, 더욱 견고하고 확장 가능한 프론트엔드 웹 애플리케이션을 구축하는 데 큰 도움이 될 것입니다.
관련 게시글
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의 강력한 이점을 이해하고 웹 애플리케이션 성능을 최적화하는 방법을 알아봅니다.
Next.js Middleware: 강력한 요청 처리 활용법
Next.js Middleware를 활용하여 사용자 인증, 국제화, A/B 테스트 등 다양한 요청 처리 로직을 효율적으로 구현하는 방법을 심층적으로 알아봅니다. 실전 코드 예제를 통해 Next.js 애플리케이션의 프론트엔드 기능을 강화하세요.