[React] 검색창 구현하기 - API 호출 최적화

Inu·2023년 8월 28일
0

검색창 구현하기

목록 보기
3/3

개요

검색어가 변경될 때마다 추천 검색어 API 요청을 보내면 네트워크 비용이 많이 발생할 것이다. 이러한 네트워크 요청을 줄이는 것도 최적화 방법이 될 수 있다. 그렇다면 검색어 입력으로 보내지는 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번이 더 직관적이라 선호한 이유도 있다.

useFetch 커스텀 훅

구현 과제이기 때문에 검색창만 만들고 추천 검색어를 불러오는 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 };
}
  • 패칭한 데이터의 타입을 모르니까 제네릭으로 선언할 수 있게 했다.
  • 데이터 로딩 여부를 나타내는 state도 하나 만들었다.
  • fetcher는 프로미스를 반환하는 API 요청 로직이 있는 함수다.
  • API 요청 전에 로딩 중임을 표시하고 성공 여부에 상관없이 요청이 끝나면 로딩이 끝났음을 표시하기 위해 finally 블록을 추가했다.
  • queryKey, cache는 캐싱에 필요한 매개 변수다.

추천 검색어도 서버 데이터고 캐싱이 필요하기 때문에 React-Query로 관리하기 좋을 것 같은데 과제 조건에 React-Query를 사용하지 말라고 했다. 아직 사용해본 적은 없지만 요새 점점 사용하는 곳이 늘어나는 핫한 라이브러리이기 때문에 한번 공부하고 적용해봐야겠다. 😁

profile
될때까지 해보기

0개의 댓글