티스토리 뷰

Redux, zustand, Recoil은 클라이언트의 상태를 관리한다면 React Query는 서버의 상태를 관리합니다. 

 

프론트에서 사용하는 데이터는 크게 두 가지로 분류할 수 있는데

정말 프론트에서만 쓰는 view를 위한 데이터가 있고, API에서 가지고 온 데이터(실제로는 서버에 존재하는 데이터) 이렇게 두 가지 상대값이 존재합니다. 리액트 쿼리는 이 두 가지 중 서버의 상태를 관리하기 위한 상태관리 라이브러리 입니다. 

 

React-query 는 서버 상태를 관리하기 위한 상태 관리 라이브러리입니다.
우리가 어떤 데이터를 서버에 요청하고 나서부터 요청을 받은 후까지 데이터를 받아오기 전까지 참조 못하게 하는 기능, 게시글 목록 중 한 데이터 수정 api를 호출했다면 게시글 목록 자체를 리패칭하기 등 직접할 일이 참 많습니다. 리액트 쿼리는 이런 비동기 처리를 편하게 할 수 있도록 많은 기능을 제공합니다. 

리액트 쿼리와 리덕스를 비교하면 상태관리의 흐름이 다릅니다. 

-> 리액트 쿼리는 API요청을 보내는 것에 특화되어 있습니다. 

React Query의 상태 관리 흐름

  • fetching: 데이터 요청 중
  • fresh(신선한 데이터): 데이터를 갓 받아온 직후/ 컴포넌트의 상태가 변하더라도 데이터 재요청하지 않음
  • stale(상한 데이터): 데이터 만료/ 최신화가 필요한 데이터
  • inactive: 쿼리가 언마운트된 상태 (더는 사용하지 않는 상태)
    • 🔥주의🔥 쿼리가 언마운트된다고해서 비동기 요청이 취소되는 것은 아닙니다. 프로미스가 일단 만들어지고 언마운트된 거라면 데이터는 캐시에 살아있을 수 있습니다. 
  • delete: 완전히 삭제된 상태(캐시 데이터가 메모리에서 삭제)

주요 개념

  • query(쿼리): 쿼리 키 + 쿼리 함수 
  • query key(쿼리 키): 쿼리를 구분하기 위한 특정 값(문자, 배열, 딕셔너리 등)
  • query function(쿼리 함수): 서버에서 데이터를 요청하고 Promise를 리턴하는 함수(= 비동기 요청)
  • data: 쿼리 함수가 리턴한 Promise가 resolve된 값 
  • staleTime: 쿼리 데이터가 fresh에서 stale로 전환되는데 걸리는 시간. 기본 값: 0 
  • cacheTime: unused 또는 inactive 캐시 데이터가 메모리에서 유지될 시간. 기본 값은 5분이며 설정한 시간을 초과하면 메모리에서 제거된다. 

리액트 쿼리에서 가장 중요한 개념은 Query(쿼리)입니다. -> 리덕스에서 Store 정도의 개념이라고 보면 된다. 

Store는 직접 무슨 데이터를 바꿔야만 했지만(임의로 Store의 값을 변경해야 데이터가 바뀜) 쿼리는 쿼리 키(Query Key)값과 쿼리 펑션(Query Function)이 있습니다.

*여기서 Query function은 보통 API를 요청하는 함수입니다. 

쿼리 키에 같은 쿼리 키를 주고 어디선가 쿼리 키가 호출되면 이 키를 갖고 있는 요소가 뭔가 변했을 때 쿼리 펑션을 통해 API호출을 다시하게 된다. 즉, Store과 다르게 키 값에 엮여있는 무언가가 바뀌게 되면 다시 API에 요청을 해서 같은 키 값에 있는 데이터를 바꿉니다. 

 

리액트 쿼리를 사용하려면 당연히 API가 있어야합니다. (서버: ㅎㅇ) 

저는 이전에 만들어둔 mock API를 사용하겠습니다.

 

먼저, yarn(패키지 매니저)을 통해 axios 와 react-query를  설치해줍니다. 

API요청시 axios를 사용하고 서버데이터 관리를 실습해볼 것이기 때문에 당연히 react-query를 설치해야겠죠?

 

리덕스가 Store를 통해 데이터를 주입하는 것을 기억하시나요? 

리액트 쿼리도 동일하게 데이터를 주입해주는 단계가 필요합니다. 

src/App.js

🙋‍♂️리액트 쿼리는 자체적으로 dev tool을 제공합니다. (따로 추가로 설치할 필요 없어요!)
아래와 같이 react-query/devtool 에서 ReactQueryDevtools 컴포넌트를 가져오고 
initialIsOpen의 값을 true로 주면 화면에 react query devtool이 활성화됩니다. 

dev tool을 사용하면, 데이터의 상태를 알 수 있습니다. fresh/ fetching/stale/inactive
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import { ReactQueryDevtools } from "react-query/devtools";
import { QueryClient, QueryClientProvider } from "react-query";

const queryClient = new QueryClient();
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <QueryClientProvider client={queryClient}>
    <ReactQueryDevtools initialIsOpen={true} />
    <App />
  </QueryClientProvider>
);
데이터를 가져오는 기능은 useQuery라는 훅을 사용하고, 데이터를 수정할 때는 useMutation을 사용합니다. 
  • React-query가 API 통신과 밀접한 것을 기억하면 GET 요청은 useQuery훅, POST PUT DELETE 요청은 보통 useMutation훅을 사용합니다. 

useQuery() 훅 사용하여 데이터 가져오기

  • useQuery를 사용하여 쿼리 인스턴스를 만들어봅시다. 

src/App.js

import React from "react";
import "./App.css";
import { useQuery } from "react-query";
import axios from "axios";

const getDummyList = () => {
  return axios.get("http://localhost:5001/DUMMY_ROOM_LIST");
};

function App() {
  //✅useQuery 훅은 첫번째 인자로 쿼리 키를 받는다.
  //✅두번째 인자로 API 요청 함수입니다. getData(), 프라미스 객체 이런거
  //✅세번째 인자로는 옵션객체(요청 성공시 머할지, 실패시 등등) 를 넘길 수 있습니다.
  const dummy_query = useQuery("dummy_list", getDummyList, {
  //dummy_query를 5번호출하면 getDummyList함수는 1번실행되는데 
   //옵션에 넣은 것들은 5번 연속실행된다. 이 점을 염두해둬야 한다.
    onSuccess: (data) => {
      console.log(data);
    },
    //fresh -> stale로 가는 과정의 시간을 10초로 늘려줬다.
    //데이터가 stale상태가 되면 다른 탭에서 원래 탭으로 다시 돌아올 때 API호출을 자동으로 하기 때문(기본옵션때문임)
    staleTime: 10000, 
  });

  return <div className="App"></div>;
}

export default App;
 console.log(dummy_query.data.data); 
 /* 해보면 우리가 API로 가져오고 싶은 데이터가 들어있다. 
 하지만 맨 처음 상태, 즉 status가 loading일 때에는 dummyList.data.data에 접근할 수 없기 때문에
 따로 처리해줘야 한다. */
 //리액트 쿼리로 가져온 데이터는 isLoading이라는 state를 따로 만들 필요가 없다.
 //아래와 같이 바로 사용하면 된다.
 if(dummy_query.isLoading){
 return null;
 }

useMutation() 훅 사용하여 데이터 수정하기

import React, { useRef } from "react";
import "./App.css";
import { useQuery, useMutation } from "react-query";
import axios from "axios";

const getDummyList = () => {
  return axios.get("http://localhost:5001/DUMMY_ROOM_LIST");
};

const addSleepData = (data) => {
  return axios.post("http://localhost:5001/DUMMY_ROOM_LIST", data);
};

function App() {
  const day_input = useRef("");
  const time_input = useRef("");
  //useQuery 훅은 첫번째 인자로 쿼리 키를 받는다.
  //두번째 인자로 API 요청 함수입니다. getData(), 프라미스 객체 이런거
  //세번째 인자로는 옵션객체(요청 성공시 머할지, 실패시 등등) 를 넘길 수 있습니다.
  const dummy_query = useQuery("dummy_list", getDummyList, {
    //dummyList를 5번호출하면 getDummyList함수는 1번실행되는데
    //옵션에 넣은 것들은 5번 연속실행된다. 이 점을 염두해둬야 한다.
    onSuccess: (data) => {
      console.log(data);
    },
  });

  //useQuery와 다르게 유니크 키값이 필요없이 바로 첫번째인자로 함수를 바로 넣어주고
  //두번째인자로 옵션이 들어갑니다.
  // mutate라는 함수를 구조분해할당으로 받아와야 합니다.
  //보통은 mutate함수를 이름을 따로 붙여서 뽑아내는데 그냥해도된다.

  // const { mutate: addSleepDataMutate } = useMutation(addSleepData);
  const { mutate } = useMutation(addSleepData);

  //isLoading은 기본 프로퍼티로 제공된다. 따로 만들 필요가 없다.
  if (dummy_query.isLoading) {
    return null;
  }
  return (
    <div className="App">
      {dummy_query.data.data.map((el) => {
        return (
          <div key={el.id}>
            <p>{el.day}</p>
            <p>{el.sleep_time}</p>
          </div>
        );
      })}
      <input ref={day_input} />
      <input ref={time_input} />
      <button
        onClick={() => {
          const data = {
            day: day_input.current.value,
            sleep_time: time_input.current.value,
          };
          mutate(data);
        }}
      >
        데이터 추가하기
      </button>
    </div>
  );
}

export default App;
  • 데이터를 추가할 때, 데이터가 화면에 바로 반영되지 않는 이유캐싱된 데이터를 후처리해주지 않기 때문입니다.
  • 즉, 현재 쿼리가 가지고 있는 값은 stale된 값, 업데이트가 되기 전의 상한 데이터 이기 때문에 이 데이터를 최신형으로 업데이트하려면 해당 쿼리를 무효화시키면 됩니다.
  • useMutate의 2번째 인자인 옵션객체에 성공 시 후처리 메소드를 만들어주면 되는데 onSuccess시  invalidateQueries를 호출해주면 됩니다.
    • useQueryClient라는 훅을 사용하면 어떤 유니크한 키값을 가진 쿼리를 무효화시킬 수 있는 함수(invalidateQueries)를 사용할 수 있습니다. invalidateQueries의 인자로는 지우고자하는 쿼리의 유니크 키를 넣어주면 되는데 아무 값도 넣어주지 않으면 모든 캐싱된 쿼리들이 날라가게되니 주의해야 합니다.
import React, { useRef } from "react";
import "./App.css";
import { useQuery, useMutation, useQueryClient } from "react-query";
import axios from "axios";

const getDummyList = () => {
  return axios.get("http://localhost:5001/DUMMY_ROOM_LIST");
};

const addSleepData = (data) => {
  return axios.post("http://localhost:5001/DUMMY_ROOM_LIST", data);
};

function App() {
  const day_input = useRef("");
  const time_input = useRef("");

  //현재 쿼리데이터를 무효화시켜주는 hook useQueryClient
  //이 queryClient의 invalidateQueries라는 함수가 쿼리(유니크한 쿼리 키를 가지고 있는 쿼리)를 무효화 시켜준다.
  const queryclient = useQueryClient();

  //useQuery 훅은 첫번째 인자로 쿼리 키를 받는다.
  //두번째 인자로 API 요청 함수입니다. getData(), 프라미스 객체 이런거
  //세번째 인자로는 옵션객체(요청 성공시 머할지, 실패시 등등) 를 넘길 수 있습니다.
  const dummy_query = useQuery("dummy_list", getDummyList, {
    //dummyList를 5번호출하면 getDummyList함수는 1번실행되는데
    //옵션에 넣은 것들은 5번 연속실행된다. 이 점을 염두해둬야 한다.
    onSuccess: (data) => {
      console.log(data);
    },
  });

  //useQuery와 다르게 유니크 키값이 필요없이 바로 첫번째인자로 함수를 바로 넣어주고
  //두번째인자로 옵션이 들어갑니다.
  // mutate라는 함수를 구조분해할당으로 받아와야 합니다.
  //보통은 mutate함수를 이름을 따로 붙여서 뽑아내는데 그냥해도된다.

  // const { mutate: addSleepDataMutate } = useMutation(addSleepData);
  const { mutate } = useMutation(addSleepData, {
    onSuccess: () => {
      //데이터 목록을 다시 불러오면 ok!
      //invalidateQueries의 인자로 무효화시킬 쿼리의 키값을 넘겨주면된다.
      //쿼리 키를 안넘기게 되면 모든 쿼리가 날라갑니다.
      queryclient.invalidateQueries("dummy_list");
      //성공 시 input값 비워주기
      day_input.current.value = "";
      time_input.current.value = "";
    },
  });

  //isLoading은 기본 프로퍼티로 제공된다. 따로 만들 필요가 없다.
  if (dummy_query.isLoading) {
    return null;
  }
  return (
    <div className="App">
      {dummy_query.data.data.map((el) => {
        return (
          <div key={el.id}>
            <p>{el.day}</p>
            <p>{el.sleep_time}</p>
          </div>
        );
      })}
      <input ref={day_input} />
      <input ref={time_input} />
      <button
        onClick={() => {
          const data = {
            day: day_input.current.value,
            sleep_time: time_input.current.value,
          };
          mutate(data);
        }}
      >
        데이터 추가하기
      </button>
    </div>
  );
}

export default App;

 

+

🐱‍👤Suspense 사용해보기(React v18)

원래도 리액트에 있던 기능이지만 test버전에서 진짜 기능으로 리액트 18부터 사용이 가능합니다. 
  • 서스펜스는 로딩 중, 에러가 난 경우 처럼 어떤 상황마다 화면 분기 처리를 할 때 편히하기 위해 탄생한 컴포넌트입니다. 
  • 서스펜스는 클라이언트에서만 사용하는 것이 아니라 서버 컴포넌트와 같이 조합해서 쓰면 강력한 기능을 구사할 수 있다고 합니다. 

이전 코드에 isLoading이라는 프로퍼티를 사용하여, 비동기 데이터가 아직 완료되지 않은 경우 return null로 에러가 뜨지않게 했던 것을 기억하시나요? 이 부분을 Suspense를 통해 간편하게 대체할 수 있습니다. 

if (dummy_query.isLoading) {
    return null;
  }

Suspense 사용방법 

 

React.suspsense를 사용하고 싶은 범위의 상위에 위치시켜 컴포넌트를 감싸줍니다.

index.js

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.Suspense fallback={<div>로딩 중입니다.</div>}>
    <QueryClientProvider client={queryClient}>
      <ReactQueryDevtools initialIsOpen={true} />
      <App />
    </QueryClientProvider>
  </React.Suspense>
);
  • 이렇게만 하면 우리가 지정한 fallback 컴포넌트가 loading중에 보이지 않습니다. 아래와 같은 작업을 추가로 해주어야 합니다. 

기존에 client값으로 넘겨준 QueryClient객체의 초기값으로 객체를 넘겨줍니다. 

import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import { ReactQueryDevtools } from "react-query/devtools";
import { QueryClient, QueryClientProvider } from "react-query";

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      suspense: true,
    },
  },
});
  • 이렇게하면 오류 대신 데이터가 fetching되는 중에 <div>로딩중입니다.</div>라는 fallback이 대신 보이게 됩니다. 
🤸‍♀️ 리액트 쿼리와 서스펜스가 같이 사용하기 좋은 이유는 바로 defaultOptions의 queries 의 suspense가 기본적으로 속성으로 들어가 있기 때문입니다. 그렇기 때문에 suspense 의 값을 true로 설정해주면 그제서야 화면에 loading시 표시하려고 했던 fallback 컴포넌트가 보이게 됩니다. 

 

댓글