2차 공부/TIL

24.09.05 Tanstack Query 세팅과 사용법

공대탈출 2024. 9. 5. 15:02
import React, { useState, useEffect } from "react";
import axios from "axios";

const App = () => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      setLoading(true);
      try {
        const response = await axios.get("http://localhost:4000/todos");
        setData(response.data);
      } catch (error) {
        setError(error);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, []);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      <h1>Fetched Data</h1>
      //...
    </div>
  );
};

export default App;

우리는 데이터 페칭을 useEffect를 사용하여 컴포넌트가 마운트 될 때 비동기 요청을 통해 데이터를 불러왔다. 그리고 그 데이터를 state에 저장하고, 로딩이나 에러관련도 useState hook을 사용하여 관리를 했었다.

하지만 이 방식에는 여러 문제가 있다.

  1. 상태 관리의 복잡성
    • 컴포넌트 내에서 여러 상태를 관리해야 함
    • 상태 관리가 복잡하고 각 상태에 따른 로직이 분산되어있어 코드가 복잡해짐
  2. 중복된 코드
    • 여러 컴포넌트에서 동일한 데이터를 페칭하는 경우 각 컴포넌트마다 동일한 비동기 로직을 반복해야함
    • 코드 중복이 되며 유지보수에 어려움을 줌
  3. 비즈니스 로직의 분리 부족
    • 비동기 로직이 컴포넌트 내부에 포함되면, 비즈니스 로직과 UI 로직이 합쳐져 코드 가독성을 저해하고 유지보수를 어렵게함
  4. 서버 상태 관리의 어려움
    • 서버상태(API로부터 페칭된 데이터)를 효율적으로 관리하기 어려움
    • 데이터의 캐싱, 동기화, 리페칭 등의 기능을 구현하려면 많은 노력과 시간이 들어가야함

이를 해결하고자 Redux에서 middleware(thunk)를 제시해주었다. 일반적인 redux는 dispatch가 일어났을 때 action 객체를 dispatch하여 리듀서로 전달하고 store을 업데이트하지만...

redux thunk는 dispatch가 일어날 때 thunk함수를 dispatch한다. 전달된 thunk함수에 의해 redux thunk middleware가 해당 함수를 오출하고, 비동기 작업을 수행한 뒤 필요에 따라 액션객체를 생성하여 리듀서로 dispatch한다. 그 후 store을 업데이드 한다.

이러한 redux thunk는 상태관리의 중앙집결으로 일관성을 제공하며, 비즈니스 로직 분리를 가능케하고, 로딩 및 에러 상태를 관리하기 쉬워지지만, 아래와 같은 단점이 존재한다.

 

  1. 복잡성 증가
    • 안그래도 보일러플레이트 코드가 긴 리덕스에 더 긴 코드들을 추가하면서 유지보수에 어려움을 준다.
  2. 테스트 복잡성 증가
    • 비동기 로직을 포함한 액션 크리에이터를 테스트하는 것이 복잡하며, 다양한 응답상태와 비동기 작업을 시뮬레이션 하기위한 별도의 장치가 필요해 테스트코드가 복잡해진다.

끔찍한 리덕스코드는 안녕!

이런 비동기 로직의 복잡성을 해결하고, 서버 상태 관리의 어려움을 해결하기 위해 Tanstack Query(react query)가 등장했다.

tanstack query는 크게 세가지만 기억하면된다.

useQuery - Get요청, CRUD의 R

useMutataion - POST, PUT/PATCH, DELETE 요청, CRUD의 CUD

invalidateQueries - 데이터 리페칭하여 최신상태 유지


//tanstack query, json-server, axios 설치

yarn add @tanstack/react-query
yarn add json-server
yarn add axios

context나 redux같이 Provider로 전체를 감싼다.

// main.jsx

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>
);

여기서 queryClient를 만들어 주고, 사용할떄는 new QueryClient가 아닌 useQueryClient를 사용한다.


useQuery - GET

import { useQuery } from "@tanstack/react-query";
import axios from "axios";

const App = () => {
  const fetchTodos = async () => {
    const response = await axios.get("http://localhost:4000/todos");
    return response.data;
  };

  const {
    data: todos,
    isPending,
    isError,
  } = useQuery({
    queryKey: ["todos"],
    queryFn: fetchTodos,
  });
  
  if (isPending) {
    return <div>로딩중입니다...</div>;
  }
  if (isError) {
    return <div>데이터 조회 중 오류가 발생했습니다.</div>;
  }

  return (
    <div>
      <h3>TanStack Query</h3>
      <ul>
        {todos.map((todo) => {
          return (
          	//...
          );
        })}
      </ul>
    </div>
  );
};

export default App;

앞에서 말했듯 useQuery는 get요청에 사용된다. useQuery()내부에 객체 형식으로 queryKey와 queryFn을 넣어 사용한다.

내가 이해한 것으로는 최초 요청시 queryFn으로 데이터를 받아오고, 그걸 queryKey에 맞게 저장을 해주는 것이다.

이때 저장된 값은 캐싱되어 같은 요청에 캐싱된 데이터를 주어 추가적인 데이터 요청이 없다.

const GetTodos2 = () => {
    const fetchTodos = async () => {
        const response = await axios.get("http://localhost:4000/todos");
        return response.data;
    };
    const { data, isPending, isError } = useQuery({ queryKey: ["todos"], queryFn: fetchTodos });
    return (
        <div>
            {data?.map((todo) => {
                return <div key={todo.id}> {todo.title}</div>;
            })}
        </div>
    );
};

같은 요청을 다른 컴포넌트에서 하고있음에도 불구하고, 캐싱된 데이터를 주어 네트워크 요청이 한번만 일어난다.

이렇게 반복되어 사용되는 fetchTodos같은 함수는 별도 파일에 분리보관하는 것이 유지보수에 용이하다.


useMutation - POST, PUT/PATCH, DELETE

 

import { useMutation, useQuery } from "@tanstack/react-query";
import axios from "axios";
import { useState } from "react";

const App = () => {
  const [todoItem, setTodoItem] = useState("");
  const fetchTodos = async () => {
    //...
  };

  const {
    data: todos,
    isPending,
    isError,
  } = useQuery({
    queryKey: ["todos"],
    queryFn: fetchTodos,
  });
  
  const addTodo = async (newTodo) => {
    await axios.post("http://localhost:4000/todos", newTodo);
  };
  const { mutate } = useMutation({
    mutationFn: addTodo,
  });

  return (
    <div>
      <h3>TanStack Query</h3>
      <form
        onSubmit={(e) => {
          e.preventDefault();
          const newTodoObj = { title: todoItem, isDone: false };
          // useMutation 로직 필요
          mutate(newTodoObj);
        }}
      >
        <input
          type="text"
          value={todoItem}
          onChange={(e) => setTodoItem(e.target.value)}
        />
        <button>추가</button>
      </form>
      <ul>
        {todos?.map((todo) => {
          return (
          	//...
          );
        })}
      </ul>
    </div>
  );
};

export default App;

axios.post하는 함수를 만들어두고, useMutation에서 mutationFn에 해당 함수를 지정한다.

그리고 추가동작을 하는 이벤트에 mutation.mutate(추가할 데이터)이렇게 넣어두면 된다.

const { mutate } = useMutation({
	mutationFn: addTodo,
});

보통은 이렇게 mutate를 구조분해할당으로 받아 mutation.mutate()가 아닌 mutate()로 사용한다고 한다.

그런데 이렇게 요청을 하면 화면에 변경이 일어나지 않는다. 그 이유는 특정 쿼리를 무효화하여 데이터를 다시 페칭하게 하는 invalidateQueries를 사용하지 않았기 때문이다.

useMutation과 함께 사용되며 데이터 변경 후 다시 관련 쿼리를 가져오도록 하는 함수이다.


invalidateQueries - 쿼리 최신화

const { mutate } = useMutation({
	mutationFn: addTodo,
	onSuccess: () => {
	// alert("데이터 삽입이 성공했습니다.");
	queryClient.invalidateQueries(["todos"]);
	},
});

useMutation안에 onSuccess로 해당 queryClient를 invalidateQueries하면 된다. 이때 해당 컴포넌트에서 사용한 queryKey를 배열 형태로 집어넣어 줘야한다.

 

React query devtools를 사용하면 애플리케이션의 상태를 시각적으로 디버깅할 수 있다. 쿼리상태, 캐시데이터, 오류 등을 실시간으로 간리할 수 있어 개발중에 상태관리와 데이터 페칭을 쉽게 모니터링할 수 있다.

yarn add @tanstack/react-query-devtools