나의 TanStack Query 사용법

신석진( Seokjin Shin)·2024년 2월 20일
2
post-thumbnail

사실 일반 사용법과 그닥 다르지 않을 것이다.

TanStack Query는 웹 애플리케이션에서 서버 상태 가져오기, 캐싱, 동기화 및 업데이트를 매우 쉽게 만드는 라이브러리이다. (참조)

더 자세하게 보면 아래와 같은 일들을 도와준다.

  • 캐싱
  • 동일한 데이터에 대한 여러 요청을 단일 요청으로 중복 제거
  • 백그라운드에서 "오래된" 데이터 업데이트
  • 데이터가 "오래된" 시기 알기
  • 페이지 매김 및 지연 로딩 데이터와 같은 성능 최적화
  • 서버 상태의 메모리 및 가비지 수집 관리
  • 구조적 공유를 통해 쿼리 결과 메모

위와 같은 기능을 글로만 보면 잘 이해가 가지 않는다. 코드를 통해 살펴보자.

useQuery

현업 코드를 직접 들고 올 수는 없어서 투두로 대체한다. 편의상 파일 하나에 타입과 fetch 하는 부분과 query를 몰아두었다. 기본적 용법은 다음과 같다.

import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import axios from "axios";

const key = "todos";

const url = "/todos";

interface Todo {
  todoId: string;
  title: string;
  completed: boolean;
}

interface CreateDto {
  title: string;
  completed: boolean;
}

const findAll = () => axios.get<FindAllTodoDto>(url).then((res) => res.data);


const useFindAll = () =>
  useQuery({
    queryKey: [key],
    queryFn: findAll,
    staleTime: Infinity,
  });

export const todoQuery = {
  key,
  url,
  useFindAll,
};

TanStack Query에서 제공하는 훅인 useQuery는 기본적으로 queryKey, queryFn을 필요로한다.
필자는 보통 특정 함수를 바로 export하지 않고 한번 래핑해서 export한다. 그 이유는 테스트할 때 해당 객체에 접근해서 바로 모킹해주기 위함이다.
useQuery에서는 많은 옵션들을 줄 수 있는데, 위의 예시에서는 해당 데이터의 유효 기간을 Infinity로 설정하였다. stale과 cache의 시간 값을 기반으로 실제 데이터를 계산하여 필요한 경우에만 네트워크 요청을 보낸다.

useMutation

기본적으로 mutation을 사용할 때는 Optimistic updates 을 사용하였다. 위에서 선언한 것과 크게 다르지 않기에 타입과 함수는 생략하도록 하겠다.

const useCreate = () => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: create,
    async onMutate(newTodo) {
      await queryClient.cancelQueries({ queryKey: [key] });
      const previousTodos = queryClient.getQueryData([key]);
      queryClient.setQueryData([key], (old: FindAllTodoDto) => ({
        items: [...old.items, newTodo],
        totalCount: old.totalCount,
      }));

      return {
        rollback: () => {
          queryClient.setQueryData([key], previousTodos);
        },
      };
    },
    onError(err, newTodo, context) {
      context?.rollback();
    },
    onSettled() {
      queryClient.invalidateQueries({
        queryKey: [key],
      });
    },
  });
};

update와 delete도 동일하게 적용할 수 있기에 별도로 작성하지 않았다.
위 코드에서 Optimistic이 적용된 위치는 onMutate 시점이다. onMutate는 데이터 변경 요청 API 호출되는 순간 불리는 콜백이다. 이때 기존에 있던 동일한 키의 쿼리들을 취소시키고 queryClient로 직접 해당 쿼리에 값을 변경하면 이것이 UI에도 선반영되어 보이게 된다. 이후에 에러가 발생하면 기존 previousTodos로 복구 시키는 로직은 onError에 들어있다. 에러나 성공이 지나면 onSettled가 호출된다. 이때 필자는 보통 해당 키의 invalidate를 선언하여 새로 데이터를 불러온다.

UI

아래의 코드에서는 data가 undefined일 때 Loading 중임을 나타내고 있는데, useSuspenseQuery 를 사용하면 Suspense로 감싸서 undefined 없이 코드를 작성할 수 있다. 다만 SSR의 경우 hydration 에러가 발생한다.

데이터가 잘 불러와져서 undefined가 아니라면 useEffect로 별도로 상태관리를 해줄 필요 없이 바로 데이터를 가져다 쓰면 된다. 쉽지 않은가?

import { todoQuery } from "@/api/todos";

const TodoList = () => {
  const { data } = todoQuery.useFindAll();
  const { mutate: updateTodo } = todoQuery.useUpdate();
  const { mutate: removeTodo } = todoQuery.useRemove();

  if (!data) return "Loading...";

  return (
    <div className="text-center">
      <div className="w-[100%] flex gap-8 max-w-[320px]">
        <div className="w-[100px]">ID</div>
        <div className="w-[80px]">TITLE</div>
        <div className="w-[80px]">COMPLETED</div>
        <div className="w-[80px]" />
      </div>
      {data.items.map((d) => (
        <div className="w-[100%] flex gap-8 max-w-[320px]" key={d.todoId}>
          <div className="w-[100px] text-ellipsis whitespace-nowrap overflow-hidden">
            {d.todoId}
          </div>
          <div className="w-[80px]">{d.title}</div>
          <input
            type="checkbox"
            className="w-[80px]"
            onChange={(e) => {
              const completed = e.target.checked;
              updateTodo({
                todoId: d.todoId,
                completed,
              });
            }}
            defaultChecked={d.completed}
          />
          <button
            onClick={() => {
              removeTodo(d.todoId);
            }}
          >
            DELETE
          </button>
        </div>
      ))}
    </div>
  );
};

export default TodoList;

정리 및 개인 의견

  • useEffect로 API 호출하고 상태관리를 별도로 해주지 않아도 되서 편리하다.
  • POST할 때 상황별 콜백을 정해주어서 UI에서 로직을 빼내기가 쉽다.
  • Optimistic update를 사용하면 좋은 사용자 경험을 주기도 쉽다.
  • 알아서 캐싱도 해줘서 네트워크 요청을 최소화 할 수 있다.
  • 키만 잘 관리한다면 명시적으로 다시 부르기도 편하다.

개인적으로 버튼을 눌러서 엑셀을 가져오는 API의 경우에는 Tanstack Query를 쓰기보단 그냥 fetch 해오는 것이 간단했던 경험이 있다. 페이지 내부에서 쓰이는 데이터를 가져오기 위한 API를 GET으로 호출할 때는 useQuery, 데이터를 변경해야하면 useMutation을 쓴다.

개발자가 신경써야할 부분을 현저하게 줄여주고 있기에 앞으로도 현업에서 유용하게 쓸 예정이다.

원본 레포지토리

0개의 댓글