Hydration Mismatch란?
Next.js의 SSR 또는 SSG 환경에서 미리 생성된 서버 측의 HTML 결과물과, 브라우저에서 JavaScript가 실행되며 구축한 클라이언트 측의 DOM 트리가 서로 일치하지 않을 때 발생하는 에러입니다.
왜 발생할까요?
Next.js는 서버에서 먼저 HTML을 렌더링하여 사용자에게 빠르게 화면을 보여준 뒤, 브라우저에서 리액트(React) 파일을 로드하여 기존 HTML에 이벤트 리스너를 결합하는 하이드레이션 과정을 거칩니다. 이때 서버가 만든 뼈대와 클라이언트가 만든 뼈대가 다르면 리액트가 경고를 던지거나 UI가 깨지게 됩니다.
주요 발생 원인
하이드레이션 미스매치는 주로 "서버와 클라이언트의 환경 차이" 때문에 발생합니다.
① 시간, 날짜, 랜덤 값의 차이
서버가 렌더링된 시간/지역(타임존)과 브라우저가 실행된 시간/지역이 다를 때 발생합니다.
-
예시:
new Date(),Math.random(),crypto.randomUUID()등을 컴포넌트 렌더링 직전에 바로 사용하는 경우.
② 브라우저 전용 API 사용
서버(Node.js 환경)에는 존재하지 않고, 브라우저(Window 환경)에만 존재하는 데이터에 의존해 렌더링할 때 발생합니다.
-
예시:
window,localStorage,sessionStorage,document등을 조건문 없이 렌더링 로직에 포함하는 경우.
③ 올바르지 않은 HTML 태그 태깅 (잘못된 마크업)
브라우저는 잘못된 HTML 구조를 발견하면 스스로 DOM을 수정합니다. 서버가 보낸 HTML 구조를 브라우저가 멋대로 바꾸면서 미스매치가 일치하게 됩니다.
-
예시:
<p>태그 내부에<div>태그를 넣는 경우 (브라우저는 p 태그를 자동으로 닫아버림). -
예시:
<table>내부에<tbody>없이 바로<tr>을 넣는 경우.
해결 방법
실무에서 적용할 수 있는 대표적인 해결 방법 4가지입니다.
방법 1: useEffect를 이용한 클라이언트 사이드 렌더링 지연
가장 확실하고 표준적인 방법입니다. 서버에서는 렌더링하지 않고, 컴포넌트가 브라우저에 마운트된 후에만 특정 UI를 그리도록 제어합니다.
import { useState, useEffect } from 'react';
export default function ClientOnlyComponent() {
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setIsMounted(true);
}, []);
// 서버에서는 null이나 로딩 스켈레톤을 반환하고, 클라이언트에서만 실제 값을 보여줌
if (!isMounted) return null;
return <div>{localStorage.getItem('theme')}</div>;
}
방법 2: Next.js dynamic 컴포넌트 사용 (ssr: false)
특정 컴포넌트 전체를 서버 사이드 렌더링에서 완전히 제외하고 브라우저에서만 로드되도록 설정합니다.
import dynamic from 'next/dynamic';
// ssr: false 옵션으로 클라이언트에서만 렌더링하도록 설정
const DynamicComponentWithNoSSR = dynamic(
() => import('../components/ClientComponent'),
{ ssr: false }
);
export default function Page() {
return (
<div>
<DynamicComponentWithNoSSR />
</div>
);
}
방법 3: suppressHydrationWarning 속성 사용
서버와 클라이언트의 텍스트 내용(예: 타임스탬프)이 다를 수밖에 없고, 그것이 UI 구조를 깨뜨리지 않는 미미한 차이라면 경고를 수동으로 끄는 방법입니다. (최후의 수단으로 권장)
// 1 depth 수준의 텍스트 불일치 경고만 무시합니다. (태그 구조가 다르면 무용지물)
<span suppressHydrationWarning>
{new Date().toLocaleTimeString()}
</span>
방법 4: 올바른 HTML 명세 준수
HTML 마크업 규칙을 철저히 지키는 것만으로도 파싱 오류로 인한 미스매치를 예방할 수 있습니다.
-
<ul>,<ol>의 직계 자식으로는 반드시<li>만 사용하기. -
<a>태그 내부에 또 다른<a>태그나 버튼 태그 넣지 않기. -
<p>태그 내부에 블록 요소(<div>,<h1>등) 넣지 않기.