2026. 7. 19.
Next.js usePathname hydration mismatch, 경로 표시만 마운트 뒤 갱신하세요
Next.js rewrite나 Proxy 뒤 usePathname을 표시할 때 새로고침에서만 hydration mismatch가 난다면, 서버 경로와 브라우저 경로를 비교하고 경로 표시 부분만 마운트 뒤 갱신하는 순서입니다.

Next.js App Router에서 usePathname()으로 활성 메뉴나 현재 경로를 표시했습니다. 개발 서버의 <Link> 이동은 괜찮은데 rewrite 주소를 새로고침하거나 배포 URL로 직접 들어가면 Hydration failed가 뜹니다. 이때 페이지 전체를 클라이언트 렌더링으로 돌리기 전에, pathname을 화면에 쓰는 작은 부분만 찾으세요.
핵심 답은 간단합니다. rewrite에서는 서버가 HTML을 만든 원본 경로와 주소창에 남는 브라우저 경로가 다를 수 있습니다. 경로 표시를 별도 Client Component로 격리하고, 서버와 브라우저 첫 렌더에는 같은 fallback을 내보낸 뒤 useEffect에서 실제 pathname을 반영하면 됩니다. 첫 렌더를 같게 만든 뒤 경로만 갱신하는 것이 이 오류의 최소 수정입니다.
왜 rewrite에서 새로고침만 깨질까요?
Next.js usePathname 공식 문서는 정적 프리렌더링 페이지를 rewrite나 Proxy 경로로 열면 hydration mismatch가 생길 수 있다고 설명합니다. 서버는 원본 pathname으로 HTML을 만들지만, 브라우저의 usePathname()은 rewrite 뒤 주소를 읽을 수 있기 때문입니다.
예를 들어 브라우저에는 /app이 보이지만 내부적으로 /dashboard를 렌더하도록 rewrite했다고 가정해 보겠습니다. 서버 첫 HTML이 현재 경로: /dashboard인데 브라우저 첫 렌더가 현재 경로: /app이면 글자 하나의 차이가 아니라 서로 다른 렌더 결과입니다. React hydrateRoot 공식 문서는 서버 출력과 최초 클라이언트 출력이 같아야 하며, mismatch를 버그로 다루라고 안내합니다.
클라이언트 이동은 이미 브라우저 라우터가 경로를 알고 있는 상태에서 일어나므로 멀쩡해 보일 수 있습니다. 반면 직접 접속과 새로고침은 서버 HTML부터 다시 받기 때문에 문제가 드러납니다. 그래서 Link 이동 성공만으로 수정 완료라고 판단하면 안 됩니다.
먼저 서버 경로와 브라우저 경로를 비교하세요
코드를 바꾸기 전에 오류가 정말 pathname 차이에서 시작하는지 확인합니다.
pathname을 출력하는 위치를 찾습니다
프로젝트에서 usePathname(을 검색합니다. 활성 메뉴, breadcrumb, 현재 경로 배지, analytics용 숨은 텍스트처럼 반환값을 JSX에 바로 넣는 위치를 우선 봅니다. 훅을 호출하기만 하고 렌더 결과에 쓰지 않는 코드는 이번 mismatch의 직접 원인이 아닐 수 있습니다.
rg "usePathname\(" app src
rewrite와 Proxy 규칙을 확인합니다
next.config.ts의 rewrites()와 프로젝트 루트의 proxy.ts를 봅니다. Next.js rewrites 문서에 따르면 rewrite는 목적지 콘텐츠를 제공하면서도 들어온 URL을 주소창에 그대로 숨겨 둡니다. Next.js 16에서는 Middleware가 Proxy라는 이름으로 바뀌었으므로, 예전 프로젝트라면 middleware.ts도 함께 확인합니다.
두 접속 경로를 같은 빌드에서 재현합니다
개발 서버만 보지 말고 production 모드에서 원본 경로와 rewrite 경로를 각각 직접 엽니다.
npm run build
npm run start
원본 경로는 괜찮고 rewrite 경로의 첫 접속만 실패하며, 콘솔 스택이 pathname 표시 컴포넌트를 가리키면 이 글의 수정 패턴이 맞습니다.
경로 표시만 마운트 뒤 갱신하세요
전체 페이지에 ssr: false를 적용하거나 모든 부모를 Client Component로 바꾸지 않습니다. 경로를 실제로 표시하는 부분만 아래처럼 분리합니다.
'use client'
import { useEffect, useState } from 'react'
import { usePathname } from 'next/navigation'
export function PathnameBadge() {
const pathname = usePathname()
const [clientPathname, setClientPathname] = useState('')
useEffect(() => {
setClientPathname(pathname)
}, [pathname])
return (
<span aria-label="현재 경로">
{clientPathname || '경로 확인 중'}
</span>
)
}
여기서 중요한 값은 clientPathname의 초기값입니다. 서버와 브라우저 첫 렌더가 모두 경로 확인 중을 내보내고, 마운트가 끝난 뒤에만 /app 같은 실제 경로로 바뀝니다. 빈 문자열을 써도 되지만 화면 폭이 갑자기 바뀐다면 같은 크기의 고정 fallback을 두는 편이 낫습니다.
활성 메뉴라면 같은 원리를 클래스 계산에 적용합니다. 서버 첫 렌더에서는 모든 메뉴를 비활성 상태로 두고, 마운트 뒤 현재 pathname과 맞는 항목만 활성화합니다. 서버 첫 HTML에서 주소창 경로를 추측하지 않는 것이 기준입니다.
수정 뒤 네 가지 이동을 확인하세요
production 서버를 띄운 상태에서 다음 순서로 확인합니다.
- rewrite 주소를 주소창에 직접 입력합니다.
- 같은 주소에서 새로고침합니다.
- 사이트 안의
<Link>로 들어갑니다. - 브라우저의 뒤로 가기와 앞으로 가기를 사용합니다.
각 경우에 경로 표시가 마운트 뒤 올바른 값으로 바뀌고, Console에 hydration 경고가 없어야 합니다. 활성 메뉴를 고쳤다면 선택 상태뿐 아니라 키보드 포커스와 aria-current="page"도 함께 확인하세요. 이 점검은 일반 원인을 넓게 찾는 Next.js Hydration failed 진단 순서와 함께 쓰면 좋습니다.
이 패턴이 아닐 때는 다른 첫 렌더 차이를 찾으세요
rewrite를 끄거나 pathname 표시를 제거해도 경고가 계속되면 원인이 다릅니다. 시간과 난수, window·localStorage 분기, 잘못 중첩된 HTML, 브라우저 확장 프로그램처럼 서버와 브라우저 첫 출력을 다르게 만드는 값을 확인합니다. App Router에서 브라우저 전용 값의 경계를 정하는 방법은 window is not defined 첫 참조 점검도 참고할 수 있습니다.
suppressHydrationWarning은 실제 경로 차이를 고치는 기능이 아닙니다. 경고를 숨기는 동안 잘못된 활성 메뉴나 breadcrumb가 남을 수 있으므로, 의도적으로 다른 단일 텍스트가 아니라면 먼저 원인을 제거하세요.
참고 출처
오늘 할 일은 pathname 표시 컴포넌트 하나를 격리하고, production 모드에서 rewrite 주소 직접 접속과 Link 이동을 모두 확인하는 것입니다. 두 경우의 콘솔이 깨끗해야 수정이 끝난 것입니다.
이 글은 AI 코딩과 개발 학습의 일반 정보 제공 목적입니다. 도구, 모델, 커리큘럼, 요금은 버전과 시점에 따라 달라질 수 있으므로 실습이나 도입 전 공식 문서와 최신 릴리스 노트를 확인하세요.
다음으로 읽을 기사
같은 흐름으로 이어 읽기 좋은 기사만 추려 보여줍니다.
첫 번째 댓글을 남겨보세요
여러분의 생각이 다른 독자에게 도움이 됩니다.
댓글 0
이 글을 읽은 독자들의 생각을 나눠보세요.