요즘 전대위키 리팩토링을 진행하며 검색 기능이 내가 원하는대로 구현이 안 되어 있다는 것을 알게 되었다.
원래 내 의도는 검색창에 매 입력마다 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 호출 횟수를 줄인다.
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의 상태에 따라 검색결과가 없다는 것을 보여주려고 했다. 검색 결과가 없을 때 빠르게 사용자에게 알려주면 좋을 것 같은데, 그 화면을 보여주기까지 너무 오래걸렸다.위의 코드를 커스텀훅에 구현해뒀다.
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!=="")의 조건도 추가했다.