리액트 어플리케이션의 경우 크게 3가지의 상태를 관리한다.
특정 컴포넌트 내부에서 사용하는 local state, 전역으로 관리하는 global state, 서버에서 받아온 데이터를 관리하는 server state가 있다.
React Query는 이 중 server state를 관리하는 최적화된 라이브러리이다.
이번 글에서는 React Query가 서버 상태를 어떻게 최적화하고 관리하는지에 대해 알아보려고 한다.

fetching, caching, 서버 데이터와의 동기화를 지원해주는 라이브러리
→ React 환경에서 서버의 상태를 불러오고, 캐싱하며, 지속적으로 동기화하고 업데이트하는 작업을 도와주는 라이브러리
useQuery Hook으로 수행되는 Query 요청은 GET 요청과 같이 서버에 저장되어 있는 상태를 불러와 사용한다.
const { data } = useQuery(
queryKey, // 이 Query 요청에 대한 응답 데이터를 캐시할 때 사용할 Unique Key (required)
queryFn, // 이 Query 요청을 수행하기 위한 Promise를 Return 하는 함수 (required)
options, // useQuery에서 사용되는 Option 객체 (optional)
);
queryKey는 쿼리를 식별하는 고유한 값으로, 배열 형태로 지정한다.useQuery Hook은 요청마다(API마다) 구분되는 Unique Key(aka. Query Key)가 필요하다. React Query는 이 Unique Key로 서버 상태(aka. API Response)로 로컬에 캐시하고 관리한다.queryFn에서 사용하는 변수는, 변수가 변경될 때마다 자동으로 다시 가져올 수 있게 쿼리 키에 포함돼야 한다.import { useQuery } from '@tanstack/react-query'
type ResponseValue = {
message: string
time: string
}
export default function DelayedData() {
const { data, error } = useQuery<ResponseValue>({
queryKey: ['delay', wait],
queryFn: async () => {
const res = await fetch(`https://api.heropy.dev/v0/delay?t=${wait}`)
const data = await res.json()
if (!data.time) {
throw new Error('문제가 발생했습니다!')
}
return data
},
staleTime: 1000 * 10, // 데이터 유효 기간(fresh -> stale)
retry: 1 // 요청 재시도 횟수
})
return (
<>
{data && <div>{JSON.stringify(data)}</div>}
{error && <div>{error.message}</div>}
</>
)
}
코드 흐름
1. queryKey로 요청 구분
2. queryFn으로 데이터 요청
3. 캐싱된 데이터가 있으면 반환, 없으면 새로 요청
4. 변수 변경 시 queryKey를 기준으로 자동 재요청
useMutation Hook으로 수행되는 Mutation 요청은 POST,PUT,DELETE 요청과 같이 서버 데이터에 변경 요청을 처리할 때 사용한다.
서버와의 상호작용으로 상태를 변경하고, 실시간으로 UI를 업데이트하거나 에러를 처리하는데 유용하다.
const { mutate } = useMutation( // mutate: 요청을 트리거 하는 함수
mutationFn, // 이 Mutation 요청을 수행하기 위한 Promise를 Return 하는 함수 (required)
options, // useMutation에서 사용되는 Option 객체 (optional)
// onSuccess: 요청 성공 콜백 함수, onError: 요청 실패 콜백 함수, onSettled: 성공 여부 상관없이 콜백 함수
);
import React from "react";
import { useMutation } from "react-query";
import axios from "axios";
const addTodo = async (newTodo) => {
const response = await axios.post("/api/todos", newTodo);
return response.data;
};
const TodoApp = () => {
const { mutate, isLoading, isError, isSuccess } = useMutation(addTodo, {
onSuccess: (data) => {
console.log("Todo 추가 성공:", data);
alert("새로운 Todo가 추가되었습니다!");
},
onError: (error) => {
console.error("Todo 추가 실패:", error);
alert("오류가 발생했습니다. 다시 시도해주세요.");
},
});
const handleAddTodo = () => {
const newTodo = { title: "React Query 배우기", completed: false };
mutate(newTodo);
};
return (
<div>
<h1>Todo List</h1>
<button onClick={handleAddTodo} disabled={isLoading}>
{isLoading ? "추가 중..." : "Todo 추가"}
</button>
{isSuccess && <p>Todo가 성공적으로 추가되었습니다!</p>}
{isError && <p>오류가 발생했습니다. 다시 시도해주세요.</p>}
</div>
);
};
export default TodoApp;
클라이언트 상태와 서버 상태는 웹 애플리케이션에서 데이터를 관리할 때 핵심적으로 구분해야 할 개념이다. React Query는 서버 상태 관리를 간소화하는 데 중점을 둔 라이브러리이다.
| 구분 | 클라이언트 상태 | 서버 상태 |
|---|---|---|
| 정의 | 사용자의 브라우저 내에서 관리되는 데이터 | 서버에 저장되어 클라이언트 요청으로 가져오는 데이터 |
| 저장 위치 | 브라우저 메모리 (ex. useState, useReducer 등) | 서버 (데이터베이스, API) |
| 특징 | 애플리케이션 실행 중 유지되며, 초기화되기 쉬움 | 외부에서 관리되며, 항상 최신 상태로 동기화 필요 |
| 갱신 방식 | 로컬에서 즉시 갱신 | 서버로 요청을 보내 데이터를 가져오거나 갱싱 |
| 예시 | 모달 열림 여부, 입력 폼 상태, 페이지 로컬 필터 값 | 유저 정보, 상품 목록 주문 상태 등 |
| 관리의 복잡성 | 간단 (내부 로직으로 제어 가능) | 복잡 (네트워크 요청, 에러 처리 등 필요) |
[ React Query의 역할 ]
React Query는 서버 상태를 가져오고, 캐싱하며, 필요할 때 자동으로 데이터를 갱신하는 작업을 처리해준다. 이를 통해 클라이언트는 데이터 요청 및 관리의 복잡성을 줄이고, 핵심 로직에만 집중할 수 있다.
Query Key로 데이터 상태를 명확히 정의
const { data } = useQuery(['users', userId], fetchUserData);
Query Key를 정의함으로써, React Query는 서버에서 데이터를 가져오거나 캐시된 데이터를 반환할 때 이 키를 기준으로 상태 추적
데이터 캐싱과 staletime의 활용
staleTime을 늘리면 데이터를 오래 동안 fresh 상태로 유지할 수 있어 불필요한 요청을 줄이고, 사용자에게 빠른 응답을 제공할 수 있다.const { data } = useQuery(
['posts'],
fetchPosts,
{
staleTime: 1000 * 60 * 5 // 5분 동안 데이터를 fresh 상태로 유지
}
);
네트워크 재연결, 요청 실패 등의 자동 갱신
const { data, error } = useQuery(
'posts',
fetchPosts,
{
retry: 3, // 요청 실패 시 최대 3번까지 재시도
refetchOnWindowFocus: true // 윈도우 포커스를 다시 얻을 때 자동으로 데이터 갱신
}
);
React Query를 알기 전, fetch나 axios로 데이터를 가져오고, 로딩 상태를 관리하며, 실패 시 재시도 로직, 에러 메세지를 표시하는 작업을 전부 직접 구현하며 서버 상태를 관리했다. 이 방식은 코드가 반복적이고, 상태관리가 복잡해지는 문제가 있었다. 또한 모든 상황을 일일이 다뤄야 해서 코드의 가독성이나 유지 보수가 어려웠다.
이런 문제를 해결하기 위해 React Query를 적용해보았고 이전에 모든 경우를 직접 설정해야 했지만, 자동 캐싱과 데이터의 자동 갱신을 통해 데이터를 효율적으로 관리해 주었다. 이후 서버 상태 관리가 훨씬 간소화되고, 코드의 중복도 줄어드는 등 많은 장점이 있었다.
이전처럼 단순히 코드 구현에만 집중하는 것이 아니라, 하나의 API 연동하더라도 서버와 클라이언트 간의 데이터 흐름 등 고려해야하는 부분이 많다는 것을 느꼈다. 앞으로도 더 나은 코드 품질과 성능을 위해 다양한 도구와 패턴을 공부하며 적용할 것이다.
Ref
카카오페이 프론트엔드 개발자들이 React Query를 선택한 이유
TanStack Query(React Query) 핵심 정리