본문 바로가기

JavaScript

[번역] useDefferedValue

 

useDeferredValue – React

The library for web and native user interfaces

react.dev

 

useDefferedValue는 UI의 일부 업데이트를 지연시킬 수 있는 React 훅이다. 

 

const deferredValue = useDeferredValue(value)

 

 

해당 값의 지연된 버전을 얻으려면 컴포넌트의 최상위 레벨에서 useDeferredValue를 호출한다. 

 

import { useState, useDeferredValue } from 'react';

function SearchPage() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  // ...
}

 

초기 렌더링 중에 반환된 지연된 값은 사용자가 제공한 값과 동일하다. 업데이트하는 동안 React는 먼저 이전 값으로 다시 렌더링을 시도하고(따라서 이전 값을 반환한다), 백그라운드에서 새 값으로 다시 렌더링을 시도한다. 

 

- useDeferredValue에 전달하는 값은 문자열이나 숫자와 값은 원시값이거나 렌더링 외부에서 생성된 객체여야한다. 렌더링 중에 새 오브젝트를 생성하고 즉시 useDeferredValue에 전달하면 렌더링할 때마다 값이 달라져 불필요한 백그라운드 리렌더링이 발생할 수 있다. 

- useDeferredValue가 현재 렌더링(여전히 이전 값을 사용하는 경우)외에 다른 값(Object.is 비교)을 받으면, 새 값으로 백그라운드에서 다시 렌더링하도록 예약한다. 백그라운드 리렌더링은 중단할 수 있다. 값에 대한 다른 업데이트가 있으면 React는 백그라운드 리렌더링을 처음부터 다시 시작한다. 예를 들어, 사용자가 지연된 값을 받는 차트가 다시 렌더링할 수 있는 속도보다 빠르게 입력하는 경우 입력을 중단한후에만 차트가 다시 렌더링된다. 

- useDeferredValue는 Suspense와 통합되었다. 새 값으로 인한 백그라운드 업데이트로 인해 UI가 일시 중단되면 사용자에게 폴백이 표시되지 않는다. 데이터가 로드될 때까지 이전 지연된 값이 표시된다. 

- useDeferredValue는 그 자체로 추가 네트워크 요청을 방지하지는 않는다. 

- useDeferredValue 훅 장체로 인한 고정된 지연은 없다. 리액트는 원래의 리렌더를 완료하자마자 새로운 지연된 값으로 백그라운드 리렌더링 작업을 즉시 시작한다. 이벤트(예: 입력)로 인한 모든 업데이트는 백그라운드 리렌더링을 중단하고 우선순위를 갖는다. 

- useDeferredValue 값으로 인한 백그라운드 리렌더링은 화면에 커밋될 때까지 Effects를 실행하지 않는다. 백그라운드 리렌더링이 일시 중단되면 데이터가 로드되고 UI가 업데이트된 후에 해당 Effect가 실행된다. 

 

Showing stale content while fresh content is loading 

import { useState, useDeferredValue } from 'react';

function SearchPage() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  // ...
}

 

 초기렌더링 중에 deferred value는 사용자가 제공한 값과 동일하다.

업데이트 중에는 지연된 값이 최신 값보다 뒤처지게 된다. 특히 리액트는 먼저 지연된 값을 업데이트하지 않고 다리 렌더링한 다음 백그라운드에서 새로 받은 값으로 다시 렌더링을 시도한다. 

How does deferring a value work under the hood

두 단계로 나누어 생각할 수 있다.

1. 먼저 리액트는 새 쿼리 "ab"로 다시 렌더링하지만 이전 deferredQuery(여전히 "a")를 사용한다. 결과 목록에 전달한 deferredQuery값은 지연되어 쿼리 값보다 "뒤처진다."

2. 백그라운드에서 React는 쿼리와 deferredQuery를 모두 "ab"로 업데이트한 상태로 다시 렌더링을 시도한다. 이 리렌더링이 완료되면 리액트는 이를 화면에 표시한다. 그러나 일시 중단되면 (ab에 대한 결과가 아직 로드되지 않은 경우) React는 이 렌덜이 시도를 포기하고 데이터가 로드된 후 다시 렌더링을 시도한다. 사용자는 데이터가 준비될 때까지 오래된 지연값을 계속보게된다. 

 

지연된 background 렌더링은 중단될 수 있다. 예를 들어, 다시 입력하면 React는 입력을 포기하고 새 값으로 다시 시작한다. React는 항상 제공된 최신 값을 사용한다. 

 

각 키 입력당 네트워크 요청이 여전히 존재한다는 점에 유의하자. 여기서 지연되는 것은 네트워크 요청 자체가 아니라 결과가 준비될 때까지 결과를 표시하는 것이다. 여기서 지연되는 것은 네트워크 요청 자체가 아니라 결과가 준비될 때까지 결과를 표시하는 것이다. 사용자가 계속 입력하더라도 각 키 입력에 대한 응답은 캐시되므로 백스페이스를 누르면 즉시 다시 가져오지 않는다. 

 

Indicating that the content is stale

위 예제에서 최신 쿼리에 대한 결과 목록이 아직 로드중이라는 표시가 없다. 새 결과를 로드하는데 시간이 오래 걸리면 사용자게에 혼란을 줄 수 있다. 결과 목록이 최신 쿼리와 일치하지 않는다는 것을 사용자에게 명확하게 알리기 위해 오래된 결과 목록이 표시될 때 시각적 표시를 추가할 수 있다. 

 

<div style={{
  opacity: query !== deferredQuery ? 0.5 : 1,
}}>
  <SearchResults query={deferredQuery} />
</div>

 

이렇게 변경하면 입력을 시작하자마자 새 결과 목록이 로드될 때까지 오래된 결과목록이 약간 어두워진다. 아래 예시처럼 CSS 전환을 추가하여 점진적인 느낌을 주도록 흐리게 표시되도록 지연시킬 수도 있다. 

 

import { Suspense, useState, useDeferredValue } from 'react';
import SearchResults from './SearchResults.js';

export default function App() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery;
  return (
    <>
      <label>
        Search albums:
        <input value={query} onChange={e => setQuery(e.target.value)} />
      </label>
      <Suspense fallback={<h2>Loading...</h2>}>
        <div style={{
          opacity: isStale ? 0.5 : 1,
          transition: isStale ? 'opacity 0.2s 0.2s linear' : 'opacity 0s 0s linear'
        }}>
          <SearchResults query={deferredQuery} />
        </div>
      </Suspense>
    </>
  );
}

 

Deferring re-rendering for a part of the UI

 

useDeferredValue를 성능 최적화로 적용할 수도 있다. UI의 일부가 다시 렌더링되는 속도가 느리고 이를 최적화할 쉬운 방법이 없으며 나머지 UI를 차단하지 않도록 하려는 경우에 유용하다. 

 

키 입력시마다 다시 렌더링되는 텍스트 필드와 컴포넌트(예: 차트 또는 긴 목록)가 있다고 생각해보자. 

 

function App() {
  const [text, setText] = useState('');
  return (
    <>
      <input value={text} onChange={e => setText(e.target.value)} />
      <SlowList text={text} />
    </>
  );
}

 

먼저, prop이 동일한 경우 리렌더링을 건너뛸 수 있도록 memo로 최적화할 수 있다. 

 

const SlowList = memo(function SlowList({ text }) {
  // ...
});

 

하지만, 이는 SlowList prop이 이전 렌더링과 동일한 경우에만 도움이 된다. 지금 직면하고 있는 문제는 서로 다를 때, 그리고 실제로 다른 시각적 출력을 표시해야할 때 속도가 느리다는 것이다. 

 

구체적으로 주요 성능 문제는 입력할 때마다 SlowList가 새로운 prop을 수신하고 전체 트리를 다시 렌더링하면 입력이 끊기는 느낌이 든다는 것이다. 이 경우 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를 다시 렌더링하는 속도가 빨라지지는 않는다. 하지만 키 입력을 차단하지 않도록 목록 리렌더링 우선순위를 낮출수 있음을 리액트에게 알려줄 수 있다. 목록은 입력보다 지연되었다가 입력을 따라잡게된다. 이전과 마찬가지로 리액트는 가능한 한 빨리 목록을 업데이트하려고 시도하지만 사용자가 입력하는 것을 차단하지는 않는다. 

 

How is deferring a value different from debouncing and throttling

이 시나리오에서 이전에 사용했을 수 있는 두가지 일반적인 최적화 기술이 있다.

- 디바운싱은 사용자가 입력이 멈출때까지 기다렸다가 목록을 업데이트하는 것을 의미한다. 

- 스로틀링은 가끔씩(예: 최대 1초에 한 번) 목록을 업데이트하는 것을 의미한다. 

 

이러한 기법도 경우에 따라 도움이 되지만, 리액트 자체와 깊이 통합되어 있고 사용자의 기기에 맞게 조정되기 때문에 렌더링을 최적화하는 데는 useDeferredValue가 더 적합하다. 

 

디바운싱, 스로틀링과 달리 고정 지연을 선택할 필요가 없다. 사용자의 디바이스가 빠른 경우(예: 고성능 노트북)지연된 리렌더링은 거의 즉시 발생하며 눈에 띄지 않는다. 사용자의 디바이스가 느리면 디바이스 속도에 비례하여 목록이 입력에 '지연'된다. 

 

또한 디바운싱과 쓰로틀링과 달리, useDeferredValue로 수행되는 지연된 리렌더링은 기본적으로 중단할 수 있다. 즉, 리액트가 큰 목록을 리렌더링하는 도중에 사용자가 다른 키를 입력하면 리액트는 해당 리렌더링을 중단하고 키 입력을 처리한 다음 백그라운드에서 다시 렌더링을 시작한다. 반면 디바운싱과 스로틀링은 렌더링이 키입력을 차단하는 순간을 연기할 뿐이기 대문에 여전히 불안정한 경험을 만들어낸다. 

 

최적화중인 작업이 렌더링 중에 발생하지 않는 경우에 디바운싱과 스로틀링은 여전히 유용하다. 예를 들어 네트워크 요청을 더 적게 실행할 수 있다. 이러한 기술을 함께 사용할 수 있다.