Ch 04. 모든 요청의 입구 — middleware.ts
공항의 출입국 심사대를 생각해보세요. 비행기에서 내린 모든 승객은 예외 없이 심사대를 통과해야 합니다. 여권이 없거나 비자가 없으면 입국할 수 없습니다. 미들웨어는 Next.js의 출입국 심사대입니다.
middleware.ts는 프로젝트 루트에 위치하며, 설정된 경로에 대한 모든 요청이 실제 페이지나 API에 도달하기 전에 이 파일의 코드가 실행됩니다.
middleware.ts 위치
프로젝트 루트/├── app/├── public/├── middleware.ts ← 여기├── auth.ts└── next.config.ts
matcher로 보호할 경로 지정
미들웨어가 모든 요청에 실행되면 정적 파일(이미지, 폰트 등)에도 실행되어 성능이 떨어집니다. matcher로 미들웨어가 실행될 경로를 제한합니다.
// 파일: middleware.tsimport { NextResponse } from "next/server";import type { NextRequest } from "next/server";export function middleware(request: NextRequest) { // 미들웨어 로직 return NextResponse.next();}export const config = { matcher: [ // 다음 경로를 제외한 모든 경로에 실행 "/((?!api/auth|_next/static|_next/image|favicon.ico).*)", ],};```text`matcher`의 정규식은 복잡해 보이지만 의미는 단순합니다. `api/auth`, `_next/static`, `_next/image`, `favicon.ico`를 제외한 모든 경로에서 미들웨어가 실행됩니다.### Edge Runtime에서 JWT 직접 파싱앞서 언급했듯, 미들웨어는 Edge Runtime에서 실행됩니다. NextAuth의 `auth()` 함수를 미들웨어에서 직접 호출하면 Node.js 전용 코드로 인해 오류가 발생할 수 있습니다.대신 `next-auth/jwt`의 `decode()` 함수로 JWT 쿠키를 직접 파싱합니다. 이 함수는 Edge Runtime에서 동작합니다.```typescript// 파일: middleware.tsimport { NextResponse } from "next/server";import type { NextRequest } from "next/server";import { decode } from "next-auth/jwt";// 로그인 없이 접근 가능한 경로const publicPaths = ["/", "/login", "/register", "/api/auth"];// 로그인 사용자가 접근하면 대시보드로 리다이렉트할 경로const authPaths = ["/login", "/register"];export async function middleware(request: NextRequest) { const { pathname } = request.nextUrl; // 공개 경로는 그대로 통과 const isPublicPath = publicPaths.some( (path) => pathname === path || pathname.startsWith(path + "/") ); // JWT 쿠키 파싱 const token = await decode({ token: request.cookies.get("authjs.session-token")?.value, secret: process.env.AUTH_SECRET!, salt: "authjs.session-token", }); const isAuthenticated = !!token; // 로그인 상태인데 로그인/회원가입 페이지 접근 → 대시보드로 if (isAuthenticated && authPaths.includes(pathname)) { return NextResponse.redirect(new URL("/dashboard", request.url)); } // 비공개 경로인데 미인증 → 로그인 페이지로 if (!isPublicPath && !isAuthenticated) { const loginUrl = new URL("/login", request.url); loginUrl.searchParams.set("callbackUrl", pathname); return NextResponse.redirect(loginUrl); } return NextResponse.next();}export const config = { matcher: [ "/((?!api/auth|_next/static|_next/image|favicon.ico).*)", ],};```text`callbackUrl`을 쿼리 파라미터로 붙이는 이유는 로그인 성공 후 원래 가려던 페이지로 돌아오기 위해서입니다. 로그인 페이지에서 이 값을 읽어 리다이렉트하면 사용자 경험이 훨씬 좋아집니다.### 쿠키 이름 주의사항NextAuth v5가 사용하는 쿠키 이름은 환경에 따라 다릅니다.| 환경 | 쿠키 이름 ||------|----------|| 개발 환경 (HTTP) | `authjs.session-token` || 프로덕션 (HTTPS) | `__Secure-authjs.session-token` |미들웨어에서 두 환경 모두 처리하려면 이렇게 작성합니다.```typescript// 파일: middleware.ts (쿠키 이름 환경별 처리)const cookieName = process.env.NODE_ENV === "production" ? "__Secure-authjs.session-token" : "authjs.session-token";const token = await decode({ token: request.cookies.get(cookieName)?.value, secret: process.env.AUTH_SECRET!, salt: cookieName,});```text### 관리자 권한 확인토큰에 `role` 정보를 넣어두었다면 미들웨어에서 역할 기반 접근 제어도 가능합니다.```typescript// 파일: middleware.ts (역할 기반 접근 제어 추가)import { NextResponse } from "next/server";import type { NextRequest } from "next/server";import { decode } from "next-auth/jwt";export async function middleware(request: NextRequest) { const { pathname } = request.nextUrl; const cookieName = process.env.NODE_ENV === "production" ? "__Secure-authjs.session-token" : "authjs.session-token"; const token = await decode({ token: request.cookies.get(cookieName)?.value, secret: process.env.AUTH_SECRET!, salt: cookieName, }); // 관리자 전용 경로 if (pathname.startsWith("/admin")) { if (!token) { return NextResponse.redirect(new URL("/login", request.url)); } if ((token as { role?: string }).role !== "admin") { return NextResponse.redirect(new URL("/403", request.url)); } } // 일반 인증 필요 경로 if (pathname.startsWith("/dashboard") && !token) { return NextResponse.redirect(new URL("/login", request.url)); } return NextResponse.next();}export const config = { matcher: ["/((?!api/auth|_next/static|_next/image|favicon.ico).*)"],};
중요한 원칙이 있습니다. 미들웨어의 권한 체크는 1차 방어선입니다. 민감한 데이터를 반환하는 API나 서버 컴포넌트에서도 반드시 다시 한번 권한을 확인해야 합니다. 미들웨어만 믿으면 안 됩니다.
다음 챕터에서는 서버 컴포넌트와 클라이언트 컴포넌트에서 세션을 활용해 보호된 페이지를 만드는 방법을 살펴봅니다.