본문 바로가기
React-study/dil

[DIL] useDeferredValue

by 어느새벽 2024. 6. 24.

useDeferredValue는 UI 일부의 업데이트를 지연시킬 수 있는 리액트 훅이다.

const deferredValue = useDeferredValue(value)

 

참조 useDeferredValue(value)

컴포넌트의 최상위 레벨에서 useDeferredValue를 호출하여 지연된 버전의 값을 가져온다.

import { useState, useDeferredValue } from 'react';

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

 

매개변수 

value: 지연시키려는 값이다. 어떤 타입이든 가능하다.

 

반환값 

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

 

주의사항 

  • useDeferredValue에 전달하는 값은 문자열 및 숫자와 같은 원시값이거나, 컴포넌트의 외부에서 생성된 객체여야 한다. 렌더링 중에 새 객체를 생성하고 즉시 useDeferredValue에 전달하면 렌더링할 때마다 값이 달라져 불필요한 백그라운드 리렌더링이 발생할 수 있다.
  • useDeferredValue가 현재 렌더링(여전히 이전 값을 사용하는 경우) 외에 다른 값(Object.is와 비교)을 받으면 백그라운드에서 새 값으로 다시 렌더링하도록 예약한다. 값에 대한 또 다른 업데이트가 있으면 백그라운드 리렌더링은 중단될 수 있다. 리액트는 백그라운드 리렌더링을 처음부터 다시 시작할 것이다. 예를 들어, 차트가 리렌더링 가능한 지연된 값을 받는 속도보다 사용자가 input에 값을 입력하는 속도가 더 빠른 경우, 차트는 사용자가 입력을 멈춘 후에만 다시 렌더링 된다.
  • useDeferredValue는 <Suspense>와 통합된다. 새 값으로 인한 백그라운드 업데이트로 인해 UI가 일시 중단되면 사용자에게 폴백이 표시되지 않는다. 데이터가 로드될 때까지 기존의 지연된 값이 계속 표시된다.
  • useDeferredValue는 그 자체로 추가 네트워크 요청을 방지하지 않는다.
  • useDeferredValue 자체로 인한 고정된 지연은 없다. 리액트가 원래의 리렌더링을 완료하자마자 리액트는 즉시 새로운 지연된 값으로 백그라운드 리렌더링 작업을 시작한다. 그러나 이벤트로 인한 업데이트(예: 타이핑)는 백그라운드 리렌더링을 중단하고 우선순위를 갖는다.
  • useDeferredValue로 인한 백그라운드 리렌더링은 화면에 커밋될 때까지 Effect를 실행하지 않는다. 백그라운드 리렌더링이 일시 중단되면 데이터가 로드되고 UI가 업데이트된 후에 해당 Effect가 실행된다.

사용법 

새 콘텐츠가 로드되는 동안 오래된 콘텐츠 표시하기 

컴포넌트의 최상위 레벨에서 useDeferredValue를 호출하여 UI의 일부 업데이트를 연기할 수 있다.

import { useState, useDeferredValue } from 'react';

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

 

초기 렌더링 시점에 지연된 값은 사용자가 제공한 값(Value)과 동일하다.

업데이트가 발생하면, 지연된 값은 최신 값보다 "뒤쳐지게" 된다. 리액트는 먼저 지연된 값을 업데이트하지 않은 채로 렌더링한 다음, 백그라운드에서 새로 받은 값으로 다시 렌더링을 시도한다.

 

이 예제에서는 검색 결과를 가져오는 동안 SearchResults 컴포넌트가 일시중단된다. "a"를 입력하고 결과를 기다린 다음 "ab"로 수정해보자. "a"에 대한 결과가 로딩 폴백으로 대체될 것이다.

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

export default function App() {
  const [query, setQuery] = useState('');
  return (
    <>
      <label>
        Search albums:
        <input value={query} onChange={e => setQuery(e.target.value)} />
      </label>
      <Suspense fallback={<h2>Loading...</h2>}>
        <SearchResults query={query} />
      </Suspense>
    </>
  );
}

 

일반적인 대체 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>
    </>
  );
}

 

quey가 즉시 업데이트되므로 input에 새 값이 표시된다. 그러나 deferredQuery는 데이터가 로드될 때까지 이전 값을 유지하므로 SearchResults는 잠시 동안 오래된 결과를 표시한다.

 

아래 예제에서 "a"를 입력하고 결과가 로드될 때까지 기다린 다음 입력을 "ab"로 편집해보자. 이제 새 결과가 로드될 때까지 일시 중단 폴백 대신 오래된 결과 목록이 표시되는 것을 확인할 수 있다.

 

import { Suspense, useState, useDeferredValue } from 'react';
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>
    </>
  );
}

 

콘텐츠가 오래되었음을 표시하기 

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

<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>
    </>
  );
}

 

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가 다를 때, 그리고 실제로 다른 시각적 출력을 표시해야 할 때 속도가 느리다는 것이다.

 

구체적으로, 주요 성능 문제는 input에 타이핑할 때마다 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의 리렌더링 속도가 빨라지지는 않는다. 하지만 키 입력을 차단하지 않도록 목록 리렌더링의 우선순위를 낮출 수 있다는 것을 리엑트에 알려준다. 목록은 입력보다 "지연"되었다가 "따라잡는다". 이전과 마찬가지로 리액트는 가능한 한 빨리 목록을 업데이트하려고 시도하지만, 사용자가 다시 입력하는 것을 차단하지는 않는다.