리액트에서 비동기통신을 쉽게 동작하게 해주는 라이브러리이다.
리액트 쿼리를 사용하기 전에는 주로 redux의 미들웨어를 api 비동기 통신에 활용했었다.
리덕스는 전역상태관리 툴임에도 불구하고 미들웨어 통신에서도 강력한 성능을 보여주었지만 오히려 그것이 리덕스를 더 사용하기 어렵게 만들고 전역상태관리가 굳이 필요하지 않은 상황에서도 리덕스에 의존하는 모양새가 많이 나오게 되었다.
이번에 리액트 쿼리를 도입한 이유는 리덕스에서 미들웨어를 분리하기 위함은 아니었고 Data polling 방식의 프로젝트를 진행하는데 redux는 데이터 폴링에는 적합하지 않다고 판단했기 떄문이다.
그런데 사용해보니 리덕스만 사용할때에 비해 api 통신을 다루는 과정이 굉장히 간소화되었고 직관적으로 변경되어서 블로그에 사용해본 후기를 작성하기로 했다.
기존에는 리덕스 툴킷을 사용하여 전역 상태 관리가 필요하지 않은 상황에서도 slice를 만들고 createAsyncThunk를 이용해 미들웨어를 설정하고 extraReducer를 만들어서 store에 저장하고 dispatch함수와 useSelector 함수를 통해 state에 저장하고 가져오는 굉장히 복잡한 과정을 거쳐야했다.
리액트 쿼리에서는 이 모든 과정 없이 데이터를 불러올 수 있다.
useQuery() hook 만 사용하면 데이터를 불러오는 것은 물론 loading, 성공, 에러처리 등 셀수도 없이 많은 기능들을 사용할 수 있다.
import { useQuery } from "react-query";
const { data, isLoading, error } = useQuery(queryKey, queryFn, options)
기본 골격은 이러하다.
querykey에 key를 적어서 해당 쿼리의 key를 설정할 수 있고 queryFn에서 axios, fetch api 등으로 통신 요청을 보낼 수 있다. options에서는 페이지를 벗어났다가 돌아오면 다시 fetching 시켜주는 기능, 폴링을 위한 refetchInterval 등의 기능들이 제공된다.
리액트 쿼리를 통해 get 요청을 보내기 위해서 useQuery를 사용하면 편하다는 것은 알겠는데 get 요청이 필요한 파일안에 useQuery를 사용하고 data를 받아서 뿌려주는 부분이 파일의 가독성을 떨어뜨리는건 아닐까 하는 생각이 들었다.
내가 작성한 코드를 봐도 컴포넌트 단위로 떨어지는 예쁜 모양새에서 쿼리 코드가 포함되니 지저분해보였다.
그래서 useQuery를 훅으로 분리해 가져다쓰면 한줄만 입력하면 되니 조금 더 깔끔해지지 않을까하는 생각이 들어서 실행으로 옮겨보았다.
먼저 src 폴더에 hooks라는 폴더를 만들고 그 안에서 관리했다.
마인드맵의 정보를 가져오는 코드의 일부이다.
import { URL } from '../API';
import { useQuery } from 'react-query';
const fetchNodeList = async nodeTableId => {
if (!nodeTableId) return;
const { data } = await URL.get(`/node/all/${nodeTableId}`);
return data;
};
export const useNode = nodeTableId => {
return useQuery(['node', nodeTableId], () => fetchNodeList(nodeTableId), {
enabled: !!nodeTableId,
refetchInterval: 2000,
});
};
useNode라는 커스텀 훅을 만들었다. 'node'라는 이름의 쿼리키를 설정하고 fetchNodeList 라는 쿼리함수를 통해 get 요청을 받아온다. 요청을 보낼때 필요한 파라미터를 만들어줘서 useNode를 사용하는 파일에서 파라미터 값을 넣어주면 실행되도록 작성했다.
import { useNode } from '../../hooks/useNode';
const { status, data: nodeList, error, isFetching } = useNode(nodeTableId);
그 결과 위의 코드만으로 useQuery를 실행시킬 수 있게 되었다.
get 해온 data를 콜백으로 받아오면 쿼리의 status 값에 맞추어 원하는 정보를 보여줄 수 있다.
그리고 refetching 될 때 해당 화면만 교체하여 보여주기 때문에 리액트의 virtual DOM을 좀 더 현명하게 활용할 수 있다.
const renderByStatus = useCallback(() => {
switch (status) {
case 'loading':
return <div>loading</div>;
case 'error':
if (error instanceof Error) {
return <span>Error: {error.message}</span>;
}
break;
default:
return (
<>
{nodeList?.map(data => (
<Square
key={data.nodeId}
width={data.width}
height={data.height}
radius={data.radius}
color={data.color}
fontColor={data.fontColor}
fontSize={data.fontSize}
text={data.text}
xval={data.xval}
yval={data.yval}
nodeId={data.nodeId}
nodeTableId={data.nodeTableId}
isChecked={data.isChecked}
/>
))}
</>
);
}
}, [status, isFetching]);
useQuery는 API의 get 요청을 받아올때만 사용할 수 있다. 다른 요청을 보낼때도 사용해도 되는지는 잘 모르겠지만 나머지 요청에 대한 부분은 리액트 쿼리 공식 문서에서 useMutation을 사용하라고 적혀있었다.
useMutation을 사용해 post 요청을 보내는 코드는 이러하다.
const searchMutation = useMutation(sendingData => {
URL.post(`/project/searching`, sendingData)
});
const inputEnter = e => {
if (e.keyCode === 13) {
searchMutation.mutate(sendingData);
}
};
검색 기능에서 사용한 useMutation 함수를 가져왔다.
useMutation을 사용하여 어떤 데이터를 보내고 어디로 보내는지만 설정해주면 .mutate 함수를 통해 쉽게 사용할 수 있다.
그렇다면 post 요청을 보내고 받아온 response는 어떻게 확인할까?
잠시 리덕스의 얘기를 해보면 createAsyncThunk를 통해 post요청을 보내면 받은 response를 payload에 담아 상태에 저장해서 다른 파일에서 불러올 수 있었다.
export const postNode = createAsyncThunk(
"node/post",
async(_, { rejectWithValue}) => {
try {
return await URL.post("/node", _).then((response) => response.data.nodeInfo);
} catch (error) {
console.error(error);
return rejectWithValue(error.response);
}
}
)
그렇다면 상태관리 없이 useMutation으로 받은 데이터를 다른 js파일에 전달하는 방법이 없을까?
그것을 가능하게 해주는 것이 setQueryData이다.
const searchMutation = useMutation(sendingData => {
URL.post(`/project/searching`, sendingData).then(res => {
queryClient.setQueryData(['searchResult'], res);
});
});
위에 있던 코드에서 .then() 부분이 추가되었다.
response로 받은 데이터를 setQueryData를 통해 쿼리키를 설정해주고 데이터로 넣어주면 끝이다.
initialState를 설정할 필요도 없고 store에 구독시켜줄 필요도 없다.
쿼리키만 있으면 원하는 곳 어디든 렌더링 해줄 수 있다.
검색API가 실행되는 페이지와 결과를 보여주는 페이지는 나눠져있다.
하지만 상태관리없이도 데이터를 반영할 수 있다.
const { data: searchResult } = useQuery(['searchResult'], () => {});
해당 코드만 원하는 위치에 추가해주면 데이터를 받을수 있다. 쿼리키를 통해 값을 받아온 것이다.
뒤에 있는 빈 함수는 setQueryData를 보낼때 쿼리함수를 전달하지 않았지만 받아오는 곳에서 쿼리함수가 없으면 warning 메시지를 띄워서 넣어준 함수이다.
일단 지금 당장 사용해본 리액트 쿼리의 기능은 이정도인것 같다.
더 많은 기능들이 많이 있지만 지금 당장 소개하기는 시기상조인듯 하다