Notice
Recent Posts
Recent Comments
Link
«   2025/03   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31
Tags
more
Archives
Today
Total
관리 메뉴

likeornament 님의 블로그

Tanstack Query(React Query)에 대해 알아보자 본문

공부

Tanstack Query(React Query)에 대해 알아보자

likeornament 2024. 12. 31. 22:15

Tanstack Query(예전 명칭: React Query)는 데이터 패칭, 캐싱, 동기화 및 상태 관리 라이브러리다.

 

입대 전에 학교 선배들과 했던 프로젝트에서는 상태 관리할 때 Redux를 사용했다.

그런데, 데이터를 가져올 때 세 가지 동작(loading, error, success)에 대한 코드를 다 작성해야 하니 코드량에 대한 부담이 엄청났다.

 

그래서 이번 프로젝트에서는 코드량의 부담이 적은 Tanstack Query를 사용했다.


[특징]

1) 데이터 패칭과 캐싱

  • Tanstack Query의 큰 장점인데, 데이터를 요청하고 응답을 캐시하여 동일한 요청에 대해 반복적으로 서버와 통신하지 않도록 한다.
  • 사용자가 매우 빠른 속도로 컨텐츠를 볼 수 있어 UX 증대 

2) 자동 리패치

  • 여러 데이터 상태를 제공하기에 개발자가 수동으로 패칭을 관리해야 하는 부담을 줄여준다.

3) Query 상태 관리

  • 쿼리 요청의 상태를 추적(loading, success, error)하여 사용자에게 적절한 데이터를 제공한다.

4) Infinite Scrolling 및 Pagination

  • Infinite Scrolling 및 pagination과 같은 기능을 쉽게 구현할 수 있는 유틸리티를 제공한다.

5) 편리한 Mutation 처리

  • 서버에 데이터를 추가, 수정, 삭제하는 작업을 쉽게 처리하도록 돕는 useMutation 훅 제공

 


[Tanstack Query 데이터 상태]

Tanstack Query는 쿼리 데이터를 다룰 때 데이터 상태를 관리하기 위해 여러 상태를 정의한다.

 

 

1) Fresh

  • 데이터를 서버에서 가져온 이후 staleTime(기본값: 0ms) 동안 유지되는 상태.
  • Fresh 상태에서는 새로고침(refresh) 등의 동작이 발생해도 다시 서버에 데이터를 요청하지 않는다.
  • staleTime이 만료되면 Stale 상태로 변경된다.

 

2) Stale

  • staleTime이 지난 후, Fresh에서 변경되는 상태.
  • 새로고침(refresh) 동작이 발생하면 서버에 요청을 다시 보내 데이터를 받아온다.
  • 아래의 조건에 따라 데이터를 새로 가져온다.
const TodosComponent = () => {
  const { data, isFetching } = useQuery(
    ["todos"],
    fetchTodos,
    {
      staleTime: 10000, // 데이터를 10초 동안 Fresh 상태로 유지
      refetchOnWindowFocus: true, // 브라우저 탭으로 돌아오면 데이터 갱신
      refetchOnMount: "always", // 컴포넌트가 마운트될 때마다 데이터 가져오기
      refetchOnReconnect: true, // 네트워크 재연결 시 데이터 갱신
      refetchInterval: 5000, // 5초마다 데이터 갱신
      refetchIntervalInBackground: true, // 탭이 비활성화되어도 데이터 폴링 유지
    }
  );

 

 

3) Inactive

  • 사용자가 보고 있는 화면에서 해당 queryKey를 사용하는 query가 사용되지 않는다면 변경되는 상태.
  • inactive 상태가 되면 gcTime이 흘러가기 시작한다.
더보기

💡gcTime(Garbage Collect Time)이란?

 

gcTime은 캐시된 데이터가 메모리에서 제거되기까지의 시간을 지정한다.

Inactive 상태로 전환된 query는 지정된 gcTime 동안 캐시에 유지되고, 이 시간이 지나면 메모리에서 삭제된다.

 

❗staleTime은 gcTime보다 짧아야 한다.

 

staleTime을 설정하는 이유가 그 시간 동안 Fresh 상태를 유지해서 캐싱된 데이터를 사용하기 위함인데, 만약 gcTime이 더 짧아 inactive 상태일 때 해당 query가 삭제된다면 staleTime을 설정하는 이유가 없다.

 

 

4) Fetching

  • 네트워크 요청을 통해 데이터를 가져오는 상태.
  • 데이터 로딩 중 isFetching 상태를 참조하여, UI를 통해 로딩 애니메이션 등을 표시할 때 사용할 수 있다.

 

5) Paused

  • fetching이 일시 중단된 상태.
  • 네트워크 상태가 offline이거나 오류로 인해 fetching이 중단되면 isPaused 상태가 된다.

 


[Tanstack Query 액션 타입]

아래의 액션 타입으로 데이터를 가져오거나 수정, 무효화하는 등의 동작이 가능하다.

 

1) Refetch

  • 데이터를 서버에서 새로 가져온다.
  • refetchQueries를 사용하여 하나 이상의 query를 다시 fetch 할 수 있다.
queryClient.refetchQueries(['todos']);

 

2) Invalidate

  • 특정 query를 무효화하여 Stale 상태로 변경한다.
  • query가 Inactive일 때는 데이터를 새로 가져오지 않지만, Observers가 1 이상이 될 경우에 서버에 데이터를 새로 요청해서 가져온다.
더보기

💡Observers란?

 

현재 페이지에서 해당 query를 사용하고 있는 곳의 수를 지칭한다.

  • invalidateQueries를 사용하여 지정된 query를 무효화할 수 있다.(무효화된 query는 Stale 상태로 변환되며, 다음 렌더링 시 Fetch를 트리거한다.)
queryClient.invalidateQueries(['todos']);

 

3) Reset

  • useQuery()의 옵션 중 initialData가 있다면, 해당 데이터로 초기화되고 없다면 새로 데이터를 가져온다.
  • resetQueries를 사용하여 해당 query를 초기 상태로 되돌릴 수 있다.
queryClient.resetQueries(['todos']);

 

4) Remove

  • removeQueries를 사용하여 query를 캐시에서 완전히 제거할 수 있다.
queryClient.removeQueries(['todos']);

 

5)Trigger Loading/Trigger Error

  • 로딩 상태와 에러 상태일 때를 확인할 수 있다. 

 


[설치와 사용 방법]

 

1. 설치

npm i @tanstack/react-query@5
npm i @tanstack/react-query-devtools@5 -D #devtools 설치
  • 위의 명령어를 입력하여 라이브러리를 설치한다.

 

2. 사용 방법

 

1) Provider 만들기

//RQProvider.tsx
"use client";

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

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

function RQProvider({children}: Props) {
  const [client] = useState(
    new QueryClient({
      defaultOptions: {  // react-query 전역 설정
        queries: {
          refetchOnWindowFocus: false, //탭 전환
          retryOnMount: true, // 마운트 됐을 때
          refetchOnReconnect: false, // 인터넷 접속이 끊겼다가 다시 연결 됐을 때
          retry: false, // 데이터를 가져오는 중 오류가 생겼을 때
        },
      },
    })
  );

  return (
    <QueryClientProvider client={client}>
      {children} {/*children들은 react query를 공유 */}
      <ReactQueryDevtools initialIsOpen={process.env.NEXT_PUBLIC_MODE === 'local' }/> {/* 개발모드에서만 devTools 사용하는 옵션 */}
    </QueryClientProvider>
  );
}

export default RQProvider;
  • 데이터 패칭을 할 그룹을 감싸기 위한 Provider를 만들어 준다.

 

//layout.tsx
import {ReactNode} from "react";
import style from "@/app/(main)/layout.module.css";
import NavMenu from "./_component/NavMenu";
import RQProvider from "./_component/RQRovider";

type Props = { children: ReactNode, modal: ReactNode };
export default async function AfterLoginLayout({ children, modal }: Props) {

  return (
    <div className={style.container}>
      <RQProvider>
        <main className={style.main}>
          {children}
        </main>
        <div className={style.bottomSectionWrapper}>
          <nav className={style.nav}>
            <NavMenu/>
          </nav>
        </div>
        {modal}
      </RQProvider>
    </div>
  )
}
  • layout에서 데이터를 패칭할 부분을 Provider로 감싸준다.
  • 프로젝트 전체에서 데이터 패칭이 일어나기에 루트 layout의 children을 감싸주었다.

 

2) 데이터를 fetch할 함수 작성

// lib\getUser.ts
import { QueryFunction } from "@tanstack/react-query";
import { User } from "@/model/User";

export const getUser: QueryFunction<User, [_1: string, _2: string]>
  = async ({queryKey}) => {
    const [_1, username] = queryKey;
    const res = await fetch(`/myPage`, {
      next: {
        tags: ['myPage', username],
      },
      cache: 'no-store',
    });

    if(!res.ok) {
      throw new Error('Failed to fetch data');
    }

    return res.json();
  }

 

  • 데이터를 fetch 할 함수를 lib 폴더의 getPosts.ts 파일에 적어주었다.
  • 함수의 타입으로 Tanstack Query에서 사용되는 쿼리 함수의 타입을 의미하는 QueryFucntion을 적어준다.
  • QueryFunction의 타입 서명은 <함수가 반환할 데이터 타입, 쿼리 키의 타입>으로 적어준다.
  • tags에는 해당 데이터의 queryKey가 될 문자열 들을 입력하면 된다.
  • cache 속성에 'no-store'를 적어주면 언제나 새로운 데이터를 가져온다.(cache 하지 않음)

 

// lib\getUserPosts.ts
import { QueryFunction } from "@tanstack/react-query";
import { PageInfo } from "@/model/PageInfo";

type Props = { pageParam?: number };

export const getUserPosts: QueryFunction<PageInfo, [_1: string, _2: string, _3: string], number>
  = async ({queryKey, pageParam}) => {
    const [_1, _2, username] = queryKey;
    const res = await fetch(`${process.env.NEXT_PUBLIC_BACKEND_API_SERVER}/find/${username}/photos?page=${pageParam}&size=15`, {
      next: {
        tags: ['posts', 'users', username],
      },
      cache: 'no-store',
    });

    if(!res.ok) {
      throw new Error('Failed to fetch data');
    }

    return res.json();
  }
  • 위의 함수와 같이 pagination을 사용하는 함수라면 타입 서명을  <함수가 반환할 데이터 타입, 쿼리 키의 타입, 페이지 번호>로 적어준다.

 

3) 함수 사용

import style from './person.module.css';
import { HydrationBoundary, QueryClient, dehydrate } from '@tanstack/react-query';
import UserInfo from './_component/UserInfo';
import { getUser } from './_lib/getUser';
import { getUserPosts } from './_lib/getUserPosts';
import {auth} from "@/auth";

export default async function Profile() {
  const session = await auth();
  const username = session?.user?.name;
  const queryClient = new QueryClient();
  
  if (username) {
    await queryClient.prefetchQuery({queryKey: ['users', username], queryFn: getUser});
    await queryClient.prefetchInfiniteQuery({
      queryKey: ['posts', 'users', username], 
      queryFn: getUserPosts,
      initialPageParam: 0,
    });
  }

  const dehydrateState = dehydrate(queryClient);

  return (
    <>
      <div className={style.main}>
        <HydrationBoundary state={dehydrateState}>
          <UserInfo username={username || ''}/>
        </HydrationBoundary>
      </div>
    </>
  )
  }
  • 위에서 만든 두 함수로 데이터를 가져와 화면에 그리는 page.tsx 파일이다. 
  • 코드를 살펴보도록 하겠다.

 

import { HydrationBoundary, QueryClient, dehydrate } from '@tanstack/react-query';

const queryClient = new QueryClient();
  • @tanstack/react-query에서 QueryClient를 불러와 QueryClient 객체를 만든다.
  • QueryClient는 쿼리 상태를 관리하는 객체로 쿼리 캐시, 상태 관리, re-fetching  등 쿼리와 관련된 로직을 관리할 수 있다.

 

await queryClient.prefetchQuery({queryKey: ['users', username], queryFn: getUser});
  • 페이지가 그려지기 전 query를 미리 fetch하기 위해 prefetchQuery()를 사용한다.
  • queryKey와 아까 만든 함수를 QueryFn에 적어준다.

 

await queryClient.prefetchInfiniteQuery({
  queryKey: ['posts', 'users', username], 
  queryFn: getUserPosts,
  initialPageParam: 0,
});
  • pagination이 사용되는 query는 prefetchInfiniteQuery()를 사용한다.
  • initialPageParam에는 초기 페이지 번호를 적어준다.

 

const dehydrateState = dehydrate(queryClient);
  • SSR시 Tanstack Query가 서버에서 가져온 초기 데이터를 클라이언트에서 사용할 수 있도록 직렬화(dehydrate)하는 작업이 필요하다.
  • 이를 위해, dehydrate() 함수를 사용해 queryClient 객체에서 관리하는 데이터와 쿼리 상태를 직렬화(dehydrate)해서 그 결과물을 dehydrateState에 저장한다.

 

  return (
    <>
      <div className={style.main}>
        <HydrationBoundary state={dehydrateState}>
          <UserInfo username={username || ''}/>
        </HydrationBoundary>
      </div>
    </>
  )
  • dehydrate() 함수에 의해 직렬 직렬화 된 쿼리 상태를 클라이언트에서 사용하려면, 재수화(hydrate)를 통해 Tanstack Query가 해당 데이터를 활용하여 클라이언트의 초기 상태로 사용할 수 있게 만드는 과정이 필요하다.
  • 이를 위해 해당 데이터를 사용하는 부분을 <HydrationBoundary state={dehydrateState}>로 감싸준다.
  • hydrate(재수화)는 직렬화된 쿼리 상태를 다시 복원하는 것을 의미한다.

 

더보기

💡page.tsx에서 prefetchQuery()와 dehydrate()를 함께 사용한 이유?

 

 SSR에서는 prefetchQuery()를 사용하여 서버에서 데이터를 미리 로드하고, 이를 dehydrate()를 통해 직렬화 하여 클라이언트로 전달하기 위함.

 

 이는 클라이언트가 페이지를 로드할 때 데이터를 다시 요청하지 않고 초기 상태로 데이터를 미리 사용할 수 있게 해준다.

 

'공부' 카테고리의 다른 글

Auth.js(NextAuth.js) 사용법  (0) 2024.12.30