여러가지 간단한 케이스로 라이브 코딩 테스트 준비를 하는 과정에서 useDeferredValue를 새롭게 알게 되었고, 앞으로 활용도가 많은 훅인 것 같아 어떤 역할을 하고, 어떤 경우에 사용하면 좋을지 생각해보았다.
useDeferredValue는 UI 업데이트를 지연시키는 역할을 하는 React Hook이고, 용법은 아래와 같다.
const deferedValue = useDeferredValue(value, initialValue);
계속 '지연'시킨다는 표현을 사용하고 있는데, 정확히 무엇을 지연시키는 걸까?
useDeferredValue는 값의 업데이트를 지연시킨다. 상태가 변경되어도 무거운 컴포넌트에 전달되는 값을 즉시 업데이트하지 않고, React의 우선순위 스케줄링을 통해 나중에 업데이트한다. 이를 통해 사용자 입력과 같은 긴급한 UI 업데이트가 무거운 렌더링에 의해 블로킹되지 않도록 한다.
공식문서의 예시를 가져오면 아래와 같다.
// App.js
import SearchResults from './SearchResults.js';
export default function App() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
return (
<>
<label>
Search albums:
<input value={query} onChange={e => setQuery(e.target.value)} />
</label>
<Suspense fallback={<h2>Loading...</h2>}>
<SearchResults query={deferredQuery} />
</Suspense>
</>
);
}
// SearchResult
import {use} from 'react';
import { fetchData } from './data.js';
export default function SearchResults({ query }) {
if (query === '') {
return null;
}
const albums = use(fetchData(`/search?q=${query}`));
if (albums.length === 0) {
return <p>No matches for <i>"{query}"</i></p>;
}
return (
<ul>
{albums.map(album => (
<li key={album.id}>
{album.title} ({album.year})
</li>
))}
</ul>
);
}
여기서 쿼리가 변함에 따라 검색 결과가 바뀌게 된다. useDeferredValue를 쓰지 않는다면 유저가 타이핑을 치는 모든 순간에 SearchResult가 리렌더링된다.
useDeferredValue로 인해서 query로 인한 SearchResult의 리렌더링을 상태 업데이트 전에 UI를 임시로 보여줌으로서 매번 리렌더링 되는 것을 막는다.
useDeferredValue를 보면서 헷갈리는 지점이 꽤 있었다.
Suspense와 useDeferredValue를 함께 사용할 때 어떤 UI가 표시되는지가 가장 헷갈렸다.
처음에는 검색어를 타이핑한 후 Loading 컴포넌트 없이 바로 UI가 리렌더링되는 것을 보고, useDeferredValue로 인해 보이는 이전 UI가 Fallback UI 역할을 하는 것처럼 느껴졌다.
하지만 이는 잘못된 이해였다. Suspense는 하위 컴포넌트에서 Promise를 감지했을 때, 해당 범위 내에 이전에 commit된 컴포넌트가 없으면 Fallback UI를 표시한다.
useDeferredValue와 함께 사용하면, Suspense가 새로운 Promise를 감지하더라도 이미 이전에 commit된 UI(null 포함)가 존재하기 때문에 Fallback UI를 표시하지 않고 기존 UI를 유지한다. 이것이 useDeferredValue와 Suspense를 조합했을 때의 핵심 동작 방식이다.
공식 문서 마지막에서 다루는 내용이 바로 이 부분이었다.
평소 스크롤이나 키보드 입력에 debounce를 주로 사용해왔기 때문에, 비슷해 보이는 useDeferredValue라는 Hook이 왜 등장했는지, 어떤 차이가 있는지 궁금했다.
결론적으로 두 방식 모두 최적화를 목적으로 하지만, 최적화 대상이 다르다. Debounce와 throttle은 함수 호출 횟수를 제어한다. Debounce는 일정 시간 내 마지막 함수 호출만 처리하고, throttle은 일정 시간 내 중복 호출을 무시하는 방식으로 최적화한다.
반면 useDeferredValue는 렌더링 최적화에 특화되어 있다. 상태 변경으로 인한 렌더링을 지연시키되, 이전에 commit된 UI를 유지함으로써 무거운 렌더링이 다른 UI 업데이트를 블로킹하지 않도록 한다. 따라서 백그라운드에서 useDeferredValue는 함수 호출 자체를 최적화하지는 않는다.
처음에는 "어느 것이 더 좋은가"의 관점으로 접근했지만, 둘의 목적이 다르다는 것을 이해하고 나니 각각의 사용 시점이 명확해졌다.
이번 훅을 이해해보면서, 기존에 debouce와 throttle로 해결하던 문제를 React의 렌더링 시스템과 통합하여 효과적으로 해결하려는 의도가 있지 않았나 생각해보았다.
새로운 훅을 보면서 어떤 부분들을 지속적으로 개선하려고하는지 상상해보는 점은 늘 흥미로운 것 같다.