"매 server-rendered HTML 과 매 client first render 가 매 다르면 매 React 가 매 throw — 매 일관된 input 이 매 핵심.". 매 typical 원인: Date.now / Math.random / window 접근 / locale / 매 third-party DOM 조작. React 19 부터 매 error message 가 매 어떤 attribute 가 매 mismatch 인지 매 정확히 표시.
매 핵심
매 원인 분류
Time / random: server 의 매 시각 ≠ client 의 매 시각.
Locale / timezone: Intl.DateTimeFormat 의 매 다른 결과.
Window / document: server 에 매 없음.
User-agent branching: useragent 의 매 다른 처리.
Third-party script: AdSense, Cookiebot 의 매 DOM 변경.
Browser extension: Grammarly, Dark Reader 의 매 inject.
Conditional rendering on typeof window: 매 안티패턴.
매 React 의 매 행동 (19)
매 mismatch detection — 매 server HTML 을 매 polluted attribute 로 매 표시.
매 partial recovery — 매 mismatch boundary 만 매 client re-render.
매 보존되는 attribute (data-, aria-, custom): 매 dev warning.
매 Debug 도구
Next.js: ?_rsc_no_cache 로 매 server payload 확인. next dev 의 매 colored diff.
React DevTools: Profiler — hydration milestone.
Browser: 매 view-source vs DOM inspect 비교.
매 응용
e-commerce — 매 product price (locale), cart count (cookie).
Auth — 매 logged-in / logged-out 분기.
A/B test — 매 server vs client variant 의 매 일치.
💻 패턴
Detect mismatch source (Next.js 15)
// Bad — Date.now() differs
functionNow() {return<span>{Date.now()}</span>;}// Good — pass server time as prop, render same on both
functionNow({serverTime}:{serverTime: number}){return<spansuppressHydrationWarning>{newDate(serverTime).toISOString()}</span>;}
useEffect for client-only side effects
functionClientGreeting() {const[name,setName]=useState<string|null>(null);useEffect(()=>setName(localStorage.getItem('name')),[]);if(!name)returnnull;// server returns null, client fills after mount
return<p>Hi{name}</p>;}
dynamic() with ssr: false (Next.js)
importdynamicfrom'next/dynamic';constChart=dynamic(()=>import('./Chart'),{ssr: false});// Renders nothing on server, only on client
Avoid Math.random in render
// Bad
functionCard() {return<divdata-id={Math.random()}>...</div>;}// Good — useId
functionCard() {constid=useId();return<divid={id}>...</div>;}
// Server and client must agree — pass timezone explicitly
constfmt=newIntl.DateTimeFormat('ko-KR',{timeZone:'Asia/Seoul'});return<span>{fmt.format(newDate(props.iso))}</span>;
Replay server HTML offline
# Capture server HTML
curl -s https://app.example.com/page > server.html
# Open in fresh browser, compare with hydrated DOM via outerHTML diff