(React) React Query를 이용해서 검색창 구현하기

호두파파·2022년 2월 27일
27

React

목록 보기
31/39


원티드 코드스테이츠 프리온보딩 1주차 2번째 과제는 코로나 확진으로 인해 홀로 진행하게 되었다.

병가로 인정받을 수 있어서 굳이 과제에 참가하지 않아도 되었지만, 과제의 난이도가 낮고 마침 리액트 쿼리를 공부한차라 혼자 제작해보기로 결정했다.

🤯 요구조건

  • 함수형 컴포넌트
  • API 호출 최적화 (advance)
  • 키보드만으로도 추천 검색어들로 이동이 가능
  • 웹에서 바로 사용할 수 있도록 배포

API 호출 횟수 최적화하기

키보드를 클릭하거나, 롤 스크롤을 움직일때마다 컴포넌트가 랜더링된다면 혹은 함수가 발동한다면 크나큰 네트워크 리소스 낭비로 이어질 수 있을 것이다.

이것을 방지하고자 사용하는 두 가지 개념이 있는데 바로 그것은 쓰로틀링디바운싱이다.

쓰로틀링과 디바운싱

쓰로틀링

쓰로틀링은 마지막 함수가 호출된 후 일정시간이 지나기 전에 다시 호출되지 않도록 하는 것을 말한다. 주로 스크롤 액션에서 과도한 리랜더링을 피하고자 사용한다.

디바운싱

연이어 호출된 함수들 중 가장 마지막에 호출된 함수만 호출하도록 하는 것을 말한다.

위 두 개념 모두 underScore(_)라이브러리sk `loadash를 이용하면 쉽게 사용할 수 있다.

이중 나는 검색어를 입력하기 위해 검색어를 치더라도, 검색어의 마지막 호출이 끝난 순간 스테이트가 업데이트될 수 있도록 onChage 함수 바깥에 디바운싱 함수를 한 번 더 감싸줄 것이다.

// 디바운싱 함수 라이브러리없이 구현하기 
const debounce = (callback, duration) => {
  let timer; 
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => callback(...args), duration)
  };
};

App.js

import React, { useState, useCallback } 
import { QueryClientProvider, QueryClient } from 'react-query';
import { debounce } from './util/util';

const queryClient = new QueryClient();

const App = () => {
  const [inputValue, setInputValue] = useState('');

  const onChangeInput = useCallback(
    e => {
      debounce(setInputValue(e.target.value), 500);
    },
    [inputValue],
  );

  return (
    <QueryClientProvider client={queryClient}>
      <GlobalStyle />
      <Layout>
        <Container>
          <HeadLine />
          <InputField onChangeInput={onChangeInput} inputValue={inputValue} />
          <br />
          <ResultField inputValue={inputValue} setInputValue={setInputValue} />
        </Container>
      </Layout>
    </QueryClientProvider>
  );
};

export default App;

로컬캐싱을 직접 구현하면 좋았겠지만, 리액트 쿼리를 사용해서 server state를 관리하는 것을 체험해보고 싶었기에 리액트 쿼리를 도입했다. 별도의 코드를 작성하지 않아소 로컬캐싱을 구현해준다는 점에 큰 장점을 갖는다.

사용자가 인풋창에 입력할때, useCallback으로 감싸진 onChangeInput 함수를 호출하게 되는데, 타이핑될때마다 함수가 호출되면 리소스에 크나큰 낭비가 생기기 때문에, 콜백함수를 다시 util함수로 작성해두었던 debounce 함수로 감싸주었다.

const onChangeInput = useCallback(
    e => {
      debounce(setInputValue(e.target.value), 500);
    },
    [inputValue],
  );

🌟 useCallback
주로 렌더링 성능을 최적화해야 하는 상황에서 사용한다.
디펜던시로 등록된 어떤 값이 바뀌었을때만 새로 함수를 생성할 수 있도록 함수를 재사용한다.
useMemo와 useCallback


util.js

import axios from 'axios';
import { useQuery } from 'react-query';

const getResultByKeyword = async keyword => {
  const { data } = await axios.get(
    `API주소/name=${keyword}}`,
  );
  return data;
};

export const useResults = keyword => {
  return useQuery(['keyword', keyword], () => getResultByKeyword(keyword), {
    enabled: !!keyword,
    select: (data) => data.slice(0, 10),
  });
};

api 서버와 통신을 하기 위해 util함수를 작성했고, 데이터 반환을 위한 비동기 처리는 useQuery 이용했다.

useQuery의 3번째 파라미터로 options를 등록할 수 있는데, enabled옵션을 함수의 파라미터로 전달되는 keyword가 있을때만 쿼리를 반환하라는 옵션이고, select옵션은 반환되는 쿼리 데이터를 가공할 수 있도록 하는 옵션이다.

나는 검색 결과를 10개 이내에서만 보여주고 싶었기에 slice 함수를 사용해 response 데이터를 가공해주었다.


ResultField.js

import React from 'react';
import styled from 'styled-components';
import { useQueryClient } from 'react-query';
import { useResults } from '../util/util';

const ResultField = ({ inputValue, setInputValue }) => {
  const queryClient = useQueryClient();

  const { status, data, error } = useResults(inputValue);

  const onHandleList = name => {
    setInputValue(name);
  };

  const getDataByStatus = () => {
    switch (status) {
      case 'loading':
        return <div>Loading</div>;
      case 'error':
        return <span>Error: {error.message}</span>;
      default:
        return (
          <ul>
            <ResultHeader>추천 검색어</ResultHeader>
            {data?.map(item => {
              return (
                <SearchedItem
                  key={item.id}
                  value={item.name}
                  onClick={() => onHandleList(item.name)}
                >
                  {item.name}
                </SearchedItem>
              );
            })}
          </ul>
        );
    }
  };

  return data ? <Wrapper>{getDataByStatus()}</Wrapper> : null;
};

export default ResultField;

검색결과를 렌더링하는 컴포넌트이다. server state에서 관리되는 데이터를 case 상황에 맞는 요소와 함께 렌더링될 수 있도록 코드를 지원하고 있어 깔끔하게 코드를 작성할 수 있따.

loading과 error 상황을 제외하고, default case로 검색결과로 반환된 객체의 값을 배열화해 map함수로 렌더링해주었다.


구현 모습

잘 구현된다 😇

별도의 비동기 처리를 하지 않고 구현한 조원들의 결과물보다 훨씬 빠르게 렌더링되는 것을 확인할 수 있었다.

여러모로 리액트 쿼리를 사용해 server state를 관리하는 것이 편리하게 느껴지는 프로젝트였다.


profile
안녕하세요 주니어 프론트엔드 개발자 양윤성입니다.

0개의 댓글