Next.js Middleware 활용 가이드: 사용자 인증과 라우팅 제어
Next.js Middleware를 활용하여 사용자 인증, 라우팅 제어, A/B 테스트 등 강력한 요청 처리 로직을 구현하는 방법을 TypeScript 코드 예제와 함께 자세히 알아봅니다.
Next.js Middleware 활용 가이드: 사용자 인증과 라우팅 제어
모던 웹 애플리케이션 개발에서 사용자 요청을 효율적으로 처리하고, 보안을 강화하며, 동적인 라우팅을 구현하는 것은 매우 중요합니다. Next.js는 이러한 요구사항을 충족시키기 위해 강력한 Middleware 기능을 제공합니다. 이 글에서는 Next.js Middleware가 무엇인지부터 시작하여, 실제 애플리케이션에서 사용자 인증, 라우팅 제어, A/B 테스트와 같은 복잡한 로직을 어떻게 구현할 수 있는지 상세한 코드 예제와 함께 살펴보겠습니다.
Next.js Middleware란 무엇인가요?
Next.js Middleware는 사용자의 요청(request)이 실제 페이지나 API 라우트로 전달되기 전에 실행되는 코드입니다. 마치 서버리스 함수처럼 동작하며, 주로 Edge Runtime 환경에서 실행되어 매우 빠른 응답 속도를 제공합니다. 이를 통해 개발자는 특정 경로로 들어오는 모든 요청에 대해 조건부 로직을 적용하거나, 요청 및 응답을 수정하는 등 다양한 작업을 수행할 수 있습니다.
Middleware의 주요 역할은 다음과 같습니다.
- 인증 및 권한 부여: 사용자 세션 또는 토큰을 검사하여 로그인 여부를 확인하고, 접근 권한이 없는 페이지로의 접근을 차단합니다.
- 라우팅 제어: 특정 조건에 따라 다른 페이지로 리다이렉트(redirect)하거나, URL을 다시 작성(rewrite)하여 사용자에게 다른 콘텐츠를 제공합니다.
- 국제화 (i18n): 사용자의 언어 설정에 따라 적절한 로케일(locale)로 리다이렉트하거나 콘텐츠를 제공합니다.
- A/B 테스팅: 특정 사용자 그룹에게 다른 버전의 UI를 제공하여 테스트를 수행합니다.
- 로깅 및 분석: 모든 요청에 대한 정보를 기록하여 애플리케이션의 동작을 모니터링합니다.
이러한 기능들을 클라이언트 측 JavaScript나 개별 서버 API에서 처리하는 대신, Middleware에서 중앙 집중식으로 관리함으로써 코드의 응집도를 높이고 성능을 최적화할 수 있습니다.
Middleware 설정 및 기본 사용법
Next.js에서 Middleware를 사용하려면 프로젝트의 루트 디렉토리(예: src 폴더가 있다면 src/, 없다면 프로젝트 루트)에 middleware.ts 또는 middleware.js 파일을 생성해야 합니다. 이 파일에 정의된 함수는 Next.js 애플리케이션의 모든 요청에 대해 실행됩니다.
기본 Middleware 작성
가장 기본적인 형태의 Middleware는 다음과 같습니다.
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// 모든 요청에 대해 콘솔에 로그를 출력합니다.
console.log(`요청 경로: ${request.nextUrl.pathname}`);
// 요청을 다음 단계로 계속 진행합니다.
return NextResponse.next();
}
// Middleware를 실행할 경로를 지정합니다.
// 이 설정이 없으면 모든 경로에 대해 Middleware가 실행됩니다.
export const config = {
matcher: [
/*
* 다음 경로를 제외한 모든 경로에 대해 Middleware를 적용합니다.
* - _next/static (정적 파일)
* - _next/image (이미지 최적화 파일)
* - favicon.ico
* - /api (API 라우트)
* - 기타 파일 (예: .png, .jpg 등)
*/
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
};
위 코드에서 middleware 함수는 NextRequest 객체를 인자로 받아 요청에 대한 정보를 확인할 수 있습니다. NextResponse.next()는 요청을 다음 미들웨어 체인 또는 실제 페이지/API 라우트로 전달하도록 지시합니다.
config 객체의 matcher 속성은 Middleware가 실행될 경로를 정의합니다. 정규 표현식을 사용하여 특정 경로만 선택하거나, 특정 경로를 제외할 수 있습니다. 예를 들어, '/'는 모든 경로에 적용되며, ['/dashboard/:path*', '/settings']는 /dashboard 아래의 모든 경로와 /settings 경로에만 적용됩니다.
matcher 활용 예시
matcher는 Middleware의 성능과 효율성에 직접적인 영향을 미치므로 신중하게 설정해야 합니다.
| 패턴 | 설명 | 예시 경로 | ||
|---|---|---|---|---|
/about | /about 경로에만 적용 | /about | ||
/users/:path* | /users 아래의 모든 경로에 적용 | /users/123, /users/settings | ||
/dashboard | /dashboard 경로에만 적용 | /dashboard | ||
| `/((?!api | _next/static | _next/image).*)` | api, _next/static, _next/image 경로를 제외한 모든 경로에 적용 | /, /posts/1, /about, /dashboard/settings |
요청 및 응답 객체 다루기
Middleware는 NextRequest 객체를 통해 요청의 다양한 정보에 접근하고, NextResponse 객체를 통해 응답을 조작할 수 있습니다.
NextRequest 객체 활용
NextRequest는 표준 Request 객체를 확장한 것으로, Next.js 환경에서 유용한 속성들을 추가로 제공합니다.
-
request.nextUrl:URL객체를 반환하며, 현재 요청의 URL 정보를 상세하게 제공합니다.pathname,searchParams,hostname등을 확인할 수 있습니다. -
request.cookies: 요청에 포함된 쿠키들을ReadonlyRequestCookies객체로 제공합니다. -
request.headers: 요청 헤더들을Headers객체로 제공합니다.
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const userAgent = request.headers.get('user-agent');
const hasAuthCookie = request.cookies.has('auth_token');
console.log(`요청 경로: ${pathname}`);
console.log(`User-Agent: ${userAgent}`);
console.log(`인증 쿠키 존재 여부: ${hasAuthCookie}`);
// 특정 경로에 대한 조건부 처리
if (pathname.startsWith('/admin') && !hasAuthCookie) {
console.log('관리자 페이지 접근 시도, 인증 쿠키 없음.');
// 로그인 페이지로 리다이렉트
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/admin/:path*', '/'], // 관리자 페이지와 루트 경로에만 적용
};
NextResponse 객체 활용
NextResponse는 표준 Response 객체를 확장한 것으로, Middleware에서 응답을 생성하거나 수정할 때 사용합니다.
-
NextResponse.next(): 요청을 다음 Middleware 또는 페이지로 전달합니다. -
NextResponse.redirect(url): 지정된 URL로 리다이렉트합니다. HTTP 상태 코드 307 (Temporary Redirect)을 사용합니다. -
NextResponse.rewrite(url): URL을 다시 작성하여 다른 경로의 콘텐츠를 보여주지만, 브라우저의 URL은 변경하지 않습니다. -
NextResponse.json(data): JSON 형식의 응답을 반환합니다. -
NextResponse.cookie: 응답에 쿠키를 설정하거나 삭제합니다.
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// 1. 특정 경로로 리다이렉트
if (pathname === '/old-page') {
return NextResponse.redirect(new URL('/new-page', request.url));
}
// 2. URL 다시 작성 (rewrite)
// '/about' 경로로 접근 시 실제로는 '/marketing/about-us' 페이지의 콘텐츠를 보여줍니다.
// 하지만 브라우저 주소창에는 여전히 '/about'으로 표시됩니다.
if (pathname === '/about') {
return NextResponse.rewrite(new URL('/marketing/about-us', request.url));
}
// 3. 응답 헤더 및 쿠키 설정
const response = NextResponse.next();
response.headers.set('X-Custom-Header', 'Hello from Middleware');
response.cookies.set('my_session_id', 'some_unique_id', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 60 * 60 * 24 * 7, // 7일
});
return response;
}
export const config = {
matcher: ['/old-page', '/about', '/'],
};
rewrite는 사용자에게는 동일한 URL로 보이게 하면서 서버 내부적으로는 다른 리소스를 제공할 때 유용합니다. 예를 들어, A/B 테스트나 동적 콘텐츠 제공에 활용될 수 있습니다.
사용자 인증 및 권한 부여 구현
Next.js Middleware의 가장 강력한 활용 사례 중 하나는 사용자 인증(Authentication) 및 권한 부여(Authorization) 로직을 중앙 집중화하는 것입니다. 이를 통해 특정 페이지 그룹을 보호하고, 로그인하지 않은 사용자를 로그인 페이지로 리다이렉트할 수 있습니다.
여기서는 JWT(JSON Web Token) 기반의 인증 시스템을 가정하고 Middleware를 구성해보겠습니다.
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
// 보호할 경로들을 정의합니다.
const PROTECTED_ROUTES = ['/dashboard', '/profile', '/settings'];
// 로그인 페이지 경로를 정의합니다.
const LOGIN_PAGE = '/login';
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// 1. 보호된 경로인지 확인합니다.
const isProtectedRoute = PROTECTED_ROUTES.some(route => pathname.startsWith(route));
// 2. 인증 토큰(JWT)을 쿠키에서 가져옵니다.
const authToken = request.cookies.get('auth_token')?.value;
// 3. 보호된 경로에 접근하려 하지만 토큰이 없는 경우
if (isProtectedRoute && !authToken) {
console.log(`[Middleware] 보호된 경로 접근 시도 (${pathname}), 인증 토큰 없음. 로그인 페이지로 리다이렉트.`);
// 로그인 페이지로 리다이렉트합니다.
const url = new URL(LOGIN_PAGE, request.url);
url.searchParams.set('redirect', pathname); // 로그인 후 원래 페이지로 돌아가기 위한 쿼리 파라미터
return NextResponse.redirect(url);
}
// 4. 로그인 페이지에 접근하려 하지만 이미 토큰이 있는 경우 (이미 로그인됨)
if (pathname === LOGIN_PAGE && authToken) {
console.log(`[Middleware] 로그인 페이지 접근 시도 (${pathname}), 이미 인증됨. 대시보드로 리다이렉트.`);
// 대시보드 페이지로 리다이렉트합니다.
return NextResponse.redirect(new URL('/dashboard', request.url));
}
// 5. 그 외의 경우, 요청을 다음 단계로 계속 진행합니다.
return NextResponse.next();
}
export const config = {
// 보호된 경로, 로그인 페이지, 그리고 루트 경로에 Middleware를 적용합니다.
matcher: [
...PROTECTED_ROUTES.map(route => `${route}/:path*`), // 모든 하위 경로 포함
LOGIN_PAGE,
'/', // 루트 경로도 필요하다면 추가
],
};
이 예제는 다음과 같이 동작합니다.
- 사용자가
PROTECTED_ROUTES배열에 정의된 보호된 경로(예:/dashboard)에 접근하려고 합니다. - Middleware는
auth_token쿠키의 존재 여부를 확인하여 사용자의 로그인 상태를 판별합니다. - 만약 토큰이 없다면, 사용자를
/login페이지로 리다이렉트합니다. 이때, 로그인 후 원래 접근하려던 페이지로 돌아갈 수 있도록redirect쿼리 파라미터를 추가합니다. - 반대로, 이미 로그인된 사용자가
/login페이지에 접근하려고 하면,/dashboard와 같은 기본 페이지로 리다이렉트하여 불필요한 로그인 시도를 막습니다.
실제 프로덕션 환경에서는 auth_token의 유효성을 검증하는 로직(예: JWT 디코딩 및 만료 시간 확인)이 추가되어야 합니다. 이는 Middleware 내에서 직접 수행하거나, API 라우트를 통해 서버에서 검증하도록 설계할 수 있습니다.
고급 Middleware 활용: A/B 테스트와 국제화 (i18n)
Next.js Middleware는 단순한 인증을 넘어, 사용자 경험을 향상시키고 비즈니스 목표를 달성하는 데 기여하는 고급 기능들을 구현할 수 있습니다.
A/B 테스트 구현
rewrite 기능을 활용하여 특정 사용자 그룹에게 다른 버전의 UI를 제공하는 A/B 테스트를 구현할 수 있습니다.
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// A/B 테스트를 위한 쿠키 확인 또는 설정
let variant = request.cookies.get('ab_test_variant')?.value;
if (!variant) {
// 쿠키가 없으면 랜덤으로 A 또는 B를 할당
variant = Math.random() < 0.5 ? 'A' : 'B';
const response = NextResponse.next();
response.cookies.set('ab_test_variant', variant, { maxAge: 60 * 60 * 24 * 30 }); // 30일 유지
console.log(`[Middleware] A/B 테스트 변형 설정: ${variant}`);
return response; // 쿠키를 설정한 후 요청을 계속 진행
}
// 특정 페이지에 대해 A/B 테스트 적용
if (pathname === '/') {
if (variant === 'B') {
// 'B' 그룹 사용자에게는 다른 랜딩 페이지를 보여줍니다.
console.log('[Middleware] A/B 테스트: B 그룹에게 /landing-b 페이지 제공');
return NextResponse.rewrite(new URL('/landing-b', request.url));
}
}
return NextResponse.next();
}
export const config = {
matcher: ['/'], // 루트 경로에만 A/B 테스트 적용
};
이 코드는 다음과 같이 동작합니다.
- 사용자가 처음 방문하면
ab_test_variant쿠키가 있는지 확인합니다. - 쿠키가 없으면
A또는B를 랜덤으로 할당하고, 이 값을 쿠키에 저장한 후 응답을 반환합니다. - 루트 경로(
/)로 접근 시,ab_test_variant쿠키 값에 따라A그룹 사용자에게는 기본 랜딩 페이지를,B그룹 사용자에게는/landing-b페이지의 콘텐츠를 제공합니다. 브라우저의 URL은 여전히/로 유지됩니다.
국제화 (i18n) 구현
Next.js는 내장된 국제화 기능을 제공하지만, Middleware를 사용하면 보다 유연하게 로케일(locale)을 감지하고 리다이렉트할 수 있습니다.
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
const PUBLIC_FILE = /\.(.*)$/; // public 폴더에 있는 정적 파일
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// 정적 파일 요청은 무시합니다.
if (
pathname.startsWith('/_next') ||
pathname.startsWith('/api') ||
PUBLIC_FILE.test(pathname)
) {
return NextResponse.next();
}
// 사용자의 선호 언어 감지 (여기서는 예시로 'ko'를 기본으로 합니다.)
// 실제로는 `request.headers.get('accept-language')`를 파싱하여 사용합니다.
const defaultLocale = 'ko';
const supportedLocales = ['en', 'ko', 'ja'];
// 현재 경로가 로케일 접두사를 포함하는지 확인합니다.
const pathnameHasLocale = supportedLocales.some(
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
);
// 로케일 접두사가 없는 경우
if (!pathnameHasLocale) {
// 사용자의 선호 언어를 감지하여 적절한 로케일로 리다이렉트합니다.
// 여기서는 간단히 기본 로케일로 리다이렉트합니다.
const locale = request.cookies.get('NEXT_LOCALE')?.value || defaultLocale;
console.log(`[Middleware] 로케일 없음. ${locale}로 리다이렉트.`);
return NextResponse.redirect(new URL(`/${locale}${pathname}`, request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};
이 Middleware는 사용자가 로케일 접두사 없이 페이지에 접근했을 때, NEXT_LOCALE 쿠키나 기본 로케일(ko)을 기반으로 적절한 로케일 접두사(예: /ko/)를 붙여 리다이렉트합니다. 이를 통해 다국어 웹사이트에서 일관된 URL 구조를 유지할 수 있습니다.
Middleware의 제약사항과 최적화 팁
Next.js Middleware는 매우 강력하지만, Edge Runtime 환경에서 실행되기 때문에 몇 가지 제약사항이 있습니다.
주요 제약사항
- Node.js API 사용 불가:
fs,path와 같은 Node.js 내장 모듈은 Edge Runtime에서 사용할 수 없습니다. 파일 시스템 접근이나 특정 Node.js 전용 라이브러리를 사용해야 하는 경우, API 라우트를 활용하는 것을 고려해야 합니다. - 번들 크기 제한: Edge Runtime은 빠른 시작 시간을 위해 번들 크기에 제한이 있습니다. 따라서 Middleware 코드는 가능한 한 작고 가볍게 유지해야 합니다.
- I/O 작업 제한: 복잡한 데이터베이스 쿼리나 외부 서비스와의 무거운 통신은 Middleware에서 직접 수행하기에 적합하지 않을 수 있습니다. 이러한 작업은 API 라우트나 서버 컴포넌트에서 처리하는 것이 좋습니다.
최적화 팁
-
matcher를 통한 정밀한 제어:config.matcher를 사용하여 Middleware가 실행될 경로를 최대한 구체적으로 지정하세요. 불필요한 경로에 Middleware가 실행되지 않도록 하여 성능 오버헤드를 줄일 수 있습니다. - 가볍고 빠른 로직: Middleware 내에서는 최대한 가볍고 빠르게 실행될 수 있는 로직만 작성하세요. 복잡한 계산이나 외부 API 호출은 피하는 것이 좋습니다.
- Edge Functions 최적화: Middleware는 Edge Functions로 배포되므로, Edge 환경에 최적화된 라이브러리(예:
jose같은 경량 JWT 라이브러리)를 사용하는 것을 고려하세요. - 환경 변수 활용: 민감한 정보나 환경별 설정은
.env파일을 통해 환경 변수로 관리하고,process.env.MY_VAR형태로 Middleware에서 접근할 수 있습니다.
마무리
Next.js Middleware는 프론트엔드 개발자가 요청 처리 로직을 서버리스 환경에서 효율적으로 관리할 수 있게 해주는 강력한 기능입니다. 사용자 인증, 라우팅 제어, A/B 테스트, 국제화 등 다양한 고급 기능을 구현함으로써 애플리케이션의 성능과 사용자 경험을 크게 향상시킬 수 있습니다. Next.js 프로젝트에서 복잡한 요청 처리 요구사항이 있다면, Middleware를 적극적으로 활용해보시길 권장합니다.
관련 게시글
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 애플리케이션의 프론트엔드 기능을 강화하세요.