Tanstack Query는 데이터 페칭을 위한 라이브러리입니다. Tanstack Query를 이용하면 서버 상태의 관리 및  자동 재시도, 로딩 상태 관리, HTTP 요청 에러 처리, 리페칭 및 자동 데이터 업데이트 시점 설정 그리고 키를 통한 캐시 관리까지 쿼리와 관련된 모든 것을 아주 쉽게 처리할 수 있습니다. 참고로 버전 3까지는 React Query라는 이름이었으나, React 외에도 Vue, Svelte 등에서도 사용이 가능하도록 확장하면서 Tanstack Query로 이름이 바뀌었다고 합니다.

Tanstack Query 설치

Tanstack Query는 React 16.8+ 부터 사용이 가능합니다. devtools도 같이 설치해보겠습니다.

npm install @tanstack/react-query
npm i @tanstack/react-query-devtools

QueryClient

QueryClient는 Tanstack Query가 만들어주는 캐시와 상호작용하기 위해 사용하며, 리액트에서는 App 컴포넌트에서 QueryClientProvider를 이용하여 컴포넌트가 QueryClient에 연결할 수 있도록 합니다.

import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'

//...

const queryClient = new QueryClient()

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      {/* The rest of your application */}
      <ReactQueryDevtools initialIsOpen={false} />
        <ReactQueryDevtools initialIsOpen={false} />
      </QueryClientProvider>
    </>
  );
}

useQuery

useQuery는 데이터 페칭을 아주 쉽게 만들어줍니다. 다음의 코드를 보시죠.

import { useQuery } from "@tanstack/react-query";

const TanstackQuery = () => {
  const { data, isLoading, isError, error, refetch } = useQuery({
    queryKey: ["fetchedData"],
    queryFn: fetchingFn,
    retry: 5,
    staleTime: 5 * 1000,
  });

  const refechingButton = <button onClick={() => refetch()}>refech</button>;
  return (
    <>
      {isLoading && <p>Loading...</p>}
      {isError && (
        <>
          <p>error: {error.message}</p>
          {refechingButton}
        </>
      )}
      {!isLoading && !error && (
        <>
          <p>{data}</p>
          {refechingButton}
        </>
      )}
    </>
  );
};

export default TanstackQuery;

제가 프로젝트에서 작성했던 코드를 간결하게 바꾸어 본 것입니다. 제가 일반적으로 useQuery를 사용하는 방법이죠. 다음으로는 useQuery의 옵션과 반환값들을 알아볼까요? "★" 표시가 있는 option 혹은 retunr 값은 자주(혹은 필수로) 사용하게 될 항목들입니다.

 

useQuery의 option들

★  queryKey: unknown[]

queryKey는 Tanstack Query가 자동으로 진행해 주는 HTTP 요청을 캐싱한 데이터에 접근할 수 있게 해주는 키들을 적습니다. 배열로 관리되며 문자나 숫자를 넣을 수 있습니다. 만약 QueryKey가 변수에 의존하는 경우(게시글 id, 게시글 페이지 등)라면 QureyKey에 해당 변수를 포함해야 합니다.

 

★  queryFn: (context: QueryFunctionContext) => Promise<TData>

queryFn은 useQuery가 실행 시킬 함수입니다. 여기에는 다른 서버로 HTTP 요청을 보낼 함수가 들어가게 됩니다. 단순 요청을 보내는 함수를 넘어 받은 데이터를 정렬하는 기능이 포함된 함수를 넣을 수도 있습니다. 받은 데이터를 원하는 구성으로 바꾼 뒤에 반환하게 하는 등 복잡한 함수를 만들어도 컴포넌트의 코드를 복잡하지 않게 만들어줍니다.

 

queryFn의 인자인 context는 signal과 queryKey를 프로퍼티로 가지는데, siganl은 TanStack 쿼리에서 자동으로 제공하며 쿼리를 취소할 때 사용합니다. queryKey은 개발자가 설정한 queryKey입니다. 

 

★ retry: boolean | number | (failureCount: number, error: TError) => boolean

retry는 요청이 실패하였을 때 다시 요청을 보낼 횟수를 지정합니다. number가 들어가면 해당 숫자만큼 재요청을 보내고, boolean값에 따라 재요청을 보낼지 말지 결정합니다. 만약 함수가 들어간다면 failureCount와 error를 인자로 받아 boolean을 반환하는데 반환값이 true일 때, 재 요청을 보내게됩니다. 여기서 failureCount는 현재까지 보낸 재요청 횟수 error는 queryFn이 던진(throw) error가 될수도 있고 서버가 반환한 error일 수도 있습니다.. 동시에 retryDelay를 적용할 수 있는데 말 그대로 재요청을 보내기 전 대기시간을 의미합니다. number 또는 retryAttempt: number, error: TError를 인자로 받아 number를 반환하는 함수를 값으로 넣을 수 있습니다.

 

★ staleTime: number | Infinity

캐싱된 데이터가 오래된 것으로 간주되는 시간입니다. (밀리세컨드로)지정된 시간이 지나면 캐싱되어 있는 데이터를 무시하고 서버에 새로운 요청을 보냅니다. Infinity로 설정한다면 항상 캐시된 데이터를 사용합니다. 기본값은 0으로 설정되어있습니다. 비슷한 기능으로 "gcTime: number | Infinity"이 있는데, staleTime과 달리 해당 시간이 지나면 메모리에서 해당 캐시 데이터를 삭제합니다.

 

refetchInterval: number | false | ((query: Query) => number | false | undefined)

이름과 같이 지정된 시간이 지나면 다시 fetching 합니다. refetchIntervalInBackground: boolean 옵션과 함께 사용하여 해당 탭 혹은 브라우저가 백그라운드에서 실행중이더라도 시간이 지나면 refetching할 수 있게 할수도 있습니다.

 

refetchOnWindowFocus: boolean | "always" | ((query: Query) => boolean | "always")

Tanstack Query에는 브라우저(혹은 탭)가 다시 포커싱되면 refetching을 실행하는 기능이 포함되어 있습니다. 여기서 true로 설정하면 staleTime이 지나 데이터가 오래된 것으로 간주되면 refetching을 수행하고 "always"로 설정되면 브라우저가 포커싱될 때마다 refetching을 실행합니다.

 

notifyOnChangeProps: string[] | "all" | (() => string[] | "all")

지정된 상태 중 어떤 것이 변경되면 컴포넌트가 다시 렌더링됩니다. 예를 들어, ['data', 'error']로 설정하면 data 또는 error 속성이 변경될 때만 컴포넌트가 다시 렌더링됩니다. "all"로 설정한다면 쿼리가 업데이트될 때마다 컴포넌트가 다시 렌더링됩니다.

 

select: (data: TData) => unknown

쿼리 함수에서 반환된 데이터의 일부를 변환하거나 선택하는 데 사용합니다. 여러 컴포넌트에서 동일한 쿼리 함수를 사용하더라도 각각의 컴포넌트가 필요한 데이터 부분만 선택하여 사용할 수 있습니다. 이렇게 하면 코드 중복을 줄이고 더욱 효율적인 쿼리 로직을 구성할 수 있습니다. 또한 서버로부터 받은 API 응답의 형태가 항상 일정하지 않을 수도 있는 상황에서 해당 옵션을 사용하여 응답의 일관성을 유지하거나 필요한 형태로 조정할 수 있습니다.

 

initialData: TData | () => TData

컴포넌트가 처음 렌더링될 때 쿼리의 초기 데이터를 설정하는 데 사용합니다. 이 옵션을 사용하면 컴포넌트가 서버에서 데이터를 받기 전에도 초기 데이터로 화면을 렌더링할 수 있습니다. 서버에서 데이터를 가져오기 전에 UI를 렌더링할 필요가 있는 경우에 사용할 수 있습니다. 하지만 initialData가 설정된 경우에는 해당 데이터가 캐싱된 것으로 간주되기 때문에 staleTime이 함께 설정되어 있다면 컴포넌트의 첫 렌더링 시에 쿼리 함수가 실행되지 않기에 사용시 이점을 유의하셔야합니다. 이 경우 initialDataUpdatedAt: number | (() => number | undefined)를 함께 사용하는 것을 고려할 수도 있습니다. 

해당 옵션을 사용하면, 정해진 시간이 지난 뒤에 query를 요청합니다. 혹은 아래의 placeholderData를 이용할 수도 있습니다.

 

placeholderData: TData | (previousValue: TData | undefined; previousQuery: Query | undefined,) => TData

쿼리가 보류(pending) 중인 동안에만 사용되는 데이터를 지정하는 데 사용됩니다. 예를 들어, 사용자가 어떤 데이터를 요청했지만 서버로부터 아직 응답을 받지 못한 경우, 사용자가 대기 중인 동안에도 어떤 정보를 표시할 수 있도록 도와줍니다. placeholderData는 실제로 캐시에 저장되지 않으며, 쿼리가 완료되면 즉시 삭제됩니다.

    

useQery의 반환값

status: String

총 4가지의 문자열을 반환하며, 쿼리의 진행 혹은 완료 상태를 표시합니다.
pending: 캐시된 데이터가 없고 쿼리 시도가 아직 완료되지 않았을 때입니다. 데이터를 가져오는 중이거나 쿼리가 아직 실행되지 않은 상태입니다.
error: 쿼리 시도가 오류를 반환한 경우입니다.
success: 쿼리가 오류 없이 응답을 받고 데이터를 표시할 준비가 된 상태입니다. 만약 initialData가 설정되어 있다면 data가 initialData에 해당할 수 있습니다.

 

★ is~~: boolean

해당 상태를 bool값으로 나타내는 반환값입니다. 자주 사용하는 값들로는 isLoading, isPending, isError가 있습니다.

 

★ data: TData

queryFn이 반환한 값입니다. queryFn이 오류없이 성공적으로 값을 반환하기 전까지는 undefined입니다.

 

★ error: null | TError

queryFn이 오류를 던진 경우 해당 오류를 표시합니다. 오류가 발생하기 전까지는 null 값을 가집니다.

 

★ refetch: (options: { throwOnError: boolean, cancelRefetch: boolean }) => Promise<UseQueryResult>

refetch 함수는 쿼리를 수동으로 다시 실행하는 데 사용됩니다. 이 함수를 호출하면 쿼리가 다시 실행되어 새로운 데이터를 가져올 수 있습니다. options의 cancelRefetch의 기본값이 true이기 때문에 새로운 요청이 시작되기 전에 현재 실행 중인 요청이 있는 경우, 이전 요청은 취소되고 새로운 요청이 시작됩니다. 기본적으로 refetch 함수의 에러는 throw되지 않고 로그로만 표시되지만, options의 throwOnError를 true로 설정한다면 refetch에서 에러가 발생햇을 때, refetch함수가 error를 throw합니다.

만약 staleTime을 사용하고 있다면 해당 함수를 사용하기 전에 queryClien의 invalidateQueries 메서드를 호출하여 캐싱된 데이터를 삭제해주어야 합니다. 그렇지 않다면 캐싱된 데이터를 다시 사용할 수 있기 때문입니다.

참고 자료

Tanstack Query, QueryClinent, useQuery

+ Recent posts