React 18 Concurrent Features 실전 적용기

문제 상황

대시보드의 필터링 기능에서 사용자가 입력할 때마다 수천 개의 데이터를 렌더링하면서 입력이 버벅이는 문제가 있었다. debounce를 적용했지만 UX가 어색했고, 더 나은 방법을 찾다가 React 18의 Concurrent Features를 적용해보기로 했다.

useTransition 적용

먼저 검색 입력과 결과 렌더링의 우선순위를 분리했다.

function SearchDashboard() {
  const [query, setQuery] = useState('');
  const [isPending, startTransition] = useTransition();

  const handleChange = (e) => {
    const value = e.target.value;
    setQuery(value); // 즉시 반영
    
    startTransition(() => {
      setFilteredData(expensiveFilter(data, value)); // 낮은 우선순위
    });
  };

  return (
    <>
      <input value={query} onChange={handleChange} />
      {isPending && <Spinner />}
      <ResultList data={filteredData} />
    </>
  );
}

입력은 즉시 반영되고, 무거운 필터링 작업은 중단 가능한 작업으로 처리되어 입력 끊김이 사라졌다.

useDeferredValue와의 비교

코드를 더 간결하게 만들고 싶어서 useDeferredValue도 테스트했다.

function SearchDashboard() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  const filteredData = useMemo(
    () => expensiveFilter(data, deferredQuery),
    [deferredQuery]
  );

  return (
    <>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <ResultList data={filteredData} />
    </>
  );
}

두 방식 모두 동작했지만, isPending 상태를 직접 제어할 수 있는 useTransition이 로딩 UI 표시에 더 유연했다.

주의사항

  • Concurrent Rendering은 컴포넌트가 여러 번 렌더링될 수 있다. side effect는 반드시 useEffect 안에서 처리해야 한다.
  • Suspense와 함께 사용할 때 fallback 전략을 명확히 해야 한다.
  • 모든 무거운 작업에 적용할 필요는 없다. 사용자 입력과 연관된 UI 업데이트에 집중했다.

결과

체감 성능이 확실히 개선되었다. 특히 입력 반응성이 좋아져 사용자 피드백도 긍정적이었다. debounce와 달리 입력 즉시 시각적 피드백을 줄 수 있다는 점이 가장 큰 장점이었다.