리액트 쿼리라고도 불리는 Tanstack-Query, v5 버전인데, v4일 때는 명칭이 react-query 였고, v5가 정식 버전이 된 것은 그렇게 오래되지 않았다.
왜 필요할까?
화면이 전환되었을 때, 최신 데이터를 보여주고 싶은데 그때 refetch하는 기능이 있어야 하는데 이는 리액트로도 구현은 할 수 있지만, 이런 것들을 조금 더 쉽게 해준다고 보면 된다. 한 마디로, 서버의 상태 관리를 조금 더 쉽게 단순화 시켜주는 도구라고 보면 될 것이다.
주요 기능 - 자동 캐싱, 백그라운드 업데이트, 캐싱 등이 있다.
기존에는 데이터가 같은 페이지를 가져오면 이전과 같다. 그리고 수정하지 않는 이상 두 번 요청할 필요는 없다. 이전에 받아온 데이터를 첫번째는 당연히 요청을 해야한다. -> 캐싱을 해놓음. -> 사용자가 두번째 이상 방문 하게 되면 데이터 요청을 번거롭게 또 하는 것이 아니라 캐싱된 데이터를 받아와서 쓸 수 있다.
물론, 너무 많은 캐싱은 메모리에 부하를 일으키기는 하지만 왠만해선 부하가 잘 안 일어날 것이다. 근데 고려해야 하는 것은 컴퓨터뿐만 아니라 핸드폰(램이나 기타 등등의 옛날 버전 같은?)도 고려해야 한다.
tanstack-query를 쓰는 가장 큰 이유 중 하나가 비동기 데이터를 편하게 관리하는 것이다. async. 그리고 서버 상태랑 클라이언트 상태의 분리를 하는 것이고, 자동으로 재시도를 하거나 에러 핸들링을 더 쉽게 해 줄 수 있다.
페이지 구현해보기
homelayout의 navbar
import React from 'react'
import { Link } from 'react-router-dom'
const Navbar = () => {
return (
<nav className='bg-white dark:bg-gray-900 shadow-md fixed w-full z-10'>
<div className='flex itmes-center justify-between p-4'>
<Link
to="/"
className='text-xl font-bold text-gray-900 dark:text-white'
>
SpinningSpinning Domlimpan
</Link>
<div className='space-x-6'>
<Link
to={"/login"}
className='text-gray-700 dark:text-gray-300 hover:text-blue-500'
>
로그인
</Link>
<Link
to={"/signup"}
className='text-gray-700 dark:text-gray-300 hover:text-blue-500'
>
회원가입
</Link>
</div>
</div>
</nav>
)
}
export default Navbar
이렇게 구현을 하는데 여기서 로그인, 회원가입 화면에 있을 경우는 navbar에 안 보이도록 처리를 해주는 작업을 해준다. navbar에 useAuth를 불러와서 엑세스 토큰이 있는 경우에만 위에 것이 보여지도록 하면 될 것이다. 그래서 조건부 처리를 아래처럼 해주면 된다.
<div className='space-x-6'>
{accessToken && (
<>
<Link
to={"/login"}
className='text-gray-700 dark:text-gray-300 hover:text-blue-500'
>
로그인
</Link>
<Link
to={"/signup"}
className='text-gray-700 dark:text-gray-300 hover:text-blue-500'
>
회원가입
</Link>
</>
)}
</div>
이렇게 하면 이제 엑세스 토큰이 있는 경우에는 안 보이게 될 것이다.
그 다음, 마이 페이지, 검색 페이지도 추가해준다. 그리고 마이 페이지 같은 경우는 로그인 한 경우에만 볼 수 있어야 한다. 그래서 이 부분도 엑세스 토큰이 있는 경우에만 보이도록 처리.
{accessToken && (
<Link
to={"/my"}
className='text-gray-700 dark:text-gray-300 hover:text-blue-500'
>
마이 페이지
</Link>
)}
<Link
to={"/search"}
className='text-gray-700 dark:text-gray-300 hover:text-blue-500'
>
검색
</Link>
그리고 이제 HomeLayout의 뼈대를 이루는 것들 중에 남은 Footer를 구현해준다.
import { Link } from "react-router-dom"
const Footer = () => {
return (
<footer className="bg-gray-100 dark:bg-gray-900 py-6 mt-12">
<div className="container mx-auto text-center text-gray-600 dark:text-gray-400">
<p> {/* © 이걸 적으면 커퍼레이션 기호를 넣어줄 수 있음. */}
© {new Date().getFullYear()} SpinningSpinning Dollimpan. All rights
reserved.
</p>
<div className={"flex justify-center space-x-4 mt-4"}>
<Link to={"#"}>
Privacy Policy
</Link>
<Link to={"#"}>
Terms of Service
</Link>
<Link to={"#"}>
Contact
</Link>
</div>
</div>
</footer>
)
}
export default Footer
Tanstack-Query 사용
- 이제, tanstack-query를 사용해볼 것인데 그러기 위해서 아래 공식문서를 참고해, install을 해준다. https://tanstack.com/query/latest/docs/framework/react/installation
Installation | TanStack Query React Docs
You can install React Query via , or a good ol' <script via . NPM bash npm i @tanstack/react-query or bash pnpm add @tanstack/react-query or bash yarn add @tanstack/react-query or bash bun add @tansta...
tanstack.com
pnpm을 쓰고 있기 때문에
$ pnpm add @tanstack/react-query
이것을 적어주고 다운로드해주면 된다. 그 다음, Quick Start를 눌러서 사용법을 확인한다.
https://tanstack.com/query/latest/docs/framework/react/quick-start
Quick Start | TanStack Query React Docs
This code snippet very briefly illustrates the 3 core concepts of React Query:
tanstack.com
- QueryClient를 씌워준다.
export const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<AuthProvider>
<RouterProvider router={router} />
</AuthProvider>
</QueryClientProvider>
)
}
export default App;
Tanstack Query Devtools
https://tanstack.com/query/latest/docs/framework/react/devtools
Devtools | TanStack Query React Docs
Wave your hands in the air and shout hooray because React Query comes with dedicated devtools! 🥳 When you begin your React Query journey, you'll want these devtools by your side. They help visualize...
tanstack.com
- 이는 쿼리들을 좀 더 쉽게 볼 수 있게 해주는 것이다. 예를 들어, 캐싱이 되어있다는 것을 시각적으로 보기가 되게 힘든데 그런 것들을 시각적으로 편하게 볼 수 있게 해주는 것을 Tanstack Query Devtools이다.
- 이것도 pnpm에 맞춰서 다운로드 해준다.
$ pnpm add @tanstack/react-query-devtools
- 그 다음, App 내부 QueryClientProvider 안에 아래 코드를 넣어주면 된다.
<ReactQueryDevtools initialIsOpen={false} />
- 근데 이 devtools는 개발환경에서만 켜야 하는데 배포 환경에서도 켜버리면 사용자들이 이걸 다 볼 수 있기 때문에, 그런 실수를 방지하기 위해서 환경변수를 이용한다. 그걸 이용하면
- import.meta.env.DEV 이렇게 적어주면 dev 개발자 환경일 때만 띄워지도록 처리가 가능하다. 그러므로 아래처럼 조건부를 달아준다. -> 배포 환경에서는 안 떠진다.
{import.meta.env.DEV && <ReactQueryDevtools initialIsOpen={false} />}
스웨거를 통한 Lp 목록 조회 실습
- lp관련 api 파일을 만들어준다. 쿼리 파라미터로는 위의 4가지를 감쌀 것이다. 근데 다른 Api도 뭔가 4가지를 공용으로 쓰는 것을 확인해볼 수 있기 때문에 CursorBasedResponse로 공용 타입을 만들어준다. + optional 처리.
export type CursorBasedResponse<T> = {
status: boolean;
statusCode: number;
message: string;
data: T;
}
- 공용 타입 추가해준다. common.ts에서
export type CursorBasedResponse<T> = {
status: boolean;
statusCode: number;
message: string;
data: T;
nextCoursor: number;
hasNext: boolean;
}
export type PaginationDto = {
cursor?: number;
limit?: number;
search?: string;
order?: string;
}
- axios이므로 구조 분해 할당을 바로 해준다.
import { PaginationDto } from "../types/common";
import { axiosInstance } from "./axios";
export const getLpList = async (paginationDto: PaginationDto) => {
const { data } = await axiosInstance.get('/v1/lps', {
params: paginationDto,
})
}
이런 식으로 해주면 되는데, order 같은 경우는 2가지만 허용되니까 그 부분을 수정해준다.
-> enum 타입으로 바꿔서 진행. 물론, (order?: "asc" | "desc")도 가능.
- src/enums/common.ts로 따로 만들어준다.
enum PAGINATION_ORDER {
"asc" = "asc",
"desc" = "desc",
}
import { PaginationDto } from "../types/common";
import { axiosInstance } from "./axios";
export const getLpList = async (paginationDto: PaginationDto) => {
const { data } = await axiosInstance.get('/v1/lps', {
params: paginationDto,
})
return data;
}
- 이제 이 코드에 대한 response 타입 하나를 만들어준다. 타입 폴더에 lp.ts를 만들어준다.
import { CursorBasedResponse } from "./common";
export type Tag = {
id: number;
name: string;
}
export type Likes = {
id: number;
userId: number;
lpid: number;
}
export type ResponseLpListDto = CursorBasedResponse<{
data: {
id: number;
title: string;
content: string;
thumnail: string;
published: boolean;
authorId: number;
createdAt: Date;
updatedAt: Date;
tags: Tag[];
likes: Likes[];
}
}>
- 그 다음, apis/lp.ts 가서 promise로 response 연결을 해주면 된다.
export const getLpList = async (
paginationDto: PaginationDto,
): Promise<ResponseLpListDto> => {
const { data } = await axiosInstance.get('/v1/lps', {
params: paginationDto,
})
return data;
}
- 이렇게 해주면 되는데, 원래는 useEffect를 써서 api를 호출 했는데 이제는 쿼리를 쓰면 그런 것들을 쉽게 처리할 수 있다.
- 그래서 보통은 hooks 폴더 안에 queries 폴더를 만들고, 그 내부에서 관리하는 편이다.
- 그리고 훅이므로 use로 시작한다. -> useGetLpList를 만들어준다.
- 이 파일은 홈페이지에 받아서 처리해 볼 것이다. 이전에는 useCustomFetch를 사용했다면, url를 넣고, isPending, isError, data 이런 식으로 적어서 처리를 해줬을 것이다. 이것을 앞선 과정에서 배우면서 좋다고 생각했을 텐데(굳이 useEffect를 몇 페이지마다 안 만들어도 됐으니까..) -> 쿼리도 똑같다.
- 이것도 똑같이 useGetLpList를 호출하고 필요한 것들을 호출하면 된다. 그런 구조를 이제 만들어 볼 것이다.
쿼리 구조 설계하기
- 쿼리는 캐싱을 쿼리 키라는 것을 기반으로 한다. 그래서 이 쿼리 키 설정이 매우 중요하다.
- 그래서 쿼리 키를 만들어주고, queryFn도 만들어준다. 이 쿼리 펑션 같은 경우는 만약에 외부 인자 파라미터를 안 전달해줘도 된다면 queryFn: getLpList 이렇게만 적어서 처리해도 된다. dto를 주입하면 되니까.. 근데 외부에서 무너가 받아와야 하면 콜백으로 해줘야 한다.
- 쿼리 키 하면? 로컬 스토리지가 생각날 것이다. 로컬 스토리지 키도 따로 관리를 했었다.
- 쿼리 키 오타를 최대한 방지하기 위해서 -> constants에 key.ts에 쿼리 키 관련된 코드를 작성해준다.
- 만약에 키가 변경이 되서 임의의 다른 값이 들어가게 된다면, 그것을 사용하는 모든 곳에서 다 일일이 추가를 해줘야 한다. 그럴 경우 엄청 오래 걸리기 때문에 번거로워도 쿼리 관련 파일을 훅으로 빼서 관리하는 것이다. 따로 뺀 파일에서만 변경을 하주면 되니까.
- 그리고 캐싱이 됐는지 여부는 뒤로가기 버튼을 누르거나 하는 과정에서 네트워크 창을 보면 또 다시 안 불러오고 있다는 걸 알 수 있다.
- 그리고 캐싱 시간도 설정을 해줄 수 있다. 쿼리 안에서 쓸 수 있는 속성이 엄청 많은데 대표적으로, staleTime: 1000*60*5 (5분), gcTime: 1000*60*10 (10분) 이렇게 설정을 하는데
- staleTime이라는 것은 쿼리 데브툴에서 확인할 수 있는데 이 시간은 "데이터가 신선하다고 간주하는 시간"이다. 한 마디로, 이 시간 동안은 캐시된 데이터를 그대로 사용한다는 것을 의미한다. (마운트 되거나 포커스 들어오는 경우도 재요청 x.)
- gcTime은 가비지 컬렉션 타임이라는 것으로 사용되지 않는 (비활성) 상태인 쿼리 데이터가 캐시에 남아있는 시간을 의미한다. 예를 들어, 10분 동안 사용되지 않으면 해당 캐시 데이터가 삭제되어, 다시 요청 시 새 데이터를 받아오게 한다.
- 보통 자주 사용하는 데이터들은 staleTime을 좀 길게 해서 재요청 빈도를 줄이고, gcTime을 적절히 조절해서 메모리 낭비를 방지를 한다.
- 예를 들어서, 네트워크 요청 비용이 크거나 좀 느린 환경에서는 staleTime을 늘리는 것이 좋다. 그리고 gcTime을 적절히 조절해서 처리를 한다.
- 트레이드오프 관계라서, 앱 전체에 메모리 사용량을 조금 고려해서 gcTime을 늘리거나 짧게 하거나 조절해서 메모리 사용량을 최적화시킬 수 있다. 그래서 이 부분은 잘 고려해서 처리하면 된다.
- 그 다음, 많이 쓰는 속성으로는 enabled 라는 것이 있는데, 이는 조건에 따라 쿼리의 실행 여부를 제어하는 것이다. enabled: false면 어떤 상황이라도 useGetLpList가 동작하지 않는다. 이를 true로 하면 정상적으로 동작한다.
- 랭킹 시스템을 1분에 한번씩 자동으로 캐싱이 되게 해주고 싶다? refetchInterval이라는 것이 있는데 여러 개의 데이터를 자주 fetching해야 하는 요소가 있다면 이런 식으로 시간을 지정해서 refetching 시켜줄 수 있다. -> 쿼리의 장점 중 하나.
- 그 다음은 쿼리 요청이 실패했을 때 할 수 있는 것으로 retry라는 속성이 있다. 이는 쿼리 요청이 실패했을 때 자동으로 재시도할 횟수를 지정한다. 그리고 기본값은 3회 정도, 네트워크 오류 등의 임시적인 문제를 보완할 수 있다. 데모데이나 이런 것들을 준비할 때 어떻게 처리하는 게 좋냐면, QueryClient에 default로 넣어줄 수 있다. defaultOption에 retry 추가해주면 된다. -> 전역적인 모든 쿼리 요청에 설정을 해준 것.
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 3,
},
},
});
- initialData는 쿼리 실행 전 미리 제공할 초기 데이터를 설정한다. 컴포넌트가 렌더링 될 때 빈 데이터 구조를 미리 제공해서, 로딩 전에도 안전하게 UI를 구성할 수 있게 해주는 것이다.
- keepPreviousData는 페이지네이션 같은 경우는 만약 1페이지, 2페이지 눌렀을 때 파라미터가 변경이 되니까 이전 데이터를 유지하지 못 하는 현상에서 유용하다고 볼 수 있다. (ex. 처음에 불러온 10개의 데이터가 사라짐.) 이것을 쓰면 좋은 점은 파라미터가 변경될 때 이전 데이터를 유지하여 UI 깜빡임을 즉, Flicking을 줄여준다. ex) 페이지네이션 시 페이지 전환 사이에 이전 데이터를 보여주어 사용자 경험을 향상시킨다.
- 스웨거에서의 data가 하나가 더 있는 상황 -> select 옵셥 사용.
- 위 사진에서 가장 처음에 불러오는 data인데 select 옵션으로 구조를 바꿔 줄 수 있다. 그래서 데이터를 받아서 아래와 같이 적어주면 된다.
select: (data) => data.data,
- 이렇게 하면 다른 페이지에서 불렀을 때 기존에는 data?.data.data.map~~ 이런 식으로 불러왔던 걸 data?.data, select에 한번더 data.data.data를 쓰면 data?.map으로 처리가 가능하다. 이럴 식으로 위 사진의 저런 괴랄한 예제를 풀어내는 것을 할 수 있다.
import { useQuery } from "@tanstack/react-query";
import { PaginationDto } from "../../types/common";
import { getLpList } from "../../apis/lp";
import { QUERY_KEY } from "../../constants/key";
function useGetLpList({cursor, search, order, limit}: PaginationDto) {
return useQuery({
queryKey:[QUERY_KEY.lps, search, order],
queryFn: () =>
getLpList({
cursor,
search,
order,
limit,
}),
// 데이터가 신선하다고 간주하는 시간.
// 이 시간 동안은 캐시된 데이터를 그대로 사용합니다. 컴포넌트가 마운트 되거나 창에 포커스 들어오는 경우도 재요청 X.
// 5분 동안 기존 데이터를 그대로 활용해서 네트워크 요청을 줄인다.
staleTime: 1000 * 60 * 5, // 5분
// Garbage Collection Time - 사용되지 않는 (비활성) 상태인 쿼리 데이터가 캐시에 남아있는 시간.
// staleTime이 지나고 데이터가 신선하지 않더라도, 일정 시간 동안 메모리에 보관.
// 그 이후에 해당 쿼리가 전혀 사용되지 않으면 gcTime이 지난 후에 제거한다.-> 가비지 컬렉션
gcTime: 1000 * 60 * 10, // 10분
// 조건에 따라 쿼리의 실행 여부를 제어
//enabled: Boolean(search)
//refetchInterval: 100 * 60,
// retry: 쿼리 요청이 실패했을 때 자동으로 재시도할 횟수를 지정한다.
// 기본값은 3회 정도, 네트워크 오류 등의 임시적인 문제를 보완할 수 있다.
//retry: 3,
// initialData: 쿼리 실행 전 미리 제공할 초기 데이터를 설정한다.
// 컴포넌트가 렌더링 될 때 빈 데이터 구조를 미리 제공해서, 로딩 전에도 안전하게 UI를 구성할 수 있게 해주는 것이다.
//initialData: initailLpListData,
// 파라미터가 변경될 때 이전 데이터를 유지하여 UI 깜빡임을 즉, Flicking을 줄여준다.
// ex) 페이지네이션 시 페이지 전환 사이에 이전 데이터를 보여주어 사용자 경험을 향상시킨다.
// keepPreviousData: true,
select: (data) => data.data.data,
});
}
export default useGetLpList;
{data?.map((lp) => <h1>{lp.title}</h1>)}
'UMC 8th Web 워크북' 카테고리의 다른 글
💊useMutation으로 서버 상태관리 쉽게 (0) | 2025.05.14 |
---|---|
♾️무한스크롤 + 스켈레톤 UI (4) | 2025.05.09 |
🎙️Social Login (0) | 2025.05.02 |
💊리프레쉬 토큰 (0) | 2025.05.01 |
🗺️ProtectedRoute (UMC 5주차 강의 참고.) (0) | 2025.05.01 |