Next.js Middleware 실전 가이드: 경로 매칭, Edge Runtime 제한과 흔한 함정

Vercel 오류 로그를 들여다보고 있었습니다. 막 배포한 관리자 시스템은 로컬 테스트에서 모든 /dashboard 경로를 제대로 보호했습니다. 로그인하지 않은 사용자는 로그인 페이지로 리디렉션됐죠. 그런데 프로덕션 환경에서는 사용자가 /dashboard/settings/profile에 직접 접근해 인증을 우회하고 민감한 데이터까지 볼 수 있었습니다.
곧바로 코드를 열어 봤습니다. Middleware 파일은 제자리에 있었고 로직에도 문제가 없었습니다. 대체 무엇이 잘못된 걸까요?
Next.js 문서를 30분쯤 뒤진 끝에 matcher 설정에 관한 짧은 절에서 답을 찾았습니다. 저는 /dashboard/:path라고 작성했는데, 이 설정은 /dashboard/settings처럼 한 단계 아래의 경로만 매칭합니다. 여러 단계의 경로에서는 작동하지 않습니다. 올바른 설정은 /dashboard/:path*였습니다. 작은 별표 하나를 빠뜨린 탓에 큰 사고를 낼 뻔했습니다.
사실 Middleware 때문에 곤란을 겪은 건 이번이 처음이 아닙니다. Edge Runtime에서 특정 라이브러리가 작동하지 않거나, matcher 설정이 적용되지 않거나, 무한 리디렉션 루프에 빠지는 등 거의 모든 함정을 한 번씩 밟아 봤습니다.
Next.js Middleware를 사용 중이거나 인증, 국제화, A/B 테스트에 활용할 계획이라면 이 글이 도움이 될 것입니다. 자주 마주치는 함정과 이해하기 어려운 설정 규칙, 그리고 세 가지 완전한 실전 사례를 하나씩 풀어 설명하겠습니다.
“오늘날의 Web 개발에서는” 같은 상투적인 이야기는 하지 않겠습니다. 실제 문제와 작성 방법, 함정을 피하는 법, Middleware를 제대로 작동시키는 방법에만 집중하겠습니다.
Middleware란 정확히 무엇이며 왜 사용해야 할까요?
간단히 말해 Middleware는 하나의 ‘관문’입니다.
사용자의 요청은 페이지나 API에 도달하기 전에 이 관문을 통과합니다. 이곳에서 사용자 신원을 확인하고 요청 내용을 수정하거나 응답을 즉시 반환할 수도 있습니다. 예를 들어 로그인하지 않은 사용자를 로그인 페이지로 보내거나 사용자 지역에 따라 다른 언어 버전으로 이동시킬 수 있습니다.
Middleware는 Edge Runtime에서 실행됩니다. 이 점이 중요합니다. Edge Runtime은 여러분의 서버가 아니라 사용자와 가까운 에지 노드(CDN)에 배포됩니다. 거리가 가깝고 지연 시간이 짧으며 콜드 스타트가 거의 없습니다. Middleware는 사용자에게 가장 가까운 코드 계층이라고 이해하면 됩니다.
Edge Runtime과 Node.js Runtime은 무엇이 다른가요?
| 특성 | Edge Runtime | Node.js Runtime |
|---|---|---|
| 시작 속도 | 콜드 스타트가 거의 없음 | 수백 밀리초가 필요함 |
| 실행 위치 | 전 세계 에지 노드 | 특정 서버 |
| API 지원 | Web 표준 API | 완전한 Node.js API |
| 적합한 용도 | 가벼운 로직, 빠른 응답 | 복잡한 계산, 데이터베이스 작업 |
요약하면 빠르지만 기능에 제한이 있습니다. fs, path 같은 Node.js 모듈을 사용할 수 없고 대부분의 데이터베이스에도 직접 연결할 수 없습니다. 바로 이 부분에서 가장 많은 문제가 발생합니다.
Middleware는 언제 사용해야 할까요?
모든 로직을 Middleware에 넣어서는 안 됩니다. 가장 흔한 활용 사례는 다음과 같습니다.
1. 인증(Auth Gate)
가장 대표적인 용도입니다. 사용자의 로그인 여부를 확인하고 로그인하지 않았다면 로그인 페이지로 리디렉션합니다. 요청이 서버에 도달하기 전에 처리하므로 Server Component에서 판단하는 것보다 빠릅니다.
2. 국제화(i18n)
URL, Cookie, 브라우저 설정 등에서 가져온 사용자 언어 기본 설정에 따라 해당 언어 버전으로 자동 이동합니다. 예를 들면 / → /zh 또는 /en입니다.
3. A/B 테스트
사용자를 무작위로 두 그룹에 나눠 서로 다른 버전의 페이지를 보여 줍니다. Cookie로 그룹을 유지하면 새로고침할 때마다 다른 콘텐츠가 나타나는 일을 막을 수 있습니다.
4. Bot 탐지와 속도 제한
크롤러나 악의적인 요청을 차단하거나 특정 IP의 요청 빈도를 제한합니다.
5. 로그와 데이터 통계
각 요청의 경로, 출처, UA 같은 기본 정보를 기록해 분석 서비스로 보냅니다.
6. 콘텐츠 다시 쓰기(Rewrite)
사용자가 방문한 URL을 내부적으로 다른 경로에 매핑하되 브라우저 주소 표시줄은 바꾸지 않습니다. 동적 경로나 A/B 테스트를 구현할 때 특히 유용합니다.
페이지 컴포넌트에서 직접 처리하면 안 되나요?
가능하지만 더 느립니다. Server Component나 Client Component의 로직은 요청이 서버에 도달하거나 페이지 렌더링이 시작된 뒤에 실행됩니다. 반면 Middleware는 에지에서 먼저 요청을 가로채므로 응답이 빠르고 사용자 경험도 좋아집니다.
중앙에서 관리할 수 있다는 장점도 있습니다. 보호해야 할 페이지마다 인증 로직을 반복해서 작성하고 싶지는 않을 것입니다. Middleware를 사용하면 한곳에서 처리할 수 있습니다.
그렇다고 모든 것을 Middleware에 넣어서는 안 됩니다. 복잡한 비즈니스 로직, 데이터베이스 조회, 많은 계산은 API 경로나 Server Component에 맡기세요. Middleware는 가볍고 빨라야 합니다.
Middleware 기본 설정과 파일 구조
파일은 어디에 두나요?
Next.js는 Middleware 위치에 엄격한 규칙을 적용합니다. 프로젝트 루트 또는 src 디렉터리에 두고 파일 이름은 반드시 middleware.ts(또는 .js)여야 합니다.
프로젝트 루트/
├── app/
├── middleware.ts ← 여기에 배치
├── package.json
src 디렉터리를 사용한다면 다음과 같습니다.
프로젝트 루트/
├── src/
│ ├── app/
│ ├── middleware.ts ← 여기에 배치
├── package.json
주의: 프로젝트에는 middleware.ts 파일을 하나만 둘 수 있습니다. app 디렉터리나 다른 위치에 여러 Middleware 파일을 만들 수 없습니다. Middleware가 전역 ‘문지기’ 역할을 한다는 점을 생각하면 합리적인 설계입니다.
가장 간단한 Middleware의 모습
import { NextRequest, NextResponse } from 'next/server';
export function middleware(request: NextRequest) {
console.log('요청이 들어왔습니다:', request.url);
return NextResponse.next(); // 통과시키고 계속 실행
}
이게 전부입니다. NextResponse.next()는 “문제없으니 계속 진행”한다는 뜻이며, 요청은 대상 페이지나 API에 정상적으로 도달합니다.
핵심 API: NextRequest와 NextResponse
NextRequest는 표준 Web Request를 확장하며 편리한 속성을 제공합니다.
request.nextUrl: 파싱된 URL 객체로 pathname, search 등을 바로 가져올 수 있습니다.request.cookies: Cookie를 더 편리하게 읽고 쓸 수 있습니다.request.geo: 사용자 지리 정보입니다. Vercel처럼 배포 플랫폼이 지원해야 합니다.
NextResponse는 여러 가지 응답 방식을 제공합니다.
1. 통과(계속 실행)
return NextResponse.next();
2. 리디렉션(새 URL로 이동)
return NextResponse.redirect(new URL('/login', request.url));
사용자의 주소 표시줄도 바뀝니다.
3. 다시 쓰기(내부 리디렉션, URL은 그대로)
return NextResponse.rewrite(new URL('/dashboard/v2', request.url));
사용자는 /dashboard에 접근하지만 실제로는 /dashboard/v2의 콘텐츠를 받습니다. 주소 표시줄에는 계속 /dashboard가 표시됩니다. A/B 테스트나 버전 전환에 특히 유용합니다.
4. 응답 직접 반환
return new NextResponse('접근이 거부되었습니다', { status: 403 });
처리를 계속하지 않고 사용자에게 바로 콘텐츠를 반환합니다.
조금 더 실용적인 예제: 사용자 정의 Header 추가
import { NextRequest, NextResponse } from 'next/server';
export function middleware(request: NextRequest) {
const response = NextResponse.next();
// 모든 응답에 사용자 정의 header 추가
response.headers.set('x-custom-header', 'my-value');
return response;
}
모든 응답에 타임스탬프를 넣거나 요청 출처를 표시하고 싶을 때 자주 사용하는 방식입니다.
버전 안내(중요)
Next.js 15를 사용한다면 공식적으로 middleware.ts의 이름이 proxy.ts로 바뀌었다는 점에 주의하세요. middleware.ts도 하위 호환성 덕분에 계속 사용할 수 있지만 새 프로젝트에서는 새 이름을 권장합니다. 이 글의 코드는 Next.js 14/15를 기준으로 하며 두 버전 모두에 적용됩니다. 주요 개념과 API는 달라지지 않았습니다.
경로 매칭(Matcher): 가장 빠지기 쉬운 함정
솔직히 matcher에서 가장 많은 실수를 했습니다.
matcher가 필요한 이유
matcher를 설정하지 않으면 CSS, JS, 이미지, 글꼴 같은 정적 리소스를 포함해 모든 요청에서 Middleware가 실행됩니다. 페이지 하나를 불러올 때 브라우저가 정적 파일 20개를 요청하면 Middleware도 20번 실행되는 셈입니다. 리소스를 낭비할 뿐 아니라 응답 속도까지 떨어질 수 있습니다.
matcher는 Next.js에 “이 경로에서만 Middleware를 실행하고 나머지는 건드리지 말라”고 알려 줍니다.
기본 문법
middleware.ts 파일에서 config 객체를 내보냅니다.
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*']
}
이 설정은 /dashboard와 /api로 시작하는 경로에 접근할 때만 Middleware를 실행합니다.
수정자 *, +, ?의 의미
이 기호는 경로가 어느 범위까지 매칭되는지를 제어합니다.
*(0개 이상)
/dashboard/:path*가 매칭하는 경로:
/dashboard✓/dashboard/settings✓/dashboard/settings/profile✓
+(1개 이상)
/dashboard/:path+가 매칭하는 경로:
/dashboard✗/dashboard/settings✓/dashboard/settings/profile✓
?(0개 또는 1개)
/dashboard/:path?가 매칭하는 경로:
/dashboard✓/dashboard/settings✓/dashboard/settings/profile✗
대부분의 상황에서는 *면 충분합니다.
흔한 함정과 해결 방법(중요)
다음 표는 직접 실수하며 정리한 내용이니 저장해 두어도 좋습니다.
| 문제 | 잘못된 작성법 | 올바른 작성법 | 이유 |
|---|---|---|---|
| 동적 경로의 여러 계층이 매칭되지 않음 | /dashboard/:path | /dashboard/:path* | *가 없으면 한 계층만 매칭하므로 여러 계층에서 실패함 |
| 루트 경로가 빠짐 | matcher: ['/dashboard/:path*'] | matcher: ['/', '/dashboard/:path*'] | 루트 경로 /는 자동으로 포함되지 않으므로 명시해야 함 |
| 정적 리소스가 가로채짐 | matcher: ['/:path*'] | matcher: ['/((?!_next|favicon.ico).*)'] | 정규 표현식으로 _next 같은 내부 경로를 제외해야 함 |
| API 경로가 보호되지 않음 | matcher: ['/api/users'] | matcher: ['/api/:path*'] | 구체적인 경로는 하나만 매칭하므로 와일드카드로 모든 API를 포함해야 함 |
가장 자주 걸리는 정적 리소스 함정
다음과 같은 matcher를 작성했다고 가정해 봅시다.
export const config = {
matcher: ['/:path*'] // 모든 경로를 매칭하려는 의도
}
그러면 Next.js 내부의 _next/static 경로까지 매칭되어 Middleware가 계속 실행되고 페이지 로딩 속도가 크게 느려집니다.
올바른 방법: 부정형 전방 탐색으로 제외하기
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
}
이 정규 표현식은 “api, _next/static, _next/image, favicon.ico를 제외한 모든 경로를 매칭한다”는 뜻입니다.
정규 표현식이 꽤 복잡해 보이지만 저도 공식 예제를 참고했습니다. 원리를 깊이 파고들 필요 없이 그대로 사용해도 됩니다.
또 다른 함정: 동적 값을 사용할 수 없음
const lang = 'zh'; // 변수
export const config = {
matcher: [`/${lang}/:path*`] // ❌ 사용할 수 없음
}
matcher는 정적이어야 하며 컴파일 시점에 값을 확정할 수 있어야 합니다. 템플릿 문자열에 변수를 삽입하거나 런타임에 동적으로 생성할 수 없습니다.
동적 판단이 필요하다면 로직을 middleware 함수 안에 넣으세요.
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// 여기에서 동적으로 판단
if (pathname.startsWith('/zh') || pathname.startsWith('/en')) {
// 처리 로직
}
return NextResponse.next();
}
// matcher는 정적으로 유지
export const config = {
matcher: ['/:locale/:path*']
}
추천 matcher 템플릿(그대로 사용해도 됨)
특정 경로 보호(예: 관리자 페이지):
export const config = {
matcher: ['/dashboard/:path*', '/admin/:path*']
}
정적 리소스를 제외한 모든 경로 매칭:
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|.*\\.png$).*)',
],
}
모든 API 경로 보호:
export const config = {
matcher: ['/api/:path*']
}
matcher에 관한 Next.js 문서는 꽤 간략해서 세부 사항은 직접 겪으며 익혀야 할 때가 많습니다. 이 표가 시행착오를 줄이는 데 도움이 되길 바랍니다.
Edge Runtime 제한과 우회 방법
이 장에서 다룰 문제를 처음 만났을 때는 정말 당황했습니다.
Middleware에서 사용자 신원을 확인하려고 token의 유효성을 데이터베이스에서 조회하는 코드를 작성했습니다. 로컬에서 실행하자 Native Node.js APIs are not supported in the Edge Runtime 오류가 발생했습니다.
데이터베이스에 연결했을 뿐인데 왜 지원하지 않는다는 걸까요?
알고 보니 Edge Runtime은 완전한 Node.js 환경이 아니며 흔히 쓰는 API와 라이브러리 상당수를 사용할 수 없었습니다.
Edge Runtime이 지원하지 않는 것
| 기능 분류 | 지원하지 않는 API/모듈 | 영향 |
|---|---|---|
| 파일 시스템 | fs, path | 로컬 파일을 읽거나 쓸 수 없음 |
| 하위 프로세스 | child_process | 외부 명령을 실행할 수 없음 |
| 암호화 | 일부 crypto API | Web Crypto API로 대체해야 함 |
| 데이터베이스 | MongoDB, MySQL 네이티브 드라이버 | 전통적인 데이터베이스 드라이버 대부분을 사용할 수 없음 |
| 기타 | process.emit, setImmediate | 일부 저수준 Node.js API를 사용할 수 없음 |
실제로 어떤 영향을 주나요?
가장 직접적인 영향은 다음과 같습니다.
- 사용자 신원을 확인하기 위해 데이터베이스에 직접 연결할 수 없습니다.
config.json같은 설정 파일을 읽을 수 없습니다.- Node.js API에 의존하는 서드파티 라이브러리를 사용할 수 없습니다.
제약이 커 보이지만 우회할 방법이 있습니다.
우회 방법: Edge Runtime을 ‘전초 기지’로 사용하기
핵심은 Middleware에서는 가벼운 판단만 하고 복잡한 로직은 이후 단계에 맡기는 것입니다.
| 요구 사항 | ❌ Edge Runtime 제한 | ✅ 해결 방법 |
|---|---|---|
| 사용자 인증 | 데이터베이스 조회 불가 | JWT를 로컬에서 검증하거나 API 경로 호출 |
| 암호화와 복호화 | 일부 crypto 사용 불가 | Web Crypto API 사용 |
| 설정 읽기 | 파일 시스템 접근 불가 | 환경 변수(process.env) 또는 API 사용 |
| 로그 기록 | 로컬 파일에 쓸 수 없음 | Logtail 같은 외부 로그 서비스로 전송 |
| 데이터베이스 작업 | 전통적인 드라이버 미지원 | Vercel Postgres, Supabase 등 Edge 지원 데이터베이스 사용 |
실전 예제: JWT 검증(권장 방식)
JWT(JSON Web Token)는 상태 비저장 방식이므로 Middleware 인증에 가장 적합합니다. token 자체에 필요한 정보가 들어 있어 데이터베이스를 조회할 필요가 없습니다.
import { NextRequest, NextResponse } from 'next/server';
import { jwtVerify } from 'jose'; // Edge Runtime을 지원하는 라이브러리
export async function middleware(request: NextRequest) {
const token = request.cookies.get('auth-token')?.value;
// token이 없으면 로그인 페이지로 리디렉션
if (!token) {
return NextResponse.redirect(new URL('/login', request.url));
}
try {
// jose 라이브러리로 JWT 검증(Edge Runtime 지원)
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
const { payload } = await jwtVerify(token, secret);
// 검증에 성공하면 사용자 정보를 header에 추가(선택 사항)
const response = NextResponse.next();
response.headers.set('x-user-id', payload.userId as string);
return response;
} catch (error) {
// token이 유효하지 않으면 cookie를 삭제하고 리디렉션
const response = NextResponse.redirect(new URL('/login', request.url));
response.cookies.delete('auth-token');
return response;
}
}
export const config = {
matcher: ['/dashboard/:path*']
}
핵심 포인트:
jsonwebtoken은 Node.js의crypto모듈에 의존하므로 대신jose를 사용합니다.- JWT secret은 환경 변수에서 읽습니다.
process.env는 Edge Runtime에서 사용할 수 있습니다. - 검증에 실패하면 cookie를 삭제해 이후 요청에서 같은 실패가 반복되지 않게 합니다.
반드시 데이터베이스를 호출해야 한다면?
사용자가 차단되었는지 확인하는 것처럼 데이터베이스 조회가 꼭 필요한 경우에는 Middleware에서 API 경로를 호출할 수 있습니다.
export async function middleware(request: NextRequest) {
const userId = request.cookies.get('user-id')?.value;
if (!userId) {
return NextResponse.redirect(new URL('/login', request.url));
}
// API 경로를 호출해 사용자 상태 확인
const apiUrl = new URL('/api/check-user-status', request.url);
const response = await fetch(apiUrl, {
headers: { 'x-user-id': userId }
});
const { isActive } = await response.json();
if (!isActive) {
return NextResponse.redirect(new URL('/account-suspended', request.url));
}
return NextResponse.next();
}
API 경로는 Node.js Runtime에서 실행되므로 데이터베이스에 자유롭게 연결할 수 있습니다. 다만 지연 시간이 늘어나므로 꼭 필요할 때만 사용하세요.
Web Crypto API에 관하여
암호화와 복호화가 필요하다면 브라우저 네이티브 Web Crypto API를 사용합니다.
// 해시 생성
const data = new TextEncoder().encode('hello world');
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
솔직히 Node.js의 crypto보다 사용하기 번거롭지만 Edge Runtime에서는 이 방법을 써야 합니다.
특정 라이브러리가 Edge Runtime을 지원하는지 확인하는 방법
문서를 확인하거나 직접 실행해 보세요. Native Node.js APIs are not supported 오류가 발생하면 지원하지 않는 것입니다.
일부 인기 라이브러리는 Edge 전용 버전을 제공합니다.
- JWT:
jsonwebtoken대신jose - 데이터베이스: Vercel Postgres, Supabase, Prisma(일부 지원)
- 로그: Logtail, Axiom
제 권장은 간단합니다. Middleware에서 지나치게 복잡한 작업을 하지 마세요. 빠르게 판단하고 넘기는 역할만 맡기고 무거운 작업은 API 경로에 남겨 두는 편이 좋습니다.
실전 사례: 세 가지 핵심 시나리오 전체 구현
이론을 충분히 살펴봤으니 이제 코드를 보겠습니다. 다음 세 사례는 실제 프로젝트에서 사용했던 방식이며 그대로 복사해 실행할 수 있습니다.
사례 1: 인증과 경로 보호
상황: 관리자 시스템의 /dashboard 아래 모든 페이지는 로그인한 사용자만 접근할 수 있어야 합니다.
전체 코드:
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { jwtVerify } from 'jose';
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// token 가져오기
const token = request.cookies.get('auth-token')?.value;
// 로그인하지 않았다면 원래 URL을 기록하고 로그인 페이지로 리디렉션
if (!token) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('from', pathname); // 로그인 후 원래 페이지로 돌아가기
return NextResponse.redirect(loginUrl);
}
try {
// JWT 검증
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
const { payload } = await jwtVerify(token, secret);
// 선택 사항: token의 만료가 임박했으면 자동 갱신
const expiresAt = payload.exp as number;
const now = Math.floor(Date.now() / 1000);
const shouldRefresh = expiresAt - now < 3600; // 1시간 미만이면 갱신
const response = NextResponse.next();
if (shouldRefresh) {
// 여기에서 API 경로를 호출해 token 갱신 가능
// 예제를 단순화하기 위해 생략
response.headers.set('x-token-refresh-needed', 'true');
}
// 사용자 정보를 페이지에 전달(선택 사항)
response.headers.set('x-user-id', payload.userId as string);
response.headers.set('x-user-role', payload.role as string);
return response;
} catch (error) {
// token이 유효하지 않거나 만료되면 삭제하고 리디렉션
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('from', pathname);
loginUrl.searchParams.set('reason', 'expired');
const response = NextResponse.redirect(loginUrl);
response.cookies.delete('auth-token');
return response;
}
}
export const config = {
matcher: ['/dashboard/:path*']
}
핵심 포인트:
from매개변수에 사용자가 가려던 경로를 기록해 로그인 후 돌아갈 수 있게 합니다.- token 만료가 임박했는지 확인해 미리 갱신하면 작업 도중 갑자기 로그아웃되는 일을 피할 수 있습니다.
- 사용자 정보를 header에 넣으면 페이지 컴포넌트에서 바로 읽을 수 있습니다(선택 사항).
테스트 방법:
- 브라우저 cookie를 삭제하고
/dashboard에 접근 →/login?from=/dashboard로 이동해야 합니다. - 로그인 후 cookie를 설정하고 다시 접근 → 정상적으로 표시되어야 합니다.
흔한 문제:
- 문제: 로컬 테스트에서는 정상인데 배포 후 작동하지 않음
원인: 프로덕션 환경에JWT_SECRET환경 변수를 설정하지 않음
해결: Vercel, Netlify 등 배포 플랫폼의 설정에서 환경 변수를 추가합니다.
사례 2: 국제화(i18n) 경로 리디렉션
상황: 사이트가 중국어와 영어를 지원하며, 사용자가 /에 접근하면 언어 기본 설정에 따라 /zh 또는 /en으로 자동 이동해야 합니다.
전체 코드:
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
const supportedLocales = ['en', 'zh', 'ja'];
const defaultLocale = 'en';
function getPreferredLocale(request: NextRequest): string {
// 우선순위 1: URL 매개변수(수동 전환에 사용)
const urlLocale = request.nextUrl.searchParams.get('lang');
if (urlLocale && supportedLocales.includes(urlLocale)) {
return urlLocale;
}
// 우선순위 2: Cookie(사용자가 마지막에 선택한 언어)
const cookieLocale = request.cookies.get('NEXT_LOCALE')?.value;
if (cookieLocale && supportedLocales.includes(cookieLocale)) {
return cookieLocale;
}
// 우선순위 3: 브라우저 언어(Accept-Language header)
const acceptLanguage = request.headers.get('accept-language');
if (acceptLanguage) {
// "zh-CN,zh;q=0.9,en;q=0.8" 간단히 파싱
const browserLang = acceptLanguage.split(',')[0].split('-')[0];
if (supportedLocales.includes(browserLang)) {
return browserLang;
}
}
return defaultLocale;
}
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// 경로에 언어 접두사가 이미 있는지 확인
const pathnameHasLocale = supportedLocales.some(
locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
);
if (!pathnameHasLocale) {
// 언어 접두사가 없으면 언어가 포함된 경로로 리디렉션
const locale = getPreferredLocale(request);
const newUrl = new URL(`/${locale}${pathname}`, request.url);
// 쿼리 매개변수 유지
newUrl.search = request.nextUrl.search;
const response = NextResponse.redirect(newUrl);
// 사용자의 선택을 기억하도록 cookie 설정(30일)
response.cookies.set('NEXT_LOCALE', locale, {
maxAge: 60 * 60 * 24 * 30,
path: '/'
});
return response;
}
return NextResponse.next();
}
export const config = {
matcher: [
// 모든 경로를 매칭하되 정적 리소스와 API는 제외
'/((?!api|_next/static|_next/image|favicon.ico|.*\\.).*)'
]
}
핵심 포인트:
- URL 매개변수 > Cookie > 브라우저 설정의 세 단계로 언어를 감지합니다.
- Cookie로 사용자의 선택을 기억해 다음 방문에서 마지막 언어를 바로 사용합니다.
- matcher에서 정적 리소스를 제외해 이미지까지 리디렉션되는 일을 막습니다.
next-intl 라이브러리와 통합:
next-intl을 사용하면 많은 부분을 간소화할 수 있습니다.
import { createI18nMiddleware } from 'next-intl/middleware';
export default createI18nMiddleware({
locales: ['en', 'zh', 'ja'],
defaultLocale: 'en'
});
export const config = {
matcher: ['/((?!api|_next|.*\\.).)']
};
next-intl이 언어 감지와 Cookie 관리 등을 자동으로 처리하므로 훨씬 편리합니다.
사례 3: A/B 테스트와 기능 플래그
상황: 홈페이지를 개편한 뒤 사용자 50%에게 새 버전을 보여 주고, 데이터를 수집한 후 전체 배포 여부를 결정하려고 합니다.
전체 코드:
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// 홈페이지에서만 A/B 테스트 실행
if (pathname !== '/') {
return NextResponse.next();
}
// 사용자가 이미 그룹에 배정되었는지 확인
let variant = request.cookies.get('ab-test-homepage')?.value;
if (!variant) {
// 새 사용자를 A 또는 B 그룹에 무작위 배정
variant = Math.random() < 0.5 ? 'A' : 'B';
}
let response: NextResponse;
if (variant === 'B') {
// B 그룹: 새 버전 페이지로 다시 쓰기(URL은 변경하지 않음)
response = NextResponse.rewrite(new URL('/homepage-v2', request.url));
} else {
// A 그룹: 기존 버전 사용
response = NextResponse.next();
}
// 사용자 그룹을 일관되게 유지하도록 cookie 설정(7일)
response.cookies.set('ab-test-homepage', variant, {
maxAge: 60 * 60 * 24 * 7,
path: '/'
});
// 데이터 분석을 위해 사용자의 그룹을 header에 표시
response.headers.set('x-ab-variant', variant);
return response;
}
export const config = {
matcher: ['/']
}
핵심 포인트:
redirect대신rewrite를 사용하므로 사용자에게 보이는 URL은 계속/이고 경험도 더 자연스럽습니다.- Cookie가 사용자 그룹을 일관되게 유지해 새로고침할 때마다 다른 버전이 나타나는 일을 막습니다.
- header로 그룹을 표시하면 데이터 분석 플랫폼이 사용자의 그룹을 쉽게 식별할 수 있습니다.
데이터 추적 제안:
페이지 컴포넌트에서 header를 읽습니다.
// app/page.tsx
import { headers } from 'next/headers';
export default function HomePage() {
const headersList = headers();
const abVariant = headersList.get('x-ab-variant');
// 추적 데이터 전송
useEffect(() => {
analytics.track('page_view', {
page: 'homepage',
variant: abVariant
});
}, [abVariant]);
return <div>...</div>;
}
이렇게 하면 분석 도구에서 A/B 두 그룹의 전환율 차이를 확인할 수 있습니다.
고급: 무작위가 아니라 사용자 ID로 그룹 배정
같은 사용자가 여러 기기에서도 동일한 버전을 보게 하려면 다음과 같이 처리합니다.
const userId = request.cookies.get('user-id')?.value;
if (userId) {
// 사용자 ID의 해시로 그룹 배정(안정적인 매핑)
const hash = simpleHash(userId);
variant = hash % 2 === 0 ? 'A' : 'B';
} else {
// 로그인하지 않은 사용자는 cookie 사용
variant = request.cookies.get('ab-test-homepage')?.value ||
(Math.random() < 0.5 ? 'A' : 'B');
}
// 간단한 해시 함수
function simpleHash(str: string): number {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) - hash) + str.charCodeAt(i);
hash |= 0;
}
return Math.abs(hash);
}
이 세 가지 사례는 Middleware의 가장 흔한 활용법을 대부분 다룹니다. 인증에 국제화를 더하는 식으로 조합해서 사용할 수도 있습니다.
성능 최적화와 모범 사례
Middleware는 작성하기 쉽지만 잘 작성하려면 몇 가지 요령이 필요합니다.
Middleware 로직을 모듈로 구성하기
프로젝트가 커지면 모든 로직을 하나의 middleware.ts에 넣었을 때 관리하기 어려워집니다. Next.js는 middleware 파일을 하나만 허용하지만, 로직은 여러 함수 모듈로 나눌 수 있습니다.
권장 디렉터리 구조:
프로젝트 루트/
├── middleware/
│ ├── auth.ts # 인증 로직
│ ├── i18n.ts # 국제화 로직
│ ├── ab-test.ts # A/B 테스트 로직
│ └── rate-limit.ts # 속도 제한 로직
├── middleware.ts # 진입점 파일
진입점 파일 예제:
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { checkAuth } from './middleware/auth';
import { handleI18n } from './middleware/i18n';
import { handleABTest } from './middleware/ab-test';
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// 1. 국제화를 먼저 처리
const i18nResponse = handleI18n(request);
if (i18nResponse) return i18nResponse;
// 2. 다음으로 인증 확인
if (pathname.startsWith('/dashboard')) {
const authResponse = await checkAuth(request);
if (authResponse) return authResponse;
}
// 3. 마지막으로 A/B 테스트 처리
if (pathname === '/') {
return handleABTest(request);
}
return NextResponse.next();
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)']
}
auth.ts 예제:
// middleware/auth.ts
import { NextRequest, NextResponse } from 'next/server';
import { jwtVerify } from 'jose';
export async function checkAuth(request: NextRequest): Promise<NextResponse | null> {
const token = request.cookies.get('auth-token')?.value;
if (!token) {
return NextResponse.redirect(new URL('/login', request.url));
}
try {
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
await jwtVerify(token, secret);
return null; // 검증에 성공하면 계속 진행한다는 의미로 null 반환
} catch {
return NextResponse.redirect(new URL('/login', request.url));
}
}
이렇게 나누면 각 모듈의 책임이 명확해지고 수정하기도 쉽습니다.
캐시 전략: 반복 계산 줄이기
사용자 권한 확인처럼 일부 로직은 캐시해 매 요청마다 다시 계산하지 않아도 됩니다. token이 유효한 동안에는 매번 검증할 필요가 없는 경우도 있습니다.
Vercel Edge Config로 설정 캐시하기:
import { get } from '@vercel/edge-config';
export async function middleware(request: NextRequest) {
// Edge Config에서 기능 플래그 읽기(캐시됨)
const featureFlags = await get('feature-flags');
if (featureFlags?.newDashboard) {
return NextResponse.rewrite(new URL('/dashboard-v2', request.url));
}
return NextResponse.next();
}
Edge Config는 Vercel이 제공하는 전 세계 분산 키-값 저장소입니다. 읽기 속도가 매우 빨라 자주 바뀌지 않는 설정을 저장하기에 적합합니다.
지나치게 큰 Header 피하기
Middleware에서 설정한 header는 응답에 추가됩니다. header가 너무 많거나 크면 431 Request Header Fields Too Large 오류가 발생할 수 있습니다.
권장 사항:
- Header 전체 크기를 8KB 이하로 유지합니다.
- 필요한 정보만 전달하고 사용자 객체 전체를 넣지 않습니다.
- 많은 데이터를 전달해야 한다면 암호화한 token을 고려합니다.
잘못된 예:
// ❌ 이렇게 하지 마세요
response.headers.set('x-user-data', JSON.stringify(userData)); // 너무 클 수 있음
올바른 방법:
// ✅ 핵심 정보만 전달
response.headers.set('x-user-id', user.id);
response.headers.set('x-user-role', user.role);
Matcher 최적화: 와일드카드보다 정확한 매칭
matcher가 정확할수록 Next.js의 실행 효율이 높아집니다.
좋지 않은 작성법:
export const config = {
matcher: ['/:path*'] // 모든 경로 매칭
}
더 좋은 작성법:
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'] // 필요한 경로만 매칭
}
Middleware가 몇 개의 특정 경로만 보호하면 된다면 와일드카드로 모든 경로를 매칭하지 마세요.
모니터링과 디버깅
개발 환경 디버깅:
export function middleware(request: NextRequest) {
if (process.env.NODE_ENV === 'development') {
console.log('Middleware 실행:', {
path: request.nextUrl.pathname,
method: request.method,
cookies: request.cookies.getAll()
});
}
// 로직...
}
프로덕션 환경 로그:
Edge Runtime의 console.log는 Vercel의 Edge Function Logs 같은 플랫폼 로그에 출력됩니다. 로그가 너무 많으면 실행 시간이 늘어나므로 주의하세요.
권장 로그 방식: 외부 서비스로 전송
import { Logger } from '@logtail/edge';
const logger = new Logger(process.env.LOGTAIL_TOKEN);
export async function middleware(request: NextRequest) {
try {
// 로직...
} catch (error) {
// 오류가 발생할 때만 기록
await logger.error('Middleware error', {
path: request.nextUrl.pathname,
error: error.message
});
throw error;
}
}
모범 사례 체크리스트
지금까지 직접 겪으며 정리한 내용을 요약하면 다음과 같습니다.
✅ 해야 할 일:
- Middleware 로직을 가볍게 유지하고 복잡한 로직은 API 경로에 맡깁니다.
- matcher로 처리할 경로를 정확히 지정합니다.
- 데이터베이스 조회를 피하고 JWT로 인증합니다.
- 기능별 모듈로 코드를 나눕니다.
- 개발 환경에서 디버그 정보를 출력합니다.
- Cookie 만료 시간을 적절히 설정합니다.
❌ 하지 말아야 할 일:
- Middleware에서 많은 계산이나 데이터베이스 작업을 수행하지 않습니다.
- matcher를 생략해 모든 요청에서 실행되게 하지 않습니다.
- Node.js API에 의존하는 서드파티 라이브러리를 사용하지 않습니다.
- 너무 큰 header(>8KB)를 설정하지 않습니다.
- 프로덕션 환경에서 지나치게 많은 로그를 남기지 않습니다.
- 무한 리디렉션 루프를 만들지 않도록 재귀 실행 여부를 확인합니다.
이 원칙을 따르면 Middleware에서 큰 문제를 겪을 가능성이 줄어듭니다.
흔한 오류와 디버깅 팁
마지막으로 특히 골치 아픈 오류들을 살펴보겠습니다. 모두 직접 겪어 봤고 일부는 해결하는 데 꽤 오랜 시간이 걸렸습니다.
오류 1: Middleware가 전혀 실행되지 않음
증상: Middleware를 작성했지만 아무 효과가 없는 것처럼 요청이 그대로 통과합니다.
가능한 원인과 해결 방법:
| 원인 | 확인 방법 | 해결 방법 |
|---|---|---|
| 파일 위치가 잘못됨 | middleware.ts가 루트 또는 src 디렉터리에 있는지 확인 | 올바른 위치로 이동 |
| matcher가 현재 경로를 포함하지 않음 | request.nextUrl.pathname을 출력해 경로 확인 | matcher 설정 조정 |
| 문법 오류 | 콘솔에 컴파일 오류가 있는지 확인 | 문법 오류 수정 |
| 함수를 올바르게 내보내지 않음 | export function middleware를 사용하는지 확인 | 내보내기 문법 확인 |
| 캐시 문제 | .next 디렉터리 삭제 | rm -rf .next && npm run dev 실행 |
디버깅 팁:
Middleware 맨 앞에 로그 한 줄을 추가합니다.
export function middleware(request: NextRequest) {
console.log('🔥 Middleware가 실행되었습니다! 경로:', request.nextUrl.pathname);
// 나머지 로직...
}
이 로그조차 출력되지 않는다면 Middleware가 실행되지 않은 것이므로 위 원인을 확인하세요.
오류 2: Native Node.js APIs are not supported in the Edge Runtime
증상: 실행 중 이 오류가 발생하며 지원하지 않는 API 또는 모듈이 표시됩니다.
문제 찾기:
오류 스택에서 어떤 라이브러리나 코드가 Node.js API를 호출했는지 찾습니다. 흔한 원인은 다음과 같습니다.
fs,path같은 파일 시스템 모듈jsonwebtoken(jose로 대체)- MongoDB, MySQL 네이티브 드라이버(Edge 지원 라이브러리 사용)
해결 방법:
- 대체 라이브러리 찾기: 라이브러리 문서에서 Edge Runtime 호환 버전이 있는지 확인합니다.
- 로직을 API 경로로 옮기기: Node.js API가 꼭 필요하면 Middleware에 두지 않습니다.
- Web 표준 API 사용하기: 예를 들어 Node.js의
crypto대신 Web Crypto API를 사용합니다.
예제:
// ❌ 사용할 수 없음
import jwt from 'jsonwebtoken';
// ✅ 이것을 사용
import { jwtVerify } from 'jose';
오류 3: Invalid middleware found
증상: 프로젝트를 시작할 때 이 오류가 발생합니다.
가능한 원인:
1. matcher가 빈 배열임
// ❌ 사용할 수 없음
export const config = {
matcher: []
}
2. middleware 함수를 올바르게 내보내지 않음
// ❌ 사용할 수 없음
const middleware = (request: NextRequest) => { ... }
// ✅ 이렇게 작성
export function middleware(request: NextRequest) { ... }
3. middleware 함수에 반환값이 없음
// ❌ 사용할 수 없음
export function middleware(request: NextRequest) {
console.log('무언가 처리');
// return을 빠뜨림
}
// ✅ 반환값이 있어야 함
export function middleware(request: NextRequest) {
return NextResponse.next();
}
오류 4: 무한 리디렉션 루프
증상: 브라우저에 ERR_TOO_MANY_REDIRECTS가 표시되고 페이지를 열 수 없습니다.
원인: 리디렉션 대상 URL이 Middleware를 다시 실행해 루프가 발생합니다.
흔한 상황:
export function middleware(request: NextRequest) {
const token = request.cookies.get('auth-token');
if (!token) {
// ❌ 문제: /login으로 리디렉션하지만 /login도 이 Middleware를 실행함
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/:path*'] // /login을 포함한 모든 경로 매칭
}
해결 방법: 로그인 페이지나 다른 공개 페이지를 제외합니다.
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// ✅ 공개 페이지를 먼저 제외
if (pathname === '/login' || pathname === '/') {
return NextResponse.next();
}
const token = request.cookies.get('auth-token');
if (!token) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
또는 matcher를 조정합니다.
export const config = {
matcher: ['/dashboard/:path*'] // 로그인해야 하는 경로만 보호
}
오류 5: 환경 변수를 읽을 수 없음
증상: process.env.XXX가 undefined입니다.
원인:
.env.local파일이 없거나 변수 이름이 잘못되었습니다.- Vercel, Netlify 같은 배포 플랫폼에 환경 변수를 설정하지 않았습니다.
- 변수 이름이 규칙에 맞지 않습니다. Next.js에는 일부 제한이 있습니다.
해결 방법:
로컬 개발:
// .env.local
JWT_SECRET=your-secret-here
프로덕션 환경:
Vercel 또는 Netlify 프로젝트 설정에서 환경 변수를 추가한 뒤 다시 배포합니다.
주의:
- 환경 변수를 수정한 뒤에는 개발 서버를 다시 시작해야 합니다.
- 배포 플랫폼의 환경 변수는 서버와 Edge Runtime에서만 사용할 수 있습니다. 클라이언트에서 사용하려면
NEXT_PUBLIC_접두사가 필요합니다.
디버깅 팁 요약
1. x-middleware-next header로 추적하기
export function middleware(request: NextRequest) {
const response = NextResponse.next();
// 이 요청이 Middleware를 거쳤다는 표시
response.headers.set('x-middleware-executed', 'true');
response.headers.set('x-middleware-path', request.nextUrl.pathname);
return response;
}
브라우저 개발자 도구에서 응답 header를 확인하면 Middleware의 실행 여부를 알 수 있습니다.
2. 코드를 구간별로 주석 처리해 문제 찾기
어느 줄에서 오류가 발생하는지 모르겠다면 Middleware 로직을 단계적으로 주석 처리합니다.
export function middleware(request: NextRequest) {
console.log('1단계');
// ... 일부 로직
console.log('2단계');
// ... 추가 로직
console.log('3단계');
return NextResponse.next();
}
로그가 어느 단계까지 출력되는지 보면 문제가 발생한 위치를 찾을 수 있습니다.
3. Vercel의 Edge Function Logs 확인하기
Vercel에 배포했다면 프로젝트의 Functions 탭에서 console.log 출력과 오류를 포함한 Edge Function 로그를 확인할 수 있습니다.
4. 로컬 테스트에서 next dev --turbo 사용하기
Next.js 15+는 Turbo 모드를 지원합니다. 시작이 빠르고 오류 메시지도 더 명확합니다.
npm run dev -- --turbo
문제가 생겨도 당황하지 마세요. 이 방법을 하나씩 적용하면 원인을 찾을 수 있습니다. 그래도 해결되지 않는다면 Next.js GitHub Discussions나 Stack Overflow에서 검색해 보세요. 이미 같은 문제를 겪은 사람이 있을 가능성이 큽니다.
마무리
지금까지의 내용을 세 문장으로 요약해 보겠습니다.
1. Middleware의 적용 범위를 명확히 이해하세요.
모든 것을 넣지 마세요. Middleware는 인증, 경로 리디렉션, A/B 테스트 같은 가벼운 판단과 전달에 적합합니다. 복잡한 비즈니스 로직, 데이터베이스 조회, 많은 계산은 API 경로나 Server Component에 맡기세요. Middleware는 집안일을 모두 맡는 관리자가 아니라 빠르게 출입을 판단하는 문지기여야 합니다.
2. matcher 설정이 가장 중요합니다.
가장 실수하기 쉬운 부분이라 이 글에서 여러 번 강조했습니다. 동적 경로에는 *를 붙이고, 정적 리소스는 제외하며, 로그인 페이지 같은 공개 페이지는 가로채지 마세요. 잘 모르겠다면 위 템플릿을 사용하거나 개발 환경에서 로그를 출력해 어떤 경로가 매칭되는지 확인하세요.
3. Edge Runtime의 제한을 이해하고 받아들이세요.
Edge Runtime은 완전한 Node.js가 아닙니다. 이는 기능 일부를 포기하는 대신 속도와 전 세계 분산 실행을 얻는 설계상의 선택입니다. 어떤 API를 사용할 수 없는지, JWT, Web Crypto API, API 경로 호출로 어떻게 우회하는지 알면 Edge Runtime을 제대로 활용할 수 있습니다.
Next.js Middleware 자체는 복잡하지 않지만 세부 사항이 많습니다. matcher 규칙, Edge Runtime 제한, 무한 리디렉션 함정은 모두 제가 직접 겪었던 문제입니다. 이 글을 쓴 이유는 여러분이 같은 시행착오를 덜 겪게 하기 위해서입니다.
전역에서 요청을 가로채야 하는 기능을 만들고 있다면 Middleware를 사용해 보세요. 먼저 코드를 실행한 뒤 천천히 최적화하면 됩니다. 문제가 생기면 이 글의 오류 해결 부분을 다시 확인해 보세요. 대부분 해결할 수 있을 것입니다.
이 글이 도움이 됐다면 Next.js를 사용하는 다른 분에게도 공유해 주세요. 다음 글에서는 Server Actions를 다룰 예정입니다. 이 역시 주의할 점이 많은 주제입니다.
여러분의 Middleware가 한 번에 정상 작동하길 바랍니다.
Next.js Middleware 전체 설정 절차
Middleware 파일 생성부터 경로 보호, 국제화, A/B 테스트 구현까지의 전체 과정
⏱️ Estimated time: 3 hr
- 1
Step 1: Middleware 파일 만들기
파일을 만듭니다.
• 위치: middleware.ts(프로젝트 루트 디렉터리)
• config 객체를 내보내 matcher 설정
• middleware 함수를 내보내 요청 처리
기본 구조:
export const config = {
matcher: '/dashboard/:path*'
}
export function middleware(request: NextRequest) {
// 처리 로직
} - 2
Step 2: matcher 규칙 설정하기
매칭 규칙:
• 단일 경로: '/dashboard'
• 동적 경로: '/dashboard/:path*'(별표가 여러 계층을 매칭한다는 점에 주의)
• 여러 경로: ['/dashboard/:path*', '/admin/:path*']
• 경로 제외: '/((?!api|_next/static|_next/image|favicon.ico).*)' 같은 부정형 전방 탐색 사용
주의 사항:
• 동적 경로에 *를 붙여야 여러 계층을 매칭할 수 있음
• 정적 리소스(_next/static, _next/image 등)는 제외해야 함
• 공개 페이지(로그인 페이지)는 가로채지 않아야 함 - 3
Step 3: 경로 보호 구현하기(인증)
단계:
1. cookie에서 token 읽기
2. token 유효성 검증(JWT 또는 API 호출 사용 가능)
3. 로그인하지 않은 사용자를 로그인 페이지로 리디렉션
4. 로그인한 사용자의 요청은 계속 진행
코드 핵심:
• NextRequest.cookies로 cookie 가져오기
• NextResponse.redirect로 리디렉션
• NextResponse.next로 요청 계속 진행
• 무한 리디렉션 루프가 생기지 않도록 주의 - 4
Step 4: 국제화 구현하기(언어 전환)
단계:
1. 사용자 언어 기본 설정 감지(cookie, header, 기본값)
2. 경로를 보고 리디렉션 필요 여부 판단
3. URL에 언어 접두사 추가
4. 언어 cookie 설정
코드 핵심:
• request.headers.get('accept-language') 사용
• request.nextUrl.pathname으로 경로 가져오기
• NextResponse.rewrite로 URL 다시 쓰기
• URL 구조를 바꾸지 않고 언어 전환 지원 - 5
Step 5: Edge Runtime 제한 처리하기
제한과 해결 방법:
• Node.js API 미지원 → Web 표준 API 사용
• 파일 시스템 미지원 → 환경 변수 또는 API 호출 사용
• 일부 npm 패키지 미지원 → 패키지의 Edge Runtime 지원 여부 확인
• JWT 검증 필요 → Web Crypto API 또는 API 경로 호출
디버깅 팁:
• console.log로 디버그 정보 출력
• Vercel Edge Functions 로그 확인
• try-catch로 오류 포착 - 6
Step 6: 테스트와 디버깅
테스트 항목:
• 매칭되는 모든 경로 테스트
• 매칭되지 않는 경로 테스트(가로채지 않는지 확인)
• 리디렉션 로직 테스트
• Edge Runtime 호환성 테스트
디버깅 방법:
• middleware에 console.log 추가
• 브라우저 Network 탭 확인
• Vercel Edge Functions 로그 확인
• Next.js 개발 모드에서 경고 확인
FAQ
Middleware의 matcher 설정이 적용되지 않을 때는 어떻게 해야 하나요?
1) matcher 경로가 올바른지 확인합니다(동적 경로에는 * 필요).
2) 정적 리소스를 제외했는지 확인합니다.
3) 경로 형식이 올바른지 확인합니다(임의의 정규 표현식이 아니라 Next.js가 지원하는 형식 사용).
middleware에 console.log를 추가해 어떤 경로가 매칭되는지 확인할 수 있습니다.
여러 계층으로 된 경로가 매칭되지 않는 이유는 무엇인가요?
Edge Runtime에서 특정 라이브러리를 지원하지 않으면 어떻게 해야 하나요?
해결 방법:
1) 패키지가 Edge Runtime을 지원하는지 확인합니다.
2) Web 표준 API로 대체합니다.
3) 복잡한 로직은 API 경로나 Server Component로 옮깁니다.
4) Edge Runtime을 지원하는 대체 라이브러리를 사용합니다.
무한 리디렉션 루프를 피하려면 어떻게 해야 하나요?
Middleware에서 데이터베이스에 접근할 수 있나요?
데이터베이스 조회가 필요하다면:
1) Middleware에서 API 경로를 호출합니다.
2) 환경 변수에 설정을 저장합니다.
3) 복잡한 로직은 API 경로나 Server Component로 옮깁니다.
Middleware는 어떻게 디버깅하나요?
1) middleware 함수에 console.log를 추가해 디버그 정보를 출력합니다.
2) 브라우저 Network 탭에서 요청과 응답을 확인합니다.
3) Vercel Edge Functions 로그를 확인합니다.
4) Next.js 개발 모드에서 경고와 오류 정보를 확인합니다.
Middleware와 API 경로의 차이는 무엇인가요?
• Edge Runtime에서 실행됩니다.
• 요청이 페이지나 API 경로에 도달하기 전에 실행됩니다.
• 가벼운 요청 가로채기와 전달에 적합합니다.
API 경로:
• Node.js 런타임에서 실행됩니다.
• 완전한 Node.js API와 데이터베이스에 접근할 수 있습니다.
• 복잡한 비즈니스 로직에 적합합니다.
9분 읽기 · 게시일: 2025년 12월 25일 · 수정일: 2026년 9월 4일
Next.js 완전 가이드
검색으로 들어왔다면 같은 시리즈의 이전 글이나 다음 글로 이동하는 것이 가장 빠릅니다.
이전
Next.js Server Actions로 폼 처리와 검증 구현하기
Next.js Server Actions로 폼을 처리하는 방법부터 Zod 검증, 보안 점검, 로딩 상태와 낙관적 업데이트까지 실제 코드로 설명합니다.
45편 중 8편
다음
Next.js 라우트 보호와 권한 제어: Middleware와 다층 방어 완벽 가이드
Next.js 라우트 보호와 권한 제어를 Middleware부터 다층 방어 아키텍처까지 자세히 살펴보고, NextAuth와 getServerSession으로 안전한 RBAC 시스템을 구현하는 전체 코드 예제를 소개합니다.
45편 중 10편



댓글
GitHub로 로그인하여 댓글을 남기세요