Post

React에서 useDeferredValue 사용해 보기

개인 프로젝트를 리팩토링하기 위해 리액트 공식 문서를 살펴보던 중 useDeferredValue 훅을 발견했습니다. 이 훅은 제 프로젝트의 검색 기능의 useDebounce 훅과 비슷한 역할을 하는 것 같아, 오늘은 useDeferredValue의 기능과 사용법, 그리고 저의 개인 프로젝트에 적용할 수 있을 지를 고민한 내용을 소개하고자 합니다.

이 글은 2024-05-24 에 업데이트 되었습니다.

이 글은 react, typescript 에 대한 기본지식을 알고 있어야 합니다.

useDeferredValue 란?

useDeferredValue는 UI의 일부 업데이트를 지연시킬 수 있는 React Hook입니다. 기본적으로 리액트는 아래의 코드처럼 상태 변경이 일어나면 가상의 돔을 비교하여 즉시 리랜더링 시키게 됩니다.

1
2
3
4
5
6
7
8
9
10
11
import { useState } from "react";

function Search() {
  const [query, setQuery] = useState("");

  const handleChange = (e) => {
    setQuery(e.target.value);
  };

  return <input value={query} onChange={handleChange} />;
}

이렇게 값을 즉시 변경하면 좋은 점도 있지만 검색 기능과 같은 값이 변경될 때 마다 api를 호출하는 기능을 구현할 경우 사용자 경험 및 서버에 의도지 않은 요청을 계속해서 보낼 수 있게 됩니다.

이러한 문제를 해결하기 위해서 보통은 useDebounce 라는 커스텀 훅을 활용하여 UI의 일부 업데이트를 지연시킨 후 호출하는 방법을 많이 사용하였습니다. 이 방법은 이전에 제가 블로그에 글을 정리해 두었기 때문에 아래의 링크를 통해 확인하실 수 있습니다.

react에서 useDebounce 사용하기

하지만 useDeferredValue 훅을 사용하면 이와 비슷한 기능을 아주 간단하게 구현할 수 있었습니다.

사용법

사용법은 매우 간단합니다. useDeferredValue 라는 훅을 호출하고 이전에 state로 호출 된 값을 넣어주면 됩니다. 단 최 상위에서 호출하고 값을 보낼 때는 자식 컴포넌트에 props 로 전달해야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { useState, useDeferredValue } from "react";

function SearchPage() {
  const [query, setQuery] = useState("");
  const deferredQuery = useDeferredValue(query);

  const handleChange = (e) => {
    setQuery(e.target.value);
  };

  return (
    <>
      <input value={query} onChange={handleChange} />
      <SearchResult searchValue={deferredQuery}>
    </>
  );
}

파라미터

  • value: 지연시키려는 값으로 어떤 number, string 등 다양한 타입을 사용할 수 있습니다.
  • Canary only option initialValue : 두 번째 파라미터는 Canary 버전에서 사용할 수 있는 값으로 초기화 값을 설정할 수 있습니다.

동작원리

예를 들어 사용자가 input“ab” 라는 값을 입력할 경우

React는 새 쿼리(“ab”)로 다시 렌더링하지만 이전 deferredQuery(이전에 사용하던 값인 "")를 사용합니다.

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

이전에 사용하던 커스텀 useDebounce 를 useDeferredValue로 리팩토링 하기

이제 이전에 codesandboxuseDebounce를 이용하여 간단한 나라 이미지 검색 프로젝트를 useDeferredValue로 리팩토링 해 보겠습니다.

기존에는 최 상위의 컴포넌트인 App.tsx 에서 useDebounce 훅을 불러와서 모든 것을 처리 했지만 useDeferredValue는 최 상위 컴포넌트에서 값을 자식 컴포넌트로 전달해야 사용이 가능합니다. 따라서 SearchResult라는 컴포넌트에 값을 props로 받아와 api요청을 실행하였습니다.

바뀐 코드는 아래와 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// App.tsx

import "./App.css";
import { useState, useDeferredValue } from "react";
import BackToTheTop from "./back-to-the-top";
import SearchResult from "./search-result";

function App() {
  const [searchValue, setSearchValue] = useState < string > "";
  // 지연 시킬 값
  const deferredSearchValue = useDeferredValue(searchValue);

  return (
    <>
      <BackToTheTop />
      <h1>Hello Debounce!</h1>
      <input
        type="text"
        placeholder="나라 이름을 입력하세요 ex) usa"
        value={searchValue}
        onChange={(e) => setSearchValue(e.target.value)}
      />
      <SearchResult searchValue={deferredSearchValue} />
    </>
  );
}

export default App;

분리된 SearchResult 컴포넌트에서는 App컴포넌트로부터 지연된 값을 받아와서 api요청을 하는 코드 입니다. 또한 같은 값을 입력했을 경우 리 랜더링을 방지하기 위해 memo를 사용하였습니다. 이렇게 하면 useDebounce라는 훅을 작성하지 않아도 되고, 컴포넌트를 분리시키는 것도 명확하게 할 수 있어서 좋았습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// SearchResult.tsx
import { useState, useEffect, memo } from "react";

const SearchResult = memo(({ searchValue }: { searchValue: string }) => {
  const [isLoading, setIsLoading] = useState(false);
  const [isError, setIsError] = useState(false);
  const [imageInfo, setImageInfo] = useState < any > [];

  useEffect(() => {
    const fetchData = async () => {
      setIsLoading(true);

      if (!searchValue) {
        setImageInfo([]);
        setIsLoading(false);
        return;
      }

      try {
        const res = await fetch(
          `https://restcountries.com/v3.1/name/${searchValue}`
        );
        if (!res.ok) {
          const errorData = await res.json();
          throw new Error(errorData.message || "Something went wrong");
        }
        const countryData = await res.json();
        setImageInfo(countryData);
        setIsError(false);
      } catch (error) {
        console.error("API 호출 에러:", error);
        setImageInfo([]);
        setIsError(true);
      } finally {
        setIsLoading(false);
      }
    };

    fetchData();
  }, [searchValue]);

  if (isError) {
    return <h2>Something Error!</h2>;
  }

  if (isLoading) {
    return <h2>Loading...</h2>;
  }

  return (
    <ul>
      {imageInfo.length === 0 && (
        <h2>"{searchValue}"로 검색된 나라가 없습니다!</h2>
      )}
      {imageInfo?.map((el: any) => (
        <img
          key={el.flags.alt + el.flags.png}
          src={el.flags.png}
          alt={el.flags.alt}
        />
      ))}
    </ul>
  );
});

export default SearchResult;

결과 확인하기

CodeSandbox에서 결과를 확인하실 수 있습니다. 다양한 나라를 입력해 보세요

useDebounce와의 차이점

useDebounce 훅은 목록을 업데이트하기 전에 사용자가 입력을 멈출 때까지(예: 1초 동안) 기다린다는 의미로 사용자가 컴포넌트를 의도적으로 지연시킬 수 있어 매우 편리하게 사용할 수 있습니다. 하지만 리 랜더링 최적화에는 적합하지 않을 수 있습니다.

useDeferredValueReact 자체에서 만든 훅이기 때문에 컴포넌트를 지연할 시 렌더링 최적화에 더 적합합니다. 하지만 지연 시간을 임의로 선택할 수 없고 네트워크 요청이 지연되는 시간동안 추가 네트워크 요청을 방지하지는 않다고 공식 문서에 쓰여 있었습니다.

특성useDebounceuseDeferredValue
목적사용자 입력 후 일정 시간 동안 대기React에서 렌더링 최적화를 위해 제공
제공사용자 정의 훅 (custom hook)React 내장 훅
지연 시간 설정가능 (예: 1초)불가능
렌더링 최적화덜 최적화됨더 최적화됨
네트워크 요청 제어입력 지연 동안 추가 네트워크 요청 방지네트워크 요청 지연 시간 동안 추가 요청 방지 안함
사용 예시검색 입력 필드, 자동완성 기능복잡한 계산이나 트리거된 UI 업데이트 지연

위는 useDebounceuseDeferredValue의 차이점을 표로 나타낸 결과 입니다.

결론

저의 프로젝트에서는 검색 입력 필드에서 rapid.api를 사용하여 데이터를 불러오기 때문에, 불필요한 API 요청이 추가 요금으로 이어질 수 있습니다. 따라서 useDeferredValue를 사용하기에는 적합하지 않았습니다. 그러나 간단한 내용을 불러오거나, 중요하지 않은 API 요청이 없는 경우에는 useDeferredValue를 사용하는 것이 좋다는 결론을 내렸습니다.

참고 사이트

https://react.dev/reference/react/useDeferredValue

This post is licensed under CC BY 4.0 by the author.