useDeferredValue
는 UI의 일부 업데이트를 늦춰주는 리액트 훅이에요.
const deferredValue = useDeferredValue(value)
Reference | 레퍼런스
useDeferredValue(value)
값의 연기된 버전을 얻으려면 컴포넌트의 최상위에서 useDeferredValue
를 호출하세요.
import { useDeferredValue, useState } from "react";
function SearchPage(){
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
// ...
}
Parameters | 매개변수(파라미터)
value
: 연기하고 싶은 값. 모든 타입이 들어갈 수 있어요.
Returns | 반환값
초기 렌더링 과정에서 반환된 연기된 값은 넘겨준 값과 똑같을 거예요. 업데이트가 되는 동안 리액트는 오래된 값으로 리렌더링을 먼저 시도할 거예요.(그래서 오래된 값을 반환할거예요.) 그리고나면 새로운 값을 가지고 백그라운드에서 다른 리렌더링을 시도해요. (그리고 업데이트된 값을 반환할 거예요.)
Caveats | 주의사항
useDeferredValue
로 넘긴 값은 (string이나 number와 같은) 원시타입이 되기도 하고 렌더링 외부에서 생성된 객체가 되기도 해요. 만약 렌더링 동안 새로운 객체를 생성하고 즉시useDeferredValue
로 그 객체를 넘긴다면 렌더링 될 때마다 달라져 불필요한 백그라운드 리렌더링을 유발해요.useDeferredValue
가 (Object.js
로 비교된) 다른 값을 받을 때, (여전히 이전의 값을 사용하면,) 현재 렌더링과 함께 새로운 값으로 백그라운드에서 리렌더링을 예약해요. 백그라운드 리렌더링은 중단이 가능해요. 만약 다른value
업데이트가 있다면, 리액트는 처음부터 백그라운드 리렌더링을 재시작해요. 예를 들어서 사용자가 입력창에 차트가 지연된 값을 받아 리렌더링을 하는 것보다 더 빨리 입력한다면 차트는 사용자가 타이핑을 완료한 후에만 리렌더링 될 거예요.useDeferredValue
는<Suspense>
를 포함하고 있어요. 만약 새로운 값 때문에 발생한 백그라운드 업데이트가 UI를 기다린다면 사용자는 fallback을 볼 수 없어요. 데이터가 로드될 때까지 사용자는 기존에 연기된 값만 볼 거예요.useDeferredValue
는 추가적인 네트워크 요청을 막지 않아요.useDeferredValue
자체로 발생하는 고정된 지연은 없어요. 리액트가 원래의 리렌더링을 완료하자마자 리액트는 즉시 새로운 지연된 값을 사용하여 백그라운드 리렌더링을 시작해요. (타이핑 같이) 이벤트로 발생한 어떤 업데이트도 백그라운드 리렌더링을 중단하고 그보다 우선순위를 가져요.useDeferredValue
로 발생한 백그라운드 리렌더링은 화면에 보여지기 전까지 Effects를 발생시키지 않아요. 백그라운드 리렌더링이 지연되면 그것의 Effects는 데이터가 로드되고 UI가 업데이된 후에 동작해요.
Usage | 용법
Showing stale content while fresh content is loading | 새 콘텐츠가 로딩되는 동안 오래된 콘텐츠 보여주기
UI의 일부 업데이트를 연기하려면 최상위 컴포넌트에서 useDeferredValue
를 호출하세요.
import { useState, useDeferredValue } from 'react';
function SearchPage() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
// ...
}
초기 렌더링 동안 연기된 값은 인자로 내린 값과 동일해요.
업데이트 동안 연기된 값은 가장 최근 값에서 뒤쳐져요. 특히 리액트는 연기된 값의 업데이트 없이 리렌더링하고 백그라운드에서 새롭게 받은 값으로 리렌더링을 시도할 거예요.
이 훅이 언제 유용한지는 아래의 예시를 봐주세요.
노트
이 예시는 Suspense 사용이 가능한 데이터 소스를 사용한다고 가정해요.
- Relay나 Next.js와 같은 Suspense 사용이 가능한 프레임워크로 데이터 페칭하기
-lazy
로 작성된 레이지 로딩 컴포넌트 코드
-use
를 사용한 Promise 값 읽기
이 예시에서 SearchResults
컴포넌트는 검색 결과를 페칭하는 동안 유예해요. "a"
를 타이핑하고 결과를 기다리고 "ab"
로 수정하세요. "a"
의 결과는 로딩 fallback으로 대체돼요.
일반적인 대체 UI 패턴은 새로운 결과가 준비될 때까지 결과 목록의 업데이트를 지연하고 이전의 결과를 계속 보여주는 것이에요. 쿼리의 지연된 버전을 넘겨주려면 useDeferredValue
를 호출하세요.
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>
</>
);
}
query
는 즉시 업데이트 되기 때문에 인푹은 새로운 값을 보여줘요. 그러나 deferredQuery
는 데이터가 로드될 때까지 이전 값을 보관하기 때문에 SearchResults
는 잠시동안 오래된 결과는 보여줘요.
아래의 예시에 "a"
를 입력하고 로딩될 때까지 결과를 기다리세요. 그리고나서 인풋창의 값을 "ab"
로 수정하세요. Suspense fallback 대신에 새 결과가 로드될 때까지 오래된 결과 목록이 보여지는 것을 확인하세요.
값을 지연하는 것은 내부에서 어떻게 작동하나요?
먼저, 리액트는 새로운 query
인 "ab"
로 리렌더링하지만 이전의 deferredQuery
인 "a"
를 여전히 가지고 있어요. 결과 목록으로 전달한 deferredQuery
값은 쿼리
값보다 뒤쳐져요.
백그라운드에서 리액트는 새로운 query
와 deferredQuery
를 모두 "ab"
로 업데이트하여 리렌더링을 시도해요. 만약 이 리렌더링이 완료되면 리액트는 결과를 스크린에 보여줄 거예요. 그러나 만약 이 작업이 중단된다면 (아직 "ab"
의 결과가 로딩되지 않았다면), 리액트는 이 렌더링 시도를 멈추고 데이터가 로딩되된 후 다시 리랜더링을 시도해요. 사용자는 데이터가 준비되기 전까지 이전의 연기된 값을 볼 거예요.
연기된 "백그라운드" 렌더링은 중단이 가능해요. 예를 들어 만약 다시 인풋창에 무언가를 입력한다면 리액트는 렌더링을 중간하고 새로운 값으로 (렌더링을) 재시작해요. 리액트는 항상 가장 최근에 제공받은 값을 사용해요.
네트워크 요청은 여전히 키를 하나씩 누를 때마다 전송된다는 점을 기억하세요. 연기되는 것은 (결과가 준비될 때까지) 네트워크 요청을 하지 않는 것이 아니라 값을 보여주는 거예요. 설령 사용자가 타이핑을 계속 한다고 하더라도 각 타이핑에 대한 응답은 계속 오기 때문에 백스페이스를 누르는 것은 즉각적이고 다시 페치되지는 않아요.
Indicating that the content is stale | 콘텐츠가 오래되었음을 나타내기
위의 예시에서 최근 쿼쿼리를 위한 결과 리스트가 여전히 로딩중이라는 표시가 없어요. 만약 새로운 결과가 로드하는데 시간이 걸린다면 이는 유저에게 혼란을 줄 수 있어요. 유저에게 결과 리스트가 가장 최근의 쿼리와 매칭되지 않는다는 것을 더 명백하게 하기 위해 오래된 결과리스트가 표시될 때 가시적인 표시를 추가할 수 있어요.
<div style={{
opacity: query !== deferredQuery ? 0.5 : 1,
}}>
<SearchResults query={deferredQuery} />
</div>
이 변동으로 타이핑이 시작되자마자 새로운 결과 목록이 로딩될 때까지 오래된 결과 목록은 약간 흐려져요. 아래 예와 같이 점진적인 느낌이 들도록 흐리게하는 효과를 지연시키는 CSS 전환을 추가할 수도 있어요.
Deferring re-rendering for a part of the UI | 일부 UI의 리렌더링 지연시키기
또한 useDeferredValue
를 성능 최적화 수단으로 사용할 수도 있어요. 일부 UI가 리렌더링이 느리게 되어야할 때 최적화할 마땅한 방법이 없고 남은 UI를 방해하는 것은 방지하고 싶을 때 유용해요.
키보드를 누를때마다 리렌더링 되어야하는 텍스트 필드와 (차트나 긴 목록과 같은) 컴포넌트가 있다고 생각해보세요.
function App() {
const [text, setText] = useState('');
return (
<>
<input value={text} onChange={e => setText(e.target.value)} />
<SlowList text={text} />
</>
);
}
먼저 props가 같을 때 리렌더링을 건너뛰도록 SlowList
를 최적화하세요. memo
로 감싸면 돼요.
const SlowList = memo(function SlowList({ text }) {
// ...
});
그러나 이 방법은 SlowList
props가 이전 렌더링 동안 동일할 때만 사용할 수 있어요. 문제는 이제 props가 서로 다를때, 그리고 실제로 다른 가시적 결과를 보여줘야할 때 느리다는 것이에요.
구체적으로, 주요한 성능 문제는 인풋란에 타이핑을 할 때마다 SlowList
는 새로운 props를 받고 전체 트리를 리렌더링 하는 과정은 조금 짜쳐요. 이런 경우에는 useDeferredValue
가 (더 느린) 결과 목록을 업데이트 하는 것보다 (더 빠른) 인풋란을 업데이트는 하는 것에 우선순위를 부여해요.
function App() {
const [text, setText] = useState('');
const deferredText = useDeferredValue(text);
return (
<>
<input value={text} onChange={e => setText(e.target.value)} />
<SlowList text={deferredText} />
</>
);
}
그렇다고 SlowList
가 더 빠르게 리렌더링 되지는 않아요. 하지만 리액트에게 목록을 리렌더링 하는 것은 우선순위가 낮기 때문에 키를 누르는 것을 막지 않는다는 것을 말해줘요. 목록은 인풋이 뒤쳐지고 따라잡을 거예요. 이전과 같이 리액트는 가능한한 빠르게 목록을 업데이트 하지만 사용자가 타이핑 하는 것을 막지는 않아요.
useDeferredValue
와 최적화되지 않은 리렌더링 간의 차이 확인하기
1. 목록의 연기된 리렌더링
이 예시에서 SlowList
컴포넌트의 각 요소는 인위적으로 속도를 낮추기 때문에 useDeferredValue
가 인풋을 반응적으로 유지하는 방법을 알 수 있어요. input에 타이핑을 하면 목록이 "뒤쳐지는" 생생한 기분을 느껴보세요.
2. 목록의 최적화되지 않은 리렌더링
이 예시에서 SlowList
컴포넌트의 각 아이템은 useDeferredValue
없이 인위적으로 속도가 낮췄어요.
인풋창에 타이핑하면 굉장히 짜치는 걸 볼 수 있어요. 왜냐하면 useDeferredValue
가 없기 때문에 키를 누를 때마다 모든 목록이 즉시 중단할 수 없는 방법으로 리렌더링을 하도록 강제받기 때문이에요.
함정
이러한 최적화는 memo로 감싸진 SlowList를 필요로 해요. 왜냐하면 text가 변경될 때마다 리액트는 빠르게 부모 컴포넌트를 리렌더링해야하기 때문이에요. 리렌더링 동안 deferredText는 이전의 값을 여전히 가지고 있어서 SlowList는 리렌더링을 건너뛸 수 있어요. memo가 없다면 최적화의 핵심이 무너지고 리렌더링을 해야해요.
값을 연기하는 것은 디바운스와 쓰로틀링과는 어떻게 다를까요?
아래의 시나리오에는 당신이 이전에 사용해 본적이 있는 두 가지 일반적인 최적화 기술이 있어요.
- 디바운싱은 목록을 업데이트 하기 전에 (예를 들면 몇초동안) 사용자가 입력을 멈출 때까지 기다리는 것이에요.
- 쓰로틀링은 (보통은 초당 한 번씩과 같이) 일정한 간격 마다 목록을 한 번씩 업데이트 하는 것이에요.
어떤 경우에 이런 기술들은 유용하지만 useDeferredValue
가 렌더링 최적화에 더 잘 맞아요. useDeferredValue
는 리액트와 잘 통합되어 있고 사용자의 기기에 맞춰져 있기 때문이에요.
디바운싱과 쓰로틀링과는 달리 고정된 딜레이를 선택할 필요가 없어요. (고성능의 노트북과 같이) 사용자의 기기는 빠르고 연기된 리렌더링은 거의 즉시 발생하여 알아차리지 못해요. 만약 사용자의 기기가 느리다면 목록은 디바이스가 느린 정도와 비례하게 인풋창보다 뒤쳐질 거예요.
또한 useDeferredValue
로 연기된 리렌더링은 디바운싱과 쓰로틀링과는 달리 기본적으로 중단이 가능해요. 이는 만약 리액트가 거대한 목록을 리렌덜이하는 중간에 있더라도 사용자가 다른 키를 누르면 리액트는 리렌더링을 중단하고 키 입력을 다루며 백그라운드에서 다시 리렌더링을 시작할 거예요. 반면 디바운싱과 쓰로틀링은 이러한 기능이 막혀있기 때문에 여전히 짜치는 경험을 제공해요. 리렌더링이 키입력을 차단할 때를 단순히 연기하는 것일 뿐이니까요.
만약 렌더링을 하는 동안 최적화하는 작업이 발생하지 않는다면 디바운싱과 쓰로틀링은 여전히 유용해요. 예를 들어 네트워크 요청을 더 적게 보낼 수도 있어요. 그리고 이러한 기술들을 같이 사용할 수도 있어요.
'리액트 공식문서 | React Docs > Reference > react@18.2.0' 카테고리의 다른 글
[Hooks] useId | useId 훅 (0) | 2024.01.21 |
---|---|
[Hooks] useEffect | useEffect 훅 (0) | 2024.01.18 |
[Hooks] useDebugValue | useDebugValue 훅 (0) | 2024.01.11 |
[Hooks] useContext | useContext 훅 (2) | 2024.01.07 |
[Hooks] useCallback | useCallback훅 (2) | 2024.01.04 |