Middleware로 할 수 있는 것과 하면 안 되는 것
Article
Middleware에 너무 많은 걸 넣었다
Next.js의 Middleware를 처음 발견했을 때, 만능 도구처럼 보였습니다.
모든 요청을 가로챌 수 있습니다. 리다이렉트할 수 있습니다. 헤더를 수정할 수 있습니다. "그러면 인증도 여기서 하고, 로깅도 여기서 하고, 데이터 검증도 여기서 하면 되겠네?"
그렇게 Middleware에 로직을 몰아넣었다가 Edge Runtime의 벽에 부딪혔습니다.
Middleware의 기본 동작
Middleware는 middleware.ts 파일 하나로 동작합니다. 프로젝트 루트(또는 src/)에 위치합니다.
// 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();
}
// 어떤 경로에서 실행할지 지정
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
};
실행 시점: 요청이 들어올 때, 라우트 핸들러나 페이지가 실행되기 전에 실행됩니다.
실행 환경: Edge Runtime. 이게 핵심입니다. 일반 Node.js가 아닙니다.
할 수 있는 것들
1. 인증 체크 & 리다이렉트
가장 흔한 사용 사례입니다.
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('auth-token')?.value;
const { pathname } = request.nextUrl;
// 인증이 필요한 경로
if (pathname.startsWith('/dashboard')) {
if (!token) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('from', pathname);
return NextResponse.redirect(loginUrl);
}
}
// 이미 로그인한 사용자가 로그인 페이지 접근 시
if (pathname === '/login' && token) {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
return NextResponse.next();
}
포인트: 토큰의 존재 여부만 확인합니다. 토큰의 유효성 검증(DB 조회 등)은 여기서 하지 않습니다. 그건 Server Component나 API Route에서 합니다.
2. 리다이렉트 & 리라이트
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// 오래된 URL 리다이렉트
if (pathname === '/old-blog') {
return NextResponse.redirect(new URL('/blog', request.url));
}
// URL은 유지하면서 다른 페이지 렌더링 (리라이트)
if (pathname === '/docs') {
return NextResponse.rewrite(new URL('/documentation/latest', request.url));
}
return NextResponse.next();
}
3. 헤더 추가/수정
export function middleware(request: NextRequest) {
const response = NextResponse.next();
// 보안 헤더 추가
response.headers.set('X-Frame-Options', 'DENY');
response.headers.set('X-Content-Type-Options', 'nosniff');
// 요청 헤더에 정보 추가 (Server Component에서 읽기 위해)
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-pathname', request.nextUrl.pathname);
return NextResponse.next({
request: { headers: requestHeaders },
});
}
4. i18n 라우팅
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// 이미 로케일이 있으면 패스
if (pathname.startsWith('/ko') || pathname.startsWith('/en')) {
return NextResponse.next();
}
// Accept-Language 헤더에서 선호 언어 감지
const locale = request.headers.get('accept-language')?.includes('ko')
? 'ko'
: 'en';
return NextResponse.redirect(
new URL(`/${locale}${pathname}`, request.url)
);
}
5. A/B 테스트
export function middleware(request: NextRequest) {
const bucket = request.cookies.get('ab-bucket')?.value;
const response = NextResponse.next();
if (!bucket) {
// 50/50 분배
const newBucket = Math.random() < 0.5 ? 'A' : 'B';
response.cookies.set('ab-bucket', newBucket);
}
return response;
}
하면 안 되는 것들
1. 무거운 인증 검증
// ❌ Middleware에서 하면 안 되는 것
export async function middleware(request: NextRequest) {
const token = request.cookies.get('token')?.value;
// DB 조회 → Edge Runtime에서 대부분의 ORM이 작동하지 않음
const user = await prisma.user.findUnique({ where: { token } });
// JWT 검증 → 일부 crypto API가 Edge에서 지원 안 됨
const decoded = jwt.verify(token, process.env.SECRET);
}
왜 안 되는가:
- Edge Runtime은 Node.js API의 일부만 지원합니다
fs,net,child_process등 사용 불가- 대부분의 ORM(Prisma, TypeORM 등)이 작동하지 않습니다
- 일부
crypto함수가 없습니다
대안: Middleware에서는 토큰 존재 여부만 확인하고, 상세 검증은 Server Component에서:
// middleware.ts — 가볍게
export function middleware(request: NextRequest) {
const token = request.cookies.get('token')?.value;
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
// app/(protected)/layout.tsx — 상세 검증
export default async function ProtectedLayout({ children }) {
const session = await verifySession(); // DB 조회, JWT 검증 등
if (!session) redirect('/login');
return <>{children}</>;
}
2. 데이터 가져오기 (fetch 남용)
// ❌ Middleware에서 외부 API 호출
export async function middleware(request: NextRequest) {
const config = await fetch('https://api.example.com/config');
const features = await fetch('https://api.example.com/features');
// ...
}
왜 안 되는가:
- 모든 요청마다 실행됩니다. 이미지, CSS, JS 요청에도.
- 외부 API가 느리면 전체 사이트가 느려집니다
- Middleware에
matcher를 설정해도, 불필요한 fetch는 성능을 잡아먹습니다
3. 복잡한 비즈니스 로직
// ❌ Middleware에서 비즈니스 로직 처리
export async function middleware(request: NextRequest) {
const user = getUserFromToken(request);
// 구독 상태 확인
if (user.subscription === 'free' && request.nextUrl.pathname.startsWith('/premium')) {
return NextResponse.redirect(new URL('/upgrade', request.url));
}
// 사용량 제한
if (user.apiCalls > user.limit) {
return NextResponse.json({ error: 'Rate limited' }, { status: 429 });
}
}
이런 로직은 Server Component나 Route Handler에서 처리해야 합니다. Middleware는 라우팅 수준의 결정만 해야 합니다.
역할 분담 정리
| 역할 | Middleware | Server Component | Route Handler |
|---|---|---|---|
| 토큰 존재 확인 | ✅ | - | - |
| 토큰 유효성 검증 | ❌ | ✅ | ✅ |
| 리다이렉트 | ✅ | ✅ (redirect) | ✅ |
| 헤더 추가 | ✅ | - | ✅ |
| DB 조회 | ❌ | ✅ | ✅ |
| 외부 API 호출 | ⚠️ (최소한) | ✅ | ✅ |
| 쿠키 설정 | ✅ | ✅ | ✅ |
| i18n 감지 | ✅ | - | - |
| 비즈니스 로직 | ❌ | ✅ | ✅ |
matcher 설정 팁
config.matcher를 제대로 설정하지 않으면, 모든 요청에 Middleware가 실행됩니다. 정적 파일 요청 포함.
export const config = {
matcher: [
// 정적 파일과 내부 Next.js 경로 제외
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
};
또는 필요한 경로만 명시적으로 지정:
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*', '/login', '/signup'],
};
팁: 명시적으로 지정하는 게 디버깅하기 쉽습니다. "왜 이 요청에 Middleware가 실행되지?"라는 질문을 줄여줍니다.
정리
Middleware는 문지기입니다. 누가 들어올 수 있는지 빠르게 확인하고, 방향을 안내하는 역할입니다.
문지기가 할 일:
- 출입증(토큰) 있는지 확인
- 올바른 방향(리다이렉트/리라이트)으로 안내
- 방문자 표시(헤더/쿠키) 부착
문지기가 하면 안 되는 일:
- 출입증의 진위 여부를 감별 (그건 보안실에서)
- 방문자의 업무를 대신 처리 (그건 담당 부서에서)
- 모든 방문자의 신상을 DB에서 조회 (그건 행정실에서)
Middleware의 경계를 아는 것, 그게 실력입니다.