react query는 캐시 관련 옵션들이 존재한다. 오늘은 캐시를 사용해서 최적화를 해보는 시간을 가져보겠다.
먼저, 캐시를 사용하는 이유와 장단점을 알아보자.
api를 호출하는데 데이터 변경이 짧은 시간에 이루어지는게 아니라 시간, 일 이렇게 변경 주기가 큰 api들이 있다. 즉, 1시간 동안 100번을 호출한다고 해도 똑같은 정보가 온다는 것이다. 이런 경우 불필요하게 매번 호출하는 것 보다는 한 번 호출해서 캐시에 저장하고 일정 시간 동안은 캐시 데이터를 가져와서 출력해주는게 프로젝트에 부담도 덜하고 api 과부하 방지에도 좋기 때문에 캐시를 사용한다.
캐시는 잘 사용하면 정말 좋지만 잘못쓰면 이상한 데이터를 보여주는 쓰레기가 된다.
그러니까 사용해야 할 곳을 정말정말 잘 판단해서 사용을 해야겠다.
이제 react query로 캐시를 사용해보자
본인은 useQuery를 커스텀 hooks로 따로 만들어서 거쳐서 사용을 하고 있다.
커스텀 hooks는 이런 형태다.
const useQuery = <T>(parameter: QueryParameter<T>) => {
const {
url,
isActive,
refreshInterval,
onSuccessCache,
onSuccess,
onSettled,
onError,
} = parameter;
const [currentUser] = ...;
const API: AxiosInstance = ...;
const interval = ...;
const active = ...;
const {
data,
isLoading,
error,
refetch,
isInitialLoading,
isFetching,
}: QueryResult<T> = query({
queryKey: [`${url}`, currentUser],
queryFn: () => getAPI(url, API),
enabled: active,
refetchOnWindowFocus: false,
refetchInterval: interval,
onSuccess,
onSettled,
onError,
});
return {
data,
isLoading: isLoading && isInitialLoading,
refetch,
error,
isFetching,
};
};
여기에 이제 interface랑 query에 staleTime, cacheTime를 추가해서 사용을 하면 된다.
staleTime에 300000ms 를 줘서 동작하는 것을 확인 했다. 그런데 예상하지 못한 문제가 있었다.
api를 사용하다보면 꼭 setState 저장을 해서 사용해야 하는경우가 있는데 이럴 때 유용하게 사용했던게 콜백함수다. onSuccess 콜백함수를 이런식으로 사용을 자주 했다.
const { data: list = [] } = useQuery<APIUrlsProps['getList'][]>({
url: APIUrls.getList(),
onSuccess: data => setList(data),
});
이렇게 사용하면 api 호출에 성공하면 state에 값이 담기게 된다. 하지만, 캐시 데이터를 가져오는 경우 api 호출 성공이 아니라서 onSuccess 콜백함수가 동작하지 않는다. 이렇게되면 매번 따로 데이터를 저장하는 로직을 넣어줘야 한다.
여러가지를 방법을 생각해보다가 커스텀 hooks 내부에서 데이터가 호출될 때 함수를 실행해서 콜백해주면 되겠다고 생각이 들어서 내부에 useEffect를 사용해서 넣어주었다.
const {
data,
isLoading,
error,
refetch,
isInitialLoading,
isFetching,
}: QueryResult<T> = query({
...
});
useEffect(() => {
if (!!onSuccessCache && !!staleTime && !(isLoading || isInitialLoading)) {
onSuccessCache(data);
}
}, [
staleTime,
onSuccess,
data,
isLoading,
isInitialLoading,
onSuccessCache,
]);
onSuccessCache 콜백 함수 옵션을 받아서 staleTime 옵션이 있으면 캐시를 사용하는 것으로 간주하고 userQuery가 뱉는 데이터가 로딩중이 아닐 때 데이터를 콜백하게 만들어서 해결했다.
문제 해결을 위해 콜백함수를 추가해서 동작하게 했는데 실무에서 사용했더니 무한 리렌더링이 발생했다. 이유는 useEffect 에서 setState가 실행되면서 렌더링을 유발한 것 같다.
그래서 이를 해결하려고 이전 데이터와 현재 데이터를 비교해서 다르거나 비어 있으면 콜백 함수를 호출하는 로직으로 변경을 했다.
/** oldData, newData가 다른 값을 가지고 있는지 확인 */
const createDataEqualityChecker = useCallback(
(oldData: T) =>
(newData: T): boolean =>
isEqual(oldData, newData),
[],
);
유틸을 만들어 이전 데이터와 현재 데이터를 비교한다
useEffect(() => {
if (isSuccess && !isFetching && onSuccessCached) {
const isDataEqual = createDataEqualityChecker(oldData);
if (!isDataEqual(data)) {
onSuccessCached(data);
setOldData(data);
}
}
}, [
isFetching,
isSuccess,
onSuccessCached,
createDataEqualityChecker,
data,
oldData,
]);
서로 다른 데이터일 때만 onSuccessCached 호출하고 이전 데이터 업데이트를 한다.
페이지 이동시 => oldData는 undefined로 초기화 하고 데이터를 한번 가져옴
데이터 리패치 => 리패치할 때 oldData를 undefined로 초기화 ...
아무튼 실무에서 사용하기위해 이렇게 세팅했다.
const { data: list = [] } = useQuery<APIUrlsProps['getList'][]>({
url: APIUrls.getList(),
onSuccessCache: data => setList(data),
staleTime: (60 * 5 * 1000), //default 0
cacheTime: (60 * 6 * 1000) //default 300000
});
캐시 사용은 장단점이 확실해서 유용하면서도 위험하다는게 나의 결론이다.
그리고 캐시 데이터를 가져오면 콜백함수가 동작을 안한다는 것을 react query 사용자라면 알아둘 필요가 있다.