이번 개인 프로젝트때 주로 사용했던 Tanstack Query에 대해 알아보자. 리액트는 정말 훅이 많다..고 항상 느낀다.. 이번 TIL은 리액트 쿼리에 대해 깊이는 아니지만 내가 이해한만큼 써보려고 한다.

 

 

Tanstack Query❓

💡 fetching, caching, 서버 데이터와의 동기화를 지원해주는 라이브러리

 

서버 상태를 관리하기 위한 라이브러리로, 데이터를 패칭하고 캐싱, 동기화, 무효화 등의 기능을 제공한다. 복잡하고 장황한 코드가 필요하지 않고 리액트 컴포넌트 내부에서 간단하고 직관적으로 API를 사용할 수 있으며 이전보다 비동기 로직을 간편하게 작성하고 유지보수성을 높일 수 있다.

 

 

 

 

 

사용하는 이유

비동기 로직의 복잡성 해결 필요성

기존의 방식은 Fetching 코드를 작성하고 데이터를 담아 둘 상태(useState) 생성, useEffect를 이용해 컴포넌트 마운트시 데이터를 Fetching 한 뒤 상태에 저장하였다. 리액트 쿼리를 사용하면 해당 로직의 복잡함을 해결할 수 있다.

 

서버 상태 관리의 어려움

서버 상태는 클라이언트 상태와 달리 캐싱, 동기화, 재검증 등 관리해야 할 요소가 많아 기존 방법으론 관리가 어려움.

 

 

 

 

주요기능

  • 데이터 캐싱: DB서버에 여러번 동일한 데이터를 요청하지 않고 캐싱하여 데이터를 빠르게 가져온다.
  • 자동 리페칭: 데이터가 변경되었을 때 자동으로 리페칭하여 최신 상태를 유지한다.
  • 쿼리 무효화: 특정 이벤트가 발생했을 때 쿼리를 무효화하고 데이터를 다시 가져온다.

 

데이터를 가져오고, 수정하고, 리프레시한다. 무조건 외우기!!

📌 get / Modify / refresh

get = useQuery
Modify = useMutation
refresh = invalidateQueries

 

 

 

 

라이브러리 설치

yarn add @tanstack/react-query

 

리액트 쿼리를 사용하려면 백엔드 데이터 베이스가 필요해서 json-server로 임시 데이터 베이스를 만들어준다.

yarn add json-server

 

 

 

단축명령어 설정

 

📂package.json에서 아래와 같이 설정해준다. 그러면 yarn json 입력시 서버가 켜진다.

{
  "scripts": {
	"json": "json-server --watch db.json --port 5000",
  }
}

 

단축 명령어 없이 서버를 키는 명령어는 아래와 같다.

json-server --watch db.json

 

 

 

 

db.json

📂src > 📄db.json

{
  "expenses": [
    {
      "id": "f70630fd-852d-4566-a830-9d318c22046a",
      "date": "2024-06-18",
      "item": "📚",
      "amount": 50000,
      "desc": "리액트",
      "month": 5,
      "user": "wnswns"
    },
  ]
}

 

 

 

 

QueryClient 생성

useQuery를 사용할 곳에 쿼리 클라이언트를 생성해준 뒤 Provider를 적용해준다. 이렇게하면 전역에서 사용할 수 있다.

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

const queryClient = new QueryClient();

ReactDOM.createRoot(document.getElementById("root")).render(
  <QueryClientProvider client={queryClient}>
    <App />
  </QueryClientProvider>
);

 

 

 

 

데이터 가져오기 : useQuery()

useQuery에 쿼리 키비동기 함수를 인자로 받아 데이터를 가져오고, 로딩 상태, 오류 상태, 그리고 데이터를 반환한다.

여기서 쿼리키는 배열로 들어간다. 

useQuery({ queryKey: [쿼리키], queryFn: 비동기 함수 })

 

실무에서 자주 사용하는 쿼리키 이름

💡 꼭 알아야 하는 것
 - 다른 API는 다른 쿼리키를 사용한다. (ex. 목록 API와 상세 API는 다른 쿼리키를 사용한다)
 - 목록 페이지에선 `["posts"]` or `["movies"]` or `["todos"]`이렇게 쓰는 경우가 많음.
 - 상세 페이지에서는 `["posts", id]` 이렇게 쓰는 경우가 많음.
 - 페이지네이션의 쿼리키는 `["posts", page]` 이렇게 쓰는 경우가 많음

 

 

 

 

서버에서 데이터를 가져오는 API 함수를 만들어보자. 

import axios from "axios";

const JSON_SERVER_HOST = "http://localhost:4000";

export const getExpense = async () => {
  try {
    const response = await axios.get(`${JSON_SERVER_HOST}/expenses`);
    return response.data;
  } catch (error) {
    console.error(error);
    alert("데이터를 가져오지 못했습니다.");
  }
};

 

 

위에서 만든 API 함수를 useQuery에 적용해보자. 아래 코드로 적용하면 데이터 가져오는 로직 완성.

useQuery({ queryKey: ["expense"], queryFn: getExpense })

 

위 코드를 아래 코드처럼 쓸 수 있다.

  • data : 화면에 보여줄 데이터 (queryFn의 return 값)
  • isLoading : 데이터 로딩 여부 (true, false)
  • error : 데이터 로딩 중 발생한 에러 데이터
const { data, isLoading, error } = useQuery({
  queryKey: ["expense"],
  queryFn: getExpense,
});

 

데이터를 받아오기 전 로딩중으로 뜨고, 데이터를 가져오지 못할 경우 예외 처리를 해줄 수 있다.

  if (isLoading) {
    return <div>로딩중입니다.</div>;
  }

  if (error) {
    return <div>데이터를 불러오지 못했습니다.</div>;
  }

 

 

 

 

데이터 추가, 수정, 삭제하기 : useMutation()

export const postExpense = async (newExes) => {
  try {
    const response = await axios.post(`${JSON_SERVER_HOST}/expenses`, newExes);
    return response.data;
  } catch (error) {
    console.error(error);
    alert("데이터를 생성하지 못했습니다.");
  }
};

 

 

다른 컴포넌트에서 리액트 쿼리를 써야할 경우 ✔️ useQueryClient() 훅을 사용해줘야한다. invalidateQueries 속성을 사용해주기 위해선 필수이다. 생성 후 데이터를 다시 가져와야하기 때문에 invalidateQueries를 사용해줬다. 기존에 있던(데이터를 생성하기전) 데이터는 무효화시키고 최신 상태(데이터가 추가된)를 가져온다

  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: postExpense,
    onSuccess: () => {
      queryClient.invalidateQueries(["expenses"]);
    },
  });

 

 

새로운 데이터가 추가되는 함수에 mutation.mutate 사용해주면 추가하는 로직도 완성이다. 차례대로 수정과 삭제도 같은 방법으로 하면 된다!

const onInsert = useCallback(
    (date, item, amount, desc) => {
      const newExes = {
        id: uuidv4(),
        date,
        item,
        amount,
        desc,
        month: selectedMonth,
        user: userInfo.id,
      };

      mutation.mutate(newExes);
    },
    [mutation, userInfo]
  );

+ Recent posts