그래프로

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()

잊어버릴 것 같은 셋

  1. redirect 기본값은 307 이고 메서드를 보존한다. 영구 이전이면 308 을 직접 쓴다.
  2. next({ request: { headers } }) 와 next({ headers }) 는 정반대다. 앞은 앱으로, 뒤는 브라우저로. 뒤는 쓰지 않는다.
  3. 쿠키·헤더는 실제로 반환하는 객체 하나에만 실린다.

버전 올릴 때

middleware.ts → proxy.ts 로 바뀐다. 코드모드 한 줄이면 되고 세 API 자체는 그대로다. 다만 문서가 "가능하면 쓰지 말라" 쪽으로 돌아섰다는 것도 같이 기억해둔다. proxy 에 로직을 쌓기 전에 라우트 안에서 해결되는지 먼저 본다.

참고

  • NextResponse — 세 메서드의 정확한 시그니처.
  • proxy.js — matcher, 실행 순서, 마이그레이션.
  • Redirecting — 307/308 을 쓰는 이유.

형식은 PKM — 기록으로 나를 디버깅하기 에서 정한 Input / Problem / Output 을 따랐다.