본문 바로가기

react/react-query

[react-query] react-query의 useQueries나 useQuery 반환 값을 useEffect에 걸면 무한 리렌더링에 빠지는 이유

문제상황

react-query로 데이터를 받아서 변수 fetchData에 저장했다. 
그리고 useEffect의 의존성 배열에 fetchData를 넣어서, 해당 작업이 완료되면 useState인 otherData에 임의의 객체인 {}를 setState로 담도록 코드를 짰다.(즉, fetchData 와 otherData는 완전히 다른 값을 저장한다)

function App(){
const [otherData, setOtherData] = useState(undefined);
 
const fetchData = useGetDatasetList();
    
 useEffect(() => {
        console.log('data 변경');
if(!fetchData.isLoading && fetchData.data)
        setOtherData({
        // 다른 데이터
        });
    }, [fetchData]);
    
    return <div>app</div>
}

    // react-query
    export const useGetDatasetList = (params = {}, options = {}) => {
    const getDatasetList = async () => {
            const response = await globalAxiosInstance.get(`/test`);
            return response;
    };
    // queryKey에 params를 포함하여 캐싱 구분
    return useQuery(['getDatasetList', params], getDatasetList, options);
};


예상 작동 흐름

useEffect는 fetchData를 의존성으로 받으므로, 최초에 한번 실행되고, fetchData의 응답이 오면 한번 더 실행되지 않는다.

setOtherData가 실행되어 otherData가 변경되고 리렌더링이 한번 발생하나, useEffect의 의존성에 없으므로 추가적인 리렌더링이 발생하지 않는다

 

실제 작동

setOtherData가 실행되면 무한 리렌더링 상태에 빠진다. 이 때, 특이한건 useEffect가 계속 실행되어 console.log('data 변경');이 찍힌다는 것이다.

useEffect의 의존성에 otherData는 없으니, fetchData가 계속 변경된다는 의미다. 어째서일까. react-query의 반환값인 data나 isLoading은 일종의 useState로서 리렌더링 상관없이 유지되는 것 아니었나?

 

이유

useQuery의 반환값인 객체 내에서 data 같은 키 밸류는 useState처럼 유지되는 것이 맞다. 하지만, 그것을 담고 있는 객체는 끊임없이 새로 대체된다. 즉 불변이며, 매 리렌더링마다 변경되는 것이다.

 

정리

 

 

다만 이 버그는 최신 버전에서는 이미 수정된 문제이다.


https://github.com/TanStack/query/issues/5137
위 깃헙의 이슈 사항을 보면, 나와 비슷한 문제를 겪는 사람들이 꽤 있었고 반환 값 자체를 메모라이징하여 리렌더링 시에도 반환값이 동일하도록 변경되었다.(당시 이슈에서는 v5 alpha.34 를 기준으로 변경되었다는데, 아마도 v5부터는 이런 오류가 안생기는 것 같은데, 테스트해보진 않았다)

하지만, 모종의 이유로 버전 업그레이드를 못한다면 어떻게 할까.

특히, useQueries는 배열인 특성상, 구조분해할당으로 각 data에 미리접근하여 변수선언하기가 까다롭다. 이럴 땐 어떻게 하면 좋을까. 

 

해결방안

방법1. 의존성을 loading 값으로 변경

useEffect의 의존성이 useQuery 혹은 useQueries의 반환 값 그 자체이므로 생기는 문제이니, 의존성을 리렌더링에 상관없이 유지되는 객체 내부 값으로 대체하자.

내 경우에는 useQueries 를 쓸 때에는 각 응답 요소의 isFetched 값을 연결하여 쿼리 작업의 완료 여부를 체크하는 의존성 값을 만들었다.

   useEffect(() => {
    	 console.log("쿼리 데이터 변경변경");
        setOtherData({});
    }, [dataList.map((el) => el.isFetched).join(',')]);

 

방법2. useRef()를 이용

방법1을 보면 뭔가 야매의 느낌이라 찝찝할 수도 있는데, 이럴 때에는 useRef으로 리렌더링에 상관없이 유지되는 count를 저장하고 쿼리가 완료될 때에만 해당 값을 증가시킨다. 그리고 useEffect에다가 queryData 대신, 해당 count를 의존성으로 걸어놓으면 된다.

개인적으로는 이 방법이 더 쿼리의 결과가 나오면 useEffect를 재실행한다는 의도가 잘 보이는 것 같다.

    const settleCount = useRef(0);
    
    const [otherData, setOtherData] = useState(undefined);
 
	const fetchData = useGetDatasetList({},{
    // 쿼리 완료 시, ref 값 증가
    onSettled:()=>{settleCount.current ++;}
    });
    
	 useEffect(() => {
        console.log('data 변경');
		if(!fetchData.isLoading && fetchData.data)
        setOtherData({
        // 다른 데이터
        });
    }, [settleCount.current]);
    
    const queryData = useGetQuery(queryParams, {
        refetchOnWindowFocus: false, // 브라우저 창 포커스 변경 시 데이터 자동 재요청 끄기
        onSettled: () => {
            console.log('쿼리 완료');
            settleCount.current++;
        },
    });