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와 달리 입력 즉시 시각적 피드백을 줄 수 있다는 점이 가장 큰 장점이었다.