Next.js 중복 슬래시 문제와 Hydration, 그리고 Client Router

KINA KIM·2025년 4월 2일
2

문제

로컬 환경에서 velog.io/////kina94와 같이 중첩된 슬래시로 라우팅 시 자동으로 velog.io/kina94로 리디렉션됐으나 ec2 배포 이후에는 리디렉션이 되지 않고, React DevTools의 컴포넌트 트리도 로딩되지 않고, 하이드레이션도 안 돼서 페이지가 먹통이 됨.

//error message
Error: invariant: invalid relative URL, router received //proudct

발생 이유

개발/프로덕션 모두 동일한 NextNodeServer 및 requestHandler 기반 라우팅 로직을 사용하고 있으나 배포 시 Nginx를 타면서 문제가 발생함.

요약하자면 내 경우 잘못된 URL로 인해 클라이언트 라우터가 동작하지 않으면서 hydration 전제 조건이 무너진 케이스였음.

서버 렌더링 시 req.url을 기반으로 HTML을 생성하고, 클라이언트에서는 window.location을 기반으로 라우팅 상태를 초기화함.

  1. 사용자가 주소창에 입력:

  2. 브라우저 -> Nginx로 요청 전송:

    • ///products
  3. Nginx에서 경로 정리 후 Next.js에 전달:

    • /products 전달
    • nginx가 정규화만 해주지, 클라이언트에게 301, 308 응답을 보내 리디렉션을 유도하지는 않음
  4. Next.js SSR:

    • req.url인 /products 기반으로 HTML 생성
  5. 브라우저는 여전히 URL: //products

    • 클라이언트 router path는 '//proudct'
    • router.push('//proudct')처럼 동작함
  6. Next.js 클라이언트 라우터:

    • 내 주소가 //products라고? 이거 URL로 파싱하려니까 도메인(origin)이 products로 잡히는데? 이건 상대 경로가 아니라 프로토콜 생략된 절대 URL인데 이거 뭐임?
      • new URL('//products', location.href);
      • new URL('/products', location.href);
// nextjs/.../router/utils
  const globalBase = new URL(
    typeof window === 'undefined' ? 'http://n' : getLocationOrigin()
  )

  const resolvedBase = base
    ? new URL(base, globalBase)
    : url.startsWith('.')
      ? new URL(
          typeof window === 'undefined' ? 'http://n' : window.location.href
        )
      : globalBase

  const { pathname, searchParams, search, hash, href, origin } = new URL(
    url,
    resolvedBase
  )

  // 바로 이 부분!!!!!
  if (origin !== globalBase.origin) {
    throw new Error(`invariant: invalid relative URL, router received ${url}`)
  }
  • 라우팅 전에 //signin이라는 이상한 URL을 해석하려다가 터져버리고, invariant: invalid relative URL (클라이언트 라우터가 경로 인식 못함) 에러 발생
  • React 앱 전체가 hydration에 실패하고, CSR도 깨짐

로컬 환경: Next.js 서버가 중첩 슬래시를 직접 정규화하고 308 응답 후 리디렉션
배포 환경: Nginx가 요청을 먼저 받아 정규화를 해버렸지만 301이나 308 응답을 따로 보내지 않았기 때문에 리디렉션이 작동하지 않아서 이상한 URL을 해석하다 터짐

해결 방법

Nginx에서 301 또는 308 응답을 보내서 브라우저가 HTTP 응답을 받아 URL을 이동할 수 있도록 있도록 수정

// 예시 (페이지가 하나임)
// /etc/nginx/conf.d/...
if ($request_uri ~ "//+") {
    return 308 $scheme://$host$uri;
}

(관련) Next.js의 URL 처리 방식

1. Trailing Slash 정규화

  • false로 설정 시 /about/ → /about으로 리디렉션 발생
  • true로 설정 시 설정하면 /about → /about/ 리디렉션됨
// 기본 설정값
module.exports = {
  trailingSlash: true,
}

2. 중복 슬래시 제거

  • //studio, ///studio → /studio로 자동 정규화 및 리디렉션
// Next.js 내부 코드
// next.js/packages/next/src/shared/lib/utils
export function normalizeRepeatedSlashes(url: string) {
  const urlParts = url.split('?')
  const urlNoQuery = urlParts[0]

  return (
    urlNoQuery
      // first we replace any non-encoded backslashes with forward
      // then normalize repeated forward slashes
      .replace(/\\/g, '/')
      .replace(/\/\/+/g, '/') +
    (urlParts[1] ? `?${urlParts.slice(1).join('?')}` : '')
  )
}

(관련) Hydration

  1. SSR
  • 사용자가 /products//category에 접근
  • 서버(Nginx + Next.js)는 정규화된 /products/category로 처리
  • Next.js는 해당 페이지에 대한 HTML을 생성
  1. 클라이언트 Hydration
  • 브라우저는 HTML을 받음
  • JS 번들이 브라우저에 로드되고 클라이언트 사이드 JS가 실행
  • 서버에서 생성된 HTML과 클라이언트에서 생성된 Virtual DOM이 일치하는지 확인하면서 필요한 부분만 수정
  • 이후 이벤트 연결까지 마치면 앱이 완전히 인터랙티브해짐

hydration 시 React는 현재 브라우저 URL을 기준으로 라우팅/상태/컴포넌트 초기화를 함

0개의 댓글