검색어가 변경될 때마다 추천 검색어 API 요청을 보내면 네트워크 비용이 많이 발생할 것이다. 이러한 네트워크 요청을 줄이는 것도 최적화 방법이 될 수 있다. 그렇다면 검색어 입력으로 보내지는 API 요청에 대해서는 어떻게 최적화를 적용할 수 있을까?
이전에 했던 무한 스크롤을 구현하기 위해 스크롤 이벤트를 적용하고 최적화를 위해 스로틀을 적용한 것과 유사하다. 특정 이벤트에 대응하는 핸들러를 매번 실행하지 않고 특정 조건을 설정해서 실행하는 것이다.
검색어의 경우, 사용자는 입력하고자 하는 검색어가 입력됐을 때 추천 검색어를 보기를 기대할 것이다. 그렇다면 일정 주기마다 핸들러를 실행하기 보다는 사용자 입력이 끝났을 때 핸들러를 실행해주면 되지 않을까? 그렇다면 디바운스를 적용하는 게 맞을 것 같다.
여기서 2가지 방법이 떠올라서 고민을 했다.
1번은 API 요청 자체에 디바운스를 적용하는 것으로 가장 일반적인 방법이다. debounce
는 디바운스 로직이 구현된 임의로 만든 함수다.
const [keyword, setKeyword] = useState('');
useEffect(() => {
debounce(() => fetchRecommendKeywords(keyword), 200);
}, [keyword]);
2번은 검색어 state에 디바운스를 적용하는 것이다.
useDebouncedValue
는 전달 받은 인자에 디바운스을 적용한 값을 반환하는 커스텀 훅이다. 이 훅으로 반환된 값이 변하면 API를 요청한다.
const [keyword, setKeyword] = useState('');
const defferedKeyword = useDebouncedValue(keyword, 200);
useEffect(() => {
fetchRecommendKeywords(defferedKeyword);
}, [defferedKeyword]);
처음에 시간이 부족해서 냅다 2번으로 하긴 했는데 리팩토링을 하면서 어떤 방법을 쓰는 것이 더 좋을지 고민을 해보았다.
1, 2번 모두 코드 상에서는 useEffect에 추천 검색어를 패치하는 로직만 넣었지만 패치한 결과를 저장하는 로직도 필요하다. 그러면 추천 검색어에 대한 state를 만들어서 불러온 추천 검색어로 업데이트하게 된다. 그러면 1, 2번 코드가 밑과 같이 각각 바뀐다.
// 1번
const [keyword, setKeyword] = useState('');
const [recommendKeywords, setRecommendKeywords] = useState<string[]>([]);
useEffect(() => {
debounce(() => {
fetchRecommendKeywords(keyword)
.then((res) => setRecommendKeywords(res.data));
}, 200);
}, [keyword]);
// 2번
const [keyword, setKeyword] = useState('');
const [recommendKeywords, setRecommendKeywords] = useState<string[]>([]);
const defferedKeyword = useDebouncedValue(keyword, 200);
useEffect(() => {
fetchRecommendKeywords(defferedKeyword)
then((res) => setRecommendKeywords(res.data));
}, [defferedKeyword]);
검색어가 변할 때마다 추천 검색어를 패치하는 구조라면 커스텀 훅으로 분리할 수도 있을 것 같았다. API 요청에 axios를 사용하기 때문에 요청을 보내면 프로미스가 반환되므로 이를 커스텀 훅 내부에서 처리해서 원하는 데이터만 반환하는 형식으로 만들 수 있다.
예를 들어 아래의 useFetch
와 같이 프로미스를 반환하는 콜백 함수를 인자로 받아 프로미스를 resolve한 state를 반환하는 커스텀 훅을 만들 수 있을 것이다.
const [keyword, setKeyword] = useState('');
const defferedKeyword = useDebouncedValue(keyword, 200);
const recommendKeywords = useFetch(() => fetchRecommendKeywords(defferedKeyword));
1번 방법을 사용하면 커스텀 훅으로 분리하기가 어려울 것 같아서 2번 방법으로 하기로 결정했다. 개인적으로 2번이 더 직관적이라 선호한 이유도 있다.
구현 과제이기 때문에 검색창만 만들고 추천 검색어를 불러오는 API만 호출하긴 하지만 범용적으로 사용할 수 있을만한 useFetch
훅을 만들어보기로 했다.
import { useEffect, useState } from 'react';
import { CacheMap } from '@/utils/cache';
export function useFetch<K>(
fetcher: (...params: any) => Promise<K>,
queryKey?: string,
cache?: CacheMap<K>,
initialState?: K,
) {
const [data, setData] = useState<K | undefined>(initialState);
const [isFetching, setIsFetching] = useState(false);
useEffect(() => {
if (cache && queryKey) {
const cachedData = cache.get(queryKey);
if (cachedData) {
setData(cachedData.data);
return;
}
}
setIsFetching(true);
fetcher()
.then((res) => {
setData(res);
if (cache && queryKey) {
cache.set(queryKey, res);
}
})
.finally(() => setIsFetching(false));
}, [fetcher, cache, queryKey]);
return { data, isFetching };
}
fetcher
는 프로미스를 반환하는 API 요청 로직이 있는 함수다.queryKey
, cache
는 캐싱에 필요한 매개 변수다.추천 검색어도 서버 데이터고 캐싱이 필요하기 때문에 React-Query로 관리하기 좋을 것 같은데 과제 조건에 React-Query를 사용하지 말라고 했다. 아직 사용해본 적은 없지만 요새 점점 사용하는 곳이 늘어나는 핫한 라이브러리이기 때문에 한번 공부하고 적용해봐야겠다. 😁