Next.js App Router에서 “텍스트 내용이 서버 렌더링된 HTML과 일치하지 않음” 오류 해결 방법
출처: Dev.to
Next.js App Router에서 “Text content does not match server‑rendered HTML” 오류 해결 방법
이 오류는 서버에서 생성된 HTML(SSR/SSG)과 클라이언트가 처음 렌더링할 때 생성되는 React 트리가 일치하지 않을 때 발생합니다.
수화(hydration) 과정에서 React는 초기 DOM이 서버가 만든 것과 정확히 일치하기를 기대하는데, 차이가 있으면 치명적인 오류가 발생합니다.
가장 흔한 원인
-
브라우저 API를 직접 사용
window,localStorage,Date.now()등은 SSR 환경에서는 사용할 수 없으므로, 서버와 클라이언트에서 서로 다른 결과를 반환합니다. -
다음과 같은 코드 패턴
typeof window !== 'undefined'를 컴포넌트 본문(useEffect 밖)에서 사용- CSS‑in‑JS 라이브러리 설정 오류 (특히
styled‑components를@emotion/react혹은@emotion/server없이 사용) - 브라우저 확장 프로그램(예: Dark Reader, 광고 차단기)으로 DOM이 변조됨
- iOS 자동 감지 메타태그(
format-detection) - CDN(Cloudflare 등)의 자동 압축·미니파이
코드에서 찾아볼 항목
Date,Math.random(),localStorage,window,navigator등typeof window를 effect hook 외부에서 사용한 조건문- 보호 없이 동적 콘텐츠를 렌더링하는 컴포넌트
🔍 빠른 팁: 의심되는 컴포넌트에 console.log('server' ? !window : 'client') 를 넣고, HTML 소스와 브라우저 DOM을 비교해 보세요.
suppressHydrationWarning 사용법
// ✅ 올바른 사용: 문제되는 요소에만 경고를 억제
{new Date().toLocaleDateString()}
⚠️ 주의: 전체 레이아웃처럼 큰 컨테이너에 적용하지 마세요. 원자적인 요소에만 사용합니다.
로직을 useEffect + 로컬 상태로 옮기기
import { useState, useEffect } from 'react';
export default function ClientOnlyComponent() {
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
}, []);
// ✅ SSR에서는 항상 fallback을 렌더링하고,
// CSR에서는 수화 후 실제 내용을 렌더링합니다.
return (
<>
{isClient ? (
<>
브라우저 내용 (예: localStorage: {localStorage.getItem('theme')})
</>
) : (
<>
로딩 중...
{/* SSR과 CSR 초기 렌더링이 동일하도록 유지 */}
</>
)}
</>
);
}
next/dynamic 으로 해당 컴포넌트의 SSR 비활성화
// components/ClientComponent.tsx
export default function ClientComponent() {
const [width, setWidth] = useState(0);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
handleResize(); // 초기값 설정
return () => window.removeEventListener('resize', handleResize);
}, []);
return <div>너비: {width}px</div>;
}
// page.tsx
import dynamic from 'next/dynamic';
const ClientComponent = dynamic(() => import('../components/ClientComponent'), {
ssr: false, // ✅ 서버 렌더링을 비활성화
});
export default function Page() {
return (
<>
<h2>메인 페이지</h2>
<ClientComponent />
</>
);
}
레이아웃에 메타태그 추가 (app/layout.tsx)
// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
{/* 여기서 메타태그를 삽입 */}
</head>
<body>{children}</body>
</html>
);
}
styled-components / @emotion 설정 (SSR 지원)
npm install @emotion/react @emotion/server
// app/layout.tsx
import { CacheProvider } from '@emotion/react';
import createCache from '@emotion/cache';
const cache = createCache({ key: 'css' });
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>{/* ... */}</head>
<body>
<CacheProvider value={cache}>{children}</CacheProvider>
</body>
</html>
);
}
🔥 추천 대안: @emotion/react 를 직접 사용하거나, 공식 Next.js 가이드를 따르는 styled-components 설정을 활용하세요.
Cloudflare 사용 시
- Auto Minify (HTML) 비활성화
- Rocket Loader 비활성화
- Brotli 압축이 HTML을 손상시키지 않는지 확인
🛠️ 빠른 테스트: CDN 없이 로컬 환경에 배포해 보세요. 오류가 사라지면 인프라가 원인입니다.
- HTML 소스(Ctrl + U)와 브라우저 DOM(F12 > Elements) 비교
- 의심되는 컴포넌트에
console.log('Hydration check:', window ? 'client' : 'server')삽입 - React DevTools Profiler에서 빨간색
hydrate표시가 뜨는 컴포넌트 찾기
프로덕션에서는 suppressHydrationWarning을 마지막 수단으로만 사용하고, 근본적인 해결책으로 삼지 마세요.
💡 골든 룰: SSR과 CSR 사이에 내용이 달라진다면 반드시 useEffect 혹은 ssr: false 로 보호해야 합니다. 사용자는 수화 과정에서 서로 다른 콘텐츠가 깜빡이는 현상을 절대 보지 않아야 합니다.
위 절차를 순서대로 적용하면 오류가 사라집니다. 그래도 해결되지 않으면 CDN 로그와 브라우저 확장 프로그램(시크릿 모드에서 테스트) 등을 점검하세요.
이 글이 도움이 되었다면, 주간 인기 오류와 예방 방법을 받아볼 수 있도록 구독해 주세요.
구독하기 → (링크 삽입)