Tanstack-Query와 useQuery

제혁·2024년 9월 22일
0
post-thumbnail

시작하기 전에

본 환경은 아래 환경에서 작업됩니다.

Next.js v14, TypeScript, Tanstack-Query v5


이전엔

2022년도 React Query 관련 글을 쓴 적이 있다. 그때도 이해를 잘 못했고, 현재도 이해를 잘 못한 상태이다. 이 글은 제가 공부한 것들을 기록하는 용도이니 틀린 부분이 있거나 더 좋은 방법이 있다면 언제든지 환영합니다. (현업에서 tanstack-query 써본적 없음...)


Tanstack-Query 시작하기

우선 tanstack-query를 사용하기 위해선 root layout에 아래와 같은 설정이 필요하다.

// QueryProviderWrapper.tsx
"use client";

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import React from "react";

type Props = {
    children: React.ReactNode;
};

export default function QueryProviderWrapper({ children }: Props) {
    const queryClient = new QueryClient();

    return (
        <QueryClientProvider client={queryClient}>
            {children}
            <ReactQueryDevtools
                initialIsOpen={process.env.NEXT_PUBLIC_MODE === "local"}
            />
        </QueryClientProvider>
    );
}

// layout.tsx

// Library
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";

// Components
import QueryProviderWrapper from "@/components/wrapper/QueryProviderWrapper";

// Hooks & Utils

// Api

// Interface & States

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
    title: "Create Next App",
    description: "Generated by create next app",
};

export default function RootLayout({
    children,
}: {
    children: React.ReactNode;
}) {
    return (
        <html lang="en">
            <body className={inter.className}>
            	<QueryProviderWrapper>{children}</QueryProviderWrapper>
            </body>
        </html>
    );
}

tanstack-query 를 이용하기 위해서는 QueryClientProvider를 최상단에 감싸주고 QueryClient 인스턴스를 client props로 주어야 한다.

이때, QueryClient는 캐시와 상호작용을 할 수 있게 한다. 즉, QueryClient 인스턴스를 넣어주지 않는다면, tanstack-query를 사용할 이유가 없다. (어차피 client props를 넣어주지 않는다면 에러가 뜬다.)

QueryClient 안에는 여러가지 defaultOption 들을 넣을 수 있다. 이는 공식문서에 잘 나와있다.

또한 ReactQueryDevtools를 사용하고 있는데, 이는 전용 devtools로 기본적으로 development 상태에서만 실행된다. (안쓰고 싶으면 안써도 되는데 전 씁니다 ㅎ)


useQuery

기본적으로 GET 요청을 위한 함수이다. 기본적인 사용법은 아래와 같다.

import { getUserApi } from "@/apis/userApi";
import { IUser } from "@/interfaces/userIFC";
import { useQuery } from "@tanstack/react-query";

const {
        data: user,
        error,
        isPending,
    } = useQuery<IUser, ErrorConstructor, IUser, [string]>({
        queryKey: ["user"],
        queryFn: getUserApi,
        staleTime: 60 * 1000,
        gcTime: 300 * 1000,
    });

내 코드 중에 user 정보를 가져오는 함수이다. 다른 것들을 보기 전에 queryKey, queryFn 부터 보면, 둘 모두 useQuery 를 사용하기 위한 필수값이다.

useQuery는 queryKey를 기반으로 쿼리의 캐싱을 관리한다. 즉, queryKey를 통해 쿼리들을 식별한다. 해당 queryKey를 통해 나중에 setQueryData, invalidateQueries 등의 함수를 사용한다. (둘 모두 캐싱된 데이터를 업데이트 해 주는 함수이다.)

다음으로 queryFnPromise를 반환하는 함수를 넣어야 한다. 위에선 getUserApi를 넣었는데 코드를 살펴보면 아래와 같다.

export const getUserApi: QueryFunction<userIFC, [string]> = async ({
    queryKey,
}) => {
    try {
        ~~~
          
    } catch (err) {
        ~~~
    }
};

try 문 안에 await가 들어갈 것이고, async/await 문법의 return 값은 Promise 이다.

어려우면 그냥 비동기 함수 넣는다고 생각해도 이해를 위해선 문제가 없다. (백엔드에서 데이터를 가져오는 함수를 넣는다고 생각해도 뭐...)

staleTime, gcTime은 마지막에 설명하고, useQuery의 return 데이터는 정말 많다. 우선 위에 쓰여진 것은 data, isError, isPending 밖에 없지만 공식문서를 찾아보면 정말 많으니 궁금하면 찾아봐도 된다.

data는 말 그대로 받아온 response, isError는 에러인지 아닌지, isPending은 isLoading이라고 생각해도 좋다. 문서 상 설명은 The query has no data yet 라고 쓰여 있지만 나도 그냥 isLoading 처럼 쓴다. (위에서도 말했지만 아직 tanstack-query를 잘 모른다...) isLoading이 v4까지는 있었으나 v5에선 사라진 것으로 안다.

다음으로는 useQuery에 쓰여진 제네릭이다. useQuery는 4개의 제네릭을 가지며 아래와 같다. (순서대로 쓰여졌다고 보면 된다.)

  1. TQueryFnData: useQuery로 실행하는 queryFn의 실행 결과의 타입을 지정한다.
  2. TError: queryFn의 error 타입이다.
  3. TData: useQuery의 data에 담기는 실질적 데이터의 타입이다. TQueryFnData와의 차이점은 아직 몰라서 그냥 똑같이 쓰고있다.
  4. TQueryKey: useQuery의 queryKey의 타입이다.

위에서 난 <IUser, ErrorConstructor, IUser, [string]> 라고 썼다. 즉, queryFn의 결과값인 데이터(물론 같지 않다는건 알지만 말했다시피 난 아직 차이를 잘 모른다.)의 Type은 IUser(미리 지정해 둔 user의 타입), error의 타입은 ErrorConstructor, 그리고 queryKey가 배열로써 ["user"] 이렇게 되어있기에 TQueryKey는 [string]이 된다.

다음으로 staleTime, gcTime이다. 둘 모두 필수값은 아니며, staleTime은 0, gcTime은 5분으로 default 설정이 되어있다. staleTime은 데이터가 fresh(신선한)에서 stale(썩은) 상태로 변경되는데 걸리는 시간으로, staleTime이 3000이면 3초 뒤에 stale 상태로 변한다는 것이다.

데이터가 fresh한 상태는 말 그대로 데이터가 아주 신선한 상태라는 것이고, 데이터가 신선하다는 것은 방금 서버에서 데이터를 가져와서 캐시에 저장한 데이터라는 것이다. 그렇기 때문에 데이터가 fresh 상태일 경우엔 해당 쿼리 인스턴스가 새롭게 mount 되어도(실행 되어도 라고 봐도 된다.) fetch(네트워크 요청)가 일어나지 않는다.

즉, staleTime을 난 1 60 1000 으로 지정했고, 이는 1분이므로, 1분 동안은 해당 query의 데이터를 캐시에서만 가져오도록 하는 것이다.

다음으로 gcTime이다. garbage collection Time의 약어로 cacheTime이라고도 많이 말한다. 쿼리 인스턴스가 unmount 되면 데이터는 inactive 상태로 변경되며, 캐시는 gcTime만큼 유지된다. 그리고 해당 시간이 지나면 가비지 콜렉터로 수집된다. 하지만 만약 gcTime이 지나기 전에 쿼리 인스턴스가 다시 mount 된다면, 데이터를 fetch 하는 동안 캐시 데이터를 보여준다.

위에서 unmount라고 했는데 나도 아직 정확한 시점은 모르겠으나 해당 쿼리를 사용하는 컴포넌트가 unmount 되거나, 해당 쿼리의 queryKey가 변경되면 unmount 된다 정도로만 알고있다.


useQuery의 사용

위 getUserApi를 예시로 하겠다. getUserApi는 말 그대로 user 정보를 가져오는 api로 난 웹을 주로 만들기 때문에 일반적인 웹페이지를 생각해보면 대부분의 페이지에 헤더가 있고 헤더에는 로그인/로그아웃 버튼이 있다. 그리고 해당 버튼은 로그인 여부에 따라 버튼이 달라지게 된다. 그래서 난 Header 부분에서 useQuery를 이용해 user데이터를 fetch 해주고있다.

Header 외에도 대부분의 페이지에서 user 정보는 필요하다. 하지만 그럴때마다 데이터를 fetch 해오는 것은 비효율적이라 생각해 Header를 제외한 나머지 컴포넌트에선 아래와 같이 캐싱된 데이터를 가져오고 있다.

const queryClient = useQueryClient();
const user = queryClient.getQueryData<IUser>(["user"]);

설명할 것도 없이 함수명 그대로 해석하면 된다. 여기서 주의할 점은 getQueryData 함수의 파라미터로 queryKey가 들어가는데, 이를 올바르게 작성해줘야 한다는 것이다. 위에서 useQuery를 사용할 때 queryKey와 동일하게 작성해준다.

이렇게 되면 해당 쿼리 인스턴스의 캐싱된 user정보를 가져온다.

이때, 무언가의 작업으로 인해 user 정보가 변경될 수도 있다. 예를 들면 마이페이지에서 프로필 수정을 한다든가...

이때 난 아래와 같이 editUser에 대한 성공 케이스를 작성한다.


// if Edit User is Success...

const queryClient = useQueryClient();

// 1번
// queryClient.invalidateQueries({ queryKey: ["user"] });

// 2번
queryClient.setQueryData(["user"], data.user);

아직 캐싱된 데이터를 업데이트 하는 방법을 위 2가지 밖에 모른다.

첫 번째는 invalidateQueries로, 캐시를 무효화하고 다시 서버에서 데이터를 가져온다.

두 번째는 setQueryData받아온 데이터를 가지고 캐시 값을 바로 업데이트 시키는 것이다.

프로필 업데이트의 경우 서버에서 user 데이터를 주고있고, 다른 곳에서 user 정보를 업데이트 시킬 일이 없다고 판단해 2번을 사용했다. 하지만 만약 다른 부분에서 user 정보를 업데이트 시키는 곳이 있다면 서버에서 직접 데이터를 받아오는 것이 더 안전한 방법이니 1번을 사용하는게 맞는 것 같다.


마치며

계속 다른 공부나 하다가 오랜만에 tanstack-query를 보게 되었고, 이를 기반으로 작업했던 프로젝트를 다시 리팩토링하고 있다. 진짜 개판이다. 아무것도 모르고 만들었다는게 눈에 보인다. 앞으로는 주말마다 해당 프로젝트를 리팩토링하고 일요일 밤에 블로그 업데이트를 하려고 한다. 다음주 내용이 뭐가 될지는 모르겠지만... 일단 tanstack-query 내용만 줄기차게 올려보려고 하니 기대해주세요!

profile
언젠가 성공할 FE 개발자입니다

0개의 댓글

관련 채용 정보