React 앱에서 사용자의 입력을 실시간으로 반영하면서 동시에 무거운 계산이나 대량의 데이터 필터링을 수행해야 하는 경우가 있습니다. 예를 들어 검색창에 타이핑할 때마다 수천 개의 목록을 필터링하여 보여주는 시나리오를 생각해 봅시다.
기존의 React에서는 모든 상태 업데이트가 동일한 우선순위를 가졌습니다. 따라서 무거운 필터링 작업이 진행되는 동안 브라우저의 메인 스레드가 점유되어, 사용자가 타이핑하는 글자가 화면에 즉시 나타나지 않고 버벅거리는 현상(Input Lag)이 발생하게 됩니다. 이는 사용자에게 매우 좋지 않은 경험을 제공합니다.
이 문제를 해결하기 위해 과거에는 debounce나 throttle 기법을 사용해 업데이트 빈도를 인위적으로 조절하곤 했습니다. 하지만 이는 근본적인 해결책이 아니며, 여전히 실제 업데이트가 일어날 때는 화면이 멈추는 현상을 피하기 어렵습니다.
React 18은 동시성(Concurrency) 모드를 도입하며 이 문제에 대한 근본적인 해결책인 useTransition Hook을 선보였습니다. 핵심 아이디어는 "업데이트의 우선순위를 나누는 것"입니다. 즉각적인 피드백이 필요한 입력값 업데이트는 높은 우선순위를 주고, 결과 목록을 렌더링하는 무거운 작업은 낮은 우선순위로 밀어내는 전략입니다.
useTransition은 상태 업데이트를 '전환(Transition)'으로 표시합니다. 전환 업데이트는 긴급한 업데이트(예: 입력창 타이핑)에 의해 중단될 수 있으며, 백그라운드에서 렌더링이 진행됩니다.
useTransition은 isPending 상태와 startTransition 함수를 반환합니다.
import { useState, useTransition } from 'react'; function SearchComponent() { const [isPending, startTransition] = useTransition(); const [query, setQuery] = useState(""); const [list, setList] = useState([]); const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { const value = e.target.value; // 1. 긴급 업데이트: 입력창에 글자가 즉시 입력됨 setQuery(value); // 2. 전환 업데이트: 무거운 리스트 필터링은 낮은 우선순위로 처리 startTransition(() => { // 대량의 데이터를 처리하는 무거운 로직 const filtered = largeData.filter(item => item.includes(value)); setList(filtered); }); }; return ( <div> <input type="text" value={query} onChange={handleChange} /> {/* 데이터 로딩 중임을 사용자에게 알림 */} {isPending && <p>목록을 갱신 중입니다...</p>} <ul> {list.map((item, index) => ( <li key={index}>{item}</li> ))} </ul> </div> ); }
startTransition 내부에는 상태 업데이트 함수만 넣어야 합니다. setTimeout이나 API 호출 같은 비동기 작업 자체를 넣는 것이 아니라, 비동기 작업의 결과를 상태에 반영하는 부분을 감싸야 합니다.value 업데이트를 startTransition으로 감싸면 안 됩니다. 입력창이 반응하지 않게 될 수 있습니다.isPending을 통해 작업이 진행 중임을 자연스럽게 알릴 수 있습니다.useTransition은 React 18 동시성의 힘을 가장 체감하기 좋은 도구입니다. 모든 작업이 긴급할 필요는 없다는 사실을 이해하고 우선순위를 적절히 배분한다면, 훨씬 더 부드럽고 전문적인 웹 애플리케이션을 만들 수 있을 것입니다.
참고 자료: