도입 계기
입사 후 첫 프로젝트에서는 서버에서 받아오는 비동기 데이터들을 redux를 사용하지 않고 state로 관리하였고 그렇게 사용하다 보니 생기는 불편함이 몇 가지 있었습니다.
1. 컴포넌트 간에 데이터를 Props로 넘겨주어 사용하는 경우가 다반수였습니다.
2. 하나의 컴포넌트에서 사용된 api가 다른 컴포넌트에서도 필요할 때 api를 요청하는 부분이 중복으로 사용되는 경우가 있었습니다.
3. 비동기데이터가 컴포넌트 마다 흩어져있어 한번에 관리하기 어려웠습니다.
새롭게 시작하는 프로젝트에서는 비동기 데이터를 잘 관리하고 싶었고 전역상태관리로만 사용했던 redux를 미들웨어를 사용하여 써볼까 했지만 큰 기업들에서react-query를사용하고 있는 추세였기에react-query와을 선택하였습니다.
React Query
React Query는 Server State를 관리하는 라이브러리로 React 프로젝트에서 Server와 Client 사이 비동기 로직들을 손쉽게 다루게 해주는 도구입니다. 공식 문서에서 설명하는 Server State는 아래와 같습니다.
- Client에서 제어하거나 소유되지 않은 원격의 공간에서 관리되고 유지됨
- Fetching이나 Updating에 비동기 API가 필요함
- 다른 사람들과 공유되는 것으로 사용자가 모르는 사이에 변경될 수 있음
- 신경 쓰지 않는다면 잠재적으로 "out of date"가 될 가능성을 지님
React query는 server state를 관리해줄 뿐만 아니라 여러 가지로 프론트엔드 개발자의 작업을 줄여주는 기능 또한 제공합니다.
- update를 하고 나면 get data를 hook 자체에서 호출할 수 있습니다.
- 데이터가 오래되었다고 판단되면 다시 데이터를 요청해줍니다.
- 동일 데이터를 여러 번 요청하는 경우가 있다면 한 번만 요청합니다.
- 데이터를 요청하는 과정에서 에러가 난다면 지속적으로 요청합니다.
- 캐싱된데이터로 인해서 불필요한 API 콜을 줄여줄 수 있습니다.
- 비동기 과정을 unique key를 사용하여 선언적으로 관리할 수 있습니다.
- 무한 스크롤에 대한 데이터 처리를 해줍니다.
시작하기
npm install react-query
react-query 라이브러리를 설치한 후 index.tsx에 사용할 수 있도록 세팅합니다.
// src> index.tsx
import React from 'react';
import { createRoot } from 'react-dom/client';
import { QueryClientProvider, QueryClient } from 'react-query';
import { ReactQueryDevtools } from 'react-query/devtools';
import Router from './Router';
const queryClient = new QueryClient();
const container = document.getElementById('root');
const root = createRoot(container!);
root.render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<ReactQueryDevtools initialIsOpen={false} position="bottom-right" /> //devtools이므로 필수X
<Router />
</QueryClientProvider>
</React.StrictMode>
);
QueryClient : React Query가 Client에서 관리하는 Server State들을 Key를 통해 꺼내서 사용할 수 있습니다.
React Query는 API 요청을 Get 방식인 Query와 Post 방식인 Mutation 이렇게 두 가지 유형으로 나뉘어 있습니다.
저희는 카카오페이의 블로그(카카오페이 프론트엔드 개발자들이 React Query를 선택한 이유)를 참고하여 Custom Hook을 만들어 API의 상태를 불러왔습니다.
useQuery (Get)
// src> Queies> useTagData.ts
import { useQuery } from 'react-query';
import instance from '../../apis/ApiController';
import { API } from '../../apis/config';
export const QUERY_KEY = ['tagData'];
const fetcher = (): Promise<string[]> => {
return instance.get(`${API.hasTag}`).then(res => res.data);
};
const useTagData = () => {
return useQuery<string[]>(QUERY_KEY, fetcher, {
onError: () => {
console.error('userData error');
},
staleTime: 50000,
});
};
export default useTagData;
저희는 기존에 axios 인터셉터를 사용하여 에러를 처리하고 있기 때문에 fetcher 함수에 인터셉터를 그대로 사용하였습니다.
useQuery Hook의 사용법
const { data } = useQuery(
queryKey, // 이 Query의 Unique Key (외부에서도 쓸 수 있으므로 중복되지 않는 이름으로 지정을 해주어야함)
fetchFn, // api 데이터를 요청, Promise를 Return 하는 함수
options, // useQuery에서 사용되는 Option 객체
);
useQuery를 사용하여 받아온 데이터를 실제 컴포넌트에서 사용하는 법
import React from 'react';
import styled from 'styled-components';
import useTagData from '../../../Queries/Main/useTagData';
export default function Main() {
const { data: tagData } = useTagData();
return (
<Wrap>
<HashTag>
{tagData?.map((tag: string, idx: number) => (
<span key={idx}>{tag}</span>
))}
</HashTag>
</Wrap>
);
}
📌 데이터를 바로 요청하지 않기
위에서 사용한 예제는 로그인 유무에 상관없이 메인에 보이는 데이터 리스트를 get 하는 방법입니다. 로그인 상태에서만 요청할 수 있는 데이터는 페이지 로드 시 바로 요청하면 에러가 나고, 로그인 상태인지를 체크한 후 데이터를 요청해야 합니다. 또한 동기적으로 데이터를 처리해야 할 때, enabled 옵션을 사용합니다.
const fetcher = async (): Promise<Type_User> => {
return await instance
.get(`${API.user}`)
.then(res => res.data);
};
const useUserData = (condition: boolean = true) => {
return useQuery<Type_User>(['userData'], () => fetcher(), {
onError: () => {
console.error('userData error');
},
staleTime: 50000,
enabled: condition,
});
};
export default useUserData;
const { data:userData } = useUserData(isLogined); //state로 true, false값 보내주기
enabled의 기본값은 true로, true 상태일 때는 데이터를 받아오게 하는 것이고 false값을 주게 되면 true로 변경될 때까지는 query를 실행하지 않습니다.
📌 데이터를 새로 불러오기
글을 쓰거나 지우는 등 업데이트가 일어났을 때, 리스트를 다시 불러와야 하는 경우나 탭이나 버튼을 클릭했을 때 데이터를 새로 요청해야 하는 경우가 있습니다. 이때는 invalidateQueries를 사용하여 데이터를 업데이트시켜줍니다.
import { useQueryClient } from 'react-query';
const queryClient = useQueryClient();
const onClickMenu = () => {
queryClient.invalidateQueries('tabData');
};
invalidateQueries의 인자로는 요청하고자 하는 데이터의 unique key값을 입력하면 됩니다.
📌 + 다양한 옵션들
const {
data,
dataUpdatedAt,
error,
errorUpdatedAt,
failureCount,
isError,
isFetched,
isFetchedAfterMount,
isFetching,
isPaused,
isLoading,
isLoadingError,
isPlaceholderData,
isPreviousData,
isRefetchError,
isRefetching,
isStale,
isSuccess,
refetch,
remove,
status,
fetchStatus,
} = useQuery(queryKey, queryFn?, {
cacheTime,
enabled,
networkMode,
initialData,
initialDataUpdatedAt,
isDataEqual,
keepPreviousData,
meta,
notifyOnChangeProps,
onError,
onSettled,
onSuccess,
placeholderData,
queryKeyHashFn,
refetchInterval,
refetchIntervalInBackground,
refetchOnMount,
refetchOnReconnect,
refetchOnWindowFocus,
retry,
retryOnMount,
retryDelay,
select,
staleTime,
structuralSharing,
suspense,
useErrorBoundary,
})
useMutation (Post)
useMutation Hook의 사용법
import { useNavigate } from 'react-router-dom';
import { useMutation, useQueryClient } from 'react-query';
import { Type_EditProfile } from '../types/CommonTypes';
import instance from '../../apis/ApiController';
import { API } from '../../apis/config';
const fetcher = async (data: Type_EditProfile) => {
return await instance.post(`${API.updateProfile}`, data);
};
const useUpdateUserData = () => {
const queryClient = useQueryClient();
const navigate = useNavigate();
return useMutation(fetcher, {
onSuccess: () => {
queryClient.invalidateQueries("userData");
alert('프로필이 성공적으로 변경되었습니다.');
navigate('/mypage');
},
onError: err => {
console.error('check nickname error', err);
},
});
};
export default useUpdateUserData;
const { mutate } = useMutation(
mutationFn, // api 데이터를 요청, Promise를 Return 하는 함수
options, // useMutation에서 사용되는 Option 객체
);
useMutation은 useQuery와 다르게 Unique Key를 필수로 정의하지 않아도 됩니다.
실제 컴포넌트에서 useMutation을 요청하는 법
const { mutate } = useUpdateUserData();
const clickSave = () => {
const body = {
profileImg: profileImg,
bg: backgroundImg,
nick: nickVal,
info: descVal
};
mutate(body);
};
api에 넘겨주어야 하는 body 값을 mutate 선언 시 함께 지정해주면 됩니다.
📌 + 다양한 옵션들
const {
data,
error,
isError,
isIdle,
isLoading,
isPaused,
isSuccess,
failureCount,
failureReason,
mutate,
mutateAsync,
reset,
status,
} = useMutation({
mutationFn,
cacheTime,
mutationKey,
networkMode,
onError,
onMutate,
onSettled,
onSuccess,
retry,
retryDelay,
useErrorBoundary,
meta
})
mutate(variables, {
onError,
onSettled,
onSuccess,
})
🤔 새로운 기술의 도입으로 처음에는 두려움이 있었지만 막상 글로만 봤을 때랑 써 봤을 때는 이해되는 체감이 정말 달랐습니다. 생각보다 정말 편리하게 비동기 데이터를 관리할 수 있었고, 코드가 줄어들면서 깔끔하게 한눈에 파악하기 쉬웠습니다. 기본적인 사용법을 먼저 습득 후, react query에서 제공하는 옵션들을 공부한다면 react query를 더 잘 사용할 수 있지 않을까요?
'Web > React & Next.js' 카테고리의 다른 글
React | web3-react를 사용하여 메타마스크(Metamask) 서명 요청하기 (0) | 2023.01.17 |
---|---|
React | web3-react를 사용하여 메타마스크(Metamask) 지갑연결하기 (0) | 2023.01.16 |
React | Swiper.js를 사용하여 카드 슬라이드 구현하기 (Slides per view) (0) | 2022.11.25 |
React | Swiper.js를 사용하여 이미지 슬라이드 구현하기 (0) | 2022.11.25 |
React | 모바일, 데스크톱기기 구분하여 url 리다이렉트 시키기 (0) | 2022.11.24 |