React 18 Concurrent Rendering과 useTransition 실전 적용기

문제 상황

대시보드에서 1만 건 이상의 데이터를 클라이언트 사이드에서 필터링하는 기능을 구현했다. 사용자가 검색어를 입력할 때마다 전체 리스트를 필터링하다 보니 입력이 버벅이는 현상이 발생했다.

기존에는 debounce로 해결했지만, 이번에는 React 18의 useTransition을 시도해봤다.

기존 코드

const [searchTerm, setSearchTerm] = useState('');
const filteredData = useMemo(() => {
  return data.filter(item => 
    item.name.toLowerCase().includes(searchTerm.toLowerCase())
  );
}, [data, searchTerm]);

<input 
  value={searchTerm}
  onChange={(e) => setSearchTerm(e.target.value)}
/>

useTransition 적용

const [searchTerm, setSearchTerm] = useState('');
const [deferredTerm, setDeferredTerm] = useState('');
const [isPending, startTransition] = useTransition();

const handleSearch = (e) => {
  const value = e.target.value;
  setSearchTerm(value); // 즉시 반영
  
  startTransition(() => {
    setDeferredTerm(value); // 낮은 우선순위
  });
};

const filteredData = useMemo(() => {
  return data.filter(item => 
    item.name.toLowerCase().includes(deferredTerm.toLowerCase())
  );
}, [data, deferredTerm]);

<input 
  value={searchTerm}
  onChange={handleSearch}
/>
{isPending && <Spinner />}

결과

입력 필드는 즉시 업데이트되고, 무거운 필터링 작업은 뒤로 밀려나면서 타이핑 지연이 사라졌다. isPending으로 로딩 상태도 표시할 수 있어 UX 측면에서도 개선되었다.

debounce와 비교했을 때, useTransition은 React의 스케줄링을 활용하기 때문에 더 자연스러운 인터랙션을 제공한다. 특히 연속된 입력에서 차이가 체감된다.

주의사항

  • startTransition 내부는 동기 함수여야 한다
  • 외부 상태 관리 라이브러리(Redux 등)와는 잘 맞지 않을 수 있다
  • 모든 느린 렌더링에 만능은 아니다. 근본적인 성능 문제는 따로 해결해야 한다