Progressive(점진적) Hydration이 뭔데

KoSH·2025년 4월 14일

patterns dev의 렌더링 패턴을 읽던 중, Progressive Hydration은 한번 구현해보면 좋을 것 같다는 생각이 들어 포스팅을 작성해보려 한다.

또한, 앞으로 SSR을 활용한 여러 기법들을 구현해보며 Next.js 와 좀더 친해지는 시간을 가져보겠다.


Hydration??

나는 Hydration 을 이해할 때 이렇게 정의하는게 가장 이해가 잘됐다.

Hydration은 그대로 번역하면 "수분 공급" 이라는 뜻이다. 그런데 왜 렌더링 기법에서 수분 공급이란 말이 나올까?
Hydration은 말그대로 정적인 HTML 문서에 수분을 공급하는 과정을 의미한다.
여기서 말하는 수분이란, React와 연결을 통해 정적인 HTML을 인터랙티브하게 만드는 과정을 의미한다.

첫째로, 서버 사이드 렌더링을 통해 HTML을 생성해서 브라우저로 전달하면, 클라이언트에서는 React가 정적인 HTML을 hydrate 과정을 거친다.
hydrate 과정을 거친 웹은 이벤트 핸들러 등을 연결하여 인터랙티브한 React 컴포넌트로 만들어진다.

즉 SSR을 통해 받은 정적인 HTML을 React와 연결하여 동적으로 만드는 작업을 뜻한다.



그럼 Progressive Hydration은?

그럼 내가 제목에 적은 Progressive Hydration 은 뭘까?
뭐든지 뭔가 문제가 있기 때문에 발생한다.

기본적인 SSR, Hydration 에서는 HTML이 서버에서 미리 만들어져 사용자에게 전달된다.
클라이언트에서는 모든 컴포넌트에 대해 한 번에 hydrate 과정을 거친다.
이렇게 되면 문제가 발생하는데, JS 번들이 크고 hydrate할 요소가 많다면 렌더링 지연이나, TTI(Time To Interactive) 가 커지게된다.

이를 해결하기 위해 Progressive Hydration 이 등장했다.

즉 페이지 전체를 한번에 hydrate 하지 않고, 우선 순위나 현재 스크롤 등에 따라 점진적으로 Hydration을 진행하는 방식이다.
이를 통해 TTI 를 줄일 수 있다. TTI 는 사용자가 페이지와 인터랙션 할 수 있을 때 까지의 시간이다.

이는 쌩 React 를 사용할 때 , lazy loading을 하는 것과 유사해보인다. lazy loading을 통해 초기 로딩에 모든 페이지를 불러오는 것이 아닌, 해당 페이지가 필요할 시점에 불러와 code spliting 이 가능했는데, SSR 에서 이와 유사한 개념인듯 하다.


이제 구현해보자

먼저 Next.js 프로젝트를 생성한다.

npx create-next-app@latest progressive-hydration-demo
cd progressive-hydration-demo

기본 코드들은 다 지워주고 app/components/ClientComponent.tsx를 만들어서 Hydration 될 클라이언트 컴포넌트들 만들어준다.

'use client';

import { useEffect, useState } from 'react';

export default function ClientComponent() {
const [count, setCount] = useState(0);
const [hydrated, setHydrated] = useState(false);

useEffect(() => {
	console.log('Client Component Hydrate');
	setHydrated(true);
}, []);

return (
	<div>
		<p>클릭 수: {count}</p>
		<div
		className={
		hydrated ? 'text-green-600 font-bold' : 'text-gray-400 italic'
		}>
			{hydrated ? '✅ Hydrated!' : '🕓 Hydrating...'}
		</div>
		<button onClick={() => setCount((prev) => prev + 1)}>클릭</button>
	</div>
	);
}

해당 코드는 hydrate 되는 것을 보여주고자 hydrated 변수를 통해 이모티콘을 통해 가시적으로 나타냈다.


그리고 클라이언트 컴포넌트를 사용하는 곳에서 dynamic을 통해 불러와주면 된다.

import dynamic from 'next/dynamic';

const ClientComponent = dynamic(() => import('../components/ClientComponent'));  

export default function Home() {
return (
	<main>
		<h1>Progressive Hydration Demo</h1>
		{/* Client Component는 나중에 hydration이 일어난다 */}
		<ClientComponent />
	</main>
	);
}

그럼 아래의 이미지와 같이 Hydration이 잘 일어나는 것을 볼 수 있다.


조금 더 확장해보면, IntersectionObserver를 활용해서 사용자가 특정 영역에 도달했을 때 동적으로 컴포넌트를 hydrate 할 수도 있겠다.



화면 감지해서 Progressive Hydration 구현

위의 프로젝트에 LazyHydrate.tsx를 구현한다.

'use client';

import { useEffect, useRef, useState } from 'react';
import dynamic from 'next/dynamic';

const ClientComponent = dynamic(() => import('./ClientComponent'), {
	ssr: false,
});

export default function LazyHydrate() {
const ref = useRef<HTMLDivElement>(null);
const [isVisible, setIsVisible] = useState(false);

useEffect(() => {
	const observer = new IntersectionObserver(
		([entry]) => {
          if (entry.isIntersecting) {
            setIsVisible(true);
            observer.disconnect();
            }
          },
          {
          	threshold: 0.1,
          },
	);
	
	if (ref.current) {
		observer.observe(ref.current);
	}
  
	return () => observer.disconnect();
}, []);

  

  return (
    <div ref={ref} style={{ minHeight: '300px', marginTop: '200px' }}>
      {isVisible ? (
        <ClientComponent />
      ) : (
        <div style={{ textAlign: 'center', padding: '20px' }}>
          👉 아래 컴포넌트가 곧 로드됩니다...
        </div>
      )}
    </div>
  );
}

IntersectionObserver 객체를 통해 해당 컴포넌트의 위치를 감지하여 isVisible 변수를 통해 로드한다.

즉, 클라이언트가 스크롤을 내려 화면에 보이게 되면 그 때 Hydration 하게 된다.

import LazyHydrate from '@/components/LazyHydrate';

export default function Home() {

  return (
    <main>
      <h1>Progressive Hydration + Lazy Load Demo</h1>
      <p>스크롤해서 아래로 내리세요</p>
      
      <div style={{ height: '1000px' }}></div>
      
      <LazyHydrate />
    </main>
  );
}


실제로 스크롤을 내려 화면상에 등장했을 때, 네트워크 탭에서 js 번들을 가져오는 것을 확인할 수 있다.


pattern.dev 에서는 점진적 Hydration의 장단점을 다음과 같이 보고 있다.

  1. 코드 스플리팅: 컴포넌트에 대해 지연 로딩 되어야 하기 떄문에 Progressive Hydration의 필수 기능
  2. 자주 사용되지 않는 부분 지연 로딩 가능: 위의 예시처럼 현재 화면에서 보이지 않는 것은 가져오지 않고, 스크롤을 통해 등장했을 때 가져오는 것을 통해 웹 성능을 향상 시킬 수 있다.
  3. 번들 사이즈를 감소시킨다: 페이지 로드할 떄 실행되는 코드량이 줄어 FCP와 TTI 간 시간차가 줄어든다.

하지만, 페이지 로드와 함께 모든 요소들이 인터랙션 해야하는 동적 앱에서는 적합하지 않다고 권고하고 있다.

profile
성장형 괴물

0개의 댓글