/logo.png

[번역]Inside React Query

[번역]Inside React Query
2025. 1. 5.

이 글은 TkDodo의 Inside React Query 를 번역한 글입니다.

최근 내부적으로 React Query가 어떻게 작동하는지에 대한 질문을 많이 받습니다. 언제 다시 렌더링할지 어떻게 알 수 있나요? 어떻게 중복을 제거하나요? 프레임워크에 구애받지 않는 이유는 무엇인가요?

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

아키텍처를 이해하려면 처음부터 시작해야 합니다:

The QueryClient

image.png

모든 것은 QueryClient에서 시작됩니다. 이는 애플리케이션을 시작할 때 인스턴스를 생성한 다음 QueryClientProvider를 통해 모든 곳에서 사용할 수 있도록 하는 클래스입니다:

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
 
// ⬇️ this creates the client
const queryClient = new QueryClient();
 
function App() {
  return (
    // ⬇️ this distributes the client
    <QueryClientProvider client={queryClient}>
      <RestOfYourApp />
    </QueryClientProvider>
  );
}

QueryClientProvider는  React Context 를 사용하여 전체 애플리케이션에 QueryClient를 배포합니다. 클라이언트 자체는 안정적인 값이며 한 번만 생성되므로(실수로 너무 자주 다시 생성 하지 않도록 주의하세요) 컨텍스트를 사용하기에 완벽한 경우입니다. 앱이 다시 렌더링되는 것은 아니며, 단지 useQueryClient를 통해 이 클라이언트에 대한 액세스 권한을 부여할 뿐입니다.

A vessel that holds the cache

잘 알려져 있지 않을 수도 있지만, QueryClient 자체는 실제로 많은 일을 하지 않습니다. new QueryClient를 생성할 때 자동으로 생성되는 QueryCacheMutationCache를 위한 컨테이너입니다.

또한 모든 쿼리와 변이에 대해 설정할 수 있는 몇 가지 기본값을 보유하고 있으며, 캐시로 작업할 수 있는 편리한 방법을 제공합니다. 대부분의 상황에서는 캐시와 직접 상호 작용하지 않고 QueryClient를 통해 캐시에 액세스합니다.

QueryCache

자, 그래서 클라이언트는 우리가 캐시로 작업할 수 있게 해줍니다 - 캐시는 무엇입니까?

image.png

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

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

Query

image.png

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

내부 상태 머신이 있어 불가능한 상태에 빠지지 않도록 합니다. 예를 들어, 이미 가져오는 중에 쿼리 함수가 트리거되어야 하는 경우 해당 가져오기를 중복 제거할 수 있습니다. 쿼리가 취소되면 이전 상태로 돌아갑니다.

가장 중요한 것은 쿼리가 쿼리 데이터에 관심이 있는 Observers를 파악하여 해당 옵저버에게 모든 변경 사항을 알릴 수 있다는 점입니다.

QueryObserver

image.png

옵저버는 Query와 그것을 사용하려는 컴포넌트 사이의 접착제 역할을 합니다. ObserveruseQuery를 호출할 때 생성되며, 항상 정확히 하나의 쿼리에 구독됩니다. 그렇기 때문에 useQueryqueryKey를 전달해야 합니다.

하지만 Observer는 대부분의 최적화가 이루어지는 곳에서 조금 더 많은 일을 합니다. Observer는 컴포넌트가 사용 중인 Query의 속성을 알고 있으므로 관련 없는 변경 사항을 알릴 필요가 없습니다. 예를 들어, 데이터 필드만 사용하는 경우 백그라운드 리프레시에서 isFetching이 변경되는 경우 컴포넌트가 다시 렌더링할 필요가 없습니다.

더 나아가 각 Observer에는 선택 옵션이 있어 데이터 필드에서 어떤 부분에 관심이 있는지 결정할 수 있습니다.

이 최적화에 대해서는 이전 글 #2: React Query Data Transformations .에서 설명한 적이 있습니다. staleTime이나 간격 가져오기와 같은 대부분의 타이머도 observer-level에서 발생합니다.

Active and inactive Queries

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

image.png

The complete picture

image.png

이 모든 것을 종합하면, 대부분의 로직이 프레임워크에 구애받지 않는 쿼리 코어 안에 있다는 것을 알 수 있습니다:

QueryClientQueryCacheQuery 그리고 QueryObserver가 모두 여기에 있습니다.

그렇기 때문에 새로운 프레임워크에 대한 어댑터를 만드는 것은 매우 간단합니다. 기본적으로 Observer를 만들고, Observer를 구독하고, Observer가 알림을 받으면 컴포넌트를 다시 렌더링하는 방법이 필요합니다. react  및 solid useQuery 어댑터는 각각 약 100줄의 코드만 있습니다.

From a component perspective

마지막으로 컴포넌트부터 시작하여 다른 각도에서 흐름을 살펴봅시다:

image.png

  • 컴포넌트가 마운트되면 useQuery를 호출하여 Observer를 생성합니다.
  • ObserverQueryCache에 있는 Query를 구독한다는 것을 의미합니다.
  • 구독이 아직 존재하지 않는 경우 Query 생성을 트리거하거나 데이터가 오래된 것으로 간주되는 경우 백그라운드 새로 고침을 트리거할 수 있습니다.
  • 가져오기를 시작하면 Query 상태가 변경되므로 Observer에게 이에 대한 알림이 표시됩니다.
  • 그러면 Observer는 몇 가지 최적화를 실행하고 잠재적으로 컴포넌트에 업데이트에 대한 알림을 보내면 새 상태를 렌더링할 수 있습니다.
  • Query 실행이 완료되면 Observer에게도 이에 대한 정보를 알려줍니다.

이것은 많은 잠재적 흐름 중 하나에 불과하다는 점에 유의하세요. 이상적으로는 컴포넌트가 마운트될 때 데이터가 이미 캐시에 있는 것이 좋습니다.

이에 대한 자세한 내용은 #17: Seeding the Query Cache 에서 확인할 수 있습니다.

모든 흐름에서 동일한 점은 대부분의 로직이 React(또는 Solid 또는 Vue) 외부에서 발생하며 상태 머신의 모든 업데이트가 Observer에게 전파되고 Observer는 컴포넌트에도 알려야 하는지 여부를 결정한다는 점입니다.