Tanstack Query 톺아보기

남다은·2024년 2월 13일
0

React

목록 보기
3/8
post-thumbnail

Why?

  • 대부분의 주요 web 프레임워크들은 데이터를 전체적으로 가져오거나 업데이트할 때 편향적인 방법을 사용하지 않는다. 때문에 개발자는 데이터 패칭에 대한 엄격한 의견들을 캡슐화(하나로 묶는)하는 meta-frameworks를 구축하거나, 그들만의 데이터 패칭 방법을 발명한다.
    • 이는 주로 component 기반 상태와 부작용을 함께 묶거나, 더 일반적인 목적의 상태 관리 라이브러리를 사용하여 앱에 동기적인 데이터를 저장하거나 제공한다.
  • 대부분의 전통적 상태 관리 라이브러리들은 클라이언트 상태 작업에는 적합한 반면, 비동기 또는 서버 상태 작업에는 적합하지 않다. 따라서 클라이언트 상태와 서버 상태를 명확히 구분하기 위해 React Query가 만들어졌다.

React Query (Tanstack Query)?

React Query(= TanStack Query)는 서버 상태 관리를 위한 최고 라이브러리 중 하나이다.

  • 별도의 설치나 구성이 필요 없이 바로 사용 가능하고,
  • 설정이 매우 편하고 간단하며,
  • 커스터마이징 될 수 있다.

기술적 측면에서 React Query의 장점

  • React query 로직을 사용해서 복잡하고 이해하기 어려운 코드를 제거할 수 있다.
  • 새로운 서버 상태 데이터 소스 연결에 대한 걱정 없이 애플리케이션을 더 유지 가능하게 하고 새로운 기능들도 쉽게 구축할 수 있게 한다.
  • 사용자에게 있어서 더 빠르고 반응성이 더 뛰어나다.
  • 잠재적으로 대역폭을 절약하고 메모리 성능을 증가시킨다.

How to use?


🍄 Query Client

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

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: Infinity,
      // ...
    },
  },
});
  • Query Client를 사용해서 캐시와 상호작용할 수 있다.
  • Query Client에서 모든 query 또는 mutation에 기본 옵션을 추가할 수 있다.
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

const queryClient = new QueryClient({/* options */});

function App() {
  return (
		<QueryClientProvider client={queryClient}>
		...
		</QueryClientProvider>
	)
}
  • React-query를 사용하기 위해선 QueryClientProvider를 최상단에서 감싸주고, QueryClient 인스턴스를 client props로 넣어서 애플리케이션에 연결해야 한다.
💡 위의 context는 앱에서 비동기 요청을 알아서 처리하는 background 계층이 된다.

🍄 useQuery

  • useQuery는 v5부터 인자로 단 하나의 객체만 받는다.
  • queryKey, queryFn 인자는 필수 값이다.
// 실제 예제
const getAllSuperHero = async () => {
  return await axios.get("http://localhost:4000/superheroes");
};

const { data, isLoading } = useQuery({
  queryKey: ["super-heroes"],
  queryFn: getAllSuperHero,
});

queryKey

const getSuperHero = async ({ queryKey }: any) => {
  const heroId = queryKey[1]; // queryKey: ['super-hero', '3'] 도 가능

  return await axios.get(`http://localhost:4000/superheroes/${heroId}`);
};

const useSuperHeroData = (heroId: string) => {
  return useQuery({
    queryKey: ["super-hero", heroId],
    queryFn: getSuperHero, 
  });
};
  • queryKey는 배열로 지정해줘야 한다.
    • 단일 문자열만 포함된 배열도 가능하며, 여러 문자열과 중첩된 객체로 구성된 배열도 가능하다.

useQuery는 queryKey를 기반으로 쿼리 캐싱을 관리한다.

  • 만약 쿼리가 특정 변수에 의존한다면 배열에 이어서 줘야 한다. ex: ["super-hero", heroId, ...]
  • 특정 쿼리의 접근이 필요할 때 초기에 설정해둔 포맷을 지켜줘야 그대로 쿼리에 접근 가능하다

queryFn

  • useQuery의 queryFnPromise를 반환하는 함수를 넣어야한다.
const getSuperHero = async (heroId: string) => {
  return await axios.get(`http://localhost:4000/superheroes/${heroId}`);
};

const useSuperHeroData = (heroId: string) => {
  return useQuery({
    queryKey: ["super-hero", heroId],
    queryFn: () => getSuperHero(heroId),
  });
};

options

  • code
    const {
      data,
      dataUpdatedAt,
      error,
      errorUpdatedAt,
      failureCount,
      failureReason,
      fetchStatus,
      isError,
      isFetched,
      isFetchedAfterMount,
      isFetching,
      isInitialLoading,
      isLoading,
      isLoadingError,
      isPaused,
      isPending,
      isPlaceholderData,
      isRefetchError,
      isRefetching,
      isStale,
      isSuccess,
      refetch,
      status,
    } = useQuery(
      {
        queryKey,
        queryFn,
        gcTime,
        enabled,
        networkMode,
        initialData,
        initialDataUpdatedAt,
        meta,
        notifyOnChangeProps,
        placeholderData,
        queryKeyHashFn,
        refetchInterval,
        refetchIntervalInBackground,
        refetchOnMount,
        refetchOnReconnect,
        refetchOnWindowFocus,
        retry,
        retryOnMount,
        retryDelay,
        select,
        staleTime,
        structuralSharing,
        throwOnError,
      },
      queryClient,
    )

📌 useQuery return 데이터

  • data : 쿼리 함수가 리턴한 Promise에서 resolved된 데이터
  • error : 쿼리 함수에 오류가 발생한 경우, 쿼리에 대한 오류 객체
  • status : data, 쿼리 결과 값에 대한 상태를 표현하는 status는 문자열 형태로 3가지의 값이 존재한다.
    1. pending : 쿼리 데이터가 없고, 쿼리 시도가 아직 완료되지 않은 상태
      • { enabled: false } 상태로 쿼리가 호출되면 이 상태로 시작된다.
        • dependent Query dependent (or serial) 쿼리는 실행 전에 완료되기 위해 이전 쿼리에 의존한다.
          • 이를 위해 enabled 옵션을 사용한다.

            // Get the user
            const { data: user } = useQuery({
              queryKey: ['user', email],
              queryFn: getUserByEmail,
            })
            
            const userId = user?.id
            
            // Then get the user's projects
            const {
              status,
              fetchStatus,
              data: projects,
            } = useQuery({
              queryKey: ['projects', userId],
              queryFn: getProjectsByUser,
              // userId가 존재할 때까지 쿼리를 실행하지 않음
              **enabled: !!userId,**
            })
    2. error : 에러 발생했을 때 상태
    3. success : 쿼리 함수가 오류 없이 요청 성공하고 데이터를 표시할 준비가 된 상태
  • fetchStatus : queryFn에 대한 정보
    • fetching : 쿼리가 현재 실행 중인 상태
    • paused : 쿼리를 요청했지만, 잠시 중단된 상태 (network mode 와 연관)
    • idle : 쿼리가 현재 아무 작업도 수행하지 않는 상태
  • isLoading : 캐싱된 데이터가 없을 때 → 처음 실행된 쿼리일 때 로딩 여부에 따라 true/false로 반환됨
    • 캐싱된 데이터가 있으면 로딩 여부에 상관 없이 false를 반환한다.
    • isFetching && isPending 과 동일
  • isFetching : 캐싱된 데이터가 있더라도 쿼리가 실행되면 로딩 여부에 따라 true/false로 반환된다
  • isSuccess : 쿼리 요청이 성공하면 true
  • isError : 쿼리 요청 중에 에러가 발생한 경우 true
  • refetch : 쿼리를 수동으로 다시 가져오는 함수

🤔 status와 fetchStatus를 나눠서 다루는 이유?

  • status : data가 있는지 없는지에 대한 상태를 의미한다.
  • fetchStatus : 쿼리 즉, queryFn 요청이 진행 중인지 아닌지에 대한 상태를 의미한다.

📌 useQuery 주요 옵션

  • staleTime (number | Infinity)

    • staleTime은 데이터가 fresh 상태에서 stale 상태로 변경되는 데 걸리는 시간을 의미한다.
    • fresh 상태에서는 쿼리가 다시 mount 되어도 fetch(네트워크 요청)가 실행되지 않는다.
    • staleTime의 기본 값은 0분이다.
  • gcTime <number | Infinity>

    • 데이터가 사용하지 않거나, inactive 상태일 때 캐싱된 상태로 남아있는 시간(밀리초)을 의미한다.
    • gcTime이 지나면 가비지 콜렉터로 수집된다.
    • Infinity시 가비지 콜렉터는 작동하지 않는다.
    • gcTime은 staleTime과 관계없이 무조건 inactive된 시점을 기준으로 캐시 데이터를 결정한다.
    • gcTime의 기본 값은 5분이며, SSR환경에서는 Infinity다.
  • refetchOnMount (boolean | “always”)

    • refetchOnMount는 데이터가 stale 상태일 경우 마운트 시마다 refetch를 실행하는 옵션이다.
    • 기본 값은 true이다.
    • always 로 설정하면 마운트 시마다 매번 refetch 를 실행한다.
    • false 로 설정하면 최초 fetch 이후에는 refetch 하지 않는다.
  • refetchOnWindowFocus (boolean | “always”)

    • refetchOnWindowFocus는 데이터가 stale 상태일 경우 윈도우 포커싱 될 때마다 refetch를 실행하는 옵션이다.
      • 윈도우 포커싱 예시) 크롬에서 다른 탭을 눌렀다가 다시 원래 보던 중인 탭을 눌렀을 때
    • 기본 값은 true이다.
    • always로 설정하면 항상 윈도우 포커싱될 때마다 refetch 실행한다.
    • fresh 상태인 1분 동안은 아무리 다른 탭을 왔다갔다 해도 fetch 요청을 하지 않는다.
  • retry (boolean | number | (failureCount: number, error: TError) => boolean)

    • 실패한 쿼리를 특정 횟수만큼 재시도 하는 옵션
    • 기본적으로 클라이언트 환경에서는 쿼리 실패시 3번 재시도, 서버 환경에서는 0번 재시도한다.
    • true로 설정하면 쿼리 실패시 무한 재시도한다.
    • false로 설정하면 재시도 하지 않는다.

v4까지 있던 onSuccess, onError, onSettled Callback은 useQuery 옵션에서는 더 이상 사용하지 않는다. 단, useMutation에서는 사용 가능하다.

  • onSuccess ((data: TDdata) ⇒ void)

    • onSuccess는 쿼리 성공시 실행되는 함수이다.
    • 매개변수 data는 성공시 서버에서 넘어오는 response 값이다.
  • onError ((error: TError) ⇒ void)

    • onError는 쿼리 실패시 실행되는 함수이다.
    • 매개변수로 에러 값을 받는다.
  • onSettled ((data?: TData, error?: TError) ⇒ void)

    • onSettled는 쿼리가 성공해서 성공한 데이터가 전달되거나, 실패해서 에러가 전달될 때 실행되는 함수이다.
    • 매개변수로 성공 시엔 성공데이터, 실패 시에는 에러가 전달된다

  • select (data: TData) => unknown

    • select 옵션을 사용하여 쿼리 함수에서 반환된 데이터의 일부를 반환하거나 선택할 수 있다.
    • 반환된 데이터 값에는 영향을 주지만 쿼리 캐시에 저장되는 내용에는 영향을 주지 않는다.
  • placeholderData (TData | (previousValue: TData | undefined; previousQuery: Query | undefined,) => TData)

    • placeholderData를 설정하면 쿼리가 pending 상태인 특정 쿼리에 대한 placeholder data로 사용된다.
    • placeholderData는 캐시에 유지되지 않으며, 서버 데이터와 관계 없는 보여주기용 가짜 데이터이다.
    • placeholderData에 함수를 제공하는 경우 첫 번째 인자로 이전에 관찰된 쿼리 데이터를 수신하고, 두 번째 인자는 이전 쿼리 인스턴스가 된다.
  • notifyOnChangeProps (string[] | "all" | (() => string[] | "all"))
    - 쿼리의 특정 프로퍼티들이 변경되었을 때만 리랜더링이 발생하도록 설정 가능하다.
    - 별도로 설정하지 않는 경우, 컴포넌트에서 접근한 값이 변경되었을 때 리랜더링이 발생한다(기본 값).
    - all로 설정할 경우 쿼리의 어떤 프로퍼티가 변경되든 컴포넌트가 리랜더링된다.

🍄 useMutation

  • code
    const {
      data,
      error,
      isError,
      isIdle,
      isPending,
      isPaused,
      isSuccess,
      failureCount,
      failureReason,
      mutate,
      mutateAsync,
      reset,
      status,
      submittedAt,
      variables,
    } = useMutation({
      mutationFn,
      gcTime,
      mutationKey,
      networkMode,
      onError,
      onMutate,
      onSettled,
      onSuccess,
      retry,
      retryDelay,
      throwOnError,
      meta,
    })
    
    mutate(variables, {
      onError,
      onSettled,
      onSuccess,
    })
  • react-query에서 기본적으로 서버에서 데이터를 GET 하는 경우에는 useQuery를 사용한다.
  • 서버의 데이터를 POST, PUT, PATCH, DELETE 와 같이 수정하고자 하는 경우에는 useMutation 을 사용한다.
const mutation = useMutation({
  mutationFn: createTodo,
  onMutate() {
    /* ... */
  },
  onSuccess(data) {
    console.log(data);
  },
  onError(err) {
    console.log(err);
  },
  onSettled() {
    /* ... */
  },
});

const onCreateTodo = (e) => {
  e.preventDefault();
  mutate({ title });
};
  • useMutation의 반환 값인 mutation 객체의 mutate 메서드를 이용해서 요청 함수를 호출할 수 있다.
  • mutate는 onSuccess, onError 메서드를 통해 성공 했을 시, 실패 했을 시 response 데이터를 핸들링 할 수 있다.
  • onMutate는 mutation 함수가 실행되기 전에 실행되고, mutation 함수가 받을 동일한 변수가 전달된다.
  • onSettled는 try…catch…finally 구문의 finally 처럼 요청의 성공 여부에 상관 없이 마지막에 실행된다.
const mutation = useMutation(addTodo);

try {
  const todo = await mutation.mutateAsync(todo);
  console.log(todo);
} catch (error) {
  console.error(error);
} finally {
  console.log("done");
}
  • useMutation을 사용할 때 promise형태의 response가 필요한 경우라면 mutateAsync를 사용해서 얻어올 수 있다.
    • mutateAsync는 Promise를 직접 다루기 때문에 에러 핸들링 같은 부분을 직접 다뤄야 한다. 만약 다루지 않는 경우 unhandled promise rejection 에러가 발생할 수 있다.

⛱️ useQuery와 useMutation의 타입

useQuery

useQuery의 제네릭은 4개이다.

  1. TQueryFnData
    • useQuery로 실행하는 query function의 실행 결과의 타입을 지정하는 제네릭 타입이다.
  2. TError
    • query function의 error 형식을 정하는 제네릭 타입이다.
  3. TData
    • useQuery의 data에 담기는 실질적인 데이터의 타입을 의미한다.
  4. TQueryKey
    • useQuery의 첫 번째 인자 queryKey의 타입을 명시적으로 지정해주는 제네릭 타입이다.

useMutation

useMutation의 제네릭도 마찬가지로 4개이다.

  1. TData
    • useMutation에 넘겨준 mutation function의 실행 결과의 타입을 지정하는 제네릭 타입이다.
      • dataonSuccess의 인자 타입으로 활용된다.
  2. TError
    • useMutation에 넘겨준 mutation function의 error 형식을 정하는 제네릭 타입이다.
  3. TVariables
    • mutate 함수에 전달할 인자를 지정하는 제네릭 타입이다.
      • onSuccess, onError, onMutate, onSettled 인자의 타입으로 활용된다.
  4. TContext
    • mutate function을 실행하기 전에 수행하는 onMutate 함수의 return 값을 지정하는 제네릭 타입이다.
      • onMutate의 결과 값의 타입을 onSuccess, onError, onSettled에서 활용하려면 해당 타입을 지정해야 한다.

출처

useQuery | TanStack Query Docs
useMutation | TanStack Query Docs
Dependent Queries | TanStack Query Docs
Queries | TanStack Query Docs
QueryClientProvider | TanStack Query Docs
QueryClient | TanStack Query Docs
GitHub - ssi02014/react-query-tutorial: 😃 TanStack Query(aka. react query) 에서 자주 사용되는 개념 정리

profile
주저리주저리 생각 정리

0개의 댓글