[Tanstack-Query] prefetchQuery와 ensureQueryData 차이

이희제·2024년 11월 16일

Tanstack-Query

목록 보기
1/2
post-thumbnail

이번에 원티드 프리온보딩 챌린지를 듣다가 리액트 쿼리의 prefetchQueryensureQueryData 차이가 궁금해서 정리를 하고자 한다.

tanstack-query를 많이 써보지 않은 입장에서 글을 정리하면 공부가 되고 좋을 것 같다.

1. prefetchQuery

prefetchQuery는 비동기 메서드이다.

해당 메서드는 데이터를 미리 가져와서 쿼리키에 캐시를 저장하는 데 사용할 수 있다.

import { QueryClient } from '@tanstack/react-query'
import { fetchData } from './api';

const queryClient = new QueryClient()
// 특정 이벤트가 발생할 때 데이터를 미리 가져온다.
await queryClient.prefetchQuery({ queryKey: ['example'], queryFn: fetchData })

동일한 쿼리키를 기준으로 useQuery를 하게 되면 캐시에서 데이터를 바로 가지고 오는 것을 확인할 수 있다.

fetchQuery 메서드와 모든 동작은 동일하지만 여기서 주의깊에 봐야할 점은 데이터 또는 에러를 반환하지 않는다는 것이다. (Promise<void>가 반환된다.)

다음과 같이 App 컴포넌트에서 prefetchQuery를 통해 데이터를 쿼리키의 캐시에 저장한다.

const prefetchData = async () => {
  await queryClient.prefetchQuery({
    queryKey: ["todo1"],
    queryFn: fetchTodo,
  });
};

function App() {
  // 컴포넌트가 마운트될 때 데이터 미리 가져오기
  useEffect(() => {
    prefetchData();
  }, []);

  return (
    <QueryClientProvider client={queryClient}>
      <MyComponent />
    </QueryClientProvider>
  );
}

그리고 실제 사용하는 컴포넌트에서 useQuery 통해 동일한 쿼리키에 대해 데이터를 캐시에서 가지고 온다.

const TanstackExample = () => {
  const { data, isPending, isLoading, isFetching } = useQuery({
    queryKey: ["todo1"],
    queryFn: fetchTodo,
  });

  return (
    <>
      <h1>Tanstack</h1>
      <h2>isLoading: {isLoading.toString()}</h2>
      <h2>isFetching: {isFetching.toString()}</h2>
      <h2>isPending: {isPending.toString()}</h2>
      <div>{data?.title}</div>
    </>
  );
};

참고로 테스트를 위해 staleTimeInfinity로 설정했다.

컴포넌트에 접근하면 이미 Fresh 상태인 것을 확인할 수 있다.

2. ensureQueryData

다음으로는 ensureQueryData 메서드이다.

내가 처음 생각했을 때 prefetchQueryensureQueryData 메서드는 미리 데이터를 가지고 오는 건 동일한거 아닌가? 왜 2개를 구분해서 사용하지? 생각이 들었다.

내가 이런 궁금증이 들었던 코드는 react-router에서 loader를 사용했을 때이다.

우선 ensureQueryData 메서드와 loader 가 뭐하는 친구인지 알아보자.

1. ensureQueryData

  • ensureQueryData는 기존 쿼리의 캐시된 데이터를 가져오는 데 사용할 수 있는 비동기 함수이다.
  • 쿼리의 캐시가 존재하지 않으면 queryClient.fetchQuery가 호출되고 그 결과가 반환된다.

간단히 말하면 캐시가 있으면 그걸 반환하고 없으면 데이터를 새로 fetch 한다는 것이다.

내부 구현 코드를 통해 어떻게 동작하고 있는지 간단히 확인해보자!

ensureQueryData<
    TQueryFnData,
    TError = DefaultError,
    TData = TQueryFnData,
    TQueryKey extends QueryKey = QueryKey,
  >(
    options: EnsureQueryDataOptions<TQueryFnData, TError, TData, TQueryKey>,
  ): Promise<TData> {
    const cachedData = this.getQueryData<TData>(options.queryKey)

    if (cachedData === undefined) return this.fetchQuery(options)
    else {
      const defaultedOptions = this.defaultQueryOptions(options)
      const query = this.#queryCache.build(this, defaultedOptions)

      if (
        options.revalidateIfStale &&
        query.isStaleByTime(resolveStaleTime(defaultedOptions.staleTime, query))
      ) {
        void this.prefetchQuery(defaultedOptions)
      }

      return Promise.resolve(cachedData)
    }
  }

getQueryData 메서드를 통해 캐시된 데이터를 들고온다. (cachedData 변수에 저장)

그리고 cachedData가 없다면 캐시된 데이터가 없는 것이기 때문에 fetchQuery 메서드를 호출해서 데이터를 가지고 온다.

캐시된 데이터가 있다면 중간 로직은 생략하고 맨 마지막을 보면 cachedData를 반환하는 것을 확인할 수 있다.


2. react-router의 loader

  • loader 옵션을 사용하면 라우트가 렌더링되기 전에 데이터를 비동기적으로 가져올 수 있다.
  • useLoaderData 훅은 이렇게 미리 가지고 온 데이터를 반환한다.

loaderensureQueryData가 어떤 것인지 알아봤으니 예시 코드를 통해 궁금증을 해결하자.

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: Infinity,
    },
  },
});

const todoLoader = (queryClient: QueryClient) => async () => {
  const data = queryClient.ensureQueryData({
    queryKey: ["todo1"],
    queryFn: fetchTodo,
  });

  return defer({
    todoData: data,
  });
};

export const route = createBrowserRouter([
  {
    path: "/",
    element: <App />,
  },
  {
    path: "/tan",
    element: (
      <Suspense fallback={<h1>Loading...</h1>}>
        <TanstackExample />
      </Suspense>
    ),
    loader: todoLoader(queryClient),
  },
]);

todoLoader 함수 내부를 보면 ensureQueryData 메서드를 사용해서 데이터를 가지고 오고 있다.

이로 인해 <TanstackExample /> 컴포넌트가 렌더링되기 전에 데이터를 미리 가지고 올 수 있고(캐시나 새로), 이를 useLoaderData 훅을 통해 데이터에 접근하여 사용할 수 있는 것이다.

const TanstackExample = () => {
  
  const { todoData } = useLoaderData() as TodoData;

  return (
    <>
      <h1>Tanstack</h1>
      <Await resolve={todoData}>
        {(loadedData) => <div>{loadedData?.title}</div>}
      </Await>
    </>
  );
};


>`todoLoader`에서 그러면 `prefetchQuery` 메서드를 사용해도 되는거 아닌가? 궁금했다.


```ts
const todoLoader = (queryClient: QueryClient) => async () => {
  const data = queryClient.prefetchQuery({
    queryKey: ["todo1"],
    queryFn: fetchTodo,
  });

  return defer({
    todoData: data,
  });
};

위와 같이 바꿔주고 테스트하면 다음과 같은 오류가 발생한다.

특정 값이나 null로 반환되어야 하는데 반환된 값이 undefined이기 때문에 발생하는 것이다.

앞서 prefetchQuery 메서드를 확인했듯이 해당 메서드는 반환값이 없다. 그래서 당연한 결과이다.

loader 옵션에 대해 더 살펴보자.

type AgnosticBaseRouteObject = {
    caseSensitive?: boolean;
    path?: string;
    id?: string;
    loader?: LoaderFunction | boolean;
    action?: ActionFunction | boolean;
    hasErrorBoundary?: boolean;
    shouldRevalidate?: ShouldRevalidateFunction;
    handle?: any;
    lazy?: LazyRouteFunction<AgnosticBaseRouteObject>;
};

type DataFunctionValue = Response | NonNullable<unknown> | null;
type DataFunctionReturnValue = Promise<DataFunctionValue> | DataFunctionValue;

export type LoaderFunction<Context = any> = {
    (args: LoaderFunctionArgs<Context>, handlerCtx?: unknown): DataFunctionReturnValue;
} & {
    hydrate?: boolean;
};

loaderLoaderFunction 또는 boolean 타입을 가진다.

LoaderFunction을 보면 반환 타입이 DataFunctionReturnValuePromise 또는 특정값임을 확인할 수 있다.

결론적으로 loader에 함수가 들어가면 무엇인가 반환을 해줘야 한다는 것이다.

그래서 ensureQueryData 메서드를 사용하는 것이다.


3. 결론

prefetchQueryensureQueryData는 일단 내부 동작이 다르고 반환값의 유무도 다르다.

오늘도 나의 소소한 궁금증이 해결됐다!

profile
그냥 하자

0개의 댓글