React에서 debounce를 사용하여 검색 기능 구현하기

baegyeong·2024년 5월 5일

react

목록 보기
1/5
post-thumbnail

요즘 전대위키 리팩토링을 진행하며 검색 기능이 내가 원하는대로 구현이 안 되어 있다는 것을 알게 되었다.

원래 내 의도는 검색창에 매 입력마다 API 요청을 하는 것이 아니라 디바운스를 적용해서 이 요청 횟수를 줄이고 싶었다.
하지만 코드를 살펴보니.. 디바운스를 적용하여 짜놓은 코드는 실질적으로 쓰이지 않고 useQuery로 매번 입력값에 따라 서버에서 상태를 가져오는 로직이 동작하고 있던 것이다.
아무래도 useQuery, 디바운스 더 나아가 리액트에 대한 이해가 부족했던 것 같다.

검색기능을 리팩토링한 코드를 살펴보자 🙌

디바운스 라이브러리

lodash의 debounce를 사용했다.

import debounce from "lodash/debounce"

검색어 입력을 디바운스로 제어

  const [keyword, setKeyword] = useState("");

  const debouncedSearch = debounce((value) => {
    setKeyword(value);
  }, 200);

사용자가 검색창에 입력하는 어떤 value 값을 0.2초마다 keyword라는 상태에 저장한다.

keyword를 서버에 요청 보낼 것이기 때문에 keyword를 매 입력마다 갱신하는 게 아니라 0.2초마다 저장되게 하여 검색 API 호출 횟수를 줄인다.

useQuery를 사용하여 서버에서 데이터 가져오기

  const { data, isLoading, isError } = useQuery(
    ["search_docs", keyword],
    () => searchDocs(keyword),
    {
      enabled: !!keyword,
      select: (data) => data?.data?.response,
      staleTime: 6 * 10 * 1000,
      cacheTime: 6 * 10 * 1000,
      retry: 0,
    }
  );

위에서 저장한 상태인 keyword를 검색API(searchDocs)에 쿼리스트링으로 넘긴다.

useQuery의 각 옵션에 대해 살펴보자면,

  • enabled: keyword에 값이 있을 때만(true) API 요청을 보낸다.

  • select: 이 검색결과를 사용하는 곳에서 데이터를 쉽게 다루기 위해 미리 데이터를 가공하여 넘긴다.

  • staleTime: 데이터가 새로운 것으로 간주되는 시간으로, 1분으로 설정했다.

  • cacheTime: 캐시된 데이터가 메모리에 유지되는 시간으로, 1분으로 설정했다.

  • retry: 쿼리가 error를 throw할 때 데이터를 재요청하는 횟수로, 0으로 설정했다.

    • 검색 컴포넌트 측에서 isError의 상태에 따라 검색결과가 없다는 것을 보여주려고 했다. 검색 결과가 없을 때 빠르게 사용자에게 알려주면 좋을 것 같은데, 그 화면을 보여주기까지 너무 오래걸렸다.
    • 찾아본 결과 retry라는 옵션이 있어서, 이것을 0으로 설정하니 검색 결과가 없을 때 빠르게 사용자에게 띄워주는 것을 확인할 수 있었다.

검색 커스텀훅

위의 코드를 커스텀훅에 구현해뒀다.

import { useQuery } from "@tanstack/react-query";
import debounce from "lodash/debounce";
import { useState } from "react";
import { searchDocs } from "@/services/document";

export const useSearchQuery = () => {
  const [keyword, setKeyword] = useState("");

  const debouncedSearch = debounce((value) => {
    setKeyword(value);
  }, 200);

  const { data, isLoading, isError } = useQuery(
    ["search_docs", keyword],
    () => searchDocs(keyword),
    {
      enabled: !!keyword,
      select: (data) => data?.data?.response,
      staleTime: 6 * 10 * 1000,
      cacheTime: 6 * 10 * 1000,
      retry: 0,
    }
  );

  return { debouncedSearch, data, isLoading, isError };
};

검색 컴포넌트

import { Link } from "react-router-dom";
import { useSearchQuery } from "@/hooks/useSearchQuery";
import Loader from "../common/layout/Loader";
// ...

const SearchBar = ({ isDisplay }) => {
  
  // ...
  
  const { debouncedSearch, data, isLoading, isError } = useSearchQuery();
  const keyword = focusRef?.current?.value;

  return (
    <>
    	// ...
        {clickedSearch && (
          <StyledSearchResult ref={searchRef}>
            {data?.length > 0 &&
              data.slice(0, 8).map((el) => (
                <SearchItem key={el.docsId}>
                  <Link to={`/document/${el.docsId}`}>{el.docsName}</Link>
                </SearchItem>
              ))}
            {isError && <p>검색 결과가 없어요.</p>}
            {keyword !== "" && isLoading && <StyledLoader />}
          </StyledSearchResult>
        )}
        <StyledSearchBar
          type="search"
          onChange={(e) => debouncedSearch(e.target.value)}
          // ...
        />
    </>
  );
};

export default SearchBar;

StyledSearchBar에서 onChange가 감지하는 value값을 debouncedSearch 에 넘겨 검색어에 대해 디바운스를 적용한다.

data의 길이가 0보다 크다면 data를 하나씩 렌더링한다. (임의로 8개씩 뿌렸다.)
isError 가 true일 때는 검색결과가 없다는 것을 알린다.
isLoading 가 true일 때는 Loader 컴포넌트를 렌더링한다.
이때 사용자가 검색창에 단어를 입력하다가 전부 지웠을 때, 즉 처음으로 돌아간 상태일 때에도 isLoading이 true 상태라서 Loader가 렌더링되는 게 어색하다고 느껴졌다. 따라서 검색창의 value가 있을 때(keyword!=="")의 조건도 추가했다.

0개의 댓글