수신 요청을 가로채고 수정하는 강력한 기능인 Next.js 미들웨어를 살펴보세요. 실제 예제를 통해 인증, 인가, 리디렉션, A/B 테스트 구현 방법을 배워보세요.
Next.js 미들웨어: 동적 애플리케이션을 위한 요청 가로채기 마스터하기
Next.js 미들웨어는 라우트에 도달하기 전에 수신 요청을 가로채고 수정할 수 있는 유연하고 강력한 방법을 제공합니다. 이 기능을 통해 인증 및 인가부터 리디렉션 및 A/B 테스트에 이르기까지 광범위한 기능을 구현하는 동시에 성능을 최적화할 수 있습니다. 이 종합 가이드는 Next.js 미들웨어의 핵심 개념을 안내하고 이를 효과적으로 활용하는 방법을 보여줍니다.
Next.js 미들웨어란 무엇인가?
Next.js의 미들웨어는 요청이 완료되기 전에 실행되는 함수입니다. 이를 통해 다음을 수행할 수 있습니다:
- 요청 가로채기: 수신 요청의 헤더, 쿠키, URL을 검사합니다.
- 요청 수정: 특정 기준에 따라 URL을 재작성하거나, 헤더를 설정하거나, 사용자를 리디렉션합니다.
- 코드 실행: 페이지가 렌더링되기 전에 서버사이드 로직을 실행합니다.
미들웨어 함수는 프로젝트의 루트에 있는 middleware.ts
(또는 middleware.js
) 파일에 정의됩니다. 이 함수는 애플리케이션 내의 모든 라우트 또는 구성 가능한 매처(matcher)를 기반으로 특정 라우트에 대해 실행됩니다.
주요 개념 및 이점
Request 객체
request
객체는 다음에 대한 정보를 포함하여 수신 요청에 대한 정보에 접근할 수 있게 해줍니다:
request.url
: 요청의 전체 URL입니다.request.method
: HTTP 메서드(예: GET, POST).request.headers
: 요청 헤더를 포함하는 객체입니다.request.cookies
: 요청 쿠키를 나타내는 객체입니다.request.geo
: 가능한 경우 요청과 관련된 지리적 위치 데이터를 제공합니다.
Response 객체
미들웨어 함수는 요청 결과를 제어하기 위해 Response
객체를 반환합니다. 다음 응답을 사용할 수 있습니다:
NextResponse.next()
: 요청을 정상적으로 계속 처리하여 의도한 라우트에 도달하도록 합니다.NextResponse.redirect(url)
: 사용자를 다른 URL로 리디렉션합니다.NextResponse.rewrite(url)
: 요청 URL을 재작성하여 리디렉션 없이 다른 페이지를 효과적으로 제공합니다. 브라우저의 URL은 동일하게 유지됩니다.- 사용자 정의
Response
객체 반환: 오류 페이지나 특정 JSON 응답과 같은 사용자 정의 콘텐츠를 제공할 수 있습니다.
매처(Matchers)
매처를 사용하면 미들웨어를 적용할 라우트를 지정할 수 있습니다. 정규식이나 경로 패턴을 사용하여 매처를 정의할 수 있습니다. 이를 통해 미들웨어가 필요할 때만 실행되도록 하여 성능을 향상시키고 오버헤드를 줄일 수 있습니다.
엣지 런타임(Edge Runtime)
Next.js 미들웨어는 사용자와 가까운 곳에 배포될 수 있는 경량 자바스크립트 런타임 환경인 엣지 런타임에서 실행됩니다. 이러한 근접성은 대기 시간을 최소화하고 특히 전 세계적으로 분산된 사용자를 위한 애플리케이션의 전반적인 성능을 향상시킵니다. 엣지 런타임은 Vercel의 엣지 네트워크 및 기타 호환 플랫폼에서 사용할 수 있습니다. 엣지 런타임에는 몇 가지 제한 사항, 특히 Node.js API 사용에 대한 제한이 있습니다.
실용적인 예제: 미들웨어 기능 구현하기
1. 인증(Authentication)
인증 미들웨어는 사용자가 로그인해야 하는 라우트를 보호하는 데 사용할 수 있습니다. 다음은 쿠키를 사용하여 인증을 구현하는 방법의 예입니다:
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const token = request.cookies.get('auth_token');
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: ['/dashboard/:path*'],
}
이 미들웨어는 auth_token
쿠키의 존재 여부를 확인합니다. 쿠키가 없으면 사용자는 /login
페이지로 리디렉션됩니다. config.matcher
는 이 미들웨어가 /dashboard
아래의 라우트에 대해서만 실행되도록 지정합니다.
글로벌 관점: 다양한 지역의 사용자를 수용하기 위해 다양한 인증 방법(예: OAuth, JWT)을 지원하고 여러 ID 공급자(예: Google, Facebook, Azure AD)와 통합하도록 인증 로직을 조정하세요.
2. 인가(Authorization)
인가 미들웨어는 사용자 역할이나 권한에 따라 리소스에 대한 접근을 제어하는 데 사용할 수 있습니다. 예를 들어, 특정 사용자만 접근할 수 있는 관리자 대시보드가 있을 수 있습니다.
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export async function middleware(request: NextRequest) {
const token = request.cookies.get('auth_token');
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
// 예시: API에서 사용자 역할 가져오기 (실제 로직으로 교체하세요)
const userResponse = await fetch('https://api.example.com/userinfo', {
headers: {
Authorization: `Bearer ${token}`,
},
});
const userData = await userResponse.json();
if (userData.role !== 'admin') {
return NextResponse.redirect(new URL('/unauthorized', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: ['/admin/:path*'],
}
이 미들웨어는 사용자의 역할을 검색하고 admin
역할이 있는지 확인합니다. 그렇지 않으면 /unauthorized
페이지로 리디렉션됩니다. 이 예제는 플레이스홀더 API 엔드포인트를 사용합니다. `https://api.example.com/userinfo`를 실제 인증 서버 엔드포인트로 바꾸세요.
글로벌 관점: 사용자 데이터를 처리할 때 데이터 개인 정보 보호 규정(예: GDPR, CCPA)을 유념하세요. 민감한 정보를 보호하고 현지 법률을 준수하기 위해 적절한 보안 조치를 구현하세요.
3. 리디렉션(Redirection)
리디렉션 미들웨어는 사용자의 위치, 언어 또는 기타 기준에 따라 사용자를 리디렉션하는 데 사용할 수 있습니다. 예를 들어, IP 주소를 기반으로 사용자를 웹사이트의 현지화된 버전으로 리디렉션할 수 있습니다.
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const country = request.geo?.country || 'US'; // 지리적 위치 확인 실패 시 미국으로 기본 설정
if (country === 'DE') {
return NextResponse.redirect(new URL('/de', request.url))
}
if (country === 'FR') {
return NextResponse.redirect(new URL('/fr', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: ['/'],
}
이 미들웨어는 사용자의 IP 주소를 기반으로 국가를 확인하고 웹사이트의 적절한 현지화 버전(독일의 경우 /de
, 프랑스의 경우 /fr
)으로 리디렉션합니다. 지리적 위치 확인에 실패하면 미국 버전으로 기본 설정됩니다. 이 기능은 geo 속성을 사용할 수 있을 때(예: Vercel에 배포된 경우)에만 의존한다는 점에 유의하세요.
글로벌 관점: 웹사이트가 여러 언어와 통화를 지원하는지 확인하세요. 사용자에게 선호하는 언어 또는 지역을 수동으로 선택할 수 있는 옵션을 제공하세요. 각 로케일에 맞는 날짜 및 시간 형식을 사용하세요.
4. A/B 테스트(A/B Testing)
미들웨어는 사용자를 페이지의 다른 변형에 무작위로 할당하고 그들의 행동을 추적하여 A/B 테스트를 구현하는 데 사용할 수 있습니다. 다음은 간단한 예제입니다:
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
function getRandomVariant() {
return Math.random() < 0.5 ? 'A' : 'B';
}
export function middleware(request: NextRequest) {
let variant = request.cookies.get('variant')?.value;
if (!variant) {
variant = getRandomVariant();
const response = NextResponse.next();
response.cookies.set('variant', variant);
return response;
}
if (variant === 'B') {
return NextResponse.rewrite(new URL('/variant-b', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/'],
}
이 미들웨어는 사용자를 'A' 또는 'B' 변형 중 하나에 할당합니다. 사용자에게 아직 variant
쿠키가 없으면 무작위로 하나가 할당되고 설정됩니다. 'B' 변형에 할당된 사용자는 /variant-b
페이지로 재작성됩니다. 그런 다음 각 변형의 성능을 추적하여 어느 것이 더 효과적인지 결정하게 됩니다.
글로벌 관점: A/B 테스트를 설계할 때 문화적 차이를 고려하세요. 한 지역에서 잘 작동하는 것이 다른 지역의 사용자에게는 공감을 얻지 못할 수 있습니다. A/B 테스트 플랫폼이 여러 지역의 개인 정보 보호 규정을 준수하는지 확인하세요.
5. 기능 플래그(Feature Flags)
기능 플래그를 사용하면 새 코드를 배포하지 않고도 애플리케이션의 기능을 활성화하거나 비활성화할 수 있습니다. 미들웨어는 사용자 ID, 위치 또는 기타 기준에 따라 사용자가 특정 기능에 접근할 수 있는지 여부를 결정하는 데 사용할 수 있습니다.
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export async function middleware(request: NextRequest) {
// 예시: API에서 기능 플래그 가져오기
const featureFlagsResponse = await fetch('https://api.example.com/featureflags', {
headers: {
'X-User-Id': 'user123',
},
});
const featureFlags = await featureFlagsResponse.json();
if (featureFlags.new_feature_enabled) {
// 새 기능 활성화
return NextResponse.next();
} else {
// 새 기능 비활성화 (예: 대체 페이지로 리디렉션)
return NextResponse.redirect(new URL('/alternative-page', request.url));
}
}
export const config = {
matcher: ['/new-feature'],
}
이 미들웨어는 API에서 기능 플래그를 가져와서 new_feature_enabled
플래그가 설정되어 있는지 확인합니다. 설정되어 있으면 사용자는 /new-feature
페이지에 접근할 수 있습니다. 그렇지 않으면 /alternative-page
로 리디렉션됩니다.
글로벌 관점: 기능 플래그를 사용하여 여러 지역의 사용자에게 점진적으로 새로운 기능을 출시하세요. 이를 통해 더 넓은 대상에게 기능을 출시하기 전에 성능을 모니터링하고 문제를 해결할 수 있습니다. 또한 기능 플래그 시스템이 전 세계적으로 확장되고 사용자 위치에 관계없이 일관된 결과를 제공하는지 확인하세요. 기능 출시에 대한 지역별 규제 제약을 고려하세요.
고급 기술
미들웨어 체이닝(Chaining)
여러 미들웨어 함수를 함께 연결하여 요청에 대한 일련의 작업을 수행할 수 있습니다. 이는 복잡한 로직을 더 작고 관리하기 쉬운 모듈로 분해하는 데 유용할 수 있습니다.
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const response = NextResponse.next();
// 첫 번째 미들웨어 함수
const token = request.cookies.get('auth_token');
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
// 두 번째 미들웨어 함수
response.headers.set('x-middleware-custom', 'value');
return response;
}
export const config = {
matcher: ['/dashboard/:path*'],
}
이 예제는 하나의 미들웨어에 두 가지 작업을 보여줍니다. 첫 번째는 인증을 수행하고 두 번째는 사용자 정의 헤더를 설정합니다.
환경 변수 사용하기
API 키 및 데이터베이스 자격 증명과 같은 민감한 정보는 미들웨어 함수에 하드코딩하는 대신 환경 변수에 저장하세요. 이는 보안을 향상시키고 애플리케이션의 구성을 더 쉽게 관리할 수 있게 합니다.
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
const API_KEY = process.env.API_KEY;
export async function middleware(request: NextRequest) {
const response = await fetch('https://api.example.com/data', {
headers: {
'X-API-Key': API_KEY,
},
});
// ...
}
export const config = {
matcher: ['/data'],
}
이 예제에서는 API_KEY
를 환경 변수에서 가져옵니다.
오류 처리
미들웨어 함수에 강력한 오류 처리 기능을 구현하여 예기치 않은 오류로 인해 애플리케이션이 중단되는 것을 방지하세요. try...catch
블록을 사용하여 예외를 잡고 오류를 적절하게 기록하세요.
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export async function middleware(request: NextRequest) {
try {
const response = await fetch('https://api.example.com/data');
// ...
} catch (error) {
console.error('Error fetching data:', error);
return NextResponse.error(); // 또는 오류 페이지로 리디렉션
}
}
export const config = {
matcher: ['/data'],
}
모범 사례
- 미들웨어 함수를 가볍게 유지하세요: 미들웨어에서 계산 집약적인 작업을 피하세요. 성능에 영향을 미칠 수 있습니다. 복잡한 처리는 백그라운드 작업이나 전용 서비스로 오프로드하세요.
- 매처를 효과적으로 사용하세요: 필요한 라우트에만 미들웨어를 적용하세요.
- 미들웨어를 철저히 테스트하세요: 단위 테스트를 작성하여 미들웨어 함수가 올바르게 작동하는지 확인하세요.
- 미들웨어 성능을 모니터링하세요: 모니터링 도구를 사용하여 미들웨어 함수의 성능을 추적하고 병목 현상을 식별하세요.
- 미들웨어를 문서화하세요: 각 미들웨어 함수의 목적과 기능을 명확하게 문서화하세요.
- 엣지 런타임의 제한 사항을 고려하세요: Node.js API가 없는 등 엣지 런타임의 제한 사항을 인지하세요. 그에 따라 코드를 조정하세요.
일반적인 문제 해결
- 미들웨어가 실행되지 않음: 매처 구성을 다시 확인하여 미들웨어가 올바른 라우트에 적용되고 있는지 확인하세요.
- 성능 문제: 느린 미들웨어 함수를 식별하고 최적화하세요. 프로파일링 도구를 사용하여 성능 병목 현상을 찾아내세요.
- 엣지 런타임 호환성: 코드가 엣지 런타임과 호환되는지 확인하세요. 지원되지 않는 Node.js API 사용을 피하세요.
- 쿠키 문제: 쿠키가 올바르게 설정되고 검색되는지 확인하세요.
domain
,path
,secure
와 같은 쿠키 속성에 주의하세요. - 헤더 충돌: 미들웨어에서 사용자 정의 헤더를 설정할 때 발생할 수 있는 잠재적인 헤더 충돌에 유의하세요. 헤더가 기존 헤더를 의도치 않게 덮어쓰지 않도록 하세요.
결론
Next.js 미들웨어는 동적이고 개인화된 웹 애플리케이션을 구축하기 위한 강력한 도구입니다. 요청 가로채기를 마스터함으로써 인증 및 인가부터 리디렉션 및 A/B 테스트에 이르기까지 광범위한 기능을 구현할 수 있습니다. 이 가이드에 설명된 모범 사례를 따르면 Next.js 미들웨어를 활용하여 글로벌 사용자 기반의 요구를 충족하는 고성능의 안전하고 확장 가능한 애플리케이션을 만들 수 있습니다. 미들웨어의 힘을 받아들여 Next.js 프로젝트에서 새로운 가능성을 열고 탁월한 사용자 경험을 제공하세요.