2. React.js: Tanstack Query
리액트로 JSON 상하차를 좀 더 효율적으로 하고 싶을 때, Tanstack Query(리액트 쿼리) 를 많이 쓴다.
외대종강시계 프로젝트 만들 때도 사용하긴 했으나, GET요청만 슥슥 보냈지 다양한 기능을 써먹어 본 것도 아니고, 좀 더 깊은 이해를 해보기 위해서 리액트 쿼리에 대해 좀 공부를 해보고자 한다.
리액트 쿼리..왜 쓰는거지?
🙌 「if(kakao)2021 - 카카오페이 프론트엔드 개발자들이 React Query를 선택한 이유」 세줄요약 🤟
React Query는 React Application에서 서버 상태를 불러오고, 캐싱하며, 지속적으로 동기화하고 업데이트하는 작업을 도와주는 라이브러리입니다.2
복잡하고 장황한 코드가 필요한 다른 데이터 불러오기 방식과 달리 React Component 내부에서 간단하고 직관적으로 API를 사용할 수 있습니다.
더 나아가 React Query에서 제공하는 캐싱, Window Focus Refetching 등 다양한 기능을 활용하여 API 요청과 관련된 번잡한 작업 없이 “핵심 로직”에 집중할 수 있습니다.
인터넷을 보다가 찾은 간단한 세 줄 요약이다.
1. 캐싱
데이터를 캐싱하는 것이 뭘까?
캐싱은 특정 데이터의 복사본을 저장해 동일한 데이터의 재접근 속도를 높이는 것
내용이 바뀌지 않았다면 굳이 서버로 요청을 보내지 않아도 되니, 부하를 줄이는 좋은 기능인 것 같다. 그런데 최신의 데이터인지 어떻게 판별을 하는걸까? Update Anomaly 를 발생시킬 수 있는 위험이 아닌가??
리액트 쿼리에서는 언제 데이터를 Refetch 할 지에 대한 다음과 같은 옵션들을 제공한다.
refetchOnWindowFocus, //default: true
refetchOnMount, //default: true
refetchOnReconnect, //default: true
staleTime, //default: 0
gcTime, //default: 5분 (60 * 5 * 1000)
- refetchOnWindowFocus -> 브라우저에 포커스가 들어온 경우
- refetchOnMount -> 새로운 컴포넌트가 마운트되었을 경우
- refetchOnReconnect -> 네트워크 재연결이 발생한 경우
이 셋은 뭐 대충 알 것 같다. 그런데 staleTime이건 뭘까?
StaleTime
staleTime은 데이터가 fresh(신선) -> stale(상한) 의 상태로 변경되기까지의 시간이다. fresh 상태일 때는 refetch 트리거가 발동(위의 3개) 해도 refetch 가 발생하지 않는다! 그럼 당연히 stale 상태가 되어야 refetch 가 일어날 것이다. isStale 로 상했는지 여부도 확인할 수 있다.
디폴트 값은 0 이다.
gcTime
옛날에는 이름이 cacheTime 이었다고 한다. gcTime은 데이터가 inactive 한 상태일 때, 캐싱된 상태로 남아있는 시간이다.
컴포넌트가 unmount 될 때, 데이터의 상태는 inactive 가 된다. 그 순간부터 cgTime의 시간이 흘러간다.
만약 gcTime이 5분이라면, 5분 간 데이터가 유지되다가, 5분 후 메모리에서 데이터를 날려버린다.
2. Client Data vs Server Data 의 분리
일반적으로는 클라이언트 측 데이터와 서버 측 데이터는 구분된다. 클라이언트 데이터는 컴포넌트 관련 데이터, 페이지 관련 데이터 같은 것들을 다룰 것이고, 서버 데이터는 유저의 정보, 비즈니스 로직과 관련된 데이터 등을 다룰 것이다.
리액트는 Props Drilling 을 방지하기 위해, Redux 나 Zustand 와 같은 전역 상태 라이브러리를 통해 데이터를 주로 관리하나, 이것이 서버 데이터 까지 관리하게 된다면 상당히 복잡해질 것이다.
애초에 용도가 다르기 때문에 두 데이터를 분리해서 관리하는게 중요할 것이다. 도메인 간의 독립성과 분리는 FSD 포스팅에서도 자주 나왔듯이 유지 보수를 위한 중요한 요소인 것 같다.
React Query 가 이를 용이하도록 만들어준다. Client 데이터는 전역상태관리 라이브러리가 관리하도록 하고, Server 데이터는 리액트쿼리가 관리하도록 둘을 분리 가능하게 만든다.
짧은 주기로 업데이트되어야 하는 서버 데이터를 전역상태로 관리한다면 비동기 처리도 따로 해야하고, 생각할 것이 많아지기 때문에 좋은 기능인 것 같다.
3. Rect-Query의 데이터 처리 방법
리액트 쿼리는 ContextAPI를 기반으로 동작한다고 한다.
그래서 보일러플레이트를 생성할 때 App.jsx에서
<QueryClientProvider client={queryClient}>로 감싸주어야 한다.
하위 컴포넌트들의 데이터를 관리하는 QueryClient가 존재하는데, 이 녀석은 Key를 기반으로 데이터를 저장한다.
이거...잘 생각해보면 그냥 zustand 같은 전역상태관리 라이브러리 역할을 할 수 있다는 의미다.
queryClient.setQueryData() 같은 함수를 사용하면 스토어 내부의 데이터도 직접 조작할 수 있다.
뭐야...이거 완전 사기아닌가??
그래서 서버 데이터와 클라이언트 데이터를 나눌 수 있다는 뜻이구나. 또 하나 깨닫고 간다.
4. 제공하는 기능들을 알아보자
그래서 리액트 쿼리에서 뭘 할 수 있는지, data-fetching 과 관련된 기능들을 좀 알아보자. 일반적으로 GET 요청은 useQuery, PUT, UPDATE, DELETE 는 useMutate 를 사용한다.
useQuery
먼저 형태를 살펴보자.
const { data, isLoading, isError, error } = useQuery<T>({
queryKey : ['Key값'], // 데이터를 식별할 고유한 키
queryFn : () => {}, // 데이터를 가져오는 비동기 함수(반드시 Promise 반환)
staleTime : 10 * 1000 // 10 * 1000ms 시간 후 상함
})
생긴건 되게 단순하다. 여러 옵션들과 반환값들이 있는데,
자주 쓰는 반환값으로는
data : 서버에서 성공적으로 가져온 데이터
isLoading : 캐싱된 데이터가 없고, 현재 처음으로 데이터를 가지고 오는 중인지?? Boolean -> 로딩바에 잘 쓰일 듯
isFetching : 데이터가 있든 없든, 서버와 통신 중인지 알려준다. Boolean
isError : 에러가 발생했는지 Boolean
error : 에러의 내용을 담고 있다.
옵션 중 queryKey 에는 배열을 담는데, 여러 값을 넣는 경우 계층구조를 사용할 수 있다.
ex)
['items'] -> 모든 아이템들을 관리
['items', 'list'] -> 아이템들의 목록을 관리
['items', 'list', 1] -> 1번 아이템의 상세 정보
각 순서가 달라지면 다른 쿼리키다.
queryFn 은 반드시 Promise를 반환하는 함수를 넣어야 한다.
또한 context 라는 파라미터를 받을 수 있는데 context.queryKey 를 이용해 위에 적은 쿼리키를 사용할 수 있다. API 주소에 들어가는 값들을 쿼리키에 잘 넣어놓으면 편리성이 뛰어날 것 같다!
또한 중요한건, Axios 를 쓸 때는 아무 문제 없지만 fetch API 를 쓸 때는 조심해야 한다.
queryFn :() await axios.get("/dummy").then(res => res.data)
Axios는 에러가 나면 new throw Error 를 던져주기 때문에 그냥 사용해도 된다.
하지만 fetch API는 에러를 자동으로 던져주지 않는다.
queryFn: async () => {
const response = await fetch('/api/items');
if (!response.ok) { // 404나 500이면 직접 에러를 던짐
throw new Error('네트워크 응답이 좋지 않습니다.');
}
return response.json();
}
그래서 이렇게 직접 에러를 던져주어야 한다. Axios를 안 쓸 이유가 진짜 전~혀 없다.
useQuery가 실행되는 상황은 다음과 같다.
- 컴포넌트가 Mount 될 때
- queryKey 내의 변수값이 변경될 때
- 데이터가 stale 할 때. -> 기본적으로는 화면 포커스, 네트워크 재연결 시에도 실행
refetch()함수를 호출할 때,useMutation성공 후,invalidateQueries를 호출할 때
useMutation
기본 구조는 다음과 같다.
const mutation = useMutation({
mutationFn : () => {}, //PUT,PATCH 등 서버에 요청하는 함수
onSuccess : () => {}, // 성공했을 때 실행할 함수
onError : () => {} // 실패 시 실행할 함수
})
mutation.mutate({ key : value }) // 이렇게 사용하면 된다.
useMutation 도 역시 반환값들이 있는데,
data : 성공적으로 가져온 데이터
error : 에러 내용
isSuccess : 데이터를 성공적으로 가져왔는지 Boolean
isLoading : 역시 현재 서버에 저장 중인지 Boolean
데이터를 새로 요청하는 것도 가능하다. 예를 들어 블로그에 글을 게시하는 요청을 useMutation 으로 날렸다면, 완료 시 새로운 데이터를 불러와야할 텐데,
const queryClient = useQueryClient();
const { mutate } = useMutation({
mutationFn: (newItem) => axios.post('/items', newItem),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['items'] });
}
});
이런 식으로 queryClient.invalidateQueries 를 통해 해당 쿼리키 값의 상태를 stale로 만들어 바로 refetching이 일어나도록 만들 수 있다.
그냥 mutation.mutate 를 사용하면 리턴값이 없다.
mutation.mutateAsync 를 사용하면 Promise를 반환하므로, await 을 써서 동기적으로 로직을 짤 수 있다.
근데 mutateAsync는 잘 쓰지는 않는 것 같다.
queryClient
쿼리클라이언트는 데이터를 전체적으로 저장해 놓는 Store 같은거라고 아까 말했었다.
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
이렇게 쿼리클라이언트 인스턴스를 먼저 생성해 준 후에,
const App = () => {
<QueryClientProvider client={queryClient}>
<MainComponent/>
</QueryClientProvider>
}
Provider를 통해 해당 인스턴스를 컴포넌트 전역에 뿌려주면 된다.
쿼리클라이언트는 defaultOptions 객체를 통해 기본 값들을 설정해줄 수 있다.
const queryClient = new QueryClient({
defaultOptions: {
queries: { // useQuery의 규칙
staleTime: 5 * 60 * 1000, // 5분
gcTime : 10 * 60 * 1000, // 10분
retry: 3,
refetchOnWindowFocus: true,
},
mutation: { // useMutate의 규칙
onError : () => {에러처리}
}
},
});
이런 식으로 전역적으로 기본값 설정이 가능하다.
또한
queryClient.invalidateQueries({ queryKey : ['키 이름']})
을 통해 데이터를 refetching 강제할 수 있으며,
queryClient.setQueryData(['키 이름'], (현재 값) => 바꿀 값);
을 통해 스토어 내부의 데이터를 변경할 수도 있다.
일단은 여기까지...
리액트 쿼리의 기능에 대해 공부를 해보았다. 이 외에도 정말 다양한 옵션들과 기능들을 제공하지만 그것을 한 번에 다 공부할 수는 없기에 기본적으로 많이 쓰는 것들만 일단 알아보았다.
오늘도 공부는 정말 해도 해도 끝이 없다는 것을 깨닫는다. 겸손하게 살아야겠다.
2026년 3월 26일 PM 4:20
