원티드 코드스테이츠 프리온보딩 1주차 2번째 과제는 코로나 확진으로 인해 홀로 진행하게 되었다.
병가로 인정받을 수 있어서 굳이 과제에 참가하지 않아도 되었지만, 과제의 난이도가 낮고 마침 리액트 쿼리를 공부한차라 혼자 제작해보기로 결정했다.
키보드를 클릭하거나, 롤 스크롤을 움직일때마다 컴포넌트가 랜더링된다면 혹은 함수가 발동한다면 크나큰 네트워크 리소스 낭비로 이어질 수 있을 것이다.
이것을 방지하고자 사용하는 두 가지 개념이 있는데 바로 그것은 쓰로틀링
과 디바운싱
이다.
쓰로틀링은 마지막 함수가 호출된 후 일정시간이 지나기 전에 다시 호출되지 않도록 하는 것을 말한다. 주로 스크롤 액션에서 과도한 리랜더링을 피하고자 사용한다.
연이어 호출된 함수들 중 가장 마지막에 호출된 함수만 호출하도록 하는 것을 말한다.
위 두 개념 모두 underScore(_)
라이브러리sk `loadash
를 이용하면 쉽게 사용할 수 있다.
이중 나는 검색어를 입력하기 위해 검색어를 치더라도, 검색어의 마지막 호출이 끝난 순간 스테이트가 업데이트될 수 있도록 onChage
함수 바깥에 디바운싱 함수를 한 번 더 감싸줄 것이다.
// 디바운싱 함수 라이브러리없이 구현하기
const debounce = (callback, duration) => {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => callback(...args), duration)
};
};
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
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 데이터를 가공해주었다.
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를 관리하는 것이 편리하게 느껴지는 프로젝트였다.