C
Next.js/라우팅/Lesson 11

Middleware — 모든 요청 가로채서 인증·리다이렉트·헤더 조작

30분·theory
이 챕터
3/3
TypeScript

Middleware — 모든 요청 가로채서 인증·리다이렉트·헤더 조작

💡 왜 배워야 할까요? — 모든 페이지 컴포넌트에 if(!session) 안 적으려면

🎯 보호된 페이지마다 'session 없으면 로그인으로 보내기' 코드 — 미들웨어 한 곳에 두면 끝.
💼 Edge Runtime 에서 실행 → 페이지 컴포넌트 도달 전에 차단. 더 빠른 응답.
matcher config 로 정확히 어떤 경로에 적용할지 통제.
🔗 A/B 테스트, 국가별 리다이렉트, 봇 차단, 응답 헤더 추가 — 라우팅 직전에 필요한 모든 로직.
📈 Next.js 의 codemaster40 도 [`src/middleware.ts`](src/middleware.ts) 로 `/dashboard`·`/admin` 같은 보호 경로 처리.
🏢 실무에서는
이 프로젝트의 middleware.ts 가 정확히 그런 일을 합니다 — `/dashboard`·`/bookmark`·`/memo-notes` 같은 개인화 경로 진입 시 session 검증, 없으면 `/login?next=...` 로 리다이렉트. 페이지 컴포넌트는 'session 있다' 가정하고 작성하면 됨.

middleware.ts · matcher · NextResponse

1. 위치와 시그니처

code
src/middleware.ts  ← src/app 과 같은 레벨 (또는 프로젝트 루트)
ts
import { NextResponse, type NextRequest } from 'next/server';

export function middleware(req: NextRequest) {
  // 요청 마다 실행
  return NextResponse.next();  // 그냥 통과
}

export const config = {
  matcher: ['/dashboard/:path*', '/admin/:path*'],
};
  • Edge Runtime 에서 실행 (Node API 일부 제한).
  • matcher 에 적은 경로에만 작동.

2. matcher 패턴

ts
export const config = {
  matcher: [
    '/dashboard/:path*',           // /dashboard/* 모두
    '/((?!api|_next|favicon).*)',  // api/_next/favicon 제외 모두
  ],
};

3. 주요 동작 4가지

ts
// (a) 그냥 통과
return NextResponse.next();

// (b) 다른 경로로 rewrite (URL 유지, 내부 라우팅만 변경)
return NextResponse.rewrite(new URL('/landing', req.url));

// (c) 리다이렉트 (URL 자체 변경)
return NextResponse.redirect(new URL('/login', req.url));

// (d) 응답 헤더 조작
const res = NextResponse.next();
res.headers.set('x-custom', 'value');
return res;

4. 실전 — 인증 미들웨어

ts
import { NextResponse, type NextRequest } from 'next/server';

export function middleware(req: NextRequest) {
  const session = req.cookies.get('session')?.value;

  if (!session) {
    const loginUrl = new URL('/login', req.url);
    loginUrl.searchParams.set('next', req.nextUrl.pathname);
    return NextResponse.redirect(loginUrl);
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/dashboard/:path*', '/bookmark/:path*', '/memo-notes/:path*'],
};

5. 응답 쿠키·헤더 조작

ts
export function middleware(req: NextRequest) {
  const res = NextResponse.next();

  // 응답 쿠키 설정
  res.cookies.set('lastVisit', new Date().toISOString(), {
    httpOnly: true,
    secure: true,
  });

  // 보안 헤더 추가
  res.headers.set('X-Frame-Options', 'DENY');
  res.headers.set('X-Content-Type-Options', 'nosniff');

  return res;
}

6. Edge Runtime 제약

  • fs·net·dns 같은 Node-only 모듈 사용 불가.
  • 응답 시간 25ms 권장 (Vercel 기준).
  • 무거운 DB 호출 X — 빠른 검증만.
💻 🅰️ 페이지마다 인증 검증 — 중복 코드 폭발
// ❌ 미들웨어 없이 — 페이지마다 인증 코드 반복

// 📁 app/dashboard/page.tsx
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';

export default async function Dashboard() {
  const session = cookies().get('session')?.value;
  if (!session) redirect('/login?next=/dashboard');
  // ... 페이지 본문
}

// 📁 app/bookmark/page.tsx
export default async function Bookmark() {
  const session = cookies().get('session')?.value;
  if (!session) redirect('/login?next=/bookmark');
  // ... 같은 코드
}

// 📁 app/memo-notes/page.tsx
export default async function MemoNotes() {
  const session = cookies().get('session')?.value;
  if (!session) redirect('/login?next=/memo-notes');
  // ... 또 같은 코드
}

// 단점:
// - 페이지마다 4줄 중복
// - 새 페이지 추가할 때 잊기 쉬움
// - 인증 정책 바뀌면 모든 페이지 수정
// - 페이지 컴포넌트가 실행되어야 검증됨 → 약간의 컴파일·전송 오버헤드
💻 🅱️ Middleware — 한 곳에서 모든 보호 경로 처리
// ✅ middleware 로 통합 — 페이지 컴포넌트는 인증 코드 없음

// 📁 src/middleware.ts
import { NextResponse, type NextRequest } from 'next/server';

const PROTECTED_ROUTES = ['/dashboard', '/bookmark', '/memo-notes'];
const ADMIN_ROUTES = ['/admin'];

export function middleware(req: NextRequest) {
  const session = req.cookies.get('session')?.value;
  const path = req.nextUrl.pathname;

  // 1. 보호 경로 → 비로그인이면 로그인 페이지로
  if (PROTECTED_ROUTES.some(p => path.startsWith(p))) {
    if (!session) {
      const loginUrl = new URL('/login', req.url);
      loginUrl.searchParams.set('next', path);
      return NextResponse.redirect(loginUrl);
    }
  }

  // 2. 관리자 경로 → 별도 권한 검증
  if (ADMIN_ROUTES.some(p => path.startsWith(p))) {
    const adminToken = req.cookies.get('admin')?.value;
    if (!adminToken) {
      return NextResponse.redirect(new URL('/admin/login', req.url));
    }
  }

  // 3. 응답 헤더 — 보안 강화
  const res = NextResponse.next();
  res.headers.set('X-Frame-Options', 'DENY');
  res.headers.set('X-Content-Type-Options', 'nosniff');
  return res;
}

export const config = {
  matcher: [
    // _next/static·_next/image·favicon·api 제외 모든 경로
    '/((?!_next/static|_next/image|favicon.ico|api).*)',
  ],
};

// 📁 app/dashboard/page.tsx — 이제 깨끗
export default async function Dashboard() {
  // session 검증 코드 없음 — 미들웨어가 이미 차단함
  // 도달했다면 인증된 사용자 확정
  const data = await db.dashboard.find();
  return <div>{/* ... */}</div>;
}

// 장점:
// - 보호 정책 한 곳에 집중
// - 새 보호 페이지 = PROTECTED_ROUTES 배열에 추가만
// - Edge Runtime → 빠른 응답 (페이지 컴포넌트 도달 전 차단)
// - 페이지 컴포넌트가 깨끗 (인증 로직 X, 비즈니스 로직만)

💡 💡 Middleware 실전 5

1. matcher 에 음수 lookahead 패턴 — '제외' 표현

ts
matcher: ['/((?!_next/static|_next/image|favicon|api).*)']

빈번한 자산 요청을 미들웨어가 안 건드리게.

2. Edge Runtime 제약 — 무거운 DB 호출 X
쿠키 검증·간단한 JWT 디코드 같은 빠른 작업만. 진짜 DB 쿼리는 페이지 컴포넌트에서.

3. NextResponse.next() 한 번에 헤더 두기

ts
const res = NextResponse.next();
res.headers.set('x-foo', 'bar');
return res;

4. 응답 쿠키 설정

ts
res.cookies.set('name', 'value', { httpOnly: true, secure: true });

httpOnly + secure 가 기본. SameSite=Lax 도 권장.

5. 디버깅 — console.log 는 서버 로그
middleware 안 console.log 는 Vultr·Vercel 의 서버 로그에 찍힘. 브라우저 콘솔에는 안 보임.

⚡ 직접 실행해보기 — middleware 동작 시뮬레이션

각 요청 경로가 middleware 에서 어떻게 처리되는지 시뮬레이션.
✏️ JS 코드
📟 콘솔 출력
▶ 실행 버튼을 눌러보세요
⚠️ 브라우저 샌드박스에서 실행 — console.log()만 지원, alert/fetch 불가

확인 퀴즈

middleware.ts 의 matcher 설정으로 가능한 것은?
💡 `matcher` 는 미들웨어가 **어떤 경로에 적용될지** 정의합니다. 빈번한 정적 자산 요청(`_next/static`·`_next/image`·`favicon`) 이나 API 요청을 미들웨어가 안 건드리게 음수 lookahead 패턴으로 제외하는 게 일반적. matcher 안 적으면 모든 경로에 적용되어 성능 영향이 큽니다.
Middleware — 인증·리다이렉트·헤더 - Next.js