Docs
Guide
External Data Loading

External Data Loading

[!IMPORTANT] 이 가이드는 외부 상태 관리 라이브러리와 TanStack Router의 통합을 위한 데이터 가져오기, SSR, 수화/탈수화 및 스트리밍에 중점을 둡니다. 표준 Data Loading 가이드를 읽지 않았다면 먼저 읽어보시기 바랍니다.

To Store or to Coordinate?

Router는 대부분의 데이터 요구를 기본적으로 저장하고 관리할 수 있지만, 때로는 더 강력한 기능이 필요할 수 있습니다!

Router는 외부 데이터 가져오기 및 캐싱 라이브러리를 위한 완벽한 조정자로 설계되었습니다. 즉, 원하는 데이터 가져오기/캐싱 라이브러리를 사용할 수 있으며, Router는 사용자의 탐색 및 최신성에 대한 기대와 일치하는 방식으로 데이터 로딩을 조정합니다.

What data fetching libraries are supported?

비동기 프로미스를 지원하는 모든 데이터 가져오기 라이브러리를 TanStack Router와 함께 사용할 수 있습니다. 여기에는 다음이 포함됩니다:

또는...

프로미스를 반환하고 데이터를 읽거나 쓸 수 있는 모든 라이브러리를 통합할 수 있습니다.

Using Loaders to ensure data is loaded

외부 캐싱/데이터 라이브러리를 Router에 통합하는 가장 쉬운 방법은 route.loader를 사용하여 경로 내부에서 필요한 데이터가 로드되고 표시할 준비가 되었는지 확인하는 것입니다.

⚠️ 왜 필요한가요? Loader에서 중요한 렌더링 데이터를 미리 로드하는 것은 다음과 같은 이유로 매우 중요합니다:

  • "로딩" 상태가 깜빡이는 현상이 없습니다.
  • 컴포넌트 기반 데이터 가져기로 인해 발생하는 워터폴 데이터 가져기가 없습니다.
  • SEO에 더 유리합니다. 렌더링 시 데이터가 사용 가능하면 검색 엔진에서 인덱싱됩니다.

다음은 일부 데이터를 캐시에 시드하기 위해 Route의 loader 옵션을 사용하는 간단한 예시입니다(이렇게 하지 마세요):

// src/routes/posts.tsx
 
let postsCache = [];
 
export const Route = createFileRoute("/posts")({
  loader: async () => {
    postsCache = await fetchPosts();
  },
  component: () => {
    return (
      <div>
        {postsCache.map((post) => (
          <Post key={post.id} post={post} />
        ))}
      </div>
    );
  },
});

이 예시는 분명히 결함이 있지만, Route의 loader 옵션을 사용하여 데이터를 캐시에 시드할 수 있음을 보여줍니다. TanStack Query를 사용하는 보다 현실적인 예시를 살펴보겠습니다.

  • fetchPosts를 선호하는 데이터 가져오기 라이브러리의 사전 가져오기 API로 대체합니다.
  • postsCache를 선호하는 데이터 가져오기 라이브러리의 읽기 또는 가져오기 API나 훅으로 대체합니다.

A more realistic example using TanStack Query

TanStack Query를 사용하는 보다 현실적인 예시를 살펴보겠습니다.

// src/routes/posts.tsx
 
const postsQueryOptions = queryOptions({
  queryKey: ["posts"],
  queryFn: () => fetchPosts(),
});
 
export const Route = createFileRoute("/posts")({
  // Use the `loader` option to ensure that the data is loaded
  loader: () => queryClient.ensureQueryData(postsQueryOptions),
  component: () => {
    // Read the data from the cache and subscribe to updates
    const {
      data: { posts },
    } = useSuspenseQuery(postsQueryOptions);
 
    return (
      <div>
        {posts.map((post) => (
          <Post key={post.id} post={post} />
        ))}
      </div>
    );
  },
});

Error handling with TanStack Query

suspense와 함께 TanStack Query를 사용할 때 오류가 발생하면, 쿼리들이 다시 렌더링될 때 다시 시도하도록 알려야 합니다. 이는 useQueryErrorResetBoundary 훅에서 제공하는 reset 함수를 사용하여 수행할 수 있습니다. 이 함수는 오류 컴포넌트가 마운트되자마자 효과에서 호출할 수 있습니다. 이렇게 하면 쿼리가 리셋되고 라우트 컴포넌트가 다시 렌더링될 때 데이터를 다시 가져오려고 시도합니다. 이는 사용자가 라우트에서 벗어나거나 retry 버튼을 클릭하지 않는 경우를 포함합니다.

export const Route = createFileRoute("/posts")({
  loader: () => queryClient.ensureQueryData(postsQueryOptions),
  errorComponent: ({ error, reset }) => {
    const router = useRouter();
    const queryErrorResetBoundary = useQueryErrorResetBoundary();
 
    React.useEffect(() => {
      // Reset the query error boundary
      queryErrorResetBoundary.reset();
    }, [queryErrorResetBoundary]);
 
    return (
      <div>
        {error.message}
        <button
          onClick={() => {
            // Invalidate the route to reload the loader, and reset any router error boundaries
            router.invalidate();
          }}
        >
          retry
        </button>
      </div>
    );
  },
});

SSR Dehydration/Hydration

TanStack Router의 편리한 Dehydration/Hydration API와 통합할 수 있는 도구들은 서버와 클라이언트 간에 탈수된 데이터를 전달하고 필요할 때 다시 수화할 수 있습니다. 3rd 파티 주요 데이터와 3rd 파티 지연된 데이터를 사용하여 이를 수행하는 방법을 살펴보겠습니다.

Critical Dehydration/Hydration

첫 렌더링/페인트에 필요한 중요한 데이터의 경우, TanStack Router는 Router를 구성할 때 dehydratehydrate 옵션을 지원합니다. 이 콜백 함수는 서버와 클라이언트에서 Router가 일반적으로 탈수 및 수화할 때 자동으로 호출되며, 탈수된 데이터를 자신의 데이터로 증강할 수 있습니다.

dehydrate 함수는 직렬화 가능한 JSON 데이터를 반환할 수 있으며, 이는 클라이언트에 전달된 탈수된 페이로드에 병합되고 주입됩니다. 이 페이로드는 DehydrateRouter 컴포넌트를 통해 제공되며, 렌더링 시 클라이언트에서 hydrate 함수에 데이터를 다시 제공합니다.

예를 들어, TanStack Query의 QueryClient를 탈수 및 수화하여 서버에서 가져온 데이터를 클라이언트에서 수화할 수 있도록 하겠습니다.

// src/router.tsx
 
export function createRouter() {
  // Make sure you create your loader client or similar data
  // stores inside of your `createRouter` function. This ensures
  // that your data stores are unique to each request and
  // always present on both server and client.
  const queryClient = new QueryClient();
 
  return createRouter({
    routeTree,
    // Optionally provide your loaderClient to the router context for
    // convenience (you can provide anything you want to the router
    // context!)
    context: {
      queryClient,
    },
    // On the server, dehydrate the loader client so the router
    // can serialize it and send it to the client for us
    dehydrate: () => {
      return {
        queryClientState: dehydrate(queryClient),
      };
    },
    // On the client, hydrate the loader client with the data
    // we dehydrated on the server
    hydrate: (dehydrated) => {
      hydrate(queryClient, dehydrated.queryClientState);
    },
    // Optionally, we can use `Wrap` to wrap our router in the loader client provider
    Wrap: ({ children }) => {
      return (
        <QueryClientProvider client={queryClient}>
          {children}
        </QueryClientProvider>
      );
    },
  });
}