[React 심화] TanStack Query 1 - 등장배경, 기본사용법

조아영·2025년 3월 17일

📌

TanStack Query는 서버 상태 관리 라이브러리.
데이터 패칭, 캐싱, 동기화, 무효화 등의 기능을 제공. 비동기 로직을 간결하게 작성할 수 있어 유지보수성을 높일 수 있음.

◼ 등장배경

  1. 비동기 로직의 복잡성 해결 필요
    • 기존의 useEffect, useState를 사용한 비동기 데이터 처리 방식은 상태 관리 로직이 분산되기 쉬움. 로딩, 에러, 데이터 상태를 각각 직접 관리해야 하므로 코드 중복이 발생할 수 있음.
    • Redux Thunk와 같은 미들웨어를 사용하더라도 비동기 로직 테스트가 복잡하고 보일러플레이트 코드가 많이 생기므로, 더 효율적인 도구가 필요.
  2. 서버 상태 관리의 어려움
    • 서버 상태는 단순 클라이언트 상태와 달리 캐싱, 동기화, 재검증 등 관리 요소가 많아 기존 방식으로는 관리가 어려움.

→ 이러한 문제를 해결하기 위해 등장한 라이브러리가 TanStack Query.

→ 복잡한 비동기 로직을 간결하게 작성할 수 있음.

→ 서버 상태 관리를 단순화할 수 있음.

◼ 주요기능

  • 데이터 캐싱: 동일한 데이터를 반복 요청하지 않도록 캐싱해 성능을 향상시킴.
  • 자동 리페칭: 데이터가 변경되면 자동으로 리페칭해 최신 상태를 유지함.
  • 쿼리 무효화: 특정 이벤트 발생 시 쿼리를 무효화하고 데이터를 다시 가져옴.

◼ 설정

프로젝트 생성

npm create vite tanstack-query-app --template react

설치

npm install @tanstack/react-query

전역 적용을 위해 Provider 사용. App.jsx 또는 main.jsx(index.jsx)에 세팅 권장.

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

◼ useQuery

useQuery는 데이터 조회 훅.
queryKeyqueryFn을 인자로 전달해 사용. 데이터, 로딩 상태, 에러 상태를 반환하므로 모든 상태를 직접 세팅할 필요 없음.

기본 사용법

fetchTodos와 같은 비동기 함수는 별도 파일 분리 권장. 현재는 학습 목적상 한 파일에서 진행.

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 (
            <li
              key={todo.id}
              style={{
                display: "flex",
                alignItems: "center",
                gap: "10px",
                backgroundColor: "aliceblue",
              }}
            >
              <h4>{todo.title}</h4>
              <p>{todo.isDone ? "Done" : "Not Done"}</p>
            </li>
          );
        })}
      </ul>
    </div>
  );
};

export default App;

테스트 환경(db.json 파일)

{
  "todos": [
    {
      "id": "1715926482394",
      "title": "리액트 공부하기",
      "isDone": true
    },
    {
      "id": "1715926492887",
      "title": "Node.js 공부하기",
      "isDone": true
    },
    {
      "id": "1715926495834",
      "title": "영화보기",
      "isDone": false
    }
  ]
}

◼ useMutation

useMutation생성(Create), 수정(Update), 삭제(Delete) 작업 전용 훅.
CUD 비동기 작업을 처리할 수 있음. 성공 또는 실패 후 추가 작업을 실행할 수 있음.
작업 완료 후 관련 쿼리를 무효화할 수 있으며, 최신 데이터 유지에 필수적임.

기본 사용법

addTodo도 별도 파일 분리 권장. 현재는 학습 목적상 한 파일에서 진행.

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 response = await axios.get("http://localhost:4000/todos");
    return response.data;
  };

  const addTodo = async (newTodo) => {
    await axios.post("http://localhost:4000/todos", newTodo);
  };

  const {
    data: todos,
    isPending,
    isError,
  } = useQuery({
    queryKey: ["todos"],
    queryFn: fetchTodos,
  });

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

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

  if (isError) {
    return <div>데이터 조회 중 오류가 발생했습니다.</div>;
  }

  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 (
            <li
              key={todo.id}
              style={{
                display: "flex",
                alignItems: "center",
                gap: "10px",
                backgroundColor: "aliceblue",
              }}
            >
              <h4>{todo.title}</h4>
              <p>{todo.isDone ? "Done" : "Not Done"}</p>
            </li>
          );
        })}
      </ul>
    </div>
  );
};

export default App;

◼ invalidateQueries

invalidateQueries는 특정 쿼리를 무효화하여 해당 데이터를 다시 패칭하도록 만드는 함수.
주로 useMutation과 함께 사용하며, 서버 데이터가 변경된 이후 관련 쿼리를 다시 가져오는 역할을 함.
이를 통해 화면에 표시되는 데이터가 항상 최신 상태로 유지될 수 있도록 도와줌.
예를 들어, 새로운 할 일을 추가한 뒤 invalidateQueries를 실행하면 기존의 할 일 목록을 다시 조회하게 되고, 추가된 항목이 포함된 최신 목록이 화면에 반영됨.

기본 사용법

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

const App = () => {
  const queryClient = useQueryClient();

  const [todoItem, setTodoItem] = useState("");

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

  const addTodo = async (newTodo) => {
    await axios.post("http://localhost:4000/todos", newTodo);
  };

  const {
    data: todos,
    isPending,
    isError,
  } = useQuery({
    queryKey: ["todos"],
    queryFn: fetchTodos,
  });

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

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

  if (isError) {
    return <div>데이터 조회 중 오류가 발생했습니다.</div>;
  }

  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 (
            <li
              key={todo.id}
              style={{
                display: "flex",
                alignItems: "center",
                gap: "10px",
                backgroundColor: "aliceblue",
              }}
            >
              <h4>{todo.title}</h4>
              <p>{todo.isDone ? "Done" : "Not Done"}</p>
            </li>
          );
        })}
      </ul>
    </div>
  );
};

export default App;

useMutation만 사용하면 “서버 변경”까지만 처리 가능(화면은 그대로).
invalidateQueries까지 추가해야 “UI 동기화”까지 완료됨.

0개의 댓글