우리는 한다, 개발을.

vue-query 사용해보기

⌛ 10 mins

TanStack Query는 비동기 작업 처리를 돕는 라이브러리입니다. v3까지는 React Query라는 이름으로 React만 지원했는데, v4 부터 React 이외의 프레임워크(Vue, Svelte, Solid)에서 사용할 수 있도록 업데이트 되며 TanStack Query로 이름이 변경되었습니다.

Vue Query는 서버의 데이터 가져오기 / 업데이트, 캐싱, 에러 처리 등을 쉽게 할 수 있도록 돕는 라이브러리입니다. 서버에서 가져온 값을 담는 객체, 에러가 발생했을 때 에러 정보를 담는 객체, 데이터 가져오기 / 업데이트 중임을 나타내는 등 각종 유틸 기능을 제공합니다.

동일한 요청을 동시에 여러 번 해도 한번만 요청을 보내어 최적화 하기 때문에 비동기 작업 (API 호출하는 등…)을 좀 더 효율적이고 간단하게 처리할 수 있게 합니다.

공식 문서

npm vue-query

요즘 프론트에서의 상태 관리는 두 가지 형태로 나눠지는 것 같아요.

  1. Redux, recoil, zustand, Vuex, Pinia 와 같은 전역 상태를 이용한 클라이언트의 상태 관리

  2. React Query, Vue Query 와 같은 라이브러리를 이용한 서버 상태 관리로 각각의 역할을 구분해서 사용되어집니다.

이번 발표에서는 VueQuery를 이용하여 간단한 게시글을 불러오는 것으로 예제를 준비해보았습니다.

실습 코드 구조

디렉토리는 위와 같이 구분하였습니다.

services 라는 디렉토리는 프론트에서 처리될 비동기 로직을 담고 있는 디렉토리 입니다.

apis는 이름 그대로 api 호출을 담당하는 함수로 모여있고, queries > hooks 는 vue-query를 사용하기 위한 쿼리의 모음, keys는 쿼리에 사용될 키를 담은 디렉토리로 구성되어있습니다.

// sevices > apis > boards > index.ts
import { axiosInstance } from "@/libs/Axios";

export const getBoardListsApi = async (date: {
  startDate: string;
  endDate: string;
}) => {
  const { data } = await axiosInstance.get(
    `/v1/unisurvey/project/status/check/${date.startDate}/${date.endDate}`
  );
  return data;
};

// sevices > queries > hooks > useBoardQuery.ts
import { useQuery } from "@tanstack/vue-query";
import { QUERY_KEYS } from "@/services/queries/keys";
import { getBoardListsApi } from "@/services/apis/boards";

export const useBoardListQuery = (date: {
  startDate: string;
  endDate: string;
}) => {
  return useQuery([QUERY_KEYS.BOARD.GET_LIST, date], () =>
    getBoardListsApi(date)
  );
};

// sevices > queries > keys > index.ts
export const QUERY_KEYS = {
  BOARD: {
    GET_LIST: "GET_BOARD_LIST_KEY",
  },
} as const;

먼저 사용에 앞서서 Vue-Query 설치하여 Vue에 플러그인 등록을 해줘야합니다.

npm i @tankstack/vue-query@4

@4는 4버전을 다운로드 한다는 의미입니다. 최근에 v5로 업데이트가 되었고 몇 몇 사용법이 변경되었습니다.

v4에 비해 번들 크기를 20% 줄이고 제공하는 API를 간소화하는데 중점을 두었다고 하는데 편의를 위해 저는 4 버전으로 설치하였습니다. (4버전이 레퍼런스 찾기도 쉬워요.)

설치를 했다면 main.ts에 VueQuery를 import 해줍니다.

import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import { VueQueryPlugin } from "@tanstack/vue-query";
import { queryClient } from "@/libs/Tanstack/queryClient";

const app = createApp(App);

app.use(store);
app.use(router);
app.use(VueQueryPlugin, { queryClient }); // <-- 등록

app.mount("#app");

저는 queryClient를 axios instance를 만들어 사용하는 것 처럼 파일을 분리해서 생성하였습니다.

// libs > Tankstack > index.ts
import { MutationCache, QueryCache, QueryClient } from "@tanstack/vue-query";

// 쿼리 에러에 대한 응답을 처리합니다.
// 예시로 toast를 이용하여 메세지를 띄울 수 있습니다.
const queryErrorHandler = (error: unknown): void => {
  const message = error instanceof Error ? error.message : "잘못된 요청입니다.";
  toast.message(message, 'error'); // toast
};

export const queryClient = new QueryClient({
  queryCache: new QueryCache({
    onError: queryErrorHandler,
  }),
  mutationCache: new MutationCache({
    onError: queryErrorHandler,
  }),
  defaultOptions: {
    queries: {
      refetchOnMount: false, // 마운트 되었을 때 데이터를 새로 불러올 것인지
      refetchOnWindowFocus: false, // 화면에 마우스가 포커스 됐을 경우 데이터를 새로 불러올 것인지
      retry: 1, // 데이터 페칭 재시도 횟수
      useErrorBoundary: true, // 에러 바운더리 사용 여부
      cacheTime: 60 * 60 * 1000 * 24, // 데이터 캐싱 시간 설정
    },
    mutations: {
      onError: queryErrorHandler,
      useErrorBoundary: true,
    },
  },
});

Vue-Query를 사용하기 위한 queryClient라는 객체를 생성합니다. (queryClinet는 전역적으로 사용할 수 있는 객체)

생성할 때 기본적으로 옵션들을 부여할 수 있어요.

예제에 사용된 옵션 이외에도 좀 더 다양한 옵션이 있어요. (이번 시간에는 이 옵션만 사용하였습니다.)

useQuery

데이터를 불러올 때에는 useQuery를 사용하여 불러오게 됩니다.

export const useBoardListQuery = (date: {
  startDate: string;
  endDate: string;
}) => {
  const query = useQuery(
    [QUERY_KEYS.BOARD.GET_LIST, date], 
    () => getBoardListsApi(date)
  );
  return query;
};

useQuery 의 첫 번째 인자로는 queryKey를 지정하고, 두 번째 인자로는 Promise 처리가 이루어지는 함수를 전달합니다.

쉽게 생각하면 fetch나 axios를 이용하여 서버에 API를 요청하는 함수가 들어갑니다.

useQuery 에서는 queryKey를 이용하여 쿼리 캐싱을 관리하게 됩니다.

(v4 이상 부터는 queryKey를 부여할 때 [ ] 형태로 넘겨줘야합니다.)

At its core, React Query manages query caching for you based on query keys. Query keys can be as simple as a string, or as complex as an array of many strings and nested objects. As long as the query key is serializable, and unique to the query’s data, you can use it!

TanStack Query Docs: Query Keys Article

쿼리 키가 변경이되면 새로운 데이터 페칭이 이루어집니다.

예제에서는 QUERY_KEYS.BOARD.GET_LISTdate 라는 2 개의 키를 전달하였고 이 둘 중 하나라도 변경이 이루어진다면 데이터를 새로 불러오게 됩니다.

ex1

export const useBoardListQuery = (date: {
  startDate: string;
  endDate: string;
}) => {
  const query = useQuery(
    [QUERY_KEYS.BOARD.GET_LIST, date], 
    () => getBoardListsApi(date),
    {
      refetchOnMount: true, 
      refetchOnWindowFocus: true, 
      retry: 3,
    }
  );
  return query;
};

세 번째 인자로는 queryClient에서 사용된 옵션이 들어가는데 해당 쿼리에만 다른 옵션을 줘야한다면 세 번째 인자에 옵션을 변경할 수 있습니다.

<script lang="ts">
import { useBoardListQuery } from "@/services/queries/hooks/useBoardQuery";
import { reactive, h } from "vue";

export type Board = {
  SNUM: number;
  TITLE: string;
  CREATED_AT: Date;
  TYPE: string;
  OWNER: string;
  START_DT: string;
  STATUS: string;
  END_DT: string;
};

export default {
  setup() {
    const date = reactive({
      startDate: "2024-01-01",
      endDate: "2024-01-08",
    });
    const { data = [], isLoading } = useBoardListQuery(date);

    // 데이터 페칭이 끝나기 전까지 화면에 보여질 태그
    if (!isLoading) return () => h("div", "....is loading");
    
    const boardLists = reactive<Board[] | any>(data);
    return {
      boardLists,
      date,
    };
  },
};
</script>

prefetching

prefetchQuery를 이용하여 쿼리를 실행해 데이터를 미리 불러와 캐시에 저장하게 됩니다.

(ex 페이징 처리)

공식 문서

import { QUERY_KEYS } from "@/services/queries/keys";
import { getBoardListsApi } from "@/services/apis/boards";
import { queryClient } from "@/libs/Tanstack/queryClient";

export const prefetchingBoardList = async (date: DateType) => {
  await queryClient.prefetchQuery(
    [QUERY_KEYS.BOARD.GET_LIST, date], 
    () => getBoardListsApi(date)
  );
};

useQuery에서 사용된 queryKey와 함수 동일하게 넘겨줍니다.

<script lang="ts">
import {
  prefetchingBoardList,
  useBoardListQuery,
} from "@/services/queries/hooks/useBoardQuery";
import { reactive, h, onBeforeMount } from "vue";

export type Board = {
  SNUM: number;
  TITLE: string;
  CREATED_AT: Date;
  TYPE: string;
  OWNER: string;
  START_DT: string;
  STATUS: string;
  END_DT: string;
};

export default {
  setup() {
    const date = reactive({
      startDate: "2024-01-01",
      endDate: "2024-01-08",
    });

    onBeforeMount(() => prefetchingBoardList); // <--- 추가

    const { data = [], isLoading } = useBoardListQuery(date);

    if (!isLoading) return () => h("div", "....IS LOADING");
    const boardLists = reactive<Board[] | any>(data);

    return {
      boardLists,
      date,
    };
  },
};
</script>

React에서는 prefetching 을 실행 시킬 때 useEffect 라는 훅을 사용하여 실행시킵니다.

Vue에서 prefeching을 시키는 예제를 제가 찾지 못해서.. 비슷한 성격의 훅인 onBeforeMount 훅에서 실행시켰습니다.

Reference

https://velog.io/@eeeve/React-Query

https://velog.io/@rlaalsrb3472/React-Query-useQuery

https://hoon1994.tistory.com/79