서론
이제는 표준처럼 취급되는 react-query에 대해 이야기해볼까 합니다.
이 도구를 왜 쓰는걸까요? 이 도구가 어떤 문제를 책임지고 있을까요? 처음 도구가 공개된지 꽤 시간이 지난 지금 여전히 이 도구의 존재 의미는 유효할까요?
이 글은 다음 개발자를 대상으로 합니다:
- 왜 쓰는지 다른 개발자에게 설명하기 어려운 개발자
- 학원에서 React Query쓰라고 해서 쓰는 개발자
- 기존 프로젝트에서 사용해서 별 고민없이 쓰는 개발자.
- Zustand 등 전역 스토어로 서버 데이터를 다루던 개발자
서버 상태와 클라이언트 상태를 명확하게 구분하고, React Query가 왜 등장했고 지금 어떤 가치를 제공하는지 정리해봅니다.
1. Client State vs Server State
리액트 쿼리는 전역 상태로 포괄적으로 분류되던 상태를 클라이언트 상태와 서버상태라는 새로운 분류 기준을 제시했습니다.
리액트 쿼리의 등장 이전에는 redux와 redux-thunk, saga등을 활용해 서버 데이터를 전역 상태로 취급하는 것이 유행했습니다.
하지만 이런 방법은 아래에서 설명될 상황속에서 문제가 발생하기 시작했습니다.

전통적인 접근 방식의 문제
React Query 등장 이전 대부분의 개발자들은 서버 데이터를 클라이언트 상태처럼 취급하며 아래와 같은 패턴을 사용했습니다.
function TodoList() {
const [todos, setTodos] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
setIsLoading(true);
fetch('/api/todos')
.then(res => res.json())
.then(data => {
setTodos(data);
setIsLoading(false);
})
.catch(err => {
setError(err);
setIsLoading(false);
});
}, []); // 의존성 배열 실수 가능
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>{/* todos 렌더링 */}</div>;
}
위 예제에서는 지역 상태로 구현했지만 전역 상태 라이브러리를 사용할 때도 마찬가지입니다.
서버 상태의 로딩/에러/데이터를 직접 관리해야 하고, 여러 컴포넌트에서 동일한 데이터를 요청할 경우 중복 요청도 발생합니다.
Server State의 특징
서버 데이터는 클라이언트 상태처럼 영속적으로 유지되거나 변하지 않는 값이 아닙니다.
const [users, setUsers] = useState([]); // ❌ 서버 상태를 클라이언트 상태처럼 관리
const [isModalOpen, setIsModalOpen] = useState(false); // ✅ 클라이언트 상태
서버 상태는 언제든지 서버에서 변경될 수 있으며, 클라이언트에 있는 데이터는 그저 스냅샷에 불과합니다.
이 때문에 서버 상태를 클라이언트에서 직접 관리하는 순간 다음과 같은 문제가 생깁니다.
- 캐싱 전략 필요
- stale 데이터 문제
- 중복 요청
- 데이터 불일치
- 무분별한 fetch
- 의존성 배열로 생기는 버그
이런 근본적 문제를 해결하기 위해 React Query가 등장했습니다.
2. React Query가 해결하는 문제들
2.1 선언적 서버 상태 접근
React Query는 "언제 데이터를 가져올지"를 직접 고민할 필요가 없습니다.
Query Key를 캐시키로 사용하여 서버 데이터를 관리하고 stale 상태 문제를 스마트하게 관리합니다.
const { data: user, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(res => res.json())
});
위와 같은 하나의 선언만으로 서버 데이터의 일부분을 UI와 손쉽게 통합, 관리할 수 있습니다.
2.2 자동 캐싱 & 중복 요청 제거
전통적 방식에서는 여러 컴포넌트가 동일한 데이터를 필요로 하면 API가 중복호출됩니다.
useEffect(() => {
fetch(`/api/users/${userId}`).then(...);
}, [userId]);
React Query는 동일한 Query Key를 가진 요청을 자동으로 그룹화(coalescing) 합니다.
useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId)
});
- 같은 데이터 요청이 동시에 발생하면 API 호출은 1번
- 이후 요청은 캐시에서 즉시 반환
- 데이터 변경 시 관련 컴포넌트만 자동 갱신
2.3 백그라운드 데이터 갱신
React Query의 가장 큰 장점은 서버 상태의 신선도 관리를 해준다는 점입니다.
const { data: todos } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 50_000
});
staleTime, refetchInterval, window focus refetch 등 다양한 제어 옵션을 제공하여 서버 상태를 '동기화된 상태'(fresh)로 유지할 수 있습니다.
2.4 필터링과 Query Key 동기화
Query Key는 사실상 서버 상태의 의존성 배열 역할을 합니다.
const { data: todos } = useQuery({
queryKey: ['todos', filter],
queryFn: () => fetchTodos(filter)
});
- filter가 변경되면 queryKey도 변경
- 쿼리가 자동으로 재요청됨
- 쿼리 키로 캐시키가 생성됨
결론: 2025년에 React Query의 가치는?

리액트 쿼리가 여러 기능을 제공하지만 제가 생각하는 핵심적인 가치는 다음과 같습니다.
- 캐싱 전략을 통한 네트워크 요청 최적화
- stale 데이터 문제 해결
- 서버 상태 관리 중 발생하는 보일러플레이트 코드 제거
- UI레이어와 네트워크 레이어의 연결 과정에 발생하는 대부분의 문제를 하나의 도구에 위임함으로서 선언적 코드 관리 가능
서론에서 얘기했듯, React Query는 포괄적 전역 상태에서 "서버 상태"라는 구분을 제시하며 서버 상태는 클라이언트 상태와 따로 취급되야 한다는 철학을 명확하게 제시합니다. 2025년 현재, 서버 데이터 동기화 문제를 선언적으로 해결할 수 있는 도구로서 여전히 유의미합니다. React Server Component와 Server Action을 적극 사용하는 환경에서도 마찬가지 입니다. 대표적으로 무한 스크롤 UI를 예로 들 수 있습니다. 사용자 인터렉션을 통해 추가적인 데이터를 조회해야 하는 상황이라면 여전히 react-query의 가치는 유효합니다.
서버 데이터와 리액트 앱의 매끄러운 통합 과정에서 문제를 겪어보신 개발자 분들께 사용을 권유드립니다.
참고
'react' 카테고리의 다른 글
| [설계] UI로직에서 비즈니스 로직 분리하기 (0) | 2025.12.17 |
|---|---|
| [최적화] Nextjs 번들 최적화 (Bundle Optimization) (0) | 2025.12.12 |
| FSD Architecture TypeScript 예시 (0) | 2025.08.29 |
| [설계] 객체지향의 유연한 구조 설계, TypeScript와 React로 이해하기 (0) | 2025.08.24 |
| [설계] 대규모 프로젝트에서 확장 가능한 의존성 설계 feat. Feature-Sliced Design (0) | 2025.08.24 |