[React Query] 리액트 쿼리 이해하기
리액트 쿼리란?
React Query는 리액트 앱에서 서버의 상태를 불러오고, 캐싱하며, 지속적으로 동기화하고 업데이트하는 작업(fetching, caching, synchronizing, updating)을 도와주는 라이브러리다.
function Coins() {
const [coins, setCoins] = useState<CoinInterface[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
(async () => {
const response = await fetch("https://api.coinpaprika.com/v1/coins");
const json = await response.json();
setCoins(json.slice(0, 100));
setLoading(false);
})();
}, []);
...
기존에 위와 같이 데이터를 fetch 하는 코드가 있다면 리액트 쿼리를 적용했을 때 다음과 한 줄로 코드가 바뀌게 된다.
function Coins() {
const { isLoading, data } = useQuery<ICoin[]>(["allCoins"], fetchCoins);
...
// api.ts
export function fetchCoins() {
return fetch("https://api.coinpaprika.com/v1/coins").then((response) =>
response.json()
);
}
리액트 쿼리는 로직을 축약해준다.
기존의 Coins 컴포넌트에는 데이터를 위한 state와 로딩을 위한 state가 있었고, 데이터가 준비되면 데이터를 state에 넣고 로딩을 false로 바꾸는 과정이 있었다.
하지만 리액트 쿼리는 이 모든 과정을 자동으로 해준다.
그렇다면 어떻게 리액트 쿼리를 적용하면 이와 같은 결과가 나오게 되는지 알아보자.
먼저 리액트 쿼리를 설치한다
npm i @tanstack/react-query
※ 리액트 버전 18에서는 npm i react-query가 아니라 위와 같이 입력하여 모듈을 설치해야 한다.
@tanstack/react-query에서 useQuery를 사용할 때 query key의 값은 대괄호로 묶어줘야 한다.
const { isLoading, data } = useQuery(["allCoins"], fetchCoins);
다음으로는 QueryClient를 만들고 Provider를 만들어 준다.
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
...
const queryClient = new QueryClient();
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
);
root.render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<ThemeProvider theme={theme}>
<App />
</ThemeProvider>
</QueryClientProvider>
</React.StrictMode>
);
이 provider 패턴은 themeProvider와 동일하다.
themeProvider 안에 있는 모든 것이 theme로 접근 할접근할 수 있다는 것을 의미하는 것처럼 리액트 쿼리도 같은 맥락으로 queryClientProvider 안에 있는 모든 것이 queryClient에 접근할 수 있다.
리액트 쿼리를 사용하기 위해서는 먼저 fetcher 함수를 만들어야 한다.
fetcher는 기본적으로 다음과 같은 것이다.
const response = await fetch("https://api.coinpaprika.com/v1/coins");
const json = await response.json();
즉 데이터를 가져오기 위한 함수를 만들면 된다.
src 밑에 api.ts 파일을 만들고 여기에 fetcher 함수를 만든다.
fetcher 함수는 json 데이터를 반환해야 한다. 정확히는 json 데이터의 Promise다.
export function fetchCoins() {
return fetch("https://api.coinpaprika.com/v1/coins").then((response) =>
response.json()
);
}
async / await 를 사용하고 싶은 경우
export async function fetchCoins() {
return await (await fetch("https://api.coinpaprika.com/v1/coins")).json();
}
aixos를 사용하고 싶은 경우
export const fetchCoins = async() =>{
return await axios.get("https://api.coinpaprika.com/v1/coins").then(res => res.data);
}
이제 useQuery라는 hook을 사용한다.
이 훅은 react-query에서 import 한다.
import { useQuery } from "@tanstack/react-query";
import { fetchCoins } from "../api";
...
function Coins() {
const { isLoading, data } = useQuery<ICoin[]>(["allCoins"], fetchCoins);
return (
<Container>
<Header>
<Title>코인</Title>
</Header>
{isLoading ? (
<Loader>Loading...</Loader>
) : (
<CoinsList>
{data?.slice(0, 100).map((coin) => (
<Coin key={coin.id}>
<Link to={`/${coin.id}`} state={{ name: coin.name }}>
<Img
src={`https://coinicons-api.vercel.app/api/icon/${coin.symbol.toLowerCase()}`}
/>
{coin.name} →
</Link>
</Coin>
))}
</CoinsList>
)}
</Container>
);
}
export default Coins;
const { isLoading, data } = useQuery<ICoin[]>(["allCoins"], fetchCoins);
useQuery는 두 개의 인자를 필요로 한다.
첫 번째 인자는 query key이고 두 번째 인자는 fetcher 함수이다.
query key는 quey의 고유 식별자를 의미한다.
그리고 세 번째 인자인 옵션에는 캐시 만료 시점, refetch 시점, 초기값 등을 설정할 수 있으며 생략 가능하다.
const { isLoading, data } = useQuery<IHistorical[]>(
["ohlcv", coinId],
() => fetchCoinHistory(coinId),
{
refetchInterval: 10000,
}
);
위와 같이 작성하면 refetch 간격을 10초로 둘 수 있다.
useQuery는 isLoading이라고 불리는 boolean 값을 반환한다.
useQuery는 fetcher함수를 부르고 데이터를 받아오는 작업이 진행 중이면 isLoading이 true, 끝난다면 false 값을 가지게 된다.
또한, useQuery는 fetcher 함수에서 반환받은 json을 가지는 data를 반환한다.
isLoading과 data를 콘솔에 찍어보면 로딩이 true, data가 undefined로 나오다가 로딩이 false, data에 API로 불러온 모든 데이터가 담기는 것을 확인할 수 있다.
현재 타입스크립트를 사용하고 있으므로 data의 타입을 알려줘야 한다.
타입을 지정하면 이제 타입 스크립트는 data가 ICon[] 혹은 undefined인 것을 알게 된다.
<CoinsList>
{data?.slice(0, 100).map((coin) => (
<Coin key={coin.id}>
<Link to={`/${coin.id}`} state={{ name: coin.name }}>
<Img
src={`https://coinicons-api.vercel.app/api/icon/${coin.symbol.toLowerCase()}`}
/>
{coin.name} →
</Link>
</Coin>
))}
</CoinsList>
따라서 data를 불러올 때 뒤에 옵셔널 체이닝('?')을 작성해준다.
리액트 쿼리를 적용하고 나면 다른 페이지로 갔다가 돌아올 때 더 이상 로딩이 보이지 않게 된다.
로딩이 보이지 않는 이유가 리액트 쿼리가 데이터를 캐시에 저장해두기 때문이다.
리액트 쿼리는 response를 caching하기 때문에 화면을 바꿨다가 돌아와도 data가 이미 캐시에 있다는 것을 알고 있다.
기존에는 API에 매번 접근했지만 리액트 쿼리는 캐시에 원하는 data가 있을 경우 API에 접근하지 않는다.
+ 댓글을 보고 추가합니다.
useQuery 옵션인 staleTime을 따로 설정해주지 않으면, 캐시가 있더라도 새로 fetch를 한다고 NamJwong님이 알려주셨습니다.
찾아보고 테스트해본 결과 세 번째 인자인 옵션에 쿼리가 fresh 상태에서 stale 상태로 전환되는 시간인 staleTime을 지정해주지 않으면 데이터가 fetch되자마자 stale상태로 바뀌는 것을 확인하였습니다.
stale이란 사전적으로 신선하지 않다는 뜻으로 최신화가 필요한 데이터라는 의미입니다.
따라서 쿼리가 stale한 상태가 되면 다음의 경우에 refetch가 됩니다.
refetch 되는 조건
1. 새로운 query instance가 마운트 될 때 (page를 이동 했다가 왔을 때 의미)
2. 브라우저 화면을 이탈 했다가 다시 focus 할 때
3. 네트워크가 다시 연결될 때
4. 특별히 설정한 refetch interval에 의한 경우 (refetchInterval)
따라서 현재 예제의 경우 Coins 컴포넌트와 Coin 컴포넌트를 왔다갈때 할 때 계속 refetch가 되고 있는 상태입니다.
관련한 내용이 아래 블로그에 잘 정리되어 있는 것 같아 링크 남깁니다.
리액트 쿼리는 시각화를 위한 DevTools를 가지고 있다.
DevTools는 render할 수 있는 컴포넌트이고 이를 통해 캐시에 있는 query를 볼 수 있다.
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
...
function App() {
<>
<GlobalStyle />
<Router />
<ReactQueryDevtools initialIsOpen={true} />
</>
);
}
이제 웹사이트를 가면 다음과 같은 개발자 도구를 볼 수 있다.
쿼리의 이름은 useQuery의 첫 번째 인자인 query key가 된다.
조금 다른 경우를 더 살펴보자.
fetcher 함수가 argument가 필요한 경우에는 어떻게 해야 할까?
export function fetchCoinInfo(coinId: string | undefined) {
return fetch(`${BASE_URL}/coins/${coinId}`).then((response) =>
response.json()
);
}
export function fetchCoinTickers(coinId: string | undefined) {
return fetch(`${BASE_URL}/tickes/${coinId}`).then((response) =>
response.json()
);
}
const { isLoading: infoLoading, data: infodata } = useQuery<InfoData>(
["info", coinId],
() => fetchCoinInfo(coinId)
);
익명 함수를 만들어서 전달해주면 된다.
그렇다면 쿼리가 두 개 이상이고 동일한 키를 가지고 있다면 어떻게 해야 할까?
const { isLoading: infoLoading, data: infodata } = useQuery<InfoData>(
["info", coinId],
() => fetchCoinInfo(coinId)
);
const { isLoading: tickersLoading, data: tickersData } = useQuery<PriceData>(
["tickers", coinId],
() => fetchCoinTickers(coinId)
);
위 개발자 도구 사진을 보면 리액트 쿼리가 키를 배열로 감싸서 표현하는 것을 볼 수 있다.
쿼리는 고유한 식별자가 필요하기 때문에 다음과 같이 작성하여 고유한 키 값을 줄 수 있다.
["info", coinId], ["tickers", coinId]
한 가지 더 문제가 있는데 isLoading과 data가 이름이 동일하다.
이 경우는 자바스크립트 문법을 이용하여 이름을 바꿔주면 된다.
📌 아래 강의의 내용을 정리한 글입니다.