🔥 Suspense

730자
9분

React Query는 React의 Suspense for Data Fetching API와 함께 사용할 수 있습니다. 이를 위해 다음과 같은 전용 훅을 제공합니다:

suspense 모드를 사용할 때는 status 상태와 error 객체가 필요하지 않으며, 대신 React.Suspense 컴포넌트(fallback prop 사용 포함)와 오류를 잡기 위한 React 오류 경계를 사용합니다. suspense 모드를 설정하는 방법에 대한 자세한 내용은 Resetting Error Boundaries를 읽고 Suspense Example을 살펴보세요.

뮤테이션이 오류를 가장 가까운 오류 경계로 전파하도록 하려면(쿼리와 유사하게) throwOnError 옵션을 true로 설정할 수도 있습니다.

쿼리에 대해 suspense 모드를 활성화하는 방법은 다음과 같습니다:

import { useSuspenseQuery } from '@tanstack/react-query'
 
const { data } = useSuspenseQuery({ queryKey, queryFn })
 
typescript

이는 TypeScript에서 잘 작동합니다. 왜냐하면 오류와 로딩 상태가 Suspense 및 ErrorBoundaries에 의해 처리되므로 data가 정의되어 있다고 보장되기 때문입니다.

반면에 쿼리를 조건부로 활성화/비활성화할 수는 없습니다. suspense를 사용하면 한 컴포넌트 내의 모든 쿼리가 직렬로 가져오기 때문에 종속 쿼리에 대해 이것이 일반적으로 필요하지 않습니다.

또한 이 쿼리에는 placeholderData가 존재하지 않습니다. 업데이트 중에 UI가 fallback으로 대체되는 것을 방지하려면 QueryKey를 변경하는 업데이트를 startTransition으로 래핑하세요.

throwOnError 기본값

기본적으로 모든 오류가 가장 가까운 오류 경계로 던져지는 것은 아닙니다. 표시할 다른 데이터가 없는 경우에만 오류를 던집니다. 즉, 쿼리가 캐시에서 성공적으로 데이터를 가져온 적이 있다면 데이터가 오래되었더라도 컴포넌트가 렌더링됩니다. 따라서 throwOnError의 기본값은 다음과 같습니다:

throwOnError: (error, query) => typeof query.state.data === 'undefined'
text

throwOnError를 변경할 수 없으므로(데이터가 잠재적으로 undefined가 될 수 있음), 모든 오류를 오류 경계에서 처리하려면 수동으로 오류를 던져야 합니다:

import { useSuspenseQuery } from '@tanstack/react-query'
 
const { data, error, isFetching } = useSuspenseQuery({ queryKey, queryFn })
 
if (error && !isFetching) {
  throw error
}
 
// data 렌더링 계속
 
typescript

오류 경계 재설정

suspense나 쿼리에서 throwOnError를 사용하든 간에, 오류가 발생한 후 다시 렌더링할 때 쿼리에 다시 시도하고 싶다는 것을 알리는 방법이 필요합니다.

쿼리 오류는 QueryErrorResetBoundary 컴포넌트 또는 useQueryErrorResetBoundary 훅으로 재설정할 수 있습니다.

컴포넌트를 사용할 때는 컴포넌트의 경계 내에서 모든 쿼리 오류를 재설정합니다:

import { QueryErrorResetBoundary } from '@tanstack/react-query'
import { ErrorBoundary } from 'react-error-boundary'
 
const App = () => (
  <QueryErrorResetBoundary>
    {({ reset }) => (
      <ErrorBoundary
        onReset={reset}
        fallbackRender={({ resetErrorBoundary }) => (
          <div>
            오류가 발생했습니다!
            <Button onClick={() => resetErrorBoundary()}>다시 시도</Button>
          </div>
        )}
      >
        <Page />
      </ErrorBoundary>
    )}
  </QueryErrorResetBoundary>
)
 
typescript

훅을 사용할 때는 가장 가까운 QueryErrorResetBoundary 내에서 모든 쿼리 오류를 재설정합니다. 경계가 정의되지 않은 경우에는 전역적으로 재설정합니다:

import { useQueryErrorResetBoundary } from '@tanstack/react-query'
import { ErrorBoundary } from 'react-error-boundary'
 
const App = () => {
  const { reset } = useQueryErrorResetBoundary()
  return (
    <ErrorBoundary
      onReset={reset}
      fallbackRender={({ resetErrorBoundary }) => (
        <div>
          오류가 발생했습니다!
          <Button onClick={() => resetErrorBoundary()}>다시 시도</Button>
        </div>
      )}
    >
      <Page />
    </ErrorBoundary>
  )
}
 
typescript

Fetch-on-render vs Render-as-you-fetch

별도의 추가 구성 없이도 suspense 모드에서 React Query는 Fetch-on-render 솔루션으로 매우 잘 작동합니다. 즉, 컴포넌트를 마운트하려고 할 때 쿼리 가져오기를 트리거하고 일시 중단하지만, 컴포넌트를 가져와서 마운트한 경우에만 그렇게 합니다. 다음 단계로 나아가 Render-as-you-fetch 모델을 구현하려면 라우팅 콜백이나 사용자 상호 작용 이벤트에서 Prefetching을 구현하여 쿼리를 마운트하기 전에, 바라건대 부모 컴포넌트를 가져오거나 마운트하기 전에 쿼리 로딩을 시작하는 것이 좋습니다.

스트리밍을 사용한 서버에서의 Suspense

NextJs를 사용 중이라면 서버에서 Suspense를 위한 실험적인 통합인 @tanstack/react-query-next-experimental을 사용할 수 있습니다. 이 패키지를 사용하면 컴포넌트에서 useSuspenseQuery를 호출하는 것만으로 서버(클라이언트 컴포넌트)에서 데이터를 가져올 수 있습니다. 그런 다음 결과는 SuspenseBoundaries가 해결됨에 따라 서버에서 클라이언트로 스트리밍됩니다.

이를 위해 앱을 ReactQueryStreamedHydration 컴포넌트로 래핑하세요:

// app/providers.tsx
'use client'
 
import {
  isServer,
  QueryClient,
  QueryClientProvider,
} from '@tanstack/react-query'
import * as React from 'react'
import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental'
 
function makeQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: {
        // SSR을 사용하면 일반적으로 기본 staleTime을
        // 0 이상으로 설정하여 클라이언트에서 즉시 다시 가져오는 것을 피합니다.
        staleTime: 60 * 1000,
      },
    },
  })
}
 
let browserQueryClient: QueryClient | undefined = undefined
 
function getQueryClient() {
  if (isServer) {
    // 서버: 항상 새 쿼리 클라이언트 생성
    return makeQueryClient()
  } else {
    // 브라우저: 아직 쿼리 클라이언트가 없다면 새로 생성
    // 초기 렌더링 중에 React가 일시 중단되는 경우
    // 쿼리 클라이언트를 다시 만들지 않도록 하는 것이 매우 중요합니다.
    // 쿼리 클라이언트 생성 아래에 suspense 경계가 있는 경우에는
    // 이것이 필요하지 않을 수 있습니다.
    if (!browserQueryClient) browserQueryClient = makeQueryClient()
    return browserQueryClient
  }
}
 
export function Providers(props: { children: React.ReactNode }) {
  // 참고: 쿼리 클라이언트를 초기화할 때 useState를 피하세요.
  //       일시 중단될 수 있는 코드와 이 코드 사이에 suspense 경계가 없는 경우
  //       초기 렌더링에서 일시 중단되고 경계가 없으면
  //       React가 클라이언트를 버릴 것이기 때문입니다.
  const queryClient = getQueryClient()
 
  return (
    <QueryClientProvider client={queryClient}>
      <ReactQueryStreamedHydration>
        {props.children}
      </ReactQueryStreamedHydration>
    </QueryClientProvider>
  )
}
 
typescript

더 자세한 내용은 NextJs Suspense Streaming ExampleAdvanced Rendering & Hydration 가이드를 확인하세요.