반응형

Next.js처럼 서버 렌더링을 사용하는 React 앱을 다루다 보면 hydration mismatch 경고를 한 번쯤 만나게 됩니다. 단순한 경고처럼 보이지만, 실제로는 서버가 만든 HTML과 브라우저에서 React가 처음 계산한 화면이 서로 맞지 않는다는 신호입니다.

이번 글에서는 hydration이 무엇인지, 왜 서버와 클라이언트의 첫 렌더 결과가 같아야 하는지, 그리고 React 18 이후의 selective hydration이 어떤 문제를 줄이려는지 정리해보겠습니다.

Hydration이란?

Hydration은 서버가 먼저 만들어 둔 HTML에 React의 이벤트와 상태를 연결하는 과정입니다.

서버 렌더링으로 내려온 HTML은 처음에는 정적인 마크업에 가깝습니다. 화면은 보이지만, 아직 버튼 클릭이나 입력 같은 React 상호작용은 완전히 연결되지 않은 상태입니다. 클라이언트에서는 hydrateRoot를 통해 같은 컴포넌트 트리를 다시 맞춰 보면서 기존 DOM을 재사용하고, 그 위에 이벤트와 상태를 붙입니다.

즉 hydration은 HTML을 다시 그리는 과정이라기보다 이미 있는 HTML을 React 앱으로 깨우는 과정에 가깝습니다.

왜 첫 렌더 결과가 같아야 할까?

Hydration이 안정적으로 동작하려면 서버가 만든 HTML과 클라이언트의 첫 렌더 결과가 같아야 합니다. React는 기존 DOM을 기준으로 컴포넌트 트리를 맞춰 보는데, 양쪽 결과가 다르면 같은 노드라고 안전하게 판단하기 어렵습니다.

대표적인 문제가 new Date(), Math.random(), window.innerWidth, localStorage 같은 값입니다.

예를 들어 new Date()는 서버에서 렌더링된 시점과 브라우저에서 렌더링된 시점이 다를 수 있습니다. window.innerWidth는 애초에 서버 환경에는 존재하지 않습니다. 이런 값을 렌더 중에 바로 사용하면 서버와 클라이언트의 첫 화면이 달라질 가능성이 큽니다.

안전한 처리 방식

브라우저에서만 알 수 있는 값은 첫 렌더에서 바로 쓰기보다, 서버와 클라이언트가 공유할 수 있는 초기 상태를 먼저 보여준 뒤 마운트 이후에 갱신하는 편이 안전합니다.

'use client';

import { useEffect, useState } from 'react';

function ViewportWidth() {
  const [width, setWidth] = useState<number | null>(null);

  useEffect(() => {
    setWidth(window.innerWidth);
  }, []);

  if (width === null) {
    return <p>화면 폭을 확인하는 중...</p>;
  }

  return <p>현재 화면 폭: {width}px</p>;
}

export default function Page() {
  return (
    <main>
      <h1>Dashboard</h1>
      <ViewportWidth />
    </main>
  );
}

이 예제에서는 첫 렌더 때 window.innerWidth를 직접 읽지 않습니다. 대신 width를 null로 두고, 브라우저에서 컴포넌트가 마운트된 뒤 useEffect 안에서 실제 화면 폭을 가져옵니다.

이렇게 하면 서버와 클라이언트가 처음에는 같은 UI를 렌더링하고, 브라우저에서만 필요한 값은 이후에 안전하게 채울 수 있습니다.

Selective Hydration이란?

React 18 이후에는 hydration을 트리 전체에 한 번에 적용하는 방식에서 더 세밀한 방향으로 발전했습니다. 이것이 selective hydration입니다.

Selective hydration은 Suspense 경계를 기준으로, 더 중요한 영역부터 우선적으로 활성화할 수 있게 해줍니다. 큰 화면 전체가 준비될 때까지 모든 상호작용이 멈춰 있는 것이 아니라, 버튼이나 입력창처럼 사용자가 먼저 만질 가능성이 높은 영역을 더 빨리 살릴 수 있습니다.

여기서 Suspense 경계는 단순한 로딩 UI 이상의 의미를 가집니다. React가 큰 트리를 여러 단위로 나누고, 준비된 부분부터 순서대로 hydrate할 수 있게 해주는 기준점이 됩니다.

실무에서 중요한 판단

Hydration 문제를 줄이려면 먼저 “이 값이 첫 화면에 꼭 필요한가?”를 물어봐야 합니다.

날짜, 랜덤 값, 브라우저 크기, 로컬 스토리지 값처럼 실행 환경에 따라 달라지는 값은 첫 렌더에 바로 넣지 않는 것이 좋습니다. 꼭 필요하다면 서버와 클라이언트가 같은 초기값을 공유하도록 만들거나, 마운트 이후에 클라이언트에서만 갱신하는 구조로 분리해야 합니다.

또한 Suspense 경계를 설계할 때는 단순히 코드를 나누는 관점이 아니라, 사용자가 먼저 상호작용해야 하는 영역이 무엇인지 함께 생각해야 합니다. 경계 설계는 곧 UX 우선순위 설계가 될 수 있습니다.

마무리

Hydration은 서버에서 받은 HTML을 React 앱으로 연결하는 과정입니다. 이때 서버와 클라이언트의 첫 렌더 결과가 다르면 hydration mismatch가 발생할 수 있습니다.

Selective hydration은 이 연결 작업을 더 똑똑하게 나누는 전략입니다. Suspense 경계를 기준으로 필요한 영역부터 활성화하면서, 느린 하위 트리 때문에 전체 UI가 굳는 상황을 줄여줍니다.

React와 Next.js에서 안정적인 SSR 경험을 만들고 싶다면, 무엇을 서버에서 결정할지와 무엇을 클라이언트로 미룰지를 먼저 구분하는 습관이 중요합니다.

참고

반응형