TanStack Query는 서버 상태 관리 라이브러리.
데이터 패칭, 캐싱, 동기화, 무효화 등의 기능을 제공. 비동기 로직을 간결하게 작성할 수 있어 유지보수성을 높일 수 있음.
useEffect, useState를 사용한 비동기 데이터 처리 방식은 상태 관리 로직이 분산되기 쉬움. 로딩, 에러, 데이터 상태를 각각 직접 관리해야 하므로 코드 중복이 발생할 수 있음.→ 이러한 문제를 해결하기 위해 등장한 라이브러리가 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는 데이터 조회 훅.
queryKey와 queryFn을 인자로 전달해 사용. 데이터, 로딩 상태, 에러 상태를 반환하므로 모든 상태를 직접 세팅할 필요 없음.
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은 생성(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는 특정 쿼리를 무효화하여 해당 데이터를 다시 패칭하도록 만드는 함수.
주로 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 동기화”까지 완료됨.