Next.js Middleware 똑똑하게 쓰는 법

질문Bot·2026년 2월 8일

Next.js

목록 보기
12/13
post-thumbnail

제목은 Next.js Middleware 똑똑하게 쓰는 법이라고 적어두었지만,
사실 대부분 한 번쯤은 다 써보셨거나, 고민하셨을 내용일 수도 있습니다.
그래도 제가 겪으면서 느낀 부분들을 정리해보고 싶어서 글을 써보려고 합니다.

Next.js로 개발을 하다 보면 미들웨어는 거의 한 번은 건드려보게 됩니다.

💡 사용 용도

보통은 아래와 같은 용도로 사용을 하실거라고 생각합니다.

  • 로그인 안 했으면 /login으로 보내기
  • 특정 페이지 접근 막기
  • locale 기준으로 rewrite 처리하기

그래서 누가 나한테 미들웨어가 뭐야? 라고 물어보면 아래와 같이 말했었다.

🙋‍♂️ : 미들웨어? 로그인 막는 그거 아니야?

틀린말은 아니다.

대신 더 자세히 알아야지 미들웨어를 통한 이슈가 생기지 않을거라고 생각을 한다.
미들웨어는 생각보다 “입구(?)에 가까운 코드”라고 생각을 해서 그렇다.


✅ 미들웨어는 모든 요청의 “입구”다

미들웨어는 말 그대로
요청이 페이지나 API로 들어가기 전에 먼저 실행되는 코드다.

요청 흐름을 아래의 플로우로 살펴봅니다~
1. 브라우저가 요청을 보냄
2. matcher로 미들웨어 적용 여부 판단
3. 적용 대상이면 미들웨어 실행
4. 여기서 redirect / rewrite / 통과 여부 결정
5. 통과하면 그 다음에야 페이지나 route handler 실행

여기서 중요한 포인트는 하나다.

미들웨어는 대부분의 요청이 지나가는 길목이다.

그래서 여기 미들웨어 로직이 무거워지면
특정 페이지가 아니라 사이트 전체가 같이 느려진다.

그리고 Next.js의 middleware는 기본적으로 Edge Runtime에서 실행된다.
이 말은 곧,

  • Node 전용 API 일부 사용 불가
  • fs, net 같은 것 못 씀
  • 환경이 서버랑 완전히 같지 않음
    즉, “서버니까 다 되겠지” 마인드로 접근하면 말자.

💡 Edge Runtime이란

Node 서버에서 도는 게 아니라, CDN 가장자리(Edge)에서 도는 런타임이다.

예전 방식은:

  • 사용자가 요청
  • 중앙 서버(한국이든 미국이든 한 곳)에 도착
  • 거기서 처리

Edge Runtime은:

  • 사용자가 요청
  • 가장 가까운 엣지 서버(전 세계에 퍼져 있는 서버)에서 바로 실행**
    즉, 사용자와 가까운 위치에서 실행되는 가벼운 서버 환경이라고 보면 된다.

빠르게 하기 위해 중앙 서버까지 보내지 않는다.
그래서 Next.js는 기본적으로 middleware를 Edge Runtime에서 실행시킨다.


2. matcher를 대충 쓰지 말자

matcher를 이렇게 아래와 같이 두는 경우도 봤었다.

export const config = {
  matcher: ["/:path*"],
}

위와 같은 matcher는

  • 정적 파일
  • 이미지
  • 폰트
  • _next/static
  • _next/image

이런 요청까지 전부 미들웨어를 탄다.
“아무 것도 안 하는 미들웨어”라도
요청마다 한 번씩 실행된다는 건 변하지 않는다.

그래서 보통 위와 같이 하지 않는다.

export const config = {
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
}

핵심은 이거다.

정책이 필요한 요청만 태워라.

미들웨어는 기본값으로 전역 적용이기 때문에
matcher가 사실상 성능의 절반이라고 봐도 된다.


3. 미들웨어에서 이 정도는 괜찮다

미들웨어는 “간단한 분기처리”에 특화되어 있다고 생각한다.

  • redirect
  • rewrite
  • 쿠키 읽기
  • 헤더 수정
  • 토큰 유무 확인

예시를 보면:

import { NextRequest, NextResponse } from "next/server"

export function middleware(req: NextRequest) {
  const token = req.cookies.get("access_token")?.value
  const { pathname } = req.nextUrl

  if (pathname.startsWith("/login")) {
    return NextResponse.next()
  }

  if (pathname.startsWith("/dashboard") && !token) {
    const url = req.nextUrl.clone()
    url.pathname = "/login"
    url.searchParams.set("next", pathname)
    return NextResponse.redirect(url)
  }

  return NextResponse.next()
}

위에 코드를 보면 토큰의 “존재 여부”까지만 본다는 것이다.
만약에 JWT 디코딩, role 검증, DB 조회 등 까지 넣어버리면 그때부터는 거의 “미니 서버”라고 불러야한다.
그리고 모든 요청을 할때 마다 저 로직이 수행을 한다고 생각하면 된다.


4. 그래서 다들 “가볍게 써라”라고 한다

가볍게 써라 라는 말이 두리물실 하다고 생각이 될수도있다.

미들웨어는 다시 말하지만,

  • 모든 요청이 통과하고
  • Edge 번들에 포함되고
  • import한 코드도 전부 묶인다

즉, 무거운 라이브러리 하나 import해도 그 비용이 모든 요청에 들어간다고 생각해야한다.

그리고 Edge 환경 특성상 암호화 연산, 큰 validation 라이브러리, DB 호출 이런 것들은 생각보다 체감이 크다.

미들웨어는 서버 로직을 수행하는 곳이 아니라
“이 요청을 어디로 보낼지 결정하는 곳”이다.

즉, 미들웨어는 판단만 하고, 무거운 처리는 뒤로 넘긴다.


5. rewrite랑 redirect는 다르다

은근히 많이 헷갈린다.
비슷해 보이지만 동작 방식이 완전히 다르다.

redirect

  • 브라우저 URL이 바뀐다 (즉, URL 변경 X)
  • 3xx 응답
  • 로그인 이동 같은 케이스

예를 들면 로그인 이동이 대표적이다.

return NextResponse.redirect(new URL("/login", req.url))

이 경우 브라우저 주소창이 /login으로 바뀐다.

즉, “여기로 가세요”라고 사용자에게 명확히 말해주는 동작이다.

그래서 인증 실패, 권한 부족, 구버전 URL 강제 변경 같은 케이스에 적합하다.

SEO 관점에서도 URL을 실제로 변경해야 하는 경우에는 redirect가 맞다.

rewrite

  • 브라우저 URL은 그대로 유지된다
  • 내부적으로 다른 경로로 매핑된다
  • 사용자는 이동을 인지하지 못한다
if (req.nextUrl.pathname === "/") {
  return NextResponse.rewrite(new URL("/home", req.url))
}

이 경우 사용자는 /에 접근했지만

실제로는 /home이 렌더링된다.

주소창은 여전히 /이다.

이게 왜 중요하냐면,

rewrite는 “보여주는 것만 바꾸는 방식”이기 때문이다.


그래서 locale 분기에 rewrite가 적합하다

예를 들어 아래와 같은 상황을 가정해봅시다.

  • 사용자는 /로 접근했다
  • 쿠키에 locale=ko가 있다
  • 우리는 내부적으로 /ko 페이지를 보여주고 싶다

이때 redirect를 쓰면:

  • 주소가 /ko로 바뀐다
  • 사용자가 URL 변화를 인지한다

rewrite를 쓰면:

  • 주소는 /
  • 내부적으로 /ko를 렌더링
  • 사용자는 자연스럽게 한국어 페이지를 본다

즉, URL은 유지하면서 “콘텐츠만 바꾸고 싶을 때” rewrite가 적합하다.

A/B 테스트도 같은 맥락이다.

  • /product는 그대로 두고
  • 내부적으로 /product-v2를 보여줄 수 있다

정리하면

  • redirect → 경로 자체를 바꾸고 싶을 때
  • rewrite → 경로는 유지하고, 내부 처리만 바꾸고 싶을 때
profile
유용한 정보를 전달하는 사람이 되고자 노력합니다.

0개의 댓글