React 18 useTransition으로 검색 UI 개선하기

문제 상황

대시보드의 데이터 테이블에 실시간 검색 기능이 있었는데, 수천 개의 행을 필터링하다 보니 타이핑할 때마다 입력이 버벅이는 문제가 있었다. debounce를 적용해봤지만 지연이 느껴져서 UX가 좋지 않았다.

React 18의 useTransition이 이런 상황에 적합하다는 글을 보고 적용해봤다.

기존 코드

const [searchTerm, setSearchTerm] = useState('');

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

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

입력할 때마다 전체 리스트가 리렌더링되면서 블로킹이 발생했다.

useTransition 적용

const [searchTerm, setSearchTerm] = useState('');
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();

const handleSearch = (e) => {
  setSearchTerm(e.target.value);
  startTransition(() => {
    setQuery(e.target.value);
  });
};

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

return (
  <>
    <input 
      value={searchTerm}
      onChange={handleSearch}
    />
    {isPending && <span>검색 중...</span>}
  </>
);

입력 상태(searchTerm)와 필터링 상태(query)를 분리하고, 필터링 업데이트를 transition으로 감쌌다.

결과

  • 입력 필드는 즉시 반응하고, 리스트 업데이트는 낮은 우선순위로 처리됨
  • debounce 없이도 자연스러운 UX
  • isPending으로 로딩 상태 표시 가능

Concurrent Features의 실용성을 체감했다. 다만 모든 상태 업데이트에 적용할 필요는 없고, 무거운 렌더링이 있는 곳에 선택적으로 사용하는 게 좋았다.