지난 프로젝트에서 TanstackQuery의 캐싱를 사용하며 "TanstackQuery를 왜 사용해야 하나" 라고 든 생각이 계속해서 머리속에 남아 내 생각을 정리하고자 이 글을 쓰기로 했다.
Tanstack Query가 뭔데?
Tanstack Query(구 React-Query)는 Tanstack에서 출시한 강력한 비동기 상태 관리를 지원하는 도구이다.
상태란?
애플리케이션에 저장된 문자열, 배열, 객체 등의 다양한 형태의 데이터이며,
페이지가 렌더링된 이후 사용자 상호작용의 모든 동작과 결과를 의미한다.
그럼 왜 사용하는데?
React에서 로짓은 위의 사진과 같이 동작한다.
View 컴포넌트를 화면에 표현하여(렌더링) 사용자에게 보여주고, Action 상호작용으로 사용자와의 상호작용을 구현, 이렇게 받은 상호작용을 State 상태로 받아 이를 다시 View 컴포넌트에 적용 및 렌더링해서 사용자에게 제공한다.
이러한 싸이클을 관리하는 것이 상태관리(State Management) 라고 한다.
기존의 상태관리 라이브러리
추가적으로 기존에 상태관리 라이브러리들을 살펴보자.
이 중 필자가 많이 사용한 Redux도 보인다.
이러한 잘 알려진 상태관리 고수들이 있는데 굳이 Tanstack을 사용해야 하는 이유는 무엇일까?
공식 문서에서 위와 같이 이야기 하고있다. 기존의 상태관리 라이브러리로는 비동기 혹은 서버 상태 관리는 적절하지 않다. 이는 Tanstack Query를 만들게된 계기라고 이야기한다.
서버 상태 관리란?
1. 클라이언트 이외의 곳에서 제어하거나 소유하지 않은 위치에서 관리된다.
2. fetching과 update를 위해 비동기 API가 필요하다.
3. 소유권이 공유되므로 사용자 이외의 제 3자로 인해 데이터가 변경될 수 있다.
4. 주의하지 않으면 애플리케이션의 데이터가 "구식 out of date"가 될 수 있다.
위의 4가지 특징을 지닌 상태를 관리하는 것이다.
기존 상태 관리 라이브러리를 사용하면 아래와 같은 문제 혹은 고난을 맞이할 수 있지만, TanstackQuery를 이용해 해결할 수 있다.
- 상태의 캐싱(프론트엔드에서 어려운 문제중 하나)
- 같은 데이터 요청 중복 제거
- 상태가 언제 "out of date" 되는지 알아내기
- 데이터 백그라운드에서 "out of date"된 데이터 업데이트
- 최대한 빠르게 데이터 업데이트 반영하기
- pagination, lazy 로딩 등과 같은 성능 최적화
- 메모리, GC 관리
- Structural sharing으로 쿼리 결과 Memoizing
이 글에서는 캐싱에 중점을 두고 다뤄볼 예정이다.
캐싱
사실 내가 이번 프로젝트에서 TanstackQuery를 사용하는데에 큰 이유인 부분이다. 캐싱을 이용한 데이터의 out of date 관리, 메모리 및 GC 관리, Memoizing, 데이터 중복 요청 제거를 처리할 수 있다.
위의 목록 중 상태의 캐싱을 왜 사용하나?
위의 개념과 비슷하게 캐싱을 이용해 데이터(상태)를 임의의 장소에 저장한 뒤 관리하며, 중복된 데이터를 재사용하고 데이터가 Out Of Date되지 않도록 관리하기 위함이다
Javascript는 GC(가비지 컬렉션)을 이용해 메모리 최적화를 진행한다.
할당된 메모리가 더이상 필요 없어지면 해당 메모리가 해제되며 동작한다.
GC에서 메모리를 해제하는 트리거는 메모리에 할당된 값(혹은 메모리 주소)의 참조 여부이다.
QueryClient
TanstackQuery에서 QueryClient를 사용해 캐시와 상호작용할 수 있다.
import { QueryClient } from '@tanstack/react-query'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: Infinity,
},
},
})
await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: fetchPosts })
QueryCache
QueryClient에서 접근 가능한 TanstackQuery의 저장 메커니즘이며, 이곳에 저장된 모든 데이터, 메타 정보 및 쿼리 상태가 저장된다.
QueryCache와 직접 상호작용하지 않고 특정 캐시에 대해 QueryClient를 사용해 상호작용한다.
export class QueryCache extends Subscribable<QueryCacheListener> {
#queries: QueryStore
constructor(public config: QueryCacheConfig = {}) {
super()
this.#queries = new Map<string, Query>()
}
build<TQueryFnData, TError, TData, TQueryKey extends QueryKey>(
client: QueryClient,
options: QueryOptions<TQueryFnData, TError, TData, TQueryKey>,
state?: QueryState<TData, TError>,
): Query<TQueryFnData, TError, TData, TQueryKey> {
const queryKey = options.queryKey!
const queryHash =
options.queryHash ?? hashQueryKeyByOptions(queryKey, options)
let query = this.get<TQueryFnData, TError, TData, TQueryKey>(queryHash)
if (!query) {
query = new Query({
cache: this,
queryKey,
queryHash,
options: client.defaultQueryOptions(options),
state,
defaultOptions: client.getQueryDefaults(queryKey),
})
this.add(query)
}
return query
}
...
위의 코드를 확인해 보면 QueryStore 타입을 갖는 #queries에 <string, Query>형식의 Map 객체가 할당된다. 여기서 우리는 useQuery나 useQueries에서 얻어노는 Query 객체를 queryKey를 통해 Map객체에서 관리하는 것을 알 수 있다.
아래의 코드에서 확인해 보도록 하자.
build<TQueryFnData, TError, TData, TQueryKey extends QueryKey>(
client: QueryClient,
options: QueryOptions<TQueryFnData, TError, TData, TQueryKey>,
state?: QueryState<TData, TError>,
): Query<TQueryFnData, TError, TData, TQueryKey> {
const queryKey = options.queryKey!
const queryHash =
options.queryHash ?? hashQueryKeyByOptions(queryKey, options)
let query = this.get<TQueryFnData, TError, TData, TQueryKey>(queryHash)
if (!query) {
query = new Query({
cache: this,
queryKey,
queryHash,
options: client.defaultQueryOptions(options),
state,
defaultOptions: client.getQueryDefaults(queryKey),
})
this.add(query)
}
return query
}
해당 build 메서드에서는 우리가 앞서 설정했던 QueryClient( QueryClientProvider의 client에 전달한 )로 간접적으로 캐시와 상호작용할 QueryClient를 인자로 전달하고, option 인자에는 아래의 네 가지 정보가 전달된다.
- TQueryFnData: useQuery로 실행하는 query Function의 실행 결과의 타입을 지정하는 타입
- TError: query funtion의 error 형식을 정하는 타입
- TData: 최종적으로 data에 담기는 값이며 이는 TQueryFnData에서 반환된 값과는 다를 수 있다.
- TQueryKey: useQuery의 queryKey의 타입을 명식적으로 지정해주는 타입
이렇게 받은 정보를 이용해 기존의 Query 객체가 없다면 Query 객체를 생성하게되는데, 동작 방식은 아래와 같다.
- 쿼리 키를 옵션에서 추출해 쿼리 해시를 계산하며, option.queryHash가 제공되면 그 값을 사용하지만 없으면 hashQueryKeyByOptions 함수에 queryKey와 options를 전달해 queryHash를 생성한다.
- 해당 QueryCache 객체에 방금 만들어낸 queryHash와 같은 해시가 있는지 get메서드를 이용해 확인하고 이미 있다면 반환해준다.
- 2번에서 같은 해시가 없다면, query에 새로운 queryHash를 만들어주는데 이때 새로 만들어진 queryHash를 이용해 query객체를 할당해 this.add(query)로 추가한 뒤 query를 반환해준다.
staleTime과 gcTime
실제 useQuery나 useQueries를 사용해 데이터를 받아올 때 할 수 있는 설정이다.
staleTime이란 쿼리 데이터가 상하기 까지의 시간을 의미하며, 쉽게 설명하자면 데이터의 유효기간이라 생각하면 된다. staleTime이 지나면 데이터가 만료됬다(상했다)고 판단해 즉시 다시 데이터를 가져온다.
gcTime은 가비지 컬렉션의 시간을 의미하며, TanstackQuery의 캐시에서의 유효기간을 의미한다. 캐시쿼리가 생성되고 나서부터 시간이 흐르며, 시간이 지났다면 캐시에서 데이터를 제거하므로 여러 쿼리로 인한 메모리 용량을 데이터를 메모리에서 해제하며 최적화할 수 있다.
const operatorImgData = useQueries({
queries: ePositions.map((position, idx) => ({
queryKey: [`image${idx}`, position],
queryFn: async () => getOperatorImgDataApi(position),
refetchOnWindowFocus: false, // 브라우저 창 포커스에 따른 자동 재요청 비활성화
gcTime: 1000 * 60 * 5, // query client에서 캐시된 데이터가 5분뒤에 삭제되도록 설정
staleTime: 1000 * 60 * 5, // 최초 호출 후 staleTime을 5분로로 설정
})),
combine: (result) => {
return ({
data: result.map(res => res.data),
isLoading: result.map(res => res.isLoading)
})
}
});
위와 같이 ms 단위로 값을 설정하면 Query Cache에 저장되는 데이터의 GC 시간과 stale Time을 설정할 수 있다.
combine 설정
combine 설정은 useQueries에서 받아오는 데이터의 형태를 지정해 주는 설정이며, callback 함수를 설정해주고 return 값으로 사용할 데이터만 지정해 객체에 담아 반환해준다.
정리
마지막으로 이 글을 끝내기 전에 두가지를 정리하고자 한다.
1. TanstackQuery를 사용하는 이유
React 애플리케이션에서 서버의 상태를 불러오고, 자동으로 캐싱하며, 데이터의 최신화 및 업데이트, 로딩 및 페칭 동작 여부를 비교적 쉽게 이용해 개발하기 위함이다.
2. TanstackQuery에서의 캐싱
캐시에 동일한 데이터의 존재 유무를 판단해 불필요한 요청을 줄일 수 있으며, 데이터 페치가 완료되기 전까지 캐시에 저장되있던 데이터를 이용해 UX를 개선할 수 있다.
또한 gcTime과 staleTime을 설정해 메모리 최적화 및 데이터의 fresh 상태 지속시간을 지정할 수 있다.
'FE 이모저모 공부' 카테고리의 다른 글
동기와 비동기 (Synchronous & Asynchronous) (1) | 2024.07.23 |
---|---|
HTML의 iframe이 무엇이고 왜 iframe의 접근을 금지하지? (0) | 2024.07.17 |
프론트엔드 개발자가 하는 업무 (0) | 2024.04.26 |
에자일 (Agile) 개발론 (0) | 2024.04.23 |
에러) jest syntaxerror: cannot use import statement outside a module (0) | 2024.03.05 |