🔥 React Query 내부 살펴보기
React Query 내부 살펴보기
최근 React Query의 내부 작동 방식에 대해 많은 질문을 받았습니다. React Query가 언제 리렌더링을 해야 하는지 어떻게 알까요? 중복을 어떻게 제거할까요? 어떻게 프레임워크에 구애받지 않을 수 있을까요?
이 모든 질문은 매우 타당합니다. 그래서 우리가 사랑하는 비동기 상태 관리 라이브러리의 내부를 살펴보고, useQuery
를 호출할 때 실제로 어떤 일이 일어나는지 분석해 보겠습니다.
아키텍처를 이해하려면 가장 기본적인 요소부터 시작해야 합니다.
QueryClient
모든 것은 QueryClient
에서 시작합니다. 이는 여러분이 애플리케이션 시작 시 인스턴스를 생성하고, QueryClientProvider
를 통해 어디서나 사용할 수 있게 만드는 클래스입니다.
import { QueryClient, QueryClientProvider } from '@tanstack/react-query' // ⬇️ 클라이언트 생성 const queryClient = new QueryClient() function App() { return ( // ⬇️ 클라이언트 배포 <QueryClientProvider client={queryClient}> <RestOfYourApp /> </QueryClientProvider> ) }
javascript
QueryClientProvider
는 React Context를 사용해 전체 애플리케이션에 QueryClient
를 배포합니다. 클라이언트 자체는 안정적인 값입니다. 한 번 생성되면 (실수로 너무 자주 재생성하지 않도록 주의하세요) Context를 사용하기에 완벽한 사례가 됩니다. 이는 앱을 리렌더링하지 않고, useQueryClient
를 통해 이 클라이언트에 접근할 수 있게 해줍니다.
캐시를 담는 그릇
잘 알려지지 않았지만, QueryClient
자체는 많은 일을 하지 않습니다. 이는 QueryCache
와 MutationCache
의 컨테이너로, 새로운 QueryClient
를 생성할 때 자동으로 만들어집니다.
또한 모든 쿼리와 뮤테이션에 대한 기본값을 설정할 수 있고, 캐시와 작업하기 위한 편리한 메서드를 제공합니다. 대부분의 경우 캐시와 직접 상호작용하지 않고 QueryClient
를 통해 접근하게 됩니다.
QueryCache
클라이언트를 통해 캐시와 작업할 수 있다고 했는데, 그렇다면 캐시란 무엇일까요?
간단히 말해서, QueryCache
는 메모리 내 객체입니다. 이 객체의 키는 쿼리키의 안정적으로 직렬화된 버전(쿼리키해시라고 함)이고, 값은 Query
클래스의 인스턴스입니다.
React Query가 기본적으로 데이터를 메모리에만 저장하고 다른 곳에는 저장하지 않는다는 점을 이해하는 것이 중요합니다. 브라우저 페이지를 새로 고치면 캐시가 사라집니다. 캐시를 로컬 스토리지와 같은 외부 저장소에 쓰고 싶다면 퍼시스터를 살펴보세요.
Query
캐시에는 쿼리가 있고, Query
는 대부분의 로직이 일어나는 곳입니다. 쿼리에 대한 모든 정보(데이터, 상태 필드, 마지막 가져오기가 언제 일어났는지와 같은 메타 정보)를 포함할 뿐만 아니라, 쿼리 함수를 실행하고 재시도, 취소, 중복 제거 로직을 포함합니다.
불가능한 상태에 빠지지 않도록 내부 상태 기계를 가지고 있습니다. 예를 들어, 이미 가져오는 중에 쿼리 함수를 다시 트리거해야 한다면, 그 가져오기는 중복 제거될 수 있습니다. 쿼리가 취소되면 이전 상태로 돌아갑니다.
가장 중요한 점은 쿼리가 누가 쿼리 데이터에 관심이 있는지 알고 있어, 그 Observer
들에게 모든 변경사항을 알릴 수 있다는 것입니다.
QueryObserver
Observer는 Query
와 그것을 사용하려는 컴포넌트 사이의 접착제입니다. Observer
는 useQuery
를 호출할 때 생성되며, 항상 정확히 하나의 쿼리를 구독합니다. 그래서 useQuery
에 queryKey
를 반드시 전달해야 합니다. 😉
Observer
는 이것보다 더 많은 일을 합니다. 대부분의 최적화가 일어나는 곳입니다. Observer
는 컴포넌트가 Query
의 어떤 속성을 사용하고 있는지 알고 있어, 관련 없는 변경사항에 대해 알릴 필요가 없습니다. 예를 들어, 데이터 필드만 사용한다면 백그라운드 리패치에서 isFetching이 변경될 때 컴포넌트를 리렌더링할 필요가 없습니다.
더 나아가, 각 Observer
는 데이터 필드의 어떤 부분에 관심이 있는지 결정할 수 있는 선택 옵션을 가질 수 있습니다. 이 최적화에 대해 이전에 '#2: React Query 데이터 변환'에서 작성한 적이 있습니다. staleTime
이나 간격 가져오기와 같은 대부분의 타이머도 observer 수준에서 일어납니다.
활성 및 비활성 쿼리
Observer
가 없는 Query
를 비활성 쿼리라고 합니다. 여전히 캐시에 있지만, 어떤 컴포넌트에서도 사용되고 있지 않습니다. React Query Devtools를 살펴보면 비활성 쿼리가 회색으로 표시된 것을 볼 수 있습니다. 왼쪽의 숫자는 쿼리를 구독하고 있는 Observer
의 수를 나타냅니다.
전체 그림
모든 것을 종합해보면, 대부분의 로직이 프레임워크에 구애받지 않는 Query Core 내부에 있음을 알 수 있습니다: QueryClient
, QueryCache
, Query
, QueryObserver
모두 거기에 있습니다.
이것이 새로운 프레임워크를 위한 어댑터를 만드는 것이 상대적으로 간단한 이유입니다. 기본적으로 Observer
를 생성하고, 그것을 구독하고, Observer
가 알림을 받으면 컴포넌트를 리렌더링하는 방법만 있으면 됩니다. react와 solid를 위한 useQuery
어댑터는 각각 약 100줄의 코드만 있습니다.
컴포넌트 관점에서
마지막으로, 다른 각도에서 흐름을 살펴보겠습니다 - 컴포넌트에서 시작하여:
- 컴포넌트가 마운트되면
useQuery
를 호출하여Observer
를 생성합니다. - 그
Observer
는QueryCache
에 있는Query
를 구독합니다. - 이 구독은
Query
의 생성을 트리거하거나 (아직 존재하지 않는 경우), 데이터가 오래된 것으로 판단되면 백그라운드 리패치를 트리거할 수 있습니다. - 가져오기를 시작하면
Query
의 상태가 변경되어Observer
에게 알립니다. - 그러면
Observer
는 일부 최적화를 실행하고 잠재적으로 컴포넌트에 업데이트에 대해 알려 새로운 상태를 렌더링할 수 있게 합니다. Query
실행이 끝난 후에도Observer
에게 그 사실을 알립니다.
이는 많은 잠재적 흐름 중 하나일 뿐임을 명심하세요. 이상적으로는 컴포넌트가 마운트될 때 이미 데이터가 캐시에 있을 것입니다. 이에 대해서는 '#17: 쿼리 캐시 시딩'에서 더 자세히 읽을 수 있습니다.
모든 흐름에서 동일한 점은 대부분의 로직이 React(또는 Solid 또는 Vue) 외부에서 일어나며, 상태 기계의 모든 업데이트가 Observer
에게 전파되고, 그 다음 Observer
가 컴포넌트에도 알려야 하는지 결정한다는 것입니다.