디바운스(Debounce)와 스로틀(Throttle) 을 대체할 수 있는 react hook인 useDeferredValue와 useTransition를 알아보겠습니다.
최신 React 버전인 React 18은 놀라운 성능 개선과 유연성을 제공하는 concurrent rendering 기능을 도입했습니다. 이를 통해 UI 업데이트와 상호작용을 더욱 부드럽게 처리할 수 있게 되었습니다. 이번 글에서는 React 18에서 도입된 중요한 변화 중 하나인 useDeferredValue와 useTransition 훅에 대해 알아보겠습니다.
Concurrent Rendering은 React 18에서 새롭게 도입된 기능으로, 병렬 처리와 우선순위 조절을 통해 더 나은 사용자 경험을 제공합니다. 이전 버전에서는 UI 업데이트나 상태 변화가 발생하면 그 작업이 완료될 때까지 메인 스레드가 블록되는 문제가 있었습니다. 하지만 Concurrent Rendering은 작업의 우선순위를 조절하여 중요한 작업에 먼저 반응할 수 있도록 합니다.
useDeferredValue 훅은 React 18에서 새롭게 도입된 훅 중 하나로, 값의 업데이트 우선순위를 조절하는 데 사용됩니다. 이전 값을 기억하면서도 업데이트를 지연시키므로, 메인 스레드가 블로킹되지 않으면서도 높은 우선순위의 작업을 처리할 수 있습니다.
예를 들어, 검색 자동완성 기능을 가진 애플리케이션에서 useDeferredValue를 활용하면 유저의 검색 쿼리가 변경되더라도 추천 검색어 업데이트가 늦어지지 않습니다. 이전 값을 유지하면서도 추천 검색어 업데이트를 지연시켜 유저 경험을 향상시킬 수 있습니다.
import { useState } from 'react';
import { useDeferredValue } from 'react';
function SearchBar() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
return (
<div>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<SearchSuggestions query={deferredQuery} />
</div>
);
}
import { useState } from 'react';
import { useDeferredValue } from 'react';
function RealtimeDataDisplay() {
const [data, setData] = useState('');
const deferredData = useDeferredValue(data);
// 데이터를 주기적으로 업데이트하는 함수
const fetchData = async () => {
const response = await fetch('api/data');
const newData = await response.json();
setData(newData);
};
return (
<div>
<button onClick={fetchData}>새로운 데이터 가져오기</button>
<DataDisplay data={deferredData} />
</div>
);
}
useTransition 훅은 상태 변화의 우선순위를 지정하는 데 사용됩니다. isPending와 startTransition을 반환하며, isPending는 작업이 지연되고 있는지 여부를 나타내는 불리언 값이고, startTransition은 낮은 우선순위로 실행될 함수를 받습니다.
예를 들어, 버튼 클릭 시 상태 업데이트를 지연시키고, 그 동안 로딩 스피너를 표시하고자 할 때 다음과 같이 활용할 수 있습니다.
import { useState } from 'react';
import { useTransition } from 'react';
function ButtonWithTransition() {
const [isPending, startTransition] = useTransition();
const [count, setCount] = useState(0);
const handleClick = () => {
startTransition(() => {
setCount((prevCount) => prevCount + 1);
});
};
return (
<div>
{isPending && <Spinner />}
<button onClick={handleClick}>Increment</button>
<p>Count: {count}</p>
</div>
);
}
import { useState } from 'react';
import { useTransition } from 'react';
function LoadingAndDataDisplay() {
const [data, setData] = useState(null);
const [isPending, startTransition] = useTransition();
const fetchData = async () => {
startTransition(() => {
setData(null); // Reset previous data
});
const response = await fetch('api/data');
const newData = await response.json();
startTransition(() => {
setData(newData);
});
};
return (
<div>
<button onClick={fetchData}>Fetch Data</button>
{isPending ? (
<p>Loading...</p>
) : (
<DataDisplay data={data} />
)}
</div>
);
}
디바운스와 스로틀링은 자주 발생하는 이벤트나 상태 업데이트를 제어하여 성능을 최적화하는데 사용되지만, 여전히 몇 가지 문제점을 가지고 있습니다.
불필요한 호출 제한:
스로틀링은 일정한 시간 간격으로 이벤트를 처리하여 불필요한 호출을 제한하나, 이로 인해 실제 이벤트가 놓치는 경우가 발생할 수 있습니다.
긴 시간 동안 이벤트 무시:
스로틀링된 함수가 일시적으로 비활성화되는 동안 긴 시간 동안 이벤트가 무시될 수 있습니다. 이는 사용자 경험에 악영향을 줄 수 있습니다.
선별적인 업데이트:
useDeferredValue는 값의 업데이트 우선순위를 조절하여 중요한 작업에 우선순위를 부여할 수 있습니다. 이를 통해 메인 스레드의 블로킹을 방지하면서도 높은 우선순위의 작업을 더 빠르게 처리할 수 있습니다.
고성능 상호작용:
유저 입력과 같이 중요한 상호작용에 대한 반응성을 높일 수 있습니다.
상태 변화 우선순위 조절:
useTransition을 활용하면 상태 변화의 우선순위를 지정하여 중요한 작업을 우선적으로 처리할 수 있습니다. 이로써 중요한 상호작용에 우선순위를 부여하면서도 백그라운드 작업을 놓치지 않게 됩니다.
부드러운 사용자 경험:
유저 입력과 같은 중요한 상호작용을 지연시키지 않으면서도 더 나은 성능과 부드러운 사용자 경험을 제공할 수 있습니다.
디바운스와 스로틀링은 과거에 사용되던 성능 최적화 기술이지만, 그만큼 어려움과 문제점을 안고 있었습니다. React 18에서 도입된 useDeferredValue와 useTransition은 concurrent rendering을 통해 이러한 문제를 해결하고, 더 높은 성능과 유연성을 제공합니다. 디바운스와 스로틀링보다 훨씬 더 강력하고 정교한 처리를 가능하게 함으로써 사용자 경험을 더욱 향상시킬 수 있습니다.