React query의 공식 사이트에 들어가보면 '강력한 비동기 상태 관리 라이브러리'라고 소개되어 있다. 공식 사이트의 소개문구대로 React query는 React 애플리케이션에서 데이터를 가져오고 관리하는 데 사용되는 라이브러리로, 주로 HTTP API호출을 처리하고 결과를 캐싱하면, 데이터 상태를 관리해 UI의 성능을 향상시키고 데이터의 로딩 및 업데이트를 관리하는 데 도움이 된다. 즉, ‘비동기 로직을 쉽게 다룰수 있게 해준다’ 라고 이해하면 된다.
query를 사용하기 전에는 API 비동기 통신을 수행하기 위해서 주로 Redux를 사용해서 비동기 데이터를 관리했다. Redux를 사용할 경우 전역적으로 비동기 데이터가 관리되기 때문에 캐싱과 같은 최적화 작업을 쉽게 수행할 수 있고 복잡한 사용자 시나리오에 대한 응대도 용이해지기 때문이다. 하지만 Redux를 사용하기 위해서 기본 원칙이 존재한다. 이 기본 원칙을 지키면서 코드를 진행하게 되면 장황한 Boilerplate코드가 요구가 되고 이를 해결하기 위해 Redux-toolkit이 나왔지만 코드양이 많이 줄었음에도 여전히 불필요하게 느껴지는 반복되는 boilerplate코드가 필요하다. 그래서 하나의 API요청을 처리하기 위해 여러 개의 Action과 Reducer가 필요하다. 이러한 구조하에 처리해야 하는 API의 개수가 많아질수록 코드의 분량이 늘어날 뿐만 아니라 비동기 Action을 처리하기 위한 복잡성이 높아질 우려가 있다. 이런 불편함은 React query를 사용함으로서 많은 부분이 해소될 수 있었다.
1. 간편한 데이터 관리
2. 실시간 업데이트 및 동기화
3. 데이터 캐싱
4. 서버 상태 관리
5. 간편한 설정
6. 동일한 데이터에 대한 중복 요청을 제거
7. 네트워크 재요청 기능
8. 백그라운드에서 “오래된” 데이터를 업데이트 해준다.
9. devtool 제공.
npm i @tanstack/react-query @tanstack/react-query-devtools
import React from 'react';
import './App.css';
import MainProducts from './components/MainProducts';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
const queryClient = new QueryClient();
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<MainProducts />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
import { useQuery } from '@tanstack/react-query';
const { data, isLoading, isError, ... } = useQuery(queryKey, queryFn, options)
useQuery를 호출하면, 객체를 return 해주는데, 이 객체 안에는 많은 key들이 있다. 그래서 네트워크 통신을 통해 받아온 data뿐만 아니라, data가 언제 update되었는지, 에러인지 아닌지등 다양한 데이터를 확인해 볼 수 있다.
useQuery를 호출할 때는, queryKey와 어디서 data를 가져와야 하는지 queryFn(쿼리함수)를 전달해주고, 세 번째 함수 인자로는 option을 전달해주는데 이 많은 내용들을 option으로 설정해둘 수 있다. 아래에서 다양한 option들과 key들을 확인해 볼 수 있다.
const {
data,
dataUpdatedAt,
error,
errorUpdateCount,
errorUpdatedAt,
failureCount,
failureReason,
fetchStatus,
isError,
isFetched,
isFetchedAfterMount,
isFetching,
isInitialLoading,
isLoading,
isLoadingError,
isPaused,
isPlaceholderData,
isPreviousData,
isRefetchError,
isRefetching,
isStale,
isSuccess,
refetch,
remove,
status,
} = useQuery({
queryKey,
queryFn,
cacheTime,
enabled,
networkMode,
initialData,
initialDataUpdatedAt,
keepPreviousData,
meta,
notifyOnChangeProps,
onError,
onSettled,
onSuccess,
placeholderData,
queryKeyHashFn,
refetchInterval,
refetchIntervalInBackground,
refetchOnMount,
refetchOnReconnect,
refetchOnWindowFocus,
retry,
retryOnMount,
retryDelay,
select,
staleTime,
structuralSharing,
suspense,
useErrorBoundary,
})
import React, { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
export default function Products() {
const [checked, setChecked] = useState(false);
const {
isLoading,
error,
data: products,
} = useQuery({
queryKey: ['products', checked],
queryFn: async () => {
console.log('fetching...')
const response = await fetch(`data/${checked ? 'sale_' : ''}products.json`)
return response.json()
},
});
const handleChange = () => setChecked((prev) => !prev);
if (isLoading) return <p>Loading...</p>;
if (error) return <p>{error}</p>;
return (
<>
<label>
<input type='checkbox' checked={checked} onChange={handleChange} />
Show Only 🔥 Sale
</label>
<ul>
{products.map((product) => (
<li key={product.id}>
<article>
<h3>{product.name}</h3>
<p>{product.price}</p>
</article>
</li>
))}
</ul>
</>
);
}
이렇게 작성하면 useQuery가 호출 될 때 두 번째 인자로 등록해놓은 함수가 호출 되면서 반환된 data를 useQuery가 내부적으로 가지고 있게 된다. 그럼 객체구조할당분해를 통해서 변수에 할당하도록 만들어준다.
이렇게 데이터 fetching 로직을 따로 분리한다면 데이터를 사용하는 컴포넌트에서 작성되는 코드는 useQuery를 호출해주면 되니까 간편하다. 또한 useEffect를 사용하지 않음으로서 코드의 흐름을 파악하기 쉬워졌다.
그리고 React Query는 캐싱 기능이 있기 때문에 다른 컴포넌트에서 useQuery의 같은 로직을 사용해서 data를 fetching 해오려고 한다면 같은 key를 사용하게 됐을 때 캐싱된 data를 가져오게 되므로 최적화에 도움을 준다. 하지만 여기서 알아둬야 할 점이 있다.
React Query를 사용하면 data가 캐싱된다고 했는데 현재 사이트를 벗어났다가 다시 원래있던 window로 돌아만 와도 fetching이 된다. 그리고 다른 이벤트를 실행시키면 실행시키는 만큼 또 네트워크 통신이 일어난다. 이게 어떻게 된 일인지 알아보기 위해 React Query DevTools를 사용해보자. 설치는 이미 위해서 react-query를 설치할 때 같이 해줬고 App.js에서 initialIsOpen을 true로 해주면
바로 이렇게 react-query 개발툴이 보인다. 현재 우리 APP에 cache된 게 어떤 게 있는지 확인해 볼 수 있다. 여기서 봐야될 부분은 'stale'이라 표시된 부분이다. 이 stale은 '오래된 데이터'라는 뜻인데, 공식 문서를 확인해 보면,
Query instance는 useQuery를 사용하던지 아니면 useInfiniteQuery를 사용할 수 있는데 이 두개를 사용하면 캐시된 데이터는 stale이라고 간주한다 한다. 신선하지 않은 데이터라고 간주해서 자동으로 refetch를 시키는 거다. 그래서 이렇게 refetch가 일어나는 조건이 있는데,
1. refetchOnWindowFocus
2. refetchOnMount
3. refetchOnReconnect
기본적으로 React Query는 위의 세가지 기능의 기본값은 모두 true 상태이다. 이외에도 queryKey와 함께 State값을 같이 넘겨준 경우 State값이 변경된다면 refetch가 일어나게 된다.
이런 기본적인 행동을 바꿀 수 있게 해주는 option이 있다.
const { data } = useQuery(['data', getServerData,{
staletime: 1000 * 60 * 5; // 5분
})
자주 변경되지 않는 데이터의 경우 캐시된 데이터를 사용함으로써 불필요한 네트워크 요청을 줄이고, 애플리케이션의 성능을 향상시킬 수 있다.
애플리케이션에서 더이상 useQuery나 useInfiniteQuery를 사용하지 않는다면, inactive상태로 표기가 되는데 ‘inactive’상태로 5분정도 지나면 아무도 참조하지 않으니까 자동으로 garbage collected가 된다고 한다. 그렇게 되면 메모리를 자동으로 청소해 주니까 cache에서도 사라지게 된다. 이걸 바꾸기 위한 option이 cacheTime이다.‘cacheTime’을 조금 더 긴 시간으로 설정해주면 된다.
const { data } = useQuery(['data', getServerData], {
cachetime: 30 * 60 * 1000, // 30분
})
staleTime을 설정해서 캐시된 데이터를 사용하게 해줬는데, 여기서 또 다른 문제가 생길 수 있는 게 data의 update가 빈번하게 일어나게 되면 background에 데이터가 새롭게 추가 되어도 우리는 staleTime을 5분으로 설정해뒀기 때문에 캐시된 데이터를 5분동안 사용해야 한다. 이렇게 되면 background에서는 새롭게 데이터가 update되었는데 화면상에 우리는 캐시된 데이터를 사용하고 있기 때문에 오래된 데이터를 사용하게 되는 것이다. 그래서 이럴 때 데이터가 업데이트 되었다면, 연관있는 캐시된 데이터는 모두 invalidate하게 해주는 옵션이 있다.
기존에 캐싱되어 있는 데이터를 무효화하는 함수이다. 기존의 데이터를 무효화하게 되면 refetch가 일어나면서 업데이트 된다. 이때 조심해야 할 것은 리스트의 개수가 많다면 refetch하는 시간이 소요되기때문에 사용자는 업데이트 되는데 느리다고 생각할 수 있다.
import { useQueryClient } from '@tanstack/react-query';
export default function MainProducts() {
const client = useQueryClient();
return (
<main className='container'>
<button onClick={() => { client.invalidateQueries({ queryKey: ['products'] }) }}>정보가 업데이트 되었음!</button>
</main>
);
}
App.js에서 우리가 QueryClientProvider를 씌워줬기 때문에 모든 자식 컴포넌트들에서 useQueryClient를 이용해서 client를 가지고 올 수 있다.
그럼 client를 가지고 onClick에서 client에게 invalidateQueries라는 API를 사용할 수 있다. 그래서 cache된 key인 products의 데이터를 모두 invalidate해줘 라고 할 수 있다. 캐시된 정보를 보관하면서, 우리가 서버에 어떤 새로운 데이터를 추가 했을 때 우리가 수동적으로 이렇게invalidate 할 수 있다.
그동안에는 네트워크 통신을 위해서 useEffect를 사용해서 데이터 통신 코드를 작성해 주고 따로 상태관리 라이브러리를 사용하지 않으면 일일이 데이터 통신을 해야하는 컴포넌트마다 코드를 똑같이 작성해주는 번거로움이 있었다. 그래서 사용하게 된 게 Redux인데 Redux같은 경우는 초기 설정이 복잡한 것도 있고 편하자고 사용했지만 작성해줘야 하는 코드도 많았다. 그에 대한 방편으로 react-query를 사용하는 건 꽤 도움이 많이 된다고 느낀 게 일단 react-query를 사용하면 다른 상태관리 라이브러리에 비해 직관적이고 깔끔한 코드를 사용할 수 있다는 것이고, 내부적으로 가지고 있는 함수를 통해서 loading중인지 error가 발생했는지 data를 받아왔는지 손쉽게 알 수 있어 네트워크 통신을 간편하게 할 수 있게 해주고, 동일한 요청에 대해서는 우리의 설정에 따라 캐시도 해준다. 그렇기 때문에 관리도 편하고 다른 컴포넌트에서 사용하더라도 동일한 요청이면 캐시된 데이터를 불러오기 때문에 불필요한 네트워크 요청을 줄여주고 성능을 향상 시키는 데 도움을 준다. 사용법도 간단하기 때문에 앞으로 프로젝트를 하게 된다면 react query를 어디에서 활용할 수 있을지 고민해볼 것 같다.
참조