C
React/최적화/Lesson 18

useTransition / useDeferredValue — 무거운 업데이트를 백그라운드로

30분·theory
이 챕터
1/2
TypeScript

useTransition / useDeferredValue — 무거운 업데이트를 백그라운드로

💡 왜 배워야 할까요? — 인풋이 끊기는 이유

🎯 검색창에 빠르게 타이핑하면 인풋이 끊기는 경험 — 입력 값 변경이 무거운 필터링·렌더링과 같은 우선순위로 처리되어서.
💼 useTransition 은 '이 state 업데이트는 급하지 않다 — 인풋이 더 중요하다' 를 React 에게 알려줍니다.
React 는 인풋 업데이트를 먼저 보여주고, 무거운 업데이트는 백그라운드로 양보 → 인풋이 매끄러워짐.
🔗 useDeferredValue 는 비슷한 효과를 '값' 단위로. 무거운 자식 컴포넌트에 deferred 된 값만 흘려보내는 패턴.
📈 예전엔 디바운스·throttle 로 처리하던 UX 문제를 React 가 시간 슬라이싱으로 해결.
🏢 실무에서는
검색 자동완성, 큰 리스트 필터링, 차트 데이터 변경, 무거운 마크다운 프리뷰 — 모두 useTransition 의 영역. 사용자는 '인풋이 부드럽다' 만 느끼고 무거운 부분은 알아서 백그라운드에서 그려짐.

useTransition · useDeferredValue · startTransition

1. useTransition — '이 setState 는 양보 가능' 표시

tsx
const [isPending, startTransition] = useTransition();

startTransition(() => {
  setResults(filter(query)); // '양보 가능'
});
  • startTransition(fn) 안의 setState 는 양보 가능으로 표시.
  • React 는 더 급한 업데이트(인풋·클릭)가 있으면 그것을 먼저 처리.
  • isPending 으로 양보된 업데이트가 진행 중인지 확인 → 스피너 표시.

2. useDeferredValue — 값을 '뒤로 미룬 복사본' 으로

tsx
const deferredQuery = useDeferredValue(query); // 한 박자 뒤따라옴

<HeavyResults query={deferredQuery} />
  • query 는 즉시 업데이트(인풋 매끄러움).
  • deferredQuery 는 React 가 한가할 때 따라옴.
  • HeavyResults 가 deferredQuery 로 무거운 작업을 해도 인풋은 영향 받지 않음.

3. 언제 무엇을?

상황권장
내가 setState 호출자 (직접 제어)useTransition
받은 값을 자식에 흘리는데, 그 자식이 무겁다useDeferredValue
양보 중 상태(isPending) 필요useTransition
값의 '최신 + 한 박자 뒤' 동시 표현useDeferredValue

4. startTransition — hook 없이도 호출 가능

ts
import { startTransition } from 'react';

button.addEventListener('click', () => {
  startTransition(() => setData(newData));
});
// isPending 추적은 불가, 단순히 '양보 가능' 표시만

5. 주의

  • 작업이 진짜 무거우면 (1초 이상) useTransition 으로도 부족. Web Worker 등이 필요.
  • fetch 자체는 양보 불가 — fetch 응답을 처리하는 setState 만 양보 가능.
  • '스케줄링 힌트' 일 뿐 — React 가 정말 양보할 수 있을 때만 양보.
💻 🅰️ 그냥 setState — 인풋이 끊김
// ❌ 양보 없이 — 입력마다 5만개 필터링
import { useState } from 'react';

const HUGE = Array.from({ length: 50000 }, (_, i) => `item-${i}`);

export function Search() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState<string[]>([]);

  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setQuery(e.target.value);
    const filtered = HUGE.filter(d => d.includes(e.target.value));
    setResults(filtered);
    // 빠르게 타이핑하면 인풋이 한 글자 뒤처짐 (jank)
  };

  return (
    <div>
      <input value={query} onChange={onChange} />
      <ul>{results.slice(0, 100).map(r => <li key={r}>{r}</li>)}</ul>
    </div>
  );
}
💻 🅱️ useTransition / useDeferredValue — 인풋 매끄럽게
// ✅ useTransition — 필터링을 양보
import { useState, useTransition, useDeferredValue } from 'react';

const HUGE = Array.from({ length: 50000 }, (_, i) => `item-${i}`);

export function Search() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState<string[]>(HUGE);
  const [isPending, startTransition] = useTransition();

  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setQuery(e.target.value); // 긴급

    startTransition(() => {
      // 비긴급 — 양보 가능
      const filtered = HUGE.filter(d => d.includes(e.target.value));
      setResults(filtered);
    });
  };

  return (
    <div>
      <input value={query} onChange={onChange} />
      {isPending && <p>업데이트 중...</p>}
      <ul>{results.slice(0, 100).map(r => <li key={r}>{r}</li>)}</ul>
    </div>
  );
}

// 또는 useDeferredValue — 자식이 무거울 때
export function SearchAlt() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);

  return (
    <div>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <HeavyList query={deferredQuery} />
    </div>
  );
}

function HeavyList({ query }: { query: string }) {
  // query 가 뒤처져 있어 인풋엔 영향 X
  const filtered = HUGE.filter(d => d.includes(query));
  return <ul>{filtered.slice(0, 100).map(r => <li key={r}>{r}</li>)}</ul>;
}

💡 💡 useTransition / useDeferredValue 실전 5

1. 인풋은 즉시, 결과는 양보 — 가장 흔한 패턴

tsx
setQuery(input);                  // 긴급
startTransition(() => setResults(filter(input))); // 양보

2. isPending 으로 진행 상태 표시

tsx
{isPending && <Spinner />}

3. fetch 응답 처리는 양보 가능, fetch 자체는 양보 불가

tsx
const data = await fetch(url).then(r => r.json());
startTransition(() => setData(data));

4. useDeferredValue = '받은 값' 단위, useTransition = '내가 호출' 단위
부모에게서 받은 props 가 자주 바뀌고 그걸 받은 자식이 무겁다 → useDeferredValue.

5. 디바운스의 부분 대체이지 완벽 대체는 아님
useTransition 은 시간 슬라이싱, 디바운스는 호출 자체를 줄임. 보통 둘 중 하나, 상황 따라 선택.

⚡ 직접 실행해보기 — startTransition 흐름

긴급 vs 양보 가능한 업데이트의 우선순위를 시뮬레이션합니다.
✏️ JS 코드
📟 콘솔 출력
▶ 실행 버튼을 눌러보세요
⚠️ 브라우저 샌드박스에서 실행 — console.log()만 지원, alert/fetch 불가

확인 퀴즈

검색창 입력 중 자동완성 렌더링이 무거워서 인풋이 끊깁니다. 가장 적절한 도구는?
💡 useTransition (직접 setState 양보) 또는 useDeferredValue (받은 값을 한 박자 뒤로) 가 React 18+ 의 정답입니다. 인풋 업데이트는 긴급으로 즉시 반영, 무거운 자동완성 렌더링은 양보 가능으로 표시되어 React 가 시간 슬라이싱으로 부드럽게 처리합니다. useMemo 는 캐싱, setTimeout 은 옛 디바운스 패턴.
useTransition / useDeferredValue - React