
react-query란 React Application에서 서버 상태를 불러오고, 캐싱하며, 지속적으로 동기화하고 업데이트하는 작업을 도와주는 react 라이브러리입니다.
redux-toolkit, mobx를 사용하여 API 통신을 수행하여 비동기 데이터를 관리하게 끔 프로젝트를 진행하였습니다. 전역으로 상태를 관리할 수 있다는 강점에서 만족을 하며 사용하였습니다. 하지만 컴포넌트가 소수인 경우에는 데이터 관리는 용이했지만 다수의 컴포넌트를 만들어야 할 경우 라이프 사이클에 따라 비동기 데이터가 관리되어서 불필요한 데이터 요청 관리 및 캐싱을 하기가 어려웠습니다.
또한 redux-toolkit을 사용함으로써 보일러 플레이트는 줄어들었지만 데이터를 관리하는 일에는 보일러플레이트 코드가 필요할 수 밖에 없었습니다.
react-query를 사용하므로써 server state를 효율적으로 관리할 수 있을 뿐만 아니라
enabled, chache, refetch, onError, onSuccess, onSettled 등 여러가지 옵션들로 간편하고 깔끔하게 데이터를 처리 할 수 있어서 편리하다고 느꼈습니다.
우선 초기에 app.js 또는 index.js 파일에 초기 셋팅을 하게 됩니다.
import React from 'react';
import App from './App';
import { QueryClient, QueryClientProvider } from 'react-query';
import { ReactQueryDevtools } from 'react-query/devtools';
import './index.css';
function App() {
const queryClient = new QueryClient();
return (
<QueryClientProvider client={queryClient}>
<React.StrictMode>
<App />
</React.StrictMode>
<ReactQueryDevtools initialIsOpen={true} />
</QueryClientProvider>
)
};
여기서 QueryClientProvider로 감싸고 queryClient에서 설정한 값을 prop으로 넘겨줘야합니다.
QueryClient에서 defaultOptions으로 제공되는 것을 공통적으로 적용할 설정을 조정할 수 있습니다.
ReactQueryDevtools는 이제 react-query의 데이터 통신에 대해서 디버깅할 수 있게 도와주는 것이고 선택적으로 사용하면 됩니다.
React-Query를 사용하기 전에 기본적으로 알아야할 4가지가 있습니다.
staleTime, chcheTime, refech 의 조건, refetch 가 실행 될 때 4가지가 있습니다.
staleTime
- fetch된 데이터가 fresh한 상태에서 stale상태로 변경되기까지의 시간
- fresh 상태일 때는 fetch가 일어나지 않음
- stale time을 0으로 설정하는 경우 데이터가 곧 바로 stale 되기 때문에 refetch가 요구 되는 상황이면 바로 refetch하게 된다.
chcheTime
- data 상태가 inactive되었을 때(unmount 되었을 때) 캐싱된 데이터로 남아 있는 시간
- cacheTime이 지나기 전에 query가 실행 되면 cache된 값이 사용 되고 background에서 다시 fetch 한다.
- cacheTime이 지나면 메모리에 존재하는 데이터가 GC에 의해 삭제가 되고 다시 active 될 경우 hard loading 한다. (hard loading: 데이터가 없기 때문에 fetch를 실행 시킨다.)
- cacheTime이 0이라면 매번 GC 되기 때문에 매번 *hard loading 한다.
refetch의 조건
- 캐시된 데이터가 없는 경우
- 데이터가 fresh 하지 않을 경우 ( 데이터가 fresh 하다면 refetch 하지 않는다. )
- useQuery 의
refetch함수를 실행한 경우- queryClient의 **
invalidateQueries를 사용하여 query를 무효화한 경우
refetch 가 실행 될 때
1. querykey가 변경 되는 경우
2. 윈도우가 다시 focus 되는 경우
3. 네트워크가 다시 연결 되는 경우
4. refetch 설정을 명시적으로 준 경우
import { useQuery } from 'react-query';
function findUsers() {
return axios.get('/users').then(...)
}
function findUser(id: string) {
return axios.get(`/user${id}`).then(...)
}
// options 다양한 설정이 있음
function useFindUsers() {
return useQuery('findUsers', findUsers, {options})
}
function useFindUser(id: string) {
return useQuery(['findUser', id], () => findUser(id), {options})
}
// 컴포넌트에서 사용할 떄
function Component () {
// component에서 useQuery 하나만 사용할 경우엔 그냥 data로 받아도 상관 없지만
// 여러개의 useQuery를 사용하는 경우 data 이름을 변경해주어 사용하면 됩니다.
// 단일로 사용하는 경우에도 어떤 데이터인지 확실히 알기위해 이름을 지정 해주는것이 좋을것 같기도 합니다.
const {data: userData} = useFindUser(['findUser', id]);
}
useQuery에 첫번째 인자로, unique key를 명시해줍니다.
해당 key는 내부적으로 데이터 재요청, 캐싱, 쿼리를 공유하기 위해 사용됩니다.
두번째 인자로 요청할 비동기 함수(fetchFindUsers)를 넣어줍니다.
세번째 인자는 useQuery 옵션이 들어갑니다.
useMutation은 post, delete, put 동작을 실행 할때 사용하게 됩니다.
import { useQueryClient, useMutation } from 'react-query';
interface Todo {
title: string;
content: string;
}
const useAddTodo = () => {
const queryClient = useQueryClient();
return useMutation(
(todo: Todo) => {
return addTodoApi(todo)
},
{
onMutate: (variables) => {
// mutate가 실행 되기 전 로직을 실행하는 곳
},
onSuccess: (data, variables, context) => {
// data => mutation의 결과값
// variables => mutation 함수의 인자값
// context => onMutate의 return 값
// mutate가 실행이 되어 성공 하고 난 후
// todos 쿼리의 데이터를 incvaild 시켜준다.
queryClient.invalidateQueries('todos')
},
onError: (err, variables, context) => {
// mutate의 실행이 실패한 후
},
onSettled: (data, err, variables, context) => {
// mutate가 성공하거나 실패하고 난 후
}
}
)
}
function TodoList() {
return (
<ul>
...
</ul>
)
}
read more 또는 infinite scroll을 구현할 때 사용하게 됩니다.
아래 예제는 infinite scroll 예제 입니다. read more 로 사용하시려면 intersection hook (useInview) 를 제거하고 클릭 이벤트에 다음 페이지 값을 가져오는 함수를 적용하시면 됩니다.
아래 예시를 보기전 useInfiniteQuery 공식 문서를 보고 전체적으로 어떤식으로 돌아 가는지 어떤 인자들을 사용하는지 파악을 먼저 하는게 이해하기 편합니다.
interface User {
name: string;
offset: number;
limit: number;
}
function findUser(parmas: User) {
axios.get(url, { params })
}
// infiniteQuery 가 받는 params 인자에서 offset을 제거하는 이유는
// offset 값을 infiniteQuery에서 관리하기 때문입니다.
const queryKeys = {
user_infinite: (params: Omit<User, 'offset'>) => ['user_infinite', params]
}
function useFindUserInfinite(params: Omit<User, 'offset'>) {
return useInfiniteQuery(
queryKeys.user_infinite(params),
// pageParam의 값은 getNextPageParam의 return 값을받습니다.
// api 응답값에
async ({pageParam = 0}) => {
const user = findUser({
...params,
offset: pageParam * params.limit,
})
return {
...user,
offset: pageParam,
}
},
{
getNextPageParam: ({ empty, offset}) => {
if(empty) {
return undefined
}
return offset + 1;
}
}
)
}
function UserList () {
const params = [state 혹은 store 값]
const {
/**
* infinity query 데이터 타입
*
* interface InfiniteData<TData> {
* pages: TData[];
* pageParams: unknown[];
* }
*/
data: userData,
isLoading, // 데이터가 패치중인지 확인하는 값
hasNextPage, // 다음 페이지가 있는지 확인하는 값
featchNextPage, // 다음 페이지를 호출 하는 함수
} = useFindUserInfinite(params)
const { ref, inView } = useInView(); // intersection hook
useEffect(() => {
// 참조 요소가 intersection 상태이고
// 데이터 패치 상태가 아니고
// 다음 페이지가 존재 한다면 다음 페이지를 가져오는 함수 호출
if(inView && !isLoading && hasNextPage) {
featchNextPage()
}
}, [inView, isLoading, hasNextPage, featchNextPage])
return (
<ul>
{userData.map(page => (
<>
{page.map(user => <li>{user}</li>)}
</>
)
}
</ul>
)
}
여러 api를 한번에 묶어서 불어올 때 사용하시면 됩니다.
동일한 api를 여러번 호출해서 값을 가져와야 하는 경우 사용하면 좋습니다.
export const useFindPalylistsByQdoSwiper = () => {
const params = (
playlistMadeBy: PlaylistMadeBy,
playlistSortOrder: PlaylistSortOrder
): PlaylistsByQdo => {
return {
keyword: '',
limit: 3,
offset: 0,
myPlaylists: false,
playlistMadeBy,
playlistSortOrder,
};
};
return useQueries([
{
queryKey: [queryKeys.findPalylistsByQdo_Swiper(params('', 'LikeDesc'))],
queryFn: () => findPlaylistsByQdo(params('', 'LikeDesc')),
},
{
queryKey: [
queryKeys.findPalylistsByQdo_Swiper(params('', 'MadeByOthersDesc')),
],
queryFn: () => findPlaylistsByQdo(params('', 'MadeByOthersDesc')),
},
{
queryKey: [
queryKeys.findPalylistsByQdo_Swiper(params('Company', 'LikeDesc')),
],
queryFn: () => findPlaylistsByQdo(params('Company', 'LikeDesc')),
},
{
queryKey: [
queryKeys.findPalylistsByQdo_Swiper(
params('Company', 'MadeByOthersDesc')
),
],
queryFn: () => findPlaylistsByQdo(params('Company', 'MadeByOthersDesc')),
},
]);
};
function useFindUserList(params) {
return useQuery(['findUserList', params], () => findUserList(params), {
keepPreviousData: true, // 이전 데이터 저장 옵션 입니다.
staleTime: 60000, // stale 타임은 유동적으로 설정 하면 될것 같습니다.
})
}
// useEffect 내부에서 돌아야 하는 함수이기 때문에 useQueryClient를 함수 내부에서 선언 할 수 없어서
// 인자로 받도록 만들었습니다.
function prefetchFindUserList(queryClient, params) {
queryClient.prefetchQuery(['findUserList', params], () => findUserList(params))
}
function UserListTable () {
const { params } = UserSevice.instance;
const queryClient = useQueryClient();
// 이전 데이터를 저장 하기 때문에 isLoading으로는 해당 api가 로딩중인지 알 수가 없어서
// isFetching 값을 사용해야 합니다.
const { data: userListData, isFetching } = useFindUserList(params);
useEffect(() => {
if(!userListData.isEmpty) {
// 예시로 offset + 1 을 하였는데 다음 페이지의 값을 부를수 있도록 만들어 주면 됩니다.
const prefetchParams = {
...params,
offset: params.offset + 1
}
usePrefetchFindUserList(prefetchParams)
}
},[quertClient, userListData])
...
}
src
- query => react-query 와 관련된 공통된 부분을 관리하는 폴더
- queryKeys => query key들을 관리하는 파일
- [pageName]
- [pageName].hooks => react-query hooks를 관리하는 파일
- querykey 는 배열 형식으로 관리합니다.
- key의 이름은 api 함수 이름을 그대로 사용합니다.
- params 값이 필요한 api 인 경우 key를 함수 형태로 만들어 인자에 params를 넘겨줍니다.
- 함수로 키값을 만들 경우 as const 를 사용하여 literal type을 지원하도록 만들어 줍니다.
- 동일한 api 를 사용하지만 목적이 다른 query인 경우 [api이름]_[목적] 형식으로 키 값을 만들어 줍니다
export const queryKeys = {
findUser: ['findUser'],
findAutoEncourageQdo: (autoEncourageParams: AutoEncourageParams) =>
['findAutoEncourageQdo', autoEncourageParams] as const,
} as const;
// as const 를 붙이지 않은 경우
findAutoEncourageQdo:
(autoEncourageParams: AutoEncourageParams) =>
(string | AutoEncourageParams)[]
// as const 를 붙인 경우
findAutoEncourageQdo:
(autoEncourageParams: AutoEncourageParams) =>
readonly ["findAutoEncourageQdo", AutoEncourageParams]
!주의할점
키를 사용하는 경우 아래와 같은 부분은 주의해야 합니다.
//쿼리 키 순서에 관계없이 다음 쿼리는 모두 동일한 것으로 간주됩니다.
useQuery(['todos', { status, page }], ...)
useQuery(['todos', { page, status }], ...)
useQuery(['todos', { page, status, other: undefined }], ...)
// 다음과 같이 배열일 경우 쿼리 키는 모두 다른 것으로 간주 됩니다.
useQuery ( [ 'todos', status, page ] , ... )
useQuery ( [ 'todos', page, status ] , ... )
useQuery ( [ 'todos', undefined, page , status ] , ... )
쿼리 키는 유니크해야 하기 때문에 아래와 같은 경우 하나의 cache만 유효하게 됩니다.
그래서 동일한 api를 사용하지만 목적이 다른 경우 위에서 말했듯이 [api이름]_[목적] 으로 키값을 만듭니다.
useQuery(['todos'], fetchTodos)
// 잘못된 사용
useInfiniteQuery(['todos'], fetchInfiniteTodos)
// 사용 가능
useInfiniteQuery(['infiniteTodos'], fetchInfiniteTodos)
// [api이름]_[목적]
const queryKeys = {
todos: ['todos']
todos_infinity: ['todos_infinity']
}
참고 사이트