useDeferredValue는 뒤처져도 되는 값의 사본을 줍니다. 값이 바뀌면 React는 먼저 이전 사본으로 화면을 다시 그려서 입력이 즉시 반응하게 하고, 그다음 백그라운드에서 새 값으로 느린 부분을 렌더링합니다.
단어를 빠르게 입력해 보세요. input은 밀리지 않고, 목록은 뒤처져 있는 동안 흐려집니다. React가 SlowList 렌더링을 시작하자마자 로그를 남기므로 Console에는 글자마다 SlowList rendering 줄이 하나씩 기록되지만, 중간 값에 대한 목록은 화면에 도달하지 않습니다. 새 글자마다 백그라운드 렌더링이 중단되었고, React는 최신 텍스트로 다시 시작했습니다. query={deferredQuery}를 query={query}로 바꾸고 다시 입력하면, 이제 키를 누를 때마다 목록 때문에 0.25초씩 기다립니다.
문법
const deferredValue = useDeferredValue(value);
const deferredValue = useDeferredValue(value, initialValue); // React 19
value는 렌더링하는 무엇이든 됩니다. input의 문자열, 숫자, 객체 등입니다. 렌더링마다 새 객체는 항상 변경으로 취급되므로 원시 값이 가장 잘 맞습니다.- 반환값은 대부분
value와 같습니다.value가 바뀐 직후에는 렌더링 한 번 동안 이전 값을 유지하고, 백그라운드 렌더링에서 따라잡습니다.
동작 방식
변경마다 렌더링이 두 번 일어납니다.
- 긴급한 렌더링: 새
value와 이전deferredValue로 렌더링합니다. input은 새 텍스트를 보여 줍니다.deferredValue만 받는 컴포넌트는 이전과 같은 props를 받습니다. - 백그라운드 렌더링: 새
deferredValue로 렌더링합니다. 이쪽이 느린 렌더링입니다. React는 이것을 조각으로 나눠 렌더링하고, 끝나기 전에value가 다시 바뀌면 포기한 뒤 최신 값으로 다시 시작합니다.
두 값을 모두 로그로 남기면 이 과정을 지켜볼 수 있습니다.
a 같은 글자 하나를 입력하면 Console에 두 줄이 더 출력됩니다. 먼저 deferred = ""와 함께 text = "a"가, 그다음 둘 다 "a"가 나옵니다. 그 첫 번째 렌더링이 입력을 빠르게 유지하지만, 느린 부분이 그것을 건너뛸 수 있을 때만 그렇습니다. 그래서 첫 번째 예제의 SlowList는 memo로 감싸져 있습니다. 긴급한 렌더링 중에는 query prop이 바뀌지 않았으므로 React가 건너뜁니다. memo가 없으면 긴급한 렌더링이 이전 텍스트로 느린 목록 전체를 여전히 실행하므로 얻는 것이 없습니다.
이전 내용 보여 주기
백그라운드 렌더링이 진행되는 동안 value와 지연 값은 다릅니다. 둘을 비교하면 내용을 숨기지 않고도 내용이 최신이 아니라고 사용자에게 알릴 수 있습니다.
const isStale = query !== deferredQuery;
<div style={{ opacity: isStale ? 0.5 : 1, transition: 'opacity 0.2s' }}>
<SlowList query={deferredQuery} />
</div>
보통 스피너보다 낫습니다. 이전 결과를 계속 읽을 수 있고, 새 결과가 준비되면 그 자리에서 바뀝니다. Suspense에서도 같은 원리가 통합니다. 지연 값 때문에 컴포넌트가 데이터를 불러오며 일시 중단되면, React는 새 내용이 준비될 때까지 대체 UI 대신 이전 내용을 계속 보여 줍니다(Suspense와 lazy 참고).
초기값(React 19)
기본적으로 첫 렌더링에는 뒤처질 대상이 없으므로 지연 값은 처음부터 실제 값입니다. React 19부터는 두 번째 인자를 넘길 수 있습니다. 첫 렌더링에서 훅은 initialValue를 반환하고, 그다음 React가 실제 값으로 백그라운드에서 다시 렌더링합니다. 덕분에 가벼운 것을 먼저 보여 주고 비용이 큰 부분을 바로 뒤이어 채울 수 있습니다.
Console에는 렌더링이 두 번 나옵니다. 먼저 ""로, 그다음 전체 텍스트로입니다. 두 번째 인자를 지우면 전체 텍스트로 바로 렌더링한 로그 하나만 남습니다.
useDeferredValue와 useTransition
둘 다 작업을 긴급하지 않은 것으로 표시하고 같은 렌더링 메커니즘을 씁니다. 차이는 어디에 두는지입니다.
- useTransition은 업데이트를 감쌉니다.
startTransition(() => setQuery(text))를 호출하므로 setter가 필요합니다.isPending은 덤으로 얻습니다. - useDeferredValue는 값을 감쌉니다. 값을 쓰는 곳에서 호출하므로, 값이 prop으로 오거나 직접 제어하지 않는 훅에서 와도 동작합니다. "대기 중" 여부는
value !== deferredValue로 직접 계산합니다.
// You own the state: either works, a transition is direct
startTransition(() => setQuery(nextQuery));
// You only receive the value
function SearchResults({ query }) {
const deferredQuery = useDeferredValue(query);
return <SlowList query={deferredQuery} />;
}
useDeferredValue와 디바운스, 스로틀
디바운스는 마지막 변경 후 정해진 시간을 기다리고, 스로틀은 간격마다 최대 한 번만 업데이트합니다. 둘 다 미리 지연 시간을 정하므로, 빠른 노트북에서는 페이지를 필요 이상으로 느리게 만들고, 느린 휴대폰에서는 그 지연도 여전히 너무 짧을 수 있습니다.
useDeferredValue에는 지연이 없습니다. 백그라운드 렌더링이 즉시 시작되므로, 빠른 기기에서는 목록이 거의 바로 업데이트되고, 느린 기기에서는 React가 input의 반응성을 유지하면서 할 수 있는 만큼 자주 목록을 렌더링합니다. 백그라운드 작업은 중단할 수도 있는데, 디바운스된 렌더링은 그렇지 않습니다. 디바운스된 렌더링은 일단 시작되면 끝날 때까지 막힙니다.
하지 못하는 것은 네트워크 요청을 줄이는 것입니다. 새 값마다 fetch가 시작된다면, 지연 값은 렌더링이 완료될 때마다 여전히 바뀌므로, 요청 수가 중요하다면 요청 자체를 디바운스하세요.
자주 하는 실수
memo를 잊는 것. 위에서 보았듯 느린 컴포넌트는 긴급한 렌더링을 건너뛸 수 있어야 합니다.memo로 감싸지 않았거나 다른 prop 중 하나가 렌더링마다 새 객체라면, 키 입력마다 한 번이 아니라 두 번 렌더링됩니다.- 새 객체를 넘기는 것.
useDeferredValue({ query })는 렌더링마다 새 객체를 만들므로, 지연 값이 항상 바뀐 것처럼 보이고 React가 매번 추가 백그라운드 렌더링을 시작합니다. 원시 값을 지연시키고(useDeferredValue(query)) 그것으로 객체를 만드세요. - 요청이 줄어들기를 기대하는 것. 지연 값을 기반으로 데이터를 가져오는 컴포넌트는 렌더링이 끝나는 모든 값에 대해 여전히 데이터를 가져옵니다.
언제 쓸까
input 하나가 렌더링이 느린 페이지의 일부를 움직이고, 그 느린 부분을 메모이제이션할 수 있으며, 잠깐 약간 이전 결과를 보여 줘도 괜찮을 때 useDeferredValue를 사용하세요. 검색 결과, 필터에 따라 다시 그리는 차트, 서식이 적용된 텍스트 미리보기 같은 것들입니다. 50개만 보이는데 5,000개 행을 렌더링하는 것처럼 느린 부분이 고칠 수 있는 이유로 느리다면, 그것부터 고치세요. setter를 가지고 있는 경우의 이 패턴은 useTransition을 참고하세요.
자주 묻는 질문
useDeferredValue는 무엇을 하나요?
실제 값보다 뒤처질 수 있는 버전의 값을 반환합니다. 값이 바뀌면 React는 먼저 이전 지연 값으로 렌더링한 다음, 새 값으로 백그라운드에서 다시 렌더링하며, 그 백그라운드 렌더링은 중단될 수 있습니다.
useDeferredValue를 써도 목록이 빨라지지 않는 이유는 무엇인가요?
느린 컴포넌트가 memo로 감싸져 있고 지연 값을 받아야 합니다. 그렇지 않으면 긴급한 렌더링이 이전 값으로 느린 컴포넌트를 여전히 렌더링하므로 입력이 계속 느립니다.
useDeferredValue와 useTransition의 차이는 무엇인가요?
useTransition은 state 업데이트를 감싸므로 setter에 접근할 수 있어야 합니다. useDeferredValue는 값을 감싸므로, prop이나 직접 제어하지 않는 훅에서처럼 값만 받는 경우에도 동작합니다.
useDeferredValue는 디바운스인가요?
아니요. 정해진 지연 시간이 없습니다. React는 긴급한 렌더링 직후 지연된 렌더링을 시작하며, 빠른 기기에서는 지연이 거의 보이지 않습니다. 그 자체로 네트워크 요청을 줄이지도 않습니다.
useDeferredValue의 두 번째 인자는 무엇인가요?
React 19부터 initialValue를 넘길 수 있습니다. 첫 렌더링에서 훅은 그 값을 반환하고, 그다음 React가 실제 값으로 백그라운드에서 다시 렌더링합니다.