🔥 React Query 내부 살펴보기

690자
10분

React Query 내부 살펴보기

최근 React Query의 내부 작동 방식에 대해 많은 질문을 받았습니다. React Query가 언제 리렌더링을 해야 하는지 어떻게 알까요? 중복을 어떻게 제거할까요? 어떻게 프레임워크에 구애받지 않을 수 있을까요?

이 모든 질문은 매우 타당합니다. 그래서 우리가 사랑하는 비동기 상태 관리 라이브러리의 내부를 살펴보고, useQuery를 호출할 때 실제로 어떤 일이 일어나는지 분석해 보겠습니다.

아키텍처를 이해하려면 가장 기본적인 요소부터 시작해야 합니다.

QueryClient

lecture image

모든 것은 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 자체는 많은 일을 하지 않습니다. 이는 QueryCacheMutationCache의 컨테이너로, 새로운 QueryClient를 생성할 때 자동으로 만들어집니다.

또한 모든 쿼리와 뮤테이션에 대한 기본값을 설정할 수 있고, 캐시와 작업하기 위한 편리한 메서드를 제공합니다. 대부분의 경우 캐시와 직접 상호작용하지 않고 QueryClient를 통해 접근하게 됩니다.

QueryCache

클라이언트를 통해 캐시와 작업할 수 있다고 했는데, 그렇다면 캐시란 무엇일까요?

lecture image

간단히 말해서, QueryCache는 메모리 내 객체입니다. 이 객체의 키는 쿼리키의 안정적으로 직렬화된 버전(쿼리키해시라고 함)이고, 값은 Query 클래스의 인스턴스입니다.

React Query가 기본적으로 데이터를 메모리에만 저장하고 다른 곳에는 저장하지 않는다는 점을 이해하는 것이 중요합니다. 브라우저 페이지를 새로 고치면 캐시가 사라집니다. 캐시를 로컬 스토리지와 같은 외부 저장소에 쓰고 싶다면 퍼시스터를 살펴보세요.

Query

lecture image

캐시에는 쿼리가 있고, Query는 대부분의 로직이 일어나는 곳입니다. 쿼리에 대한 모든 정보(데이터, 상태 필드, 마지막 가져오기가 언제 일어났는지와 같은 메타 정보)를 포함할 뿐만 아니라, 쿼리 함수를 실행하고 재시도, 취소, 중복 제거 로직을 포함합니다.

불가능한 상태에 빠지지 않도록 내부 상태 기계를 가지고 있습니다. 예를 들어, 이미 가져오는 중에 쿼리 함수를 다시 트리거해야 한다면, 그 가져오기는 중복 제거될 수 있습니다. 쿼리가 취소되면 이전 상태로 돌아갑니다.

가장 중요한 점은 쿼리가 누가 쿼리 데이터에 관심이 있는지 알고 있어, 그 Observer들에게 모든 변경사항을 알릴 수 있다는 것입니다.

QueryObserver

lecture image

Observer는 Query와 그것을 사용하려는 컴포넌트 사이의 접착제입니다. ObserveruseQuery를 호출할 때 생성되며, 항상 정확히 하나의 쿼리를 구독합니다. 그래서 useQueryqueryKey를 반드시 전달해야 합니다. 😉

Observer는 이것보다 더 많은 일을 합니다. 대부분의 최적화가 일어나는 곳입니다. Observer는 컴포넌트가 Query의 어떤 속성을 사용하고 있는지 알고 있어, 관련 없는 변경사항에 대해 알릴 필요가 없습니다. 예를 들어, 데이터 필드만 사용한다면 백그라운드 리패치에서 isFetching이 변경될 때 컴포넌트를 리렌더링할 필요가 없습니다.

더 나아가, 각 Observer는 데이터 필드의 어떤 부분에 관심이 있는지 결정할 수 있는 선택 옵션을 가질 수 있습니다. 이 최적화에 대해 이전에 '#2: React Query 데이터 변환'에서 작성한 적이 있습니다. staleTime이나 간격 가져오기와 같은 대부분의 타이머도 observer 수준에서 일어납니다.

활성 및 비활성 쿼리

Observer가 없는 Query를 비활성 쿼리라고 합니다. 여전히 캐시에 있지만, 어떤 컴포넌트에서도 사용되고 있지 않습니다. React Query Devtools를 살펴보면 비활성 쿼리가 회색으로 표시된 것을 볼 수 있습니다. 왼쪽의 숫자는 쿼리를 구독하고 있는 Observer의 수를 나타냅니다.

lecture image

전체 그림

lecture image

모든 것을 종합해보면, 대부분의 로직이 프레임워크에 구애받지 않는 Query Core 내부에 있음을 알 수 있습니다: QueryClient, QueryCache, Query, QueryObserver 모두 거기에 있습니다.

이것이 새로운 프레임워크를 위한 어댑터를 만드는 것이 상대적으로 간단한 이유입니다. 기본적으로 Observer를 생성하고, 그것을 구독하고, Observer가 알림을 받으면 컴포넌트를 리렌더링하는 방법만 있으면 됩니다. react와 solid를 위한 useQuery 어댑터는 각각 약 100줄의 코드만 있습니다.

컴포넌트 관점에서

마지막으로, 다른 각도에서 흐름을 살펴보겠습니다 - 컴포넌트에서 시작하여:

lecture image

  • 컴포넌트가 마운트되면 useQuery를 호출하여 Observer를 생성합니다.
  • ObserverQueryCache에 있는 Query를 구독합니다.
  • 이 구독은 Query의 생성을 트리거하거나 (아직 존재하지 않는 경우), 데이터가 오래된 것으로 판단되면 백그라운드 리패치를 트리거할 수 있습니다.
  • 가져오기를 시작하면 Query의 상태가 변경되어 Observer에게 알립니다.
  • 그러면 Observer는 일부 최적화를 실행하고 잠재적으로 컴포넌트에 업데이트에 대해 알려 새로운 상태를 렌더링할 수 있게 합니다.
  • Query 실행이 끝난 후에도 Observer에게 그 사실을 알립니다.

이는 많은 잠재적 흐름 중 하나일 뿐임을 명심하세요. 이상적으로는 컴포넌트가 마운트될 때 이미 데이터가 캐시에 있을 것입니다. 이에 대해서는 '#17: 쿼리 캐시 시딩'에서 더 자세히 읽을 수 있습니다.

모든 흐름에서 동일한 점은 대부분의 로직이 React(또는 Solid 또는 Vue) 외부에서 일어나며, 상태 기계의 모든 업데이트가 Observer에게 전파되고, 그 다음 Observer가 컴포넌트에도 알려야 하는지 결정한다는 것입니다.