pile·
프론트엔드·카카오 엔터테인먼트 FE카카오 엔터테인먼트 FE·

React Query의 구조와 useQuery 실행 흐름 살펴보기

React Query의 내부 소스를 따라가며 클래스 구조와 useQuery 실행 흐름, 쿼리 데이터가 바뀔 때 컴포넌트가 리렌더링되는 경로를 짚는 글이다. 가이드 문서의 사용법만 알아도 쓸 수는 있지만 내부 구조를 보면 올바르게 쓰는 데 도움이 된다는 문제의식에서 출발한다. 테스트 버전은 @tanstack/react-query 4.29.19다.

핵심 포인트
  • 핵심 로직은 query-core에 있고 react-query 패키지는 그것을 리액트에서 쓰게 해주는 얇은 층이다.
  • QueryCache의 build는 queryKey를 해시해 이미 있는 Query를 찾고 없을 때만 새로 만든다. 이것이 중복 네트워크 요청을 막는 지점이다.
  • Query는 dispatch와 reducer로 상태를 갱신한 뒤 자신을 구독하는 QueryObserver들에게 알린다.
  • QueryObserver는 이전 결과와 새 결과를 얕게 비교해 바뀐 게 없으면 알리지 않는다.
  • useQuery를 호출하는 리액트 컴포넌트마다 QueryObserver 인스턴스 하나가 붙는다.
  • 리렌더링은 useSyncExternalStore가 넘겨준 onStoreChange 함수가 observer의 listeners에 등록되면서 이어진다.
상세 정리
  • 패키지 구성: query-core에 notifyManager, query, queryCache, queryClient, queryObserver가 있고 react-query에는 useQuery와 useBaseQuery가 있다.
  • QueryClient: QueryCache를 하나 갖고 setQueryData, getQueryData, invalidateQueries, getQueryCache 같은 메서드로 캐시와 상호작용한다. 개발자는 보통 이 층까지만 접근하면 된다.
  • QueryCache: 여러 Query 인스턴스를 queries 배열과 queriesMap 해시맵에 담아 관리하며 Subscribable을 상속한다.
  • build의 동작: 옵션의 queryKey를 자체 알고리즘으로 해시해 queryHash를 만들고, 그 키로 기존 Query를 조회해 있으면 반환하고 없으면 새로 만들어 캐시에 등록한다.
  • build가 중요한 이유: 여러 컴포넌트가 같은 옵션으로 useQuery를 호출해도 같은 Query를 재사용하므로 상태가 유효하면 네트워크 요청이 중복되지 않는다.
  • Query가 가진 것: 자신을 관리하는 QueryCache 참조, queryFn 호출 결과 데이터, 그리고 상태가 바뀔 때 알려야 할 QueryObserver 목록이다.
  • 상태 갱신 방식: dispatch에 fetch, error, success 같은 액션을 넘기면 리듀서가 새 상태를 만든다. success면 data와 dataUpdateCount, dataUpdatedAt을 갱신하고 error를 비우며 status를 success로 바꾼다.
  • 알림 전파: 상태를 바꾼 뒤 notifyManager의 batch 안에서 관찰자들의 onQueryUpdate를 호출하고 캐시에도 updated 이벤트를 알린다.
  • QueryObserver의 역할: Query의 변화를 관찰하고 리스너에게 알린다. 액션이 success면 onSuccess, error면 onError 알림 옵션을 세운 뒤 updateResult를 부른다.
  • 불필요한 알림 차단: updateResult는 이전 결과와 새로 만든 결과를 shallowEqualObjects로 비교해 같으면 그대로 반환하고 아무것도 알리지 않는다.
  • 전체 흐름 요약: Query 상태 변화, 연결된 QueryObserver에 통지, Observer가 알릴 필요를 판단, 리스너가 갱신된 상태를 참조해 자신을 갱신하는 순서다.
  • useQuery의 실체: 전달받은 인자를 parseQueryArgs로 정제해 QueryObserver 클래스와 함께 useBaseQuery에 넘기는 것이 전부다.
  • useBaseQuery: queryClient에서 기본 옵션을 채우고 useState 초기화 함수 안에서 Observer 인스턴스를 한 번만 만든다. 그래서 컴포넌트 하나에 관찰자 하나가 대응된다.
  • 리렌더링 연결: useSyncExternalStore의 구독 함수 안에서 observer.subscribe에 onStoreChange를 넘기는데, 이 onStoreChange가 컴포넌트에 리렌더링을 지시하는 함수다.
  • 구독 저장소: subscribe는 부모 클래스 Subscribable이 구현하며 listeners라는 Set에 함수를 넣고 해제 함수를 반환한다.
  • 마지막 연결: QueryObserver의 notify가 listeners를 순회하며 각 listener에 현재 결과를 넘겨 실행하는데 그것이 곧 연결된 컴포넌트들의 리렌더링이다.
  • 누락 방지: 구독 직후 observer.updateResult를 한 번 더 불러 관찰자 생성과 구독 사이에 놓친 갱신이 없도록 한다.
왜 읽나React Query를 쓰면서 같은 queryKey인데 왜 요청이 한 번만 나가는지, 데이터가 바뀌면 어떻게 컴포넌트가 다시 그려지는지 궁금했다면 그 경로가 코드 단위로 이어진다.
카카오 엔터테인먼트 FE
카카오 엔터테인먼트 FE 블로그
원문은 여기서 이어서 읽을 수 있어요
원문 읽기
읽음 (0)

이 글과 비슷한

  1. 프론트엔드·여기어때 (GC컴퍼니)여기어때 (GC컴퍼니)·

    항공 프론트엔드 구축기 (7/10): 창구를 하나만 두었습니다

    여기어때 항공 서비스 프론트엔드가 웹과 앱 웹뷰 두 환경에서 동일한 함수 호출로 동작하는 앱 브릿지 추상화 레이어를 설계한 과정을 다룬다. iOS·안드로이드 규약 차이와 "웹에 존재하지 않는 브릿지를 어떻게 호출하나"라는 문제를 단일 추상화 층으로 해결한 구현 사례다.

    요약 이어보기
    #react#typescript#webview+2