
React를 사용하면서 마주치는 이슈 중 하나는 state(상태) 관리이다.
원래 React에서 상태 관리를 하려면 Redux를 활용하며
서버 데이터를 활용할 때는 반드시 Redux-saga, Redux-Thunk, RTK-Query 같은 또 다른 미들웨어를 사용해야 했다.
react-query의 장점 중 하나는 데이터를 캐싱한다는 것이다.
캐싱된 데이터로 API 콜을 줄여주며, 이는 서버에 대한 부하 감소로 이어진다.
기본적으로 데이터를 fetching 해오면 react-query는 캐싱한다.
해당 데이터가 stale이라고 판단되면 refetching 해온다.
stale한 상태란 것은 쉽게 말해서 유통기한이 지난 것이다.
캐싱은 유용하면서도 위험한 기술이다.
서버 데이터를 fetching하고 캐싱한 뒤 사용자가 해당 데이터를 확인할 때
만약 이 과정 도중 서버에서 데이터 상태가 변경되면 사용자가 잘못된 데이터를 확인할 수 있기 때문이다.
위의 3가지 경우를 제외하고는
사용자 입장에서는 굳이 신선한(fresh) 데이터가 아니어도 된다.
아래는 react-query가 기본으로 제공하고 있는 옵션들이다.
refetchOnWindowFocus, // default: true
refetchOnMount, // default: true
refetchOnReconnect, // default: true
staleTime, // default: 0
cacheTime, // default: 5 minutes (60 * 5 * 1000 = 30000)
위와 같은 react-query의 컨셉으로 사용자는 항상 fresh한 데이터를 볼 수 있게 된다.
fresh 상태에서 staleTime 이후 stale 상태로 변경됨 (기본값: 0 = fetch 되자마자 stale됨)cacheTime만큼 유지되다가 Garbage Collector가 수집 (기본값: 5분) staleTime이 지난 후 + cacheTime이 지나기 전에 A 쿼리 인스턴스가 새롭게 mount되면 데이터를 다시 fetch해오고, fresh한 값을 가져오는 동안 화면에는 캐시한 데이터를 보여줌fresh한 상태에서 stale 상태로 변경되는데 걸리는 시간 fresh한 상태일 때는 쿼리 인스턴스가 새롭게 mount되어도 refetch가 일어나지 않는다inactive 상태로 변경되지만, 해당 데이터의 캐시는 cacheTime만큼 유지된다.cacheTime이 지나면 GC가 수집cacheTime이 지나기 전에 쿼리 인스턴스가 다시 mount되면, 데이터를 fetch하는 동안 캐시 데이터를 보여준다.cacheTime은 staleTime과 관계없이, 무조건 inactive된 시점을 기준으로 캐시 데이터 삭제 여부를 결정한다.Redux, Recoil은 클라이언트에서 전역 상태를 관리하면서, 서버 데이터가 있는 경우 middleware를 붙여 관리한다. 이 과정에서 boiler-plate가 비대해지는 부작용이 발생한다.
react-query를 활용하면 이들이 본연의 역할에만 집중할 수 있도록 서버 데이터와 클라이언트 데이터 관리를 분리하게 해준다.
서버를 거치느냐, 브라우저에만 국한되느냐로 구분 가능
(게임으로 치자면 싱글 플레이 vs. 멀티 플레이)
기존에 Redux를 활용하여 서버 데이터 관리를 할 때는 redux-saga를 사용하는 과정에서 API 요청의 성공/실패 로직을 Redux에서 다루면서 store와 boiler-plate가 비대해졌다.
하지만 react-query를 사용하면 이러한 로직을 클라이언트에서 완전히 분리할 수 있다.

useQueries를 활용하여 서버 데이터를 핸들링하고 있다.
react-query 사용 시, 서버 데이터를 recoil에 전달하여 전역 상태(global State)로 활용하는 것도 가능하다.
recoil 코드
react-query 코드
위 코드는 데이터를 성공적으로 불러왔을 때(onSuccess), 불러온 서버 데이터를 recoil에 셋팅해주고 있다.
success, error를 공통으로 핸들링하고 싶다면 최상위 index.tsx에서 QueryClient의 defaultOptions의 queries를 이용하여 핸들링할 수 있다.

예를 들어 요청에 대한 응답이 실패일 때 error 코드를 recoil의 atom으로 핸들링 하는 경우이다.
위의 코드는 onError 로직이 최상단 index.tsx 에 위치하여 atom을 호출할 hook을 사용할 수 없다.
이러한 경우 app.tsx를 활용하여 다음과 같이 작성할 수 있다.
import { useQueryClient } from "react-query";
import { useRecoilState } from "recoil";
import { errorAtom } from "./common/atom";
import Router from "./Router";
function App() {
const [error, setError] = useRecoilState(errorAtom);
const queryClient = useQueryClient();
queryClient.setDefaultOptions({ // 메서드로도 defaultOptions 설정 가능
queries: {
onError: (err) => {
// 공통 error를 atom의 setError에 전달해준다
setError((prev) => [...prev, (err as any).message as string]);
},
},
});
return (
<>
{error.length !== 0 &&
error.map((err, index) => {
return <div key={index}>{err}</div>;
})}
<Router /> // 라우터 자리 (React)
</>
);
}
잘 되던 invalidateQueries가 발표에서 작동하지 않았던 경우

원인: invalidateQueries를 Destructured 구조로 꺼내서 사용했기 때문이었다
// 에러가 났던 코드
const { invalidateQueries } = useQueryClient();
const { mutate } = useMutation(postPersonInList, {
onSuccess: () => {
invalidateQueries(KEY_LIST);
},
onError: (error) => {
console.log(error);
},
});
// 수정 후 코드
const queryCache = useQueryClient();
const { mutate } = useMutation(postPersonInList, {
onSuccess: () => {
queryCache.invalidateQueries(KEY_LIST);
},
onError: (error) => {
console.log(error);
},
});
destructure 구문을 쓰지 않고 작성하니 문제가 해결되었다.
가끔 틀리지 않은 것 같은 코드에서 에러가 난다면 destructure를 하지 말고 작성해보자.
(destructure를 하면 위의 경우처럼 다른 콜백 함수 안에서 사용 못하게 되는 상황이 발생할 수 있다)
react-query가 가진 조그만 문제점은 기존에 Redux와 Redux-saga를 통해서 다뤘던 코드들이 '컴포넌트 안'으로 들어옴으로써 기존 코드에 비해서 컴포넌트가 무거워질 수 있다는 점이다.
이 코드를 잘 분리해서 사용할 방법을 찾아야 할 것이다.