
최근 프로젝트에서 TanStack Query(이하 React Query)를 사용하다가, 쿼리 캐시를 지웠는데도 다시 네트워크 요청을 하는 이상한 현상을 겪었습니다.
원인을 파악해보니 React의 렌더링 방식과 React Query의 캐시 동작 방식이 미묘하게 얽혀 있었습니다. 제가 분석한 원인과 그 해결 방법을 정리해보겠습니다.
로직을 단순하게 표현하면 다음과 같습니다.
// 부모(AppLayout)
const { data } = useUserInfoQuery(); // observer A
return (
<>
<Navbar /> // 자식 컴포넌트
<Outlet />
</>
);
// 자식(Navbar)
const { data } = useUserInfoQuery(); // observer B
oberserver : useQuery() 훅을 통해 해당 쿼리를 바라보고 있는 컴포넌트 인스턴스
active query : 현재 하나 이상의 observer가 구독 중이고, query가 “활성 상태”로 간주되는 상태
React Query에서 "구독 중"이란?
캐시가 삭제 시 해당 쿼리를 구독 중인 컴포넌트(observer)가 남아있다면, 상태가 loading(isPending: true in v5)으로 바뀌고 데이터는 undefined가 되며, 캐시가 없기 때문에 자동으로 fetch를 수행
cf: invalidateQueries
이유는 바로 “언마운트 타이밍과 removeQueries의 fetch 실행” 때문입니다.
정리하면 자식 컴포넌트도 언마운트되지만, 언마운트 직전 짧은 순간 동안 React Query가 observer의 구독 중인 캐시가 없다고 판단해 fetch를 실행했기 때문에 네트워크 요청이 발생했습니다.
정확하게 표현하자면 언마운트된 자식 컴포넌트가 아닌, 언마운트 직전에 자식 컴포넌트에서 fetch를 실행한 것이었습니다.
이 문제를 해결하기 위해, 쿼리를 부모에서만 실행하고, 자식에게는 props로 데이터를 전달하는 방식으로 변경했습니다.
// 부모 (AppLayout)
const { data: userInfo } = useUserInfoQuery();
return (
<Navbar userInfo={userInfo} />
);
// 자식 (Navbar)
const Navbar = ({ userInfo }) => {
return <div>{userInfo?.name}님 환영합니다!</div>;
};
자식 컴포넌트는 더 이상 React Query의 observer가 아니므로, removeQueries 이후에도 refetch가 발생하지 않습니다.
부모에서만 쿼리를 관리하고, 자식에게 props로 전달하는 방식으로 불필요한 네트워크 요청을 막을 수 있습니다.
React Query의 fetch는 쿼리를 호출한 컴포넌트가 마운트되어야 발생한다고 알고 있었지만, 실제로는 언마운트 직전의 컴포넌트에서도 fetch가 실행되는 경우가 있다는 점이 이슈 대응 시에 의아했습니다. (언마운트 되었는데 왜 fetch하지?)
그러다 원인을 분석하면서 React Query와 React의 렌더링 과정을 각각 분리해서 이해하려 했던 저의 사고방식을 돌아보게 되었습니다.
이번 경험을 통해, “모르는 지식은 없다. 내가 아는 지식에서 엮을 수 있다”라는 동료 개발자의 말이 떠올랐습니다.
저 또한 앞으로 이미 알고 있는 지식들을 연결하고 엮어 문제를 해결하는 힘을 키워야겠다는 생각을 하게 되었습니다.
오 몰랐는데 제법 삽질을 했겠군요 ㅋㅋㅋ 고생했네여