next · rewrite · redirect 를 언제 쓰나
셋 다 요청을 가로채지만 브라우저가 아는 게 다르다. 주소를 바꾸느냐, 보여줄 걸 바꾸느냐로 갈린다.
- Date
- Tags
- #nextjs#proxy#middleware#routing
Input
한 장 요약
| 브라우저 주소창 | 왕복 추가 | 하는 일 | |
|---|---|---|---|
next() | 그대로 | 없음 | 그냥 통과시킨다 |
rewrite() | 그대로 | 없음 | 속으로 다른 경로를 렌더한다 |
redirect() | 바뀐다 | 있다 | 브라우저를 다른 주소로 보낸다 |
/about 요청 하나를 셋으로 처리하면 이렇게 갈린다.
요청: /about
│
├─ next() → /about 을 그대로 렌더. 아무 일도 안 일어난다.
│
├─ rewrite() → 서버는 /about-2 를 렌더.
│ 주소창은 /about 그대로. 브라우저는 모른다.
│
└─ redirect() → 브라우저에 307 응답.
주소창이 /login 으로 바뀌고, 브라우저가 다시 요청한다.
핵심 차이는 브라우저가 아느냐다. rewrite 는 서버 안에서 끝나고, redirect 는 브라우저를 한 번 더 움직인다.
next() — 통과시키되 손은 댈 수 있다
그냥 return NextResponse.next() 는 "아무것도 안 함" 이다. 쓸모는 뭔가를 얹을 때 생긴다.
// 응답에 쿠키를 심는다
const response = NextResponse.next()
response.cookies.set('show-banner', 'false')
return response
요청 헤더를 바꿔서 앱 쪽으로 넘기려면 인자를 준다.
const headers = new Headers(request.headers)
headers.set('x-user-id', userId)
return NextResponse.next({ request: { headers } })
이 둘을 헷갈리면 안 된다.
| 쓰는 법 | 어디로 가나 |
|---|---|
next({ request: { headers } }) | 페이지·라우트·서버 함수 쪽으로. 클라이언트에 안 보인다 |
next({ headers }) | 브라우저로 나간다. 공식 문서가 쓰지 말라고 못박았다 |
뒤쪽이 위험한 이유는 Content-Type 같은 걸 덮어써서 서버 액션 제출이나 스트리밍을 깨뜨릴 수 있어서다.
들어온 요청 헤더를 통째로 복사하는 것도 피한다. 필요한 것만 골라 넘긴다.
rewrite() — 주소를 숨긴 채 다른 걸 보여준다
// 들어온 요청: /about → 브라우저는 /about 을 본다
// 실제로 렌더되는 것: /proxy
return NextResponse.rewrite(new URL('/proxy', request.url))
주소창이 안 바뀌니까 사용자에게 내부 구조를 숨길 수 있다. A/B 테스트, 서브도메인별 분기, 옛 경로를 새 경로로 조용히 옮기는 마이그레이션에 쓴다.
redirect() — 기본이 307 이다
const loginUrl = new URL('/login', request.url)
loginUrl.searchParams.set('from', request.nextUrl.pathname)
return NextResponse.redirect(loginUrl)
두 가지만 기억하면 된다.
1. 상태 코드가 302 가 아니라 307 이다. 영구 이전은 308.
302 는 브라우저가 메서드를 GET 으로 바꿔버리는 구현이 많다. POST /v1/users 가 GET /v2/users 로 둔갑한다.
307·308 은 메서드를 보존한다. 그래서 Next 가 이쪽을 기본으로 잡았다.
2. 절대 URL 이어야 한다. 그래서 new URL('/login', request.url) 형태로 쓴다.
이 셋이 사는 곳 — Next 16 부터는 proxy.ts
middleware.ts 가 proxy.ts 로 이름이 바뀌었고, middleware 는 deprecated 됐다 (v16).
- 이름을 바꾼 이유: Express 미들웨어와 혼동돼서. "앱 앞단의 네트워크 경계" 라는 성격을 이름에 담았다.
- 런타임: v16 부터 Node.js 가 기본이다.
- 코드모드가 있다 —
npx @next/codemod@canary middleware-to-proxy .
더 중요한 건 문서의 태도가 바뀐 것이다. "다른 방법이 없을 때 최후의 수단으로 쓰라" 고 적혀 있다.
Problem
뭘 골라야 하나
질문 두 개면 끝난다.
사용자가 주소가 바뀐 걸 알아야 하나?
│
├─ 예 → redirect()
│
└─ 아니오 → 다른 경로의 내용을 보여줘야 하나?
│
├─ 예 → rewrite()
│
└─ 아니오 → next()
상황에 대보면 이렇게 된다.
| 상황 | 고를 것 | 왜 |
|---|---|---|
| 로그인 안 된 사용자 막기 | redirect() | 주소가 /login 이 돼야 뒤로가기·북마크가 맞다 |
| 옛 URL 정리 | redirect() + 308 | 크롤러에 영구 이전을 알려야 한다 |
| A/B 테스트 | rewrite() | 사용자는 같은 주소를 봐야 한다 |
| 내부 경로 숨기기 | rewrite() | 주소창에 구조가 드러나면 안 될 때 |
| 요청에 사용자 정보 얹기 | next({ request }) | 보여줄 건 그대로, 정보만 전달 |
| 응답에 쿠키 심기 | next() + cookies.set | 통과시키면서 흔적만 남긴다 |
밟기 쉬운 함정 다섯
1. 응답 객체를 새로 만들면 앞에 붙인 게 사라진다.
next() 에 쿠키를 심어두고 조건에 따라 redirect() 를 반환하면, 그 쿠키는 안 나간다.
실제로 반환하는 객체에 다시 붙여야 한다.
2. matcher 가 없으면 모든 요청에 걸린다.
정적 파일, 이미지 최적화, public/ 까지 전부. 인증 로직이 CSS·JS 를 막아버리는 사고가 여기서 난다.
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
}
3. 무한 루프. redirect 대상이 matcher 에 또 걸리면 끝없이 돈다.
pathname 을 검사해서 빠져나갈 문을 먼저 만든다.
4. 서버 함수는 별도 라우트가 아니다. 서버 함수는 그 함수가 쓰인 라우트로 가는 POST 로 처리된다. 그래서 matcher 에서 그 경로를 빼면 서버 함수 호출도 같이 빠진다. 문서가 직접 경고한다 — 인증을 proxy 에만 맡기지 말고 각 서버 함수 안에서도 확인하라고.
5. _next/data 는 빼도 돈다. negative matcher 로 제외해도 실행된다.
페이지는 막았는데 데이터 경로는 안 막는 사고를 방지하려는 의도된 동작이다.
실행 순서를 알아야 하는 이유
1. next.config 의 headers
2. next.config 의 redirects
3. proxy ← rewrite / redirect 가 여기
4. beforeFiles rewrites
5. 파일시스템 라우트 (public/, _next/static, app/)
6. afterFiles rewrites
7. 동적 라우트
8. fallback rewrites
proxy 가 파일시스템보다 먼저다. 여기서 실수하면 정적 파일조차 못 나간다.
반대로 next.config 의 redirects 는 proxy 보다 먼저라, 거기서 이미 처리된 건 proxy 에 안 온다.
Output
한 줄 기준
- 아무것도 안 바꾼다 →
next() - 보여줄 걸 바꾼다, 주소는 그대로 →
rewrite() - 주소 자체를 바꾼다 →
redirect()
잊어버릴 것 같은 셋
- redirect 기본값은 307 이고 메서드를 보존한다. 영구 이전이면 308 을 직접 쓴다.
next({ request: { headers } })와next({ headers })는 정반대다. 앞은 앱으로, 뒤는 브라우저로. 뒤는 쓰지 않는다.- 쿠키·헤더는 실제로 반환하는 객체 하나에만 실린다.
버전 올릴 때
middleware.ts → proxy.ts 로 바뀐다. 코드모드 한 줄이면 되고 세 API 자체는 그대로다.
다만 문서가 "가능하면 쓰지 말라" 쪽으로 돌아섰다는 것도 같이 기억해둔다.
proxy 에 로직을 쌓기 전에 라우트 안에서 해결되는지 먼저 본다.
참고
- NextResponse — 세 메서드의 정확한 시그니처.
- proxy.js — matcher, 실행 순서, 마이그레이션.
- Redirecting — 307/308 을 쓰는 이유.
형식은 PKM — 기록으로 나를 디버깅하기 에서 정한 Input / Problem / Output 을 따랐다.