Debouncing & Throttling
자바스크립트에서는 짧은 시간에 유저 입력 등의 이벤트가 많이 일어나는 경우 화면이 끊길 수 있다. 그래서 이런 경우 특별한 최적화가 필요하다.
대표적인 예가 바로 자동완성이다.
구글에 검색어를 입력하면 한 글자가 바뀔 때마다 실시간으로 업데이트하면서 추천 검색어를 띄워줘야 한다.
만약 유저 입력 한 번에 추천 검색어 업데이트 한 번이 일어난다면 불필요한 리렌더링이 초당 수 번씩 필요할 것이다.
그래서 정작 필요한 진짜 검색어 에 대한 자동완성이 느려질 수 있다. 이런 걸 방지하기 위해 Debouncing 처리를 한다.
Debouncing은 특정 함수의 호출이 너무 자주 일어나지 않도록 일정 시간 동안 함수 실행을 지연시키는 것을 말한다.
const useDebouncedEffect = (func, delay, deps) => {
const callback = useCallback(func, deps);
useEffect(() => {
const timer = setTimeout(() => {
callback();
}, delay);
return () => {
clearTimeout(timer);
};
}, [callback, delay]);
};
작동은 useEffect과 유사하지만 함수를 실행하기 전에 delay시간만큼 기다리며 다시 업데이트될 경우 기존 함수를 실행하지 않도록 되어 있다.
Throttling 또한 비슷한 문제를 해결하기 위한 기법이다. 지정된 시간 동안 함수가 최대 한 번 호출되도록 제한한다.
주기적으로 네트워크 요청이 필요한 경우 Throttling을 이용해서 적당한 주기를 설정할 수 있다.
Debouncing과 Throttling에서 적절한 딜레이를 선택하는 것은 꽤 어렵다.
딜레이가 너무 길면 유저 입력에 대한 반응도 그만큼 느려지지만, 너무 짧아도 적용하는 의미가 없어진다. 또한, CPU부하가 큰 작업에는 적용해도 거의 의미가 없다.
유저가 빠르게 입력할 경우, 분명히 그걸 전부 처리하는 것은 비효율적이다.
useTransition
상태 변화의 우선순위를 지정하기 위해 useTransition Hook이 새로 도입되었다.
// useTransition 정의
export function useTransition(): [boolean, TransitionStartFunction];
주로 [loading, startTransition]을 반환한다.
- loading:
작업이 지연되고 있음을 알리는 boolean이며 로딩중을 알릴 수 있다. - startTransition:
낮은 우선순위로 실행할 함수를 인자로 받는다.
function App() {
const [loading, startTransition] = useTransition();
const [count, setCount] = useState(0);
function handleClick() {
startTransition(() => {
setCount(c => c + 1);
})
}
return (
<div>
{loading && <Component />}
<button onClick={handleClick}>{count}</button>
</div>
);
}
클릭할 때마다 일어나는 count 에 대한 상태 업데이트를 낮은 우선순위로 실행한다. 그래서 더 중요한 이벤트가 있는 경우 count의 업데이트를 지연시키고 대신 이전의 값을 보여준다. loading 이용하여 업데이트가 지연된 동안 컴포넌트를 보여줄 수도 있다.
useTransition을 통해 앞서 나온 100,000 개 배수를 보여주는 페이지를 다음과 같이 개선할 수 있다.
useDeferredValue
useTransition과 유사하게 낮은 우선순위를 지정하기 위한 훅이다.
차이점이라면 useTransition은 함수 실행의 우선순위를 지정하는 반면 useDeferredValue는 값의 업데이트 우선순위를 지정한다.
우선순위가 높은 작업을 실행하는 동안 useMemo와 유사하게 이전 값을 계속 들고 있으면서 업데이트를 지연시킨다.
useMemo와 함께 사용하면 더 효과가 좋다.
종속된 값들을 memorize시키면 불필요한 리렌더링을 막으면서 하위 컴포넌트나 상태의 업데이트를 지연시킬 수 있다.
function Typeahead() {
const query = useSearchQuery('');
const deferredQuery = useDeferredValue(query);
// Memoizing tells React to only re-render when deferredQuery changes,
// not when query changes.
const suggestions = useMemo(() =>
<SearchSuggestions query={deferredQuery} />,
[deferredQuery]
);
return (
<>
<SearchInput query={query} />
<Suspense fallback="Loading results...">
{suggestions}
</Suspense>
</>
);
}
위 코드는 검색 자동완성을 처리하는 코드이다. 사용자 입력 query의 업데이트에 대해 추천 검색어인 suggestions를 띄워 준다.
추천 검색어의 업데이트는 우선순위 높은 작업이 없을 때만 실행되기 때문에 Debouncing을 적용하지 않아도 좋은 유저 경험을 줄 수 있다.
사소한 차이가 사용자성을 개선하고 사용자에게 큰 이미지를 심어줄 수 있다.