useState의 lazy initializer로 이해하는 hydration-safe 코드

SilverAsh·2025년 10월 29일
0

React

목록 보기
1/5

플리커링 현상을 해결하기 위해 해결책을 알아보던 중 hydration-safe 코드라는 개념을 발견했다. 물론 이름 자체가 중요한 건 아니지만 개념에 대해 자세히 익혀둘 필요가 느껴져 제대로 학습해두려고 한다.

Hydration-safe 코드란?

서버 렌더링 결과와 클라이언트에서의 초기 렌더 결과가 완전히 동일한 코드

이게 hydration-safe 코드의 정의다.

React는 SSR로 만들어진 HTML을 클라이언트에서 그대로 유지하려 한다.
하지만 클라이언트가 새로 계산한 값이 다르면, React는

“어라? 서버 HTML이랑 다르네?”
하고 DOM을 재생성한다.

이것이 바로 플리커링 현상의 원인이다.
결국 핵심은 “두 환경에서 다른 결과를 내는 코드”를 피하는 것이다.


hydration-safe하지 않은 코드 예시

1. 브라우저 전용 API 사용

const width = window.innerWidth; // 서버에는 window 없음

2. 랜덤값 사용

<div>{Math.random()}</div>

이런 코드는 서버에서 한 번, 클라이언트에서 또 한 번 실행되며 서로 다른 결과를 만든다.
결과적으로 React는 이건 다른 DOM이라고 판단하고 다시 렌더링한다 → 플리커 발생


해결법과 의문

처음 봤던 해결법은 이거였다.

const [seed] = useState(() => Math.random());

처음엔 이해가 안 됐다.
Math.random()을 여전히 쓰는데, 뭐가 다르다는 거지?
하지만 알고보니 이 코드에는 React의 중요한 동작 원리가 숨어 있었다.


useState의 지연 초기화는 단 한 번만 실행된다

useState는 두 가지 방식으로 값을 초기화할 수 있다.

const [value] = useState((Math.random());        // 즉시 초기화
const [value] = useState(() => (Math.random());  // 지연 초기화 (lazy initializer)

이때 함수를 넘기면 React가 한 번만 실행한다.
즉, SSR 단계에서 생성된 초기값은 하이드레이션 시점에 다시 계산되지 않는다.

하이드레이션은 새로 렌더하는게 아니라 기존 DOM에 연결하는 과정이기 때문.

그래서 useState(() => Math.random())

  • 서버에서 한 번 실행되어 0.1234 같은 값을 만들고,
  • 클라이언트에서는 그 값을 그대로 복원한다.

결과적으로 서버와 클라이언트가 같은 값을 공유하게 되어,
DOM 불일치가 발생하지 않는다.

단 즉시 초기화도 아래와 같은 경우에는 완전히 안전함

const [count] = useState(123); // 고정값, 

로그 확인해보기

실제로 콘솔을 찍어보면 한눈에 보인다.

"use client";
import { useState } from "react";

export default function Demo() {
  console.log("[render]");
  const [seed] = useState(() => {
    console.log("[init] useState initializer");
    return Math.random();
  });
  return <div>{seed}</div>;
}
  • 서버 콘솔에는 init이 한 번 찍힌다.
  • 클라이언트 하이드레이션에서는 init이 찍히지 않는다.
    (개발 모드 StrictMode에서는 의도적으로 2번 찍히지만, 실제 프로덕션에서는 한 번만)

결론

아래와 같이 정리할 수 있다.

Hydration-safe 코드는 서버와 클라이언트가 같은 결과를 공유할 수 있도록 설계된 코드
useState의 lazy initializer는 초기값을 단 한 번만 계산하기 때문에, 하이드레이션에서 안전하다.

profile
Frontend Developer

0개의 댓글