likeornament 님의 블로그
Tanstack Query(React Query)에 대해 알아보자 본문
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 |
---|