React에는 useState, useEffect 외에도 특수한 상황에서 빛을 발하는 훅들이 있습니다. 이런 훅들은 언제 필요하고, 어떤 문제를 해결할까요?

useId — SSR 안전한 고유 ID

개념 정의

useId서버와 클라이언트에서 동일한 고유 ID를 생성 하는 훅입니다. 접근성(accessibility) 속성이나 폼 요소의 id-htmlFor 연결에 사용합니다.

왜 필요한가

JSX
// ❌ Math.random() — SSR 시 서버/클라이언트 불일치
function Input({ label }) {
  const id = `input-${Math.random()}`; // 매번 다른 값
  return (
    <>
      <label htmlFor={id}>{label}</label>
      <input id={id} />
    </>
  );
}

// ❌ 전역 카운터 — 렌더 순서에 의존
let counter = 0;
function Input({ label }) {
  const id = `input-${counter++}`; // 서버/클라이언트 순서가 다를 수 있음
}

// ✅ useId — SSR 안전
function Input({ label }) {
  const id = useId();
  return (
    <>
      <label htmlFor={id}>{label}</label>
      <input id={id} />
    </>
  );
}

여러 요소에 활용

JSX
function PasswordField() {
  const id = useId();

  return (
    <div>
      <label htmlFor={`${id}-password`}>비밀번호</label>
      <input id={`${id}-password`} type="password" aria-describedby={`${id}-hint`} />
      <p id={`${id}-hint`}>8자 이상, 특수문자 포함</p>
    </div>
  );
}

주의: useId로 생성된 값을 **리스트의 key로 사용하면 안 됩니다 **. key는 데이터에서 파생되어야 합니다.

useSyncExternalStore — 외부 스토어 구독

개념 정의

useSyncExternalStoreReact 외부의 데이터 소스를 안전하게 구독 하는 훅입니다. Concurrent Mode에서 발생할 수 있는 tearing(찢어짐) 문제를 방지합니다.

Tearing 문제

PLAINTEXT
Concurrent Mode에서 렌더링 도중 외부 스토어가 변경되면:
  컴포넌트 A: 이전 값(10)을 읽음
  → 렌더링 중단 (긴급 업데이트 처리)
  → 외부 스토어 값 변경 (10 → 20)
  컴포넌트 B: 새 값(20)을 읽음
  → 같은 렌더 트리에서 다른 값이 표시됨 (tearing)

사용법

JSX
import { useSyncExternalStore } from 'react';

// 외부 스토어 예시: 브라우저 온라인 상태
function useOnlineStatus() {
  return useSyncExternalStore(
    subscribe,    // 구독 함수
    getSnapshot,  // 현재 값을 읽는 함수
    getServerSnapshot // SSR용 (선택)
  );
}

function subscribe(callback) {
  window.addEventListener('online', callback);
  window.addEventListener('offline', callback);
  return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
  };
}

function getSnapshot() {
  return navigator.onLine;
}

function getServerSnapshot() {
  return true; // SSR에서는 항상 온라인으로 가정
}

// 사용
function StatusBar() {
  const isOnline = useOnlineStatus();
  return <span>{isOnline ? '🟢 온라인' : '🔴 오프라인'}</span>;
}

외부 상태 관리 라이브러리와의 관계

Zustand, Redux 등의 라이브러리가 내부적으로 useSyncExternalStore를 사용합니다.

JSX
// Zustand가 내부적으로 하는 일 (개념적)
function useStore(selector) {
  return useSyncExternalStore(
    store.subscribe,
    () => selector(store.getState()),
    () => selector(store.getState())
  );
}

useDeferredValue — 긴급하지 않은 업데이트 지연

개념 정의

useDeferredValue는 UI의 일부 업데이트를 ** 지연시켜** 긴급한 업데이트(타이핑 등)가 먼저 처리되도록 합니다.

사용법

JSX
function SearchPage() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);

  // query는 즉시 업데이트 (입력 필드 반응)
  // deferredQuery는 여유가 생기면 업데이트 (검색 결과 반영)
  const isStale = query !== deferredQuery;

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
      />
      <div style={{ opacity: isStale ? 0.5 : 1 }}>
        <SearchResults query={deferredQuery} />
      </div>
    </div>
  );
}

// SearchResults가 무거운 렌더링을 수행하더라도 입력이 버벅이지 않음
const SearchResults = memo(function SearchResults({ query }) {
  const results = computeExpensiveResults(query);
  return <ul>{results.map(r => <li key={r.id}>{r.text}</li>)}</ul>;
});

useDeferredValue vs debounce

특성useDeferredValuedebounce
지연 시간React가 자동 조절고정 시간 (예: 300ms)
동작 방식긴급 업데이트 우선 처리일정 시간 후 실행
취소React가 자동 관리수동 관리 필요
Suspense 통합지원미지원
JSX
// debounce: 항상 300ms 기다림
const debouncedQuery = useDebounce(query, 300);

// useDeferredValue: React가 여유가 있으면 즉시, 바쁘면 지연
const deferredQuery = useDeferredValue(query);

빠른 기기에서는 useDeferredValue가 거의 즉시 업데이트되고, 느린 기기에서는 더 오래 지연됩니다. ** 디바이스 성능에 자동으로 적응 **하는 것이 핵심 장점입니다.

useInsertionEffect — CSS-in-JS를 위한 훅

개념 정의

useInsertionEffectDOM mutation 전에 실행되는 훅입니다. CSS-in-JS 라이브러리가 <style> 태그를 주입하는 데 특화되어 있습니다.

실행 순서

PLAINTEXT
렌더링 → useInsertionEffect → DOM mutation → useLayoutEffect → 브라우저 paint → useEffect

사용법

JSX
// ⚠️ 일반 개발자가 직접 사용할 일은 거의 없음
// CSS-in-JS 라이브러리 개발자를 위한 훅

function useCSS(rule) {
  useInsertionEffect(() => {
    const style = document.createElement('style');
    style.textContent = rule;
    document.head.appendChild(style);

    return () => {
      document.head.removeChild(style);
    };
  });
}

// styled-components, emotion 등이 내부적으로 사용

일반적인 애플리케이션 코드에서는 **useInsertionEffect를 직접 사용할 일이 거의 없습니다 **. DOM 측정이나 레이아웃 관련 작업에는 useLayoutEffect를 사용합니다.

useTransition과의 비교

useDeferredValue와 자주 비교되는 useTransition도 함께 알아두면 좋습니다.

JSX
// useTransition: state 업데이트를 낮은 우선순위로 표시
function TabContainer() {
  const [isPending, startTransition] = useTransition();
  const [tab, setTab] = useState('home');

  function selectTab(nextTab) {
    startTransition(() => {
      setTab(nextTab); // 이 업데이트는 낮은 우선순위
    });
  }

  return (
    <div>
      <TabButton onClick={() => selectTab('home')}>홈</TabButton>
      <TabButton onClick={() => selectTab('posts')}>글</TabButton>
      {isPending && <Spinner />}
      <TabPanel tab={tab} />
    </div>
  );
}
특성useTransitionuseDeferredValue
대상state 업데이트 자체를 지연값의 반영을 지연
제어업데이트를 시작하는 쪽업데이트를 소비하는 쪽
pending 상태isPending 제공직접 비교 (value !== deferred)

주의할 점

useId로 생성한 ID를 key로 사용하면 안 됨

useId는 컴포넌트 인스턴스당 하나의 ID를 생성합니다. 리스트 렌더링에서 key로 사용하면 모든 항목이 같은 ID를 갖게 됩니다. key는 데이터의 고유 식별자를 사용해야 합니다.

useSyncExternalStore 없이 외부 스토어를 구독하면 tearing 발생

Concurrent Mode에서는 렌더링 도중 외부 값이 변경될 수 있습니다. useState + useEffect로 외부 스토어를 구독하면 화면의 일부가 다른 버전의 데이터를 표시하는 tearing 현상이 발생합니다.

useDeferredValue를 debounce 대용으로 오해

debounce는 고정 시간을 기다리지만, useDeferredValue는 React의 스케줄러가 여유 시간에 업데이트합니다. 빠른 기기에서는 거의 즉시, 느린 기기에서는 더 오래 지연됩니다. 네트워크 요청 최적화에는 debounce가, 렌더링 최적화에는 useDeferredValue가 적합합니다.

정리

항목설명
useIdSSR 안전한 고유 ID — 접근성 속성(aria-*) 연결에 필수
useSyncExternalStore외부 스토어를 Concurrent Mode에서 안전하게 구독
useDeferredValue긴급하지 않은 업데이트를 지연하여 UI 반응성 유지
useInsertionEffectCSS-in-JS 라이브러리 전용 — 일반 개발에서 사용할 일 거의 없음
useTransitionstate 업데이트 자체를 낮은 우선순위로 처리

이 훅들은 자주 쓰이지는 않지만, 각각이 해결하는 문제를 알면 적절한 상황에서 정확히 사용할 수 있습니다.

댓글 로딩 중...