[React] react query에 대해 알아보기

2022. 5. 19. 17:54React

    목차

*이는 노마드코더 리액트마스터 강의를 통해 배운 내용을 정리한 개념입니다 

* 챌린지 내용은 전혀 없습니다

개요

React Query : 서버 상태 관리를 도와주는 라이브러리!

장점

(1) 캐시를 사용한다

이전 페이지가 돌아왔을 때 다시 로딩되는 것이 아닌, 캐시에 저장되기 때문에 다시 한 번  API를 불러오는 작업이 없다! 한 번만 불러오고 끝!

 

(2) 자동으로 데이터를 가져온다.

가져온 데이터가 업데이트 되면 자동으로 데이터를 다시 가져온다.

 

(3) ★클린 코드 작성이 가능하다.

기존의 api 연동 방식은 fetch를 사용하여 긴 코드를 주저리 주저리 쓰는 것이었지만,

react query를 사용한다면, 짧은 코드만으로 api 연동이 가능하다!

 

예시) 기존 fetch만을 사용하여 암호화폐 정보를 받아오는 api

function Coin () {
    const {coinId} = useParams<Params>();
    const [loding, setLoding] = useState(true);
    const {state} = useLocation<RouteState>(); //react-router-dom에서 제공하는 useLocation
    const [data, setData] = useState<IInfoData>();
    const [price, setPrice] = useState<IPriceData>();
    
    useEffect ( ()=> {
        (async () => {
            const coinData = await (await fetch (`https://api.coinpaprika.com/v1/coins/${coinId}`)).json();
            setData(coinData);
            //코인 가격받기
            const coinPrice = await (await fetch(`https://api.coinpaprika.com/v1/tickers/${coinId}`)).json();
            console.log(coinPrice);
            setPrice(coinPrice);
            setLoding(false);
        }
        ) ();
        }, [coinId]);

기존 코드를 살펴보면,  loading usestate, data usestate, price,usestate, useEffect async-await 문

으로 구성되어 코드가 굉장히 복잡하고 길어진다

그러나~

React query를 쓰면

loading usestate, data usestate, price,usestate, useEffect async-await 문 필요없다!!!!!!!!!!!!!

아주 간결하게 몇줄만 적어주면 api 연동부터 Loading 함수, api로 가져온 데이터의 저장이 가능하다

 

 

 

 

환경설정하기

1. 리액트 쿼리 설치
npm i react-query

 

간단하다! react query 하나 설치해주면 끝!

 

 

2. QueryClient, QueryClientProvider  선언하기

//index.tsx

import React from "react";
import ReactDOM from "react-dom";
import { QueryClient, QueryClientProvider } from "react-query";
import { ThemeProvider } from "styled-components";
import App from "./App";
import { theme} from "./theme";


const queryClient = new QueryClient();

ReactDOM.render(
  <React.StrictMode>
    {/* QueryClientProvider는 client Props(queryClient)가 필요! */}
   <QueryClientProvider client={queryClient}>  {/* QueryClientProvider로 App 감싸줌*/}
      <ThemeProvider theme={theme}> 
        <App />
      </ThemeProvider>
   </QueryClientProvider>
  </React.StrictMode>,
  document.getElementById("root")
);

QueryClientProvider:

리액트 쿼리를 사용하기 위한 Provider로, 리액트 쿼리는 감싸주는 역할을 한다! Provider 안에 선언된 내용은 query Client에 접근 가능하다!

 

어차피 이건 index.tsx에서 딱 한 번만 선언하는 내용이므로 외우지 말고, 공식 문서를 참고하여 작성하자!

 

 

 

 

 

 

(1) fetcher 함수 만들기

우리가 위에서 사용했던 coinData, coinPrice를 api.tsx에서 따로 분리하여, 새로운 함수로 만들면

그게  fetcher 함수! 

api는 코인파프리카를 사용했다!

//api.tsx

export async function fetchCoins() { //json Data의 promise를 return하고 내보낸다
    
    //(1)
    const response = await fetch ("https://api.coinpaprika.com/v1/coins"); 
    const json = await response.json();
    
	//(2)
    return json.slice(0,100); //코인 중 100개만 가져옴!
};

(1)

가상화폐의 정보를 가져오는 웹 api를 사용했고, 그 값을  변수 response를 선언하여  fetch한 다음,

변수 json에 api의 json 값을 담았다.

 

(2)

fetcher 함수는 fetch promise를 return해야한다.!

그래서 return json;을 하였고, 그대로 출력하면 몇천개의 암호화폐 목록이 와르르르...~ 출력된다.

colde-와르르♥ 노래 좋습니다

따라서 slice() 함수를 사용하여 100개만 출력했다!

 

2.  useQuery 사용하기

 

import { useQuery } from "react-query";
import {fetchCoins} from "../api";

//중간 코드는 생략

//(1)
const {isLoading, data} = useQuery<CoinInterface[]>("coinListKey", fetchCoins)
// isLoading

(1)

useQuery는 두가지 인수를 받는다.

useQuery("queryKey", fetcher함수)

첫번째 인자 querykey는 임의의 고유 식별자를 넣어주면 된다. 

고유 식별자가 있으면, 그 변수값을 적어주고,  나는 string 형태인  "coinListKey"라고 작성했다!

 

두번째 인자는 fetcher 함수인데,

api.tsx에서 export 했던 fetchCoins 함수를 import 한 후, 적어주었다. 

 

const {      } =  useQuery("queryKey", fetcher함수);  

 

{ } 사이에, reactQuery에서 제공하는 값을 넣어줘야한다!

아무거나 막 작성하면 안된다!

isError를 사용하여 에러 핸들링을 할 수도 있고 제공되는 함수를 통해 디양한 활용 가능하다!

나는 loading state API에서 가져온 값을 관리하기 위해

    const {isLoading, data} = useQuery<CoinInterface[]>("queryKey", fetchCoins);

로 선언했다!

isLoading은 true false를 return하는데 이를 작성해주면

따로 setLoading() useState를 작성할 필요 없이,

fetchCoins를 통해 Api 값을 로딩중이면, isLoading가 로딩중이라고 알려줄 수 있다!

 

또한 두번째 itme data를 통해  isLoading이 로딩을 완료하면, api json값을 data에 넣어준다!

 

 

 

 

React Query Devtools 사용하기

 

React Query Devtools은 이름 그대로 리액트 쿼리를 개발자들이 쉽게 사용할 수 있도록 도와주는 툴이다!

이를 사용하면 쿼리가 어떻게 작동하고 있는지 예쁘게 보여준다!

 

//App.tsx
import Router from "./routes/Router";
import {ReactQueryDevtools} from "react-query/devtools"; //캐시에 있는 쿼리를 볼 수 있게 도와주는 tool


function App() {

//ReactQueryDevtools은 라우터 아래에 컴포넌트 형식으로 작성
//initialIsOpen={true}를 작성하여 콘솔로 띄울 수 있게 만듬
  return (
    <>
      <Router /> 
      <ReactQueryDevtools initialIsOpen={true}/> 
    </>
  );
}

export default App;

 

실행시,  실행 웹사이트 아래에 콘솔 형식으로 쿼리 상태를 보여준다

 

 

심화

위에선 암호화폐들의 목록을 보여주는 api를 연동했다면, 

지금은 사용자가 클릭하여 들어간 암호화폐의 정보를 가져오는 api 연동을 해보겠다!

아까와 순서는 같다

(1) fetcher 함수 만들기

우리가 지금까지 사용했던 fetch함수를 api.tsx로 분리하자.

 

export async function fetchCoins() { //암호화폐 리스트
    const response = await fetch ("https://api.coinpaprika.com/v1/coins");
    const json = await response.json();

    return json.slice(0,100); //100개로 잘라줌
};

export async function fetchCoinData(coinId:string) { //코인 하나하나의 정보 api
        const coinData = await (await fetch (`https://api.coinpaprika.com/v1/coins/${coinId}`)).json();  
        //coinId가 정의되지 않았음. 함수의 인수로 coinId 전달
            return coinData;
};

export async function fetchCoinPrice(coinId:string) { //코인 가격api
      //코인 가격받기
    const coinPrice = await (await fetch(`https://api.coinpaprika.com/v1/tickers/${coinId}`)).json(); 
    //coinId가 정의되지 않았음. 함수의 인수로 coinId 전달
        return coinPrice;
};

coinId는 Api.tsx에 선언되지 않은 변수이므로, coinId가 string이라는 것을 알려주며 함수의 인수로 전달하였다!

 

 

 

2.  useQuery 사용하기

 

//Coin.tsx
import styled from "styled-components";
import { Route, Switch, useLocation, useParams } from "react-router-dom";
import { useState } from "react";
import { useEffect } from "react";
import Price from "./Price";
import Chart from "./Chart";
import { useQuery } from "react-query";
import { fetchCoinData, fetchCoinPrice } from "./Api";



//스타일드 컴포넌트는 생략함


interface IInfoData {
    id : string;
    name : string;
    symbol : string;
    rank : number;
    is_new : boolean;
    is_active : boolean;
    type : string;
    description : string;
    message : string;
    open_source : boolean;
    started_at : string;
    development_status : string;
    hardware_wallet : boolean;
    proof_type : string;
    org_structure : string;
    hash_algorithm : string;
    first_data_at : string;
    last_data_at : string;
};

interface IPriceData {
    id: string;
    name: string;
    symbol: string;
    rank: number;
    circulating_supply: number;
    total_supply: number;
    max_supply: number;
    beta_value: number;
    first_data_at: string;
    last_updated: string;
    quotes: {
      USD: {
        ath_date: string;
        ath_price: number;
        market_cap: number;
        market_cap_change_24h: number;
        percent_change_1h: number;
        percent_change_1y: number;
        percent_change_6h: number;
        percent_change_7d: number;
        percent_change_12h: number;
        percent_change_15m: number;
        percent_change_24h: number;
        percent_change_30d: number;
        percent_change_30m: number;
        percent_from_price_ath: number;
        price: number;
        volume_24h: number;
        volume_24h_change_24h: number;
      };
    };
  }


function Coin () {
    const {coinId} = useParams<Params>();
    const {state} = useLocation<RouteState>();
    //(1)
    const {isLoading: dataLoading, data:coinData} = useQuery<IInfoData>(["coinData",coinId], () => fetchCoinData(coinId)); 
    const {isLoading:priceLoading, data:priceData} = useQuery<IPriceData>(["coinPrice",coinId], () => fetchCoinPrice(coinId));
    
    const loading = dataLoading || priceLoading; //loading은 데이터로딩 ||가격로딩 둘 다
    return (
        <Container> 
            <Header>
                 <Title>코인 목록 {state?.name || "loading"} </Title>
            </Header>
               {loading ? (
        <Loader>Loading...</Loader>
      ) : (
        <>
          <Overview>
            <OverviewItem>
              <span>순위:</span>
              <span>{coinData?.rank}</span>
            </OverviewItem>
            <OverviewItem>
              <span>심볼:</span>
              <span>${coinData?.symbol}</span>
            </OverviewItem>
            <OverviewItem>
              <span>오픈소스 가능 여부:</span>
              <span>{coinData?.open_source ? "Yes" : "No"}</span>
            </OverviewItem>
          </Overview>
          <Description>{coinData?.description}</Description>
          <Overview>
            <OverviewItem>
              <span>Total Suply:</span>
              <span>{priceData?.total_supply}</span>
            </OverviewItem>
            <OverviewItem>
              <span>Max Supply:</span>
              <span>{priceData?.max_supply}</span>
            </OverviewItem>
          </Overview>
          <Switch>
            <Route path={`/${coinId}/price`}>
              <Price />
            </Route>
            <Route path={`/${coinId}/chart`}>
              <Chart />
            </Route>
          </Switch>
        </>
      )}
        </Container>

       

    );
};

export default Coin;

(1)

const {isLoading: dataLoading, data:coinData} = useQuery<IInfoData>(["coinData",coinId], () => fetchCoinData(coinId));
const {isLoading:priceLoading, data:priceData} = useQuery<IPriceData>(["coinPrice",coinId], () => fetchCoinPrice(coinId));

 

 

 const {isLoading: dataLoading, data:coinData}

React Query를 두 개를 사용할 때 인수가 두 번 반복되는 일이 생길수 있다!

이럴 땐 동일한 인수를 사용하는 게 안되므로, 각각 이름을 알맞게 지어주자

 

여기서 잠깐!

 

 

 

useQuery<IInfoData>(["coinData",coinId]


   리액트 쿼리는 각각 다른 key를 원하기 때문에 같은 키를 쓰는 건 좋지 않다!
   

 

디벨롭툴을 사용하여 확인하면, 리액트 쿼리는 querykey를 array로 감싸서 표현한다는 것을 알 수 있다!

 


    따라서,

(1) key를 array로 만든 다음, 
 (2)  첫번째는 coinData, 두번째는 coinPrice로 주면 각각 고유한 id를 가질 수 있다!

 

 

useQuery<IInfoData>(["coinData",coinId], () => fetchCoinData(coinId));

fetchCoinData(coinId)는

함수 실행하는 것이기 때문에 함수 실행 후의 promise가 들어간다.
 따라서, 함수를 바로 실행하는 것이 아닌 () => fetchCoinData(coinId) 형식으로

함수를 실행하는 함수를 만들어 인자로 넘겨야 <함수 자체>를 넘길 수 있다!
 

//fetchCoin: 함수 자체를 넘기는 것 

fetchCoin(): 함수 실행 후의 리턴값을 넘기는 것

 

 

이를 실행해보면,

쿼리에 coinPrice와 coinData가 뜨는 것을 알 수 있고,

되돌아가도 캐시에 데이터가 저장되어 있기 때문에 api를 다시 불러와 로딩하는 현상이 없다!!!!!!!!!

 

 

참고 사이트

 

 

React Query

Hooks for fetching, caching and updating asynchronous data in React

react-query.tanstack.com