민프
[React][TypeScript] React Query-2 적용 전 / 후 비교 본문
공식문서
https://react-query-v3.tanstack.com/quick-start
설치
npm i react-query
적용 전
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);
})();
},[]);
return (
<Container>
<Header>
<Title>코인</Title>
</Header>
{loading ? <Loader>Loading...</Loader>:(
<CoinsList>
{coins.map((coin) =>
(
<Coin key={coin.id}>
<Link to={`/${coin.id}`} state={coin.name}>
<Img src={`https://coinicons-api.vercel.app/api/icon/${coin.symbol.toLocaleLowerCase()}`}/>
{coin.name} →
</Link>
</Coin>)
)
}
</CoinsList>
)}
</Container>
);
}
export default Coins;
위 코드에서 데이터를 위한 state, 로딩을 위한 state가 있고, useEffect에서 fetch를 사용하여서 데이터를 가지고 왔다.
그리고 데이터가 준비되면 데이터를 State에 넣고 로딩을 false로 두었다.
이 과정을 react query는 자동으로 해결해준다.
적용 후
import { useEffect, useState } from "react";
import { useQuery } from "react-query";
import { Link, useParams } from "react-router-dom";
import styled from "styled-components";
import { fetchCoins } from "../api";
const Container = styled.div`
padding: 0px 20px;
max-width: 480px;
margin: 0 auto;
`;
const Header = styled.header`
height: 10vh;
display: flex;
justify-content: center;
align-items: center;
`;
const CoinsList = styled.ul``;
const Coin = styled.li`
background-color: white;
color: ${(props) => props.theme.bgColor};
margin-bottom: 10px;
border-radius: 10px;
cursor: pointer;
a {
padding: 20px;
align-items: center;
transition: color 0.1s ease-in;
display: flex;
}
&:hover {
a {
color: ${(props) => props.theme.accentColor};
}
}
`;
const Title = styled.h1`
font-size: 48px;
color: ${(props) => props.theme.accentColor};
`;
const coins = [
{
id: "btc-bitcoin",
name: "Bitcoin",
symbol: "BTC",
rank: 1,
is_new: false,
is_active: true,
type: "coin",
},
{
id: "eth-ethereum",
name: "Ethereum",
symbol: "ETH",
rank: 2,
is_new: false,
is_active: true,
type: "coin",
},
{
id: "hex-hex",
name: "HEX",
symbol: "HEX",
rank: 3,
is_new: false,
is_active: true,
type: "token",
},
];
const Loader = styled.span`
text-align: center;
display: block;
`;
const Img = styled.img`
width: 25px;
height: 25px;
margin-right: 10px;
`;
interface ICoinInterface {
id: string;
name: string;
symbol: string;
rank: number;
is_new: boolean;
is_active: boolean;
type: string;
}
function Coins() {
const { isLoading, data } = useQuery<ICoinInterface[]>("allCoins", fetchCoins);
// 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);
// })();
// },[]);
return (
<Container>
<Header>
<Title>코인</Title>
</Header>
{isLoading ? (
<Loader>Loading...</Loader>
) : (
<CoinsList>
{data?.map((coin) => (
<Coin key={coin.id}>
<Link to={`/${coin.id}`} state={coin.name}>
<Img
src={`https://coinicons-api.vercel.app/api/icon/${coin.symbol.toLocaleLowerCase()}`}
/>
{coin.name} →
</Link>
</Coin>
))}
</CoinsList>
)}
</Container>
);
}
export default Coins;
적용 전 / 후의 코드를 보면 확연하게 코드가 줄어든 것을 볼 수 있다.
그럼 react query는 어떻게 사용하는 것 일까?
1. fetcher 함수를 만들자
위 코드에서 fetcher 함수는 useEffect 부분에서
const response = await fetch("https://api.coinpaprika.com/v1/coins");
const json = await response.json();
이 부분 이다.
이러한 API와 관련 된 것들은 Component들과 섞이지 않도록 api.tsx라는 파일 안에 넣을 것 이다.
이런 fetcher함수는 꼭 fetch promise를 return해줘야한다.
------api.ts------
export function fetchCoins() {
return fetch("https://api.coinpaprika.com/v1/coins").then((response)=> response.json());
}
이런식으로 response를 json으로 리턴해주는 Fetcher함수를 만들었다.
2. useQuery를 작성하자
useQuery는 2가지 argument를 필요로 하는데
2-1. queryKey
queryKey는 고유식별자다
2-2. fetcherFunction
api.ts에 있는 fetchCoins() 와 같은 함수이다.
const { isLoading, data } = useQuery<CoinInterface[]>("allCoins", fetchCoins);
이렇게 작성한 useQuery라는 hook은 isLoading이라고 불리는 boolean 값을 return해주는데
useQuery가 fetcher 함수 fetchCoins를 불러오고
fetcher 함수가 isLoading(boolean)이라면 false, true라면 return json값을 CoinInterface가 적용 된 data라는 Property로 받을 것 이다.
이렇게 하면 react query를 사용하여 fetch를 진행할 수 있다.
이때 달라진 점이 있다.
뒤로가기를 하였을 때 정보가 새로고침 되지 않고, 로딩이 되지 않는다.
이유는 무엇일까?
react-query가 데이터를 "allCoins" 라는 이름으로 캐시에 저장해두기 때문이다.
react-query는 데이터를 파괴하지 않고 데이터를 유지하고 있는다.
캐시를 직접 컨트롤 하는건 까다롭지만 react query를 이용하면 보다 쉽게 캐시를 이용할 수 있다.
조금 더 응용을 해보자
fetch함수에 argument를 넘기고 잘 동작하는지 확인해보자
api.ts
const BASE_URL = 'https://api.coinpaprika.com/v1'
export function fetchCoins() {
return fetch(`${BASE_URL}/coins`).then((response)=> response.json());
}
export function fetchCoinInfo(coinId:string) {
return fetch(`${BASE_URL}/coins/${coinId}`).then((response)=> response.json());
}
export function fetchCoinTickers(coinId:string) {
return fetch(`${BASE_URL}/tickers/${coinId}`).then((response)=> response.json());
}
위 코드와 같이 fetch함수에 coinId argument를 받아보려고 한다.
이때 useQuery는 어떻게 작성해야할까?
const { isLoading: infoLoading, data: infoData } = useQuery(
["info", coinId],
() => fetchCoinInfo(coinId)
);
const { isLoading: priceLoading, data: priceData } = useQuery(
["price", coinId],
() => fetchCoinTickers(coinId)
);
여기에서 만약 useQuery의 key를 정하는 곳에서 coinId로만 하였다면 캐시의 이름이 겹쳐서 좋지 않을 것 이다. 그래서 'info', 'price'의 이름을 붙여서 차별점을 주었고, return들에 대해서도 isLoading 뒤에 : infoLoading의 이름과 데이터 이름을 붙여서 차별점을 주었다.
결과화면은 밑 사진과 같다.