
React 애플리케이션을 개발하다 보면 서버에서 데이터를 가져오고 관리하는 작업이 매우 빈번하게 발생한다.
예를 들어 API 요청을 통해 데이터를 가져오고, 로딩 상태를 관리하고, 오류 처리를 하고, 데이터가 변경되면 다시 요청을 보내는 등의 작업이 반복된다.
보통 이러한 작업을 `useEffect`와 `useState`를 이용하여 직접 관리할 수도 있지만, 프로젝트 규모가 커질수록 코드가 복잡해지고 상태 관리가 어려워지는 문제가 발생한다.
또한 전역 상태 관리 라이브러리(Redux, Zustand 등)를 이용해 서버 데이터를 관리하려고 하면, 서버 상태와 UI 상태가 섞이면서 코드가 복잡해지는 문제가 생길 수 있다.
이러한 문제를 해결하기 위해 등장한 라이브러리가 TanStack Query (React Query) 이다.
Tanstack Query는 서버 상태(Server State) 를 효율적으로 관리하기 위한 라이브러리로, API 요청, 캐싱, 데이터 동기화 등을 쉽게 처리할 수 있도록 도와준다.

Tanstack Query 초기 설정
Tanstack Query를 사용하기 위해서는 먼저 QueryClient를 생성하고 애플리케이션을 QueryClientProvider로 감싸야 한다.
QueryClient는 서버 상태를 저장하고 관리하는 캐시 저장소 역할을 한다.
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router";
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
const queryClient = new QueryClient();
createRoot(document.getElementById("root")!).render(
<BrowserRouter>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</BrowserRouter>,
);
이렇게 설정하면 애플리케이션 전체에서 React Query를 사용할 수 있게 된다.
useQuery
useQuery는 서버에서 데이터를 조회(GET) 할 때 사용하는 훅이다.
API 요청을 실행하고 그 결과를 캐싱하며, 로딩 상태와 에러 상태까지 자동으로 관리해준다.
import { useQuery } from "@tanstack/react-query";
async function fetchTodos() {
const response = await fetch(`${API_URL}/todos`);
if (!response.ok) throw new Error("Fetch failed");
return response.json();
}
const { data, isLoading, error } = useQuery({
queryKey: ["todos"],
queryFn: fetchTodos,
});
useQuery는 다음과 같은 상태를 제공한다.
- data : 서버에서 받아온 데이터
- isLoading : 데이터 요청 중인지 여부
- error : 요청 실패 시 에러 정보
이를 통해 비동기 요청 상태를 간단하게 처리할 수 있다.
폴더 구조
Tanstack Query를 사용할 때는 보통 API 로직과 훅을 분리해서 관리한다.
src
├ api
│ └ fetch-todos.ts
├ hooks
│ └ queries
│ └ useTodosData.ts
API 요청 로직을 따로 분리하면
- 재사용성이 높아지고
- 테스트가 쉬워지며
- 코드 구조가 명확해진다.
캐싱 (Caching)
Tanstack Query의 가장 큰 특징 중 하나는 강력한 캐싱 기능이다.
Tanstack Query는 queryKey를 기준으로 데이터를 캐싱한다.
useQuery({
queryKey: ["todos"],
queryFn: fetchTodos,
});
이렇게 동일한 queryKey를 사용하는 요청은 자동으로 캐시 데이터를 재사용하게 된다.
이를 통해 불필요한 네트워크 요청 감소, 앱 성능 향상, 데이터 동기화 관리가 가능해진다.
캐싱 상태 흐름
Tanstack Query의 캐시는 다음과 같은 상태를 가진다.
fetching
: 데이터를 처음 요청하는 상태
fresh
: 데이터가 최신 상태
stale
: 데이터가 오래된 상태. stale 상태가 되면 특정 상황에서 자동 리페칭(refetch) 이 발생한다.
대표적인 리페칭 상황은 아래와 같다.
- 컴포넌트 mount
- 브라우저 탭 focus
- 네트워크 재연결
- 일정 시간 간격

staleTime
staleTime은 캐시 데이터가 fresh 상태로 유지되는 시간을 의미한다.
staleTime: 5000
위 설정은 데이터를 5초 동안 fresh 상태로 유지한다는 의미이다.

useMutation
useMutation은 서버 데이터를 수정할 때 사용하는 훅이다.
대표적으로
- POST
- PUT
- PATCH
- DELETE
요청에 사용된다.
useQuery와 달리 컴포넌트 마운트 시 자동 실행되지 않는다.
사용자 이벤트 발생 시 실행된다.
const { mutate } = useMutation({
mutationFn: createTodo,
});
mutate(content);
Mutation 이벤트
Tanstack Query는 mutation 과정에서 여러 이벤트를 제공한다.
| onMutate | 요청 시작 |
| onSuccess | 요청 성공 |
| onError | 요청 실패 |
| onSettled | 요청 완료 |
이를 활용하면 다양한 동작을 처리할 수 있다.
캐시 데이터 무효화
데이터가 변경되었을 때 기존 캐시 데이터를 무효화(invalidate) 할 수 있다.
queryClient.invalidateQueries({
queryKey: ["todos"],
});
이렇게 하면 해당 캐시 데이터가 자동으로 리페칭된다. 즉, 새로고침 없이도 UI가 최신 데이터로 업데이트된다.

쿼리키 팩토리
쿼리키를 객체 형태로 관리하면 캐시 관리가 더욱 쉬워진다.
export const QUERY_KEYS = {
todo: {
all: ["todo"],
list: ["todo", "list"],
detail: (id: string) => ["todo", "detail", id],
},
};
사용 예시)
queryKey: QUERY_KEYS.todo.list
이 방식을 Query Key Factory 패턴이라고 한다.
낙관적 업데이트
낙관적 업데이트(Optimistic Update)는 요청이 성공할 것이라고 가정하고 UI를 먼저 업데이트하는 방식이다.
onMutate: (updatedTodo) => {
queryClient.setQueryData(["todos"], (prevTodos) =>
prevTodos.map((todo) =>
todo.id === updatedTodo.id
? { ...todo, ...updatedTodo }
: todo
)
);
};
이 방식의 장점은 아래와 같다.
- 사용자 경험 향상
- 빠른 UI 반응
캐시 정규화
캐시 정규화는 중첩된 데이터를 평탄화(flatten) 해서 관리하는 방식이다.
예를 들어 투두 리스트 데이터를 그대로 저장하면 동일 데이터가 여러 곳에 중복될 수 있다.
이를 해결하기 위해
- 리스트 캐시에는 id만 저장
- 개별 데이터는 detail 캐시로 저장
하는 방식으로 관리할 수 있다.
todos.forEach((todo) => {
queryClient.setQueryData(
QUERY_KEYS.todo.detail(todo.id),
todo
);
});
이렇게 하면
- 데이터 중복 제거
- 개별 데이터 업데이트 효율 증가
- 캐시 관리 단순화
등의 장점이 있다.




회고
이번 실습을 통해 Tanstack Query가 단순히 API 요청을 편하게 만드는 라이브러리가 아니라, 서버 상태를 효율적으로 관리하기 위한 도구라는 것을 이해하게 되었다.
기존에는 useEffect와 useState를 이용하여 API 요청과 로딩 상태를 직접 관리했는데, 프로젝트 규모가 커질수록 코드가 복잡해지고 상태 관리가 어려워지는 문제가 있었다.
Tanstack Query를 사용하면
- 자동 캐싱
- 리페칭 관리
- 낙관적 업데이트
- 캐시 무효화
등의 기능을 통해 서버 상태 관리가 훨씬 단순해진다는 것을 느꼈다.
특히 캐싱과 낙관적 업데이트 개념은 실제 서비스에서도 성능과 사용자 경험을 개선하는 데 중요한 역할을 할 것이라고 생각한다.
앞으로 API 요청이 많은 프로젝트에서는 Tanstack Query를 적극적으로 활용하면 좋을 것 같다.
참고 자료 및 이미지 출처
한 입 크기로 잘라먹는 React.js 실전 프로젝트 - SNS편
'TIL' 카테고리의 다른 글
| [134일차] Tanstack Infinite Queries 톺아보기 (0) | 2026.03.24 |
|---|---|
| [130일차] 사용자 경험 향상을 위한 Skeleton UI 적용하기 (0) | 2026.03.17 |
| [122일차] 모노레포와 멀티레포 (0) | 2026.03.05 |
| [119일차] 프론트엔드 FSD 아키텍처 (0) | 2026.02.28 |
| [107일차] HLS (0) | 2026.02.10 |