Next.js App Router에서 “텍스트 내용이 서버 렌더링된 HTML과 일치하지 않음” 오류 해결 방법

발행: (2026년 5월 24일 AM 02:24 GMT+9)
6 분 소요
원문: Dev.to

출처: 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 windoweffect 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 로그와 브라우저 확장 프로그램(시크릿 모드에서 테스트) 등을 점검하세요.


이 글이 도움이 되었다면, 주간 인기 오류와 예방 방법을 받아볼 수 있도록 구독해 주세요.
구독하기 → (링크 삽입)

0 조회
Back to Blog

관련 글

더 보기 »

내 스킬

프로젝트를 위한 AI 지시문을 만들고, 설치하고, 관리하세요 — 코딩이 필요 없습니다. CREATE 이름을 정하고, 카테고리를 선택하고, 원하는 것을 설명하세요 — 마법사가 자동으로 구성합니다.