본문 바로가기

React

React useTransition vs useDeferredValue 차이와 선택 기준 정리

한 줄 요약: React useTransition은 상태를 바꾸는 코드(setState)를 낮은 우선순위로 표시하고, useDeferredValue는 이미 받은 값의 업데이트를 뒤로 미룹니다. 내가 setState를 직접 호출할 수 있으면 useTransition, props나 외부 라이브러리에서 값만 받는다면 useDeferredValue를 선택하세요. 두 훅 모두 React 18의 동시성 렌더링(Concurrent Rendering)을 기반으로 하며, 무거운 렌더링 때문에 입력이 버벅이는 문제를 줄여 줍니다.

React useTransition과 useDeferredValue는 무엇인가요?

useTransition은 상태 업데이트를 "긴급하지 않은 전환(transition)"으로 표시해 UI 차단 없이 렌더링하게 하는 훅입니다. useDeferredValue는 어떤 값의 "지연된 복사본"을 반환하는 훅으로, 긴급한 렌더링이 끝난 뒤 새 값으로 다시 렌더링합니다. 둘 다 사용자 입력처럼 급한 업데이트가 무거운 목록 렌더링에 막히지 않도록 우선순위를 나누는 도구입니다.

공식 문서는 useTransition과 useDeferredValue 레퍼런스에서 확인할 수 있습니다.

두 훅의 차이를 한눈에 비교하면?

가장 큰 차이는 "무엇을 지연시키는가"입니다. useTransition은 상태 변경 호출을, useDeferredValue는 값 자체를 지연시킵니다.

구분useTransitionuseDeferredValue
지연 대상setState 호출(업데이트)값(value)
반환값[isPending, startTransition]지연된 값 1개
대기 상태 표시isPending 제공원본 값 !== 지연 값 비교로 직접 판단
적합한 상황setState를 직접 호출할 수 있을 때props, 외부 훅 값만 받을 때

useTransition은 언제 사용하나요?

탭 전환, 페이지 이동, 필터 변경처럼 무거운 상태 업데이트를 startTransition으로 감싸야 할 때 사용합니다. 전환 중에도 이전 화면이 유지되고 새 화면 렌더링은 중단 가능(interruptible)하게 처리되므로, 사용자가 다른 탭을 눌러도 UI가 멈추지 않습니다.

import { useState, useTransition } from 'react';

function SlowItem({ text }) {
  const start = performance.now();
  while (performance.now() - start < 1) {
    // 의도적으로 느린 렌더링 시뮬레이션
  }
  return <li>{text}</li>;
}

export default function TabDemo() {
  const [tab, setTab] = useState('home');
  const [isPending, startTransition] = useTransition();

  function selectTab(next) {
    startTransition(() => {
      setTab(next);
    });
  }

  return (
    <div>
      <button onClick={() => selectTab('home')}>홈</button>
      <button onClick={() => selectTab('posts')}>게시글(느림)</button>
      {isPending && <span> 불러오는 중...</span>}
      {tab === 'home' ? (
        <p>홈 화면입니다.</p>
      ) : (
        <ul>
          {Array.from({ length: 300 }, (_, i) => (
            <SlowItem key={i} text={'게시글 ' + i} />
          ))}
        </ul>
      )}
    </div>
  );
}

동작 설명: "게시글" 탭을 누르면 isPending이 true가 되어 안내 문구가 먼저 보이고, 렌더링이 끝나면 목록이 표시됩니다. 렌더링 도중 "홈"을 누르면 진행 중이던 느린 렌더링은 버려지고 바로 홈으로 돌아갑니다.

useDeferredValue는 언제 사용하나요?

검색어 같은 값을 받아 무거운 목록을 필터링할 때, 또는 상위 컴포넌트가 내려준 props를 직접 제어할 수 없을 때 사용합니다. 입력창은 즉시 갱신하고, 무거운 결과 목록은 지연된 값으로 렌더링해서 타이핑 지연을 막습니다.

import { memo, useDeferredValue, useState } from 'react';

const ResultList = memo(function ResultList({ query }) {
  const items = [];
  for (let i = 0; i < 2000; i++) {
    if (('항목 ' + i).includes(query)) {
      items.push(<li key={i}>항목 {i}</li>);
    }
  }
  return <ul>{items}</ul>;
});

export default function SearchDemo() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery;

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="검색어 입력"
      />
      <div style={{ opacity: isStale ? 0.5 : 1 }}>
        <ResultList query={deferredQuery} />
      </div>
    </div>
  );
}

동작 설명: 타이핑하는 동안 query는 즉시 바뀌지만 deferredQuery는 이전 값을 유지하다가 여유가 생기면 따라옵니다. ResultList를 memo로 감쌌기 때문에 deferredQuery가 그대로인 동안에는 무거운 렌더링이 다시 일어나지 않습니다.

어떤 훅을 선택해야 하나요?

  1. setState를 내가 호출하는 코드라면 useTransition을 우선 고려합니다. isPending으로 로딩 표시를 바로 만들 수 있습니다.
  2. 값만 전달받는 경우(props, 서드파티 훅 반환값)에는 useDeferredValue가 적합합니다.
  3. 무거운 자식 컴포넌트에는 memo를 함께 사용해야 useDeferredValue의 효과가 큽니다.
  4. React 19에서는 useDeferredValue(value, initialValue)처럼 초기값을 줄 수 있어 첫 렌더링 때 이전 값을 먼저 보여 줄 수 있습니다.

사용할 때 주의할 점은?

  • 제어 컴포넌트(input)의 value를 바꾸는 setState는 startTransition으로 감싸면 안 됩니다. 입력은 반드시 즉시 반영되어야 하므로, 입력용 상태와 무거운 목록용 상태를 분리하세요.
  • startTransition에 전달한 함수 안에서 호출한 setState만 전환으로 처리됩니다. 공식 문서 기준으로 다른 비동기 콜백에서 호출한 업데이트는 별도 처리가 필요합니다.
  • 두 훅은 디바운스(debounce)나 쓰로틀(throttle)과 다릅니다. 고정 지연 시간이 없고, 기기 성능에 맞춰 React가 스케줄링합니다. 네트워크 요청 횟수를 줄이는 효과는 없으므로 요청 최적화에는 디바운스를 따로 사용하세요.
  • 렌더링 자체가 가볍다면 굳이 쓰지 않아도 됩니다. 먼저 React DevTools Profiler로 병목을 확인하세요.

자주 묻는 질문(FAQ)

Q. useTransition과 useDeferredValue를 같이 써도 되나요?
A. 가능하지만 같은 업데이트에 중복 적용할 필요는 없습니다. 보통 한 가지 방식만 선택해도 충분합니다.

Q. useDeferredValue는 디바운스를 대체하나요?
A. 아닙니다. 렌더링 우선순위를 조정할 뿐 요청 횟수를 줄이지 않습니다. API 호출 최적화에는 디바운스가 여전히 필요합니다.

Q. isPending은 언제 true가 되나요?
A. startTransition 안의 업데이트가 아직 화면에 반영되지 않은 동안 true입니다.

Q. React 17에서도 사용할 수 있나요?
A. 아닙니다. 두 훅은 React 18부터 제공되는 동시성 기능입니다.

정리

  • useTransition은 setState 호출을 낮은 우선순위로 표시하고 isPending을 제공합니다.
  • useDeferredValue는 값의 지연된 복사본을 반환하며, props처럼 setState를 못 쓰는 상황에 적합합니다.
  • useDeferredValue는 memo와 함께 써야 무거운 자식 렌더링을 실제로 건너뜁니다.
  • 입력용 상태는 즉시 갱신하고, 무거운 UI만 전환 또는 지연 처리하세요.

관련 글