언마운트된 컴포넌트에서 fetch 👀? : React Query 캐시 삭제와 React 렌더링

badahertz52·2025년 9월 5일
2

react

목록 보기
14/14

최근 프로젝트에서 TanStack Query(이하 React Query)를 사용하다가, 쿼리 캐시를 지웠는데도 다시 네트워크 요청을 하는 이상한 현상을 겪었습니다.

원인을 파악해보니 React의 렌더링 방식과 React Query의 캐시 동작 방식이 미묘하게 얽혀 있었습니다. 제가 분석한 원인과 그 해결 방법을 정리해보겠습니다.

오류 상황 정리

로직을 단순하게 표현하면 다음과 같습니다.

// 부모(AppLayout)
const { data } = useUserInfoQuery();  // observer A
return (
  <>
    <Navbar />  // 자식 컴포넌트
    <Outlet />
  </>
);

// 자식(Navbar)
const { data } = useUserInfoQuery();  // observer B
  • 부모와 자식 모두 useUserInfoQuery()를 호출하고 있었습니다.
  • 로그아웃 시 queryClient.removeQueries()로 캐시를 삭제한 뒤, navigate('/login')로 로그인 페이지로 이동했습니다.
  • 그런데 캐시를 삭제했음에도, 네트워크 요청이 다시 발생했습니다.

원인 파악 전, 관련 기술 살펴보기

1. Observer와 Active Query

  • oberserver : useQuery() 훅을 통해 해당 쿼리를 바라보고 있는 컴포넌트 인스턴스

  • active query : 현재 하나 이상의 observer가 구독 중이고, query가 “활성 상태”로 간주되는 상태

  • React Query에서 "구독 중"이란?

    • 현재 마운트 상태에서 useQuery로 연결된 컴포넌트가 존재한다는 의미
    • useQuery() 훅이 언마운트되면 해당 훅을 실행하는 observer는 자동으로 제거

2. removeQueries

  • 캐시가 삭제 시 해당 쿼리를 구독 중인 컴포넌트(observer)가 남아있다면, 상태가 loading(isPending: true in v5)으로 바뀌고 데이터는 undefined가 되며, 캐시가 없기 때문에 자동으로 fetch를 수행

  • cf: invalidateQueries

    • 쿼리를 그대로 유지하나, 상태를 stale로 변경
    • 해당 쿼리를 구독하는 컴포넌트가 렌더링되면 백그라운드에서 referch를 실행하고 fetch하는 동한 캐싱된 쿼리를 사용

3. navigate 함수는 동기적이나, React diffing 과정을 거쳐야함

  • navigate('/login')는 동기적으로 history를 바꾸고 React Router가 새로운 라우트를 계산하도록 트리거
  • 새로운 페이지 이동 시 렌더링 과정 : diffing 과정 (이전 렌더링된 컴포넌트와 새롭게 보여줄 컴포넌트 트리 비교) → 기존 컴포넌트 언마운트 → 새로운 컴포넌트 마운트

원인 분석 : 왜 언마운트된 자식 컴포넌트에서 fetch를 실행할까?

이유는 바로 “언마운트 타이밍과 removeQueries의 fetch 실행” 때문입니다.

  1. 부모에서 removeQueries 실행으로 캐시 즉시 삭제
  2. navigate를 호출했지만, React의 diffing 과정이 있어서 자식 컴포넌트는 언마운트되지 않은 상태
  3. 캐시 없는 oberserver 존재
    • diffing 과정 중이라 observer인 자식 컴포넌트가 존재
    • React Query 입장에서는 캐시가 없는 상태의 observer(=자식 컴포넌트)가 존재하기 때문에 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의 렌더링 과정을 각각 분리해서 이해하려 했던 저의 사고방식을 돌아보게 되었습니다.

이번 경험을 통해, “모르는 지식은 없다. 내가 아는 지식에서 엮을 수 있다”라는 동료 개발자의 말이 떠올랐습니다.
저 또한 앞으로 이미 알고 있는 지식들을 연결하고 엮어 문제를 해결하는 힘을 키워야겠다는 생각을 하게 되었습니다.

profile
세상과 사람을 잇는 개발을 꿈꾸는 프론트엔드 개발자

2개의 댓글

comment-user-thumbnail
2025년 9월 6일

오 몰랐는데 제법 삽질을 했겠군요 ㅋㅋㅋ 고생했네여

1개의 답글