React query는 서버로부터 비동기로 데이터를 조회할 때 사용된다. React query를 사용하면 기존에 isLoading, isError, refetch, 데이터 캐싱 등 개발자가 직접 구현하려면 꽤 귀찮거나 까다로웠던 기능을 제공해주기 때문에, 서버 상태를 매우 효율적으로 관리할 수 있다.
먼저, 아래 명령어로 react-query를 install해주자.
npm i --save react-query
왜 react query를 사용하는 것이 서버 상태를 효율적으로 관리할 수 있는 것일까?
아래 두 예시를 살펴보자.
React query를 사용하지 않는 경우
const [coins, setCoins] = useState<CoinInterface[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
(async () => {
const response = await fetch("https://api.coinpaprika.com/v1/coins");
const data = await response.json();
setCoins(data)
setLoading(false);
})();
}, []);
React query를 사용하는 경우
const { isLoading, data: coins } = useQuery("allCoins", async () =>
(await fetch("https://api.coinpaprika.com/v1/coins")).json();
위의 예시에서 알 수 있듯이 isLoading같은 서버 상태 관리 기능을 하나씩 직접 구현하는 것은 매우 번거러운 일이며, react query를 이용한다면 훨씬 쉽게 해당 기능들을 사용할 수 있다.
React query를 사용하기 위해서는 QueryClient 인스턴스와 QueryClientProvider로 기본 세팅을 해줄 필요가 있다.
import {QueryClient, QueryClientProvider} from "react-query";
const queryClient = new QueryClient();
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement);
root.render(
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
);
useQuery는 서버로부터 받은 데이터와 서버 상태에 대한 정보를 얻을 수 있는 custom hook으로 이를 구현하기 위해서는 다음 두 가지 개념을 이해하고 있어야 한다.
또한, useQuery는 아래와 같이 사용한다.
(1)
const res = useQuery(queryKey, queryFn);
(2)
const res = useQuery({
queryKey: queryKey,
queryFn: queryFn
});
queryKey는 useQuery마다 할당되는 고유 key이며, useQuery는 queryKey를 기반으로 데이터 캐싱을 관리할 수 있게 도와준다. Key 값은 문자열 또는 배열 형태로 입력할 수 있다.
// (1)
const res = useQuery('coins', queryFn);
// (2)
const res = useQuery(['coins'], queryFn);
// (3)
const res = useQuery(['coins', 'addId'], queryFn);
// (4)
const res = useQuery(['addId', 'coins'], queryFn);
// (5)
const res = useQuery(['coins', {type: 'add', name: 'Id'}], queryFn);
문자열로 key를 입력하면, useQuery에서 길이가 1인 배열로 인식하므로 (1)과 (2)는 결국 같은 key를 입력한 것이다. 또한 useQuery에서는 key 입력 시 배열의 순서도 고려하므로, (3)과 (4)는 다른 key를 입력한 것이다.
위에서 언급한 것처럼 queryKey는 데이터 캐싱을 관리할 수 있게 도와준다. 아래 예시를 살펴보자.
import { useQuery } from 'react-query';
function App() {
const getCoins1 = () => {
const res1 = useQuery(['coins'], queryFn1);
}
const getCoins2 = () => {
const res2 = useQuery(['coins'], queryFn2);
}
return (
<div>
{getCoins1()}
{getCoins2()}
</div>
)
}
export default App;
res1과 res2에서는 똑같은 queryKey를 사용하여 서버로부터 데이터 조회 요청을 하고 있다. 일반적으로 queryKey가 다른 상황이라면 두 가지 요청을 모두 전달하겠지만, res1과 res2의 경우 같은 queryKey를 사용하고 있기 때문에 queryFn1에 대한 요청만 전달된다. 왜냐하면 res1에서 요청을 서버에 전달하게 되면 res2에서는 이미 동일한 queryKey에 대한 결괏값이 있기 때문에 추가 요청을 하지 않고 res1의 결과를 그대로 가져와 사용하기 때문이다. 따라서, 아래 코드와 같은 결과를 얻는다.
import { useQuery } from 'react-query';
function App() {
const getCoins1 = () => {
const res1 = useQuery(['coins'], queryFn1);
}
const getCoins2 = () => {
const res2 = useQuery(['coins']);
}
return (
<div>
{getCoins1()}
{getCoins2()}
</div>
)
}
export default App;
queryFn(query function)은 서버로부터 api 데이터를 요청하는 함수로, promise 처리가 이루어지는 함수라고 보면 된다.
아래와 같은 형태로 작성한다.
import { useQuery } from 'react-query';
const BASE_URL = "https://api.coinpaprika.com/v1";
async function fetchCoins() {
return (await fetch(`${BASE_URL}/coins`)).json();
}
interface CoinInterface {
id: string;
name: string;
symbol: string;
rank: number;
is_new: boolean;
is_active: boolean;
type: string;
}
const { isLoading, data } = useQuery<CoinInterface[]>(['allCoins'], fetchCoins);
queryKey와 queryFn을 제외하고 몇가지 옵션을 더 받을 수 있다. 매우 많은 query option이 구현되어 있지만, 여기서는 내가 자주 사용하는 옵션에 대해서만 소개하겠다.
query options는 아래와 같은 형태로 작성한다.
const { isLoading, data } = useQuery<CoinInterface[]>(
['allCoins'],
fetchCoins,
{ enabled: true,
refetchInterval: 3000
}
);
query option에 대한 추가적인 정보는 react query 공식 문서에서 확인할 수 있다.
1. enabled (boolean)
2. retry (boolean | number | (failureCount: number, error: TError) => boolean)
3. staleTime (number | Infinity)
4. cacheTime (number | Infinity)
5. refetchOnMount (boolean | "always")
6. refetchOnWindowFocus (boolean | "always")
7. refetchOnReconnect (boolean | "always")
8. onSuccess ((data: TDdata) => void)
9. onError ((error: TError) => void)
10. onSettled ((data?: TData, error?: TError) => void)
11. initialData (TData | () => TData)
12. refetchInterval(number | false | ((data: TData | undefined, query: Query) => number | false)