Next.js Hydration 에러 완벽 해결: SSR과 CSR 불일치 아키텍처

[Troubleshooting] Next.js Hydration 에러 완벽 해결: SSR과 CSR 불일치 아키텍처

"Next.js로 개발을 마치고 화면을 띄웠는데, 브라우저 콘솔에 Text content does not match server-rendered HTML 혹은 Hydration failed because the initial UI does not match라는 시뻘건 에러가 화면을 덮어버립니다."

React 프론트엔드 생태계에서 Next.js를 도입한 주니어 개발자들이 가장 뼈아프게 겪는 통과의례입니다. 일반적인 리액트(SPA)에서는 겪어본 적 없는 이 에러는, 코드가 틀려서가 아니라 '시간과 공간의 엇갈림'을 통제하지 못했기 때문에 발생합니다.

초보자들은 에러가 난 UI 컴포넌트를 통째로 지우거나 강제로 클라이언트 렌더링으로 덮어버리는 땜질 처방을 합니다. 본 가이드에서는 서버와 브라우저 사이에서 발생하는 하이드레이션(Hydration)의 수학적 매커니즘을 해체하고, 불일치를 완벽하게 동기화하는 프론트엔드 아키텍처를 단호하게 제시합니다.

📌 이 글의 핵심 포인트

  • Hydration의 본질: 뼈대(HTML)에 생명(JavaScript)을 불어넣는 렌더링 접합 과정
  • 에러 발생 원인: window 객체나 Date.now() 등 서버와 클라이언트의 실행 시점이 다른 코드를 렌더링에 직접 사용할 때 발생하는 충돌
  • 해결 아키텍처: useEffect를 활용한 마운트 지연 렌더링 및 dynamic import 기법

1단계: 핵심 원인 분석 - 두 개의 세계가 빚어내는 환상

Next.js는 브라우저가 화면을 그리기 전에 서버(Node.js)에서 먼저 한 번 HTML을 그려서(Pre-rendering) 던져줍니다. 그리고 브라우저는 그 HTML을 받아 자바스크립트 이벤트를 연결하는 하이드레이션 작업을 수행합니다.

만약 당신의 코드에 typeof window !== 'undefined'라는 조건문이 있거나, localStorage에서 값을 꺼내와 화면에 렌더링하는 코드가 있다면 어떻게 될까요? 서버에는 window나 localStorage가 아예 존재하지 않습니다. 따라서 서버가 그린 1차 HTML과, 브라우저가 클라이언트 데이터를 바탕으로 그린 2차 HTML의 텍스트가 달라집니다. 리액트 엔진은 이 불일치를 '해킹'이나 '치명적 렌더링 결함'으로 간주하여 화면을 박살 내버립니다.

2단계: 완벽한 트러블슈팅 - useEffect를 통한 시공간 분리

클라이언트(브라우저)에서만 알 수 있는 데이터는, 반드시 "하이드레이션이 완전히 끝난 직후"에 렌더링되도록 렌더링 시점을 의도적으로 뒤로 미루어야 합니다.


import { useState, useEffect } from 'react';

export default function ClientOnlyComponent() {
    // 1. 하이드레이션 완료 여부를 추적하는 상태 생성
    const [isMounted, setIsMounted] = useState(false);

    // 2. useEffect는 컴포넌트가 브라우저에 마운트(하이드레이션 완료)된 후에만 실행됨
    useEffect(() => {
        setIsMounted(true);
    }, []);

    // 3. 서버 렌더링 시점에는 UI를 숨기거나 스켈레톤을 보여주고,
    // 클라이언트 렌더링이 확정된 시점에만 실제 데이터를 그림
    if (!isMounted) {
        return <div>로딩중...</div>;
    }

    return <div>로컬스토리지 데이터: {window.localStorage.getItem('theme')}</div>;
}

3단계: 엔터프라이즈 아키텍처 - Dynamic Import의 ssr: false

만약 컴포넌트 전체가 브라우저 전용 라이브러리(예: 차트 라이브러리, 카카오맵 API)에 의존하고 있다면, next/dynamic을 사용하여 아예 서버 렌더링 대상에서 원천적으로 배제해 버리는 것이 가장 깔끔한 아키텍처입니다.


import dynamic from 'next/dynamic';

// SSR 과정에서 이 컴포넌트는 무시되며, 오직 클라이언트 사이드에서만 렌더링됨
const NoSSRChart = dynamic(() => import('../components/HeavyChart'), { 
    ssr: false,
    loading: () => <p>차트 불러오는 중...</p>
});

🙋‍♂️ 자주 묻는 질문 (FAQ)

Q. 랜덤 함수(Math.random)를 썼더니 Hydration 에러가 납니다. 왜 그런가요?
A. 서버에서 렌더링할 때 나온 난수(예: 0.1)와 브라우저가 하이드레이션 할 때 뱉어낸 난수(예: 0.8)가 달라지기 때문입니다. 랜덤 값이나 현재 시간(Date)은 반드시 useEffect 안에서 생성하거나 상태(State)로 관리해야 합니다.

💡 핵심 정리 및 마무리

프레임워크의 성능을 한계까지 끌어올리기 위해서는 '언제(When)'와 '어디서(Where)' 코드가 실행되는지를 입체적으로 통제할 수 있어야 합니다. 서버 렌더링과 클라이언트 렌더링의 경계를 명확히 구분하는 시공간 분리 아키텍처를 통해 견고한 Next.js 애플리케이션을 완성하시기 바랍니다.

댓글

이 블로그의 인기 게시물

Zapier & Make.com 자동화의 덫: 무한 루프(Infinite Loop) 에러 완벽 방어 아키텍처

엑셀 보고서의 종말: 구글 스프레드시트와 루커 스튜디오(Looker Studio)로 실시간 대시보드 구축하기

Docker OOMKilled (Exit Code 137) 에러의 진실: 컨테이너 메모 누수 방어 및 리소스 최적화 아키텍처