제목은 Next.js Middleware 똑똑하게 쓰는 법이라고 적어두었지만,
사실 대부분 한 번쯤은 다 써보셨거나, 고민하셨을 내용일 수도 있습니다.
그래도 제가 겪으면서 느낀 부분들을 정리해보고 싶어서 글을 써보려고 합니다.
Next.js로 개발을 하다 보면 미들웨어는 거의 한 번은 건드려보게 됩니다.
보통은 아래와 같은 용도로 사용을 하실거라고 생각합니다.
/login으로 보내기rewrite 처리하기그래서 누가 나한테 미들웨어가 뭐야? 라고 물어보면 아래와 같이 말했었다.
🙋♂️ : 미들웨어? 로그인 막는 그거 아니야?
틀린말은 아니다.
대신 더 자세히 알아야지 미들웨어를 통한 이슈가 생기지 않을거라고 생각을 한다.
미들웨어는 생각보다 “입구(?)에 가까운 코드”라고 생각을 해서 그렇다.
미들웨어는 말 그대로
요청이 페이지나 API로 들어가기 전에 먼저 실행되는 코드다.
요청 흐름을 아래의 플로우로 살펴봅니다~
1. 브라우저가 요청을 보냄
2. matcher로 미들웨어 적용 여부 판단
3. 적용 대상이면 미들웨어 실행
4. 여기서 redirect / rewrite / 통과 여부 결정
5. 통과하면 그 다음에야 페이지나 route handler 실행
여기서 중요한 포인트는 하나다.
미들웨어는 대부분의 요청이 지나가는 길목이다.
그래서 여기 미들웨어 로직이 무거워지면
특정 페이지가 아니라 사이트 전체가 같이 느려진다.
그리고 Next.js의 middleware는 기본적으로 Edge Runtime에서 실행된다.
이 말은 곧,
fs, net 같은 것 못 씀Node 서버에서 도는 게 아니라, CDN 가장자리(Edge)에서 도는 런타임이다.
예전 방식은:
Edge Runtime은:
빠르게 하기 위해 중앙 서버까지 보내지 않는다.
그래서 Next.js는 기본적으로 middleware를 Edge Runtime에서 실행시킨다.
matcher를 이렇게 아래와 같이 두는 경우도 봤었다.
export const config = {
matcher: ["/:path*"],
}
위와 같은 matcher는
_next/static_next/image이런 요청까지 전부 미들웨어를 탄다.
“아무 것도 안 하는 미들웨어”라도
요청마다 한 번씩 실행된다는 건 변하지 않는다.
그래서 보통 위와 같이 하지 않는다.
export const config = {
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
}
핵심은 이거다.
정책이 필요한 요청만 태워라.
미들웨어는 기본값으로 전역 적용이기 때문에
matcher가 사실상 성능의 절반이라고 봐도 된다.
미들웨어는 “간단한 분기처리”에 특화되어 있다고 생각한다.
예시를 보면:
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 조회 등 까지 넣어버리면 그때부터는 거의 “미니 서버”라고 불러야한다.
그리고 모든 요청을 할때 마다 저 로직이 수행을 한다고 생각하면 된다.
가볍게 써라 라는 말이 두리물실 하다고 생각이 될수도있다.
미들웨어는 다시 말하지만,
즉, 무거운 라이브러리 하나 import해도 그 비용이 모든 요청에 들어간다고 생각해야한다.
그리고 Edge 환경 특성상 암호화 연산, 큰 validation 라이브러리, DB 호출 이런 것들은 생각보다 체감이 크다.
미들웨어는 서버 로직을 수행하는 곳이 아니라
“이 요청을 어디로 보낼지 결정하는 곳”이다.
즉, 미들웨어는 판단만 하고, 무거운 처리는 뒤로 넘긴다.
은근히 많이 헷갈린다.
비슷해 보이지만 동작 방식이 완전히 다르다.
예를 들면 로그인 이동이 대표적이다.
return NextResponse.redirect(new URL("/login", req.url))
이 경우 브라우저 주소창이 /login으로 바뀐다.
즉, “여기로 가세요”라고 사용자에게 명확히 말해주는 동작이다.
그래서 인증 실패, 권한 부족, 구버전 URL 강제 변경 같은 케이스에 적합하다.
SEO 관점에서도 URL을 실제로 변경해야 하는 경우에는 redirect가 맞다.
if (req.nextUrl.pathname === "/") {
return NextResponse.rewrite(new URL("/home", req.url))
}
이 경우 사용자는 /에 접근했지만
실제로는 /home이 렌더링된다.
주소창은 여전히 /이다.
이게 왜 중요하냐면,
rewrite는 “보여주는 것만 바꾸는 방식”이기 때문이다.
예를 들어 아래와 같은 상황을 가정해봅시다.
/로 접근했다locale=ko가 있다/ko 페이지를 보여주고 싶다이때 redirect를 쓰면:
/ko로 바뀐다rewrite를 쓰면:
//ko를 렌더링즉, URL은 유지하면서 “콘텐츠만 바꾸고 싶을 때” rewrite가 적합하다.
A/B 테스트도 같은 맥락이다.
/product는 그대로 두고/product-v2를 보여줄 수 있다