이번 트러블슈팅을 통해 단순한 UI 조건 처리라도 SSR 환경에서는 예기치 못한 예외 상황을 만들 수 있다는 사실을 알게 되었습니다. 초반에는 단순한 콘솔 경고 메시지 정도로 넘어갈 뻔했지만, 이러한 작은 오류조차 사용자 경험이나 퍼포먼스에 직접적인 영향을 줄 수 있다는 점을 다시 깨달았습니다. 이번 경험을 통해 앞으로는 에러 로그를 결코 가볍게 넘기지 않고, 꼼꼼히 확인하는 습관을 가지고 개발하려고 합니다!

Next.js의 App Router 기반 프로젝트인 TUNING 서비스를 구현하면서 예상치 못한 hydration error를 마주했다. UI 조건부 컴포넌트 숨김 처리를 진행하기 위해 처음에는 단순히 usePathname() 기반으로 페이지에 따라 처리를 진행했지만, 이 과정에서 클라이언트 사이드 렌더링과 서버 사이드 렌더링 사이의 불일치를 일으켜 트러블슈팅을 진행하게 되었다.
이 글에서는 hydration error가 무엇인지, 왜 발생했는지, 그리고 그걸 어떻게 해결했는지를 내가 경험한 흐름대로 정리해보려고 한다.
💥 hydration error란?
React 기반 프레임워크(Next.js)에서는 서버에서 HTML을 먼저 렌더링해서 보내주고, 클라이언트에서 해당 HTML에 JavaScript 이벤트를 붙이는 과정을 hydration이라고 부른다.
이때 서버에서 렌더링한 UI와 클라이언트에서 실제 렌더링된 UI가 다르면, Next.js는 아래와 같은 오류를 발생시킨다.
Hydration failed because the initial UI does not match what was rendered on the server.
초기 HTML과 클라이언트 렌더링 결과가 달라서 React가 상태를 일치시킬 수 없다는 의미인데, 이 에러는 다음과 같은 경우에 자주 발생한다.
- `window`, `document`, `localStorage`, `usePathname()` 등 브라우저 전용 API를 SSR 단계에서 사용했을 때
- 클라이언트에서만 가능한 계산을 SSR 시점에 미리 해버렸을 때
- 랜덤 값, 날짜/시간, 혹은 API 데이터가 SSR과 CSR에서 다르게 렌더링될 때
→ 나는 이 중 첫번째 경우에 해당하는 브라우저 전용 훅인 `usePathname()`을 SSR 단계에서 사용해 발생한 문제였다.
👀 왜 hydration error가 생겼을까?
앱 공통 레이아웃에 `Header`와 `BottomNavigationBar`를 기본적으로 포함시켜 두고, 특정 경로(/login, /onboarding, /not-found)등 로그인되지 않은 사용자가 접근하는 특정 페이지에는 이 컴포넌트들을 안보이게 처리하고 싶었다.
{!['/login', '/onboarding', '/not-found'].includes(pathname) && <BottomNavigationBar />}
처음엔 위와 같이 단순한 조건문으로 처리했는데 콘솔에는 hydration 에러가 발생했고, 개발자 도구에서도 경고 메시지가 뜨는 상황이였다.
🔍 원인 분석하기
Next.js 공식문서를 참고해봤을 때,
문제의 핵심은 `usePathname()` 클라이언트 사이드에서만 정확한 값을 제공한다는 점이었다.

Next.js App Router는 기본적으로 SSR을 기반으로 동작한다. 하지만 SSR 단계에서는 아직 브라우저 환경이 아니기 때문에, `usePathname()` 같은 브라우저 전용 훅은 빈 문자열이거나 "undefined"에 가까운 초기값을 갖고 있어 예상한 값을 반환하지 않는다.
내 경우에는 아래와 같은 과정으로 문제가 발생한 것으로 보이며, 순서대로 정리해보면 다음과 같다.
1. SSR 단계에서는 `usePathname()`이 정확한 경로를 알 수 없으므로, `/login`같은 경로에서도 `BottomNavigationBar`가 표시된 상태로 렌더링된다.
2. CSR 단계에 들어오면, 실제 경로(`/login`)가 적용되어 해당 컴포넌트를 숨기는 조건문이 동작한다.
3. 결국 서버에서 내려준 HTML과 클라이언트에서 렌더링된 결과가 일치하지 않게 되면서, React는 hydration 오류를 발생시킨다.
`hydration error`는 서버와 클라이언트가 동일한 UI 트리를 공유하지 못할 때 흔히 발생하며, 특히 `usePathname`, `window`, `localStorage`등 클라이언트 전용 데이터에 의존한 조건부 렌더링이 원인이 되는 경우가 많다고 한다.
결국 이번 오류는 내가 브라우저 환경에서만 안전하게 사용할 수 있는 값에 조건문을 걸고 그걸 SSR 시점에서 판단해 렌더링하려 했기 때문에 생긴 문제였다.
🙋🏻 해결 방법 - 클라이언트에서만 조건 판단하기
서버와 클라이언트 간의 UI 불일치로 발생하는 `hydration error`를 방지하기 위해, 렌더링 타이밍을 명확히 분리하고, 브라우저 전용 API(usePathname)에 의존하는 로직은 클라이언트에서만 실행되도록 처리했다.
1. 렌더링 타이밍 분리하기 (mounted 패턴)
먼저 CSR 시점에서만 특정 로직을 실행해야하기 때문에 클라이언트에서 렌더링이 시작되었는지를 감지하기 위해 `mounted` 상태를 추가했다.
const [mounted, setMounted] = useState(false); // 브라우저 환경에서만 true
useEffect(() => {
setMounted(true);
}, []);
2. pathname에 따른 숨김 조건도 useEffect 내부에서 분기
Next.js의 `usePathname()` 훅은 클라이언트 전용 훅이므로 SSR 시점에서는 신뢰할 수 없다. 따라서 해당 값을 기반으로 하는 조건 분기는 `useEffect` 내부에서 수행해야 한다.
useEffect(() => {
setIsHiddenUI(hiddenRoutes.some((route) => pathname.startsWith(route)));
}, [pathname]);
- pathname = /login, /onboarding, /not-found 등 특정 경로로 시작할 때 isHiddenUI = true로 설정하면 클라이언트 마운트 후에만 실행되므로 SSR 충돌을 방지할 수 있다.
3. 마운트 전엔 최소 구조만 렌더링해서 SSR과 CSR 결과 일치시키기
SSR 단계에서는 아직 pathname이나 mounted 상태가 반영되지 않았기 때문에, 초기 렌더링 시에는 조건부 UI를 모두 제외하고 최소한의 레이아웃 구조만 렌더링한다.
if (!mounted) {
return <div className="relative flex min-h-[100dvh] w-full max-w-[430px] flex-col" />;
}
- 이렇게 하면 서버와 클라이언트가 동일한 DOM 구조를 유지하게 되어, `hydration mismatch`를 방지할 수 있다.
- 이후 클라이언트에서 실제 UI 조건을 판별해 조건부로 UI를 숨기거나 렌더링할 수 있다.
💡 내가 배운 것
이번 경험을 통해 SSR 기반 프로젝트에서는 작은 조건문 하나도 렌더링 시점을 구분해서 작성해야 한다는 걸 느꼈다.
- usePathname(), window, localStorage처럼 브라우저에서만 사용할 수 있는 정보는 SSR에서 신뢰할 수 없다.
- App Router는 SSR이 기본이기 때문에, CSR 전용 판단은 useEffect 내부에서 분리해야 한다.
- mounted 체크는 단순하지만 SSR/CSR hydration mismatch를 방지하는 데 굉장히 효과적인 방법이다.
그리고 앞으로는 이런 조건부 UI 렌더링을 더 구조적으로 처리하기 위해 LayoutContext나 전역 UI 상태 관리 방식도 고려해보려고 한다.
'👩🏻💻 Develop > TroubleShooting' 카테고리의 다른 글
| [Next.js] 중복 ENUM 값 충돌로 인해 발생한 키워드 매핑 오류 해결하기 (2) | 2025.11.24 |
|---|---|
| [Next.js] Refresh Token 만료로 인한 페이지 무한 새로고침 오류 해결하기 (0) | 2025.09.07 |
| [Next.js] useSearchParams 사용 시 Next.js 15 빌드 실패 이슈 (0) | 2025.09.06 |
| [Next.js] next-pwa와 Turbopack 호환성 문제 해결하기 (0) | 2025.09.05 |
| [Next.js] SSE 브라우저 연결 오류 해결하기 (Nginx) (1) | 2025.09.02 |