치춘짱베리굿나이스
tanstack-query 쿼리 키 관리 본문
매직 리터럴 이제 안녕
💩
리얼월드 in Vue (뷰얼월드…) 프로젝트를 수행하면서 Tanstack Query 를 도입했는데, 규모가 그다지 크지 않은 프로젝트를 수행했을 때의 습관이 남아서 (+ 데이터 캐싱 용도로만 TQ를 대충 사용했어서… 😕) 쿼리 키를 전부 제각각인 문자열로 다뤘더니 혼돈의 도가니탕이 되었다
쿼리 키를 전부 하드코딩한 탓에 어떤 상황에서 어떤 쿼리 키를 사용해야 하는지 혼란스러워진 것;;;
추후 좋아요 버튼에 Optimistic Update 를 적용하고 싶은데, 이를 위해서 사전 작업을 할 필요도 좀 느끼게 됐다
따라서 이번 시간에는 쿼리 키를 객체로 적절하게 다뤄보는 연습을 할 것이다…
기존 쿼리 키 관리 코드
export function useGetArticles(page: Ref<number>, params?: Params) {
return useQuery<ArticlesResponse>(
['articles', page],
() => articles.get({ ...params, offset: page.value * 10 - 10, limit: 10 }),
{
cacheTime: CACHE_TIME,
staleTime: STALE_TIME,
},
);
}
export function useGetArticle(slug: string | string[]) {
if (Array.isArray(slug)) slug = slug.join('');
return useQuery<ArticleData>(['article', slug], () => articles.getBySlug(slug as string), {
cacheTime: CACHE_TIME,
staleTime: STALE_TIME,
});
}
게시물 부분의 쿼리 키는 크게 두 가지로 구분되는데, 첫 번째는 게시물 리스트를 페이지로 구분한 쿼리이고, 두 번째는 게시물 각각을 slug로 구분한 쿼리이다
매직 리터럴로 모든 것을 작성하다 보니 나중에 useMutate
에서 쿼리 cancel, invalidate 등을 시도할 때 스스로가 헷갈리는 문제가 생겼다
이 부분을 상수화해서 한 곳에서 관리할 수 있도록 해 보자
쿼리 키
Tanstack Query에 엄청나게 많은 기여를 하신 tkdodo씨의 블로그 글을 적절히 번역해서 내가 읽어볼 만한 내용만 편집하였다…
쿼리 키는 Tanstack Query 에서 매우 중요한 컨셉 중 하나이다
Tanstack Query가 내부적으로 데이터를 적절하게 캐싱하고, 디펜던시에 변화가 생겼을 때 자동적으로 데이터를 refetch할 수 있도록 도움을 주며, 쿼리 캐시에 직접적으로 접근할 때에도 필요하다
tkdodo 씨가 어떻게 쿼리 키를 관리하는지 한번 엿보도록 하자
Deterministic way로 저장되는 쿼리 키
useQuery(['articles', { page, filter }], ...);
useQuery(['articles', { filter, page }], ...);
내부적으로 쿼리 캐시는 자바스크립트 객체로 관리되는 반면, 쿼리 키는 deterministic way
로 해시화되어 저장된다
deterministic way
라 함은, 위의 예시에서 객체 내부의 값의 순서가 바뀌어도 동일한 키로 취급된다는 뜻이다
배열의 맨 앞 값 (top-level 값) 만 문자열로 잘 넣어준다면, 뒤에는 객체가 들어와도 상관없다
useQuery(['articles', 'global', 15], ...);
useQuery(['articles', 15, 'global'], ...);
한편, 배열의 값의 순서는 매우 중요하다
배열의 값의 순서가 달라질 경우 다른 키로 취급됨을 주의하자
쿼리 키의 중요한 점은, 서로 다른 쿼리에 대해 서로 다른 쿼리 키를 가져야 한다는 것이다
useQuery
와 useInfiniteQuery
의 쿼리 키 또한 다르게 작성하는 것이 옳다
배열 키를 사용하자
useQuery('articles', ...);
물론 문자열 쿼리 키도 사용할 수는 있지만, 이거 어차피 내부적으로 [’articles’]
로 변환된다고 한다
또한 Tanstack Query v4부터는 어차피 배열로만 사용하게끔 강제한다
쿼리 키 구조
useQuery(['articles', 'global', 'liked', 'chichoon', 5], ...);
// articles (게시물)
// global (전체 게시물 목록 중)
// liked (좋아요가 눌린)
// chichoon (chichoon에 의해)
// 5 (페이지는 5)
쿼리 키 내부 요소는 가급적 Generic (일반적인) 한 것부터 Specific (특정) 한 것 순으로 배치하자
이렇게 하면 쿼리 키에 구조가 잡히기 때문에 mutate 등의 이벤트가 일어났을 때 특정 키에 해당하는 모든 쿼리를 invalidate하기 조금 쉬워진다
예를 들면,
articles
쿼리를 invalidate하면 쿼리 키에articles
가 포함된 모든 쿼리들을 invalidate시킬 수 있다chichoon
쿼리를 invalidate 하면articles
,global
,liked
한 쿼리들 중chichoon
쿼리들만 invalidate할 수 있다
이런 식으로 invalidate 하고자 하는 쿼리의 범위를 조절하기 간편하다
쿼리 키 팩토리 구성하기
useQuery(['articles', 'global', 'liked', 'chichoon', 5], ...);
위의 예시들은 대부분 쿼리 키를 하나하나 선언하는 식으로 (매직-리터럴 하게) 구성하였는데, 이는 에러에 취약할 뿐더러 나중에 쿼리 키를 고치기도 어렵게 만든다
위의 예시에서, 만약 global
과 liked
사이에 visible
이라는 쿼리 키 단계를 추가하고 싶다면? 프로젝트 크기가 작으면 금방 고치겠지만, 크기가 매우 크고 복잡할 경우 하나하나 고치는 것도 아주 일일 것이다
이를 손쉽게 하기 위해 tkdodo 씨는 단일 쿼리 키 팩토리를 구성하는 것을 추천한다 - 쿼리 키 팩토리란? 쿼리 키를 반환하는 함수와 값으로 이루어진 객체이다
const articleKeys = {
all: ['articles'],
global: () => [...articleKeys.all, 'global'],
local: () => [...articleKeys.all, 'local'],
filtered: {
liked: (by: string, page: number) => [...articleKeys.global(), 'liked', by, page],
author: (by: string, page: number) => [...articleKeys.global(), 'author', by, page]
},
}
// ['articles', 'global', 'liked', 'chichoon', 5] 쿼리 키는
// articleKeys.filtered.liked('chichoon', 5) 와 같이 구성이 가능하다
위의 쿼리 키를 articleKeys
팩토리로 간단하게 구성해 보았다
어떤 쿼리가 어떤 쿼리에 의존적인지 쉽게 확인할 수 있고, 확장에 매우 용이하면서, 각 쿼리 키에 독자적으로 접근이 가능하다
쿼리 키 팩토리 객체를 만들어보기
쿼리 구분하기
리얼월드 프로젝트에서 게시물을 받아오는 케이스는 다음과 같다
게시물 목록을 받아오는 경우
- 페이지 변수를 받아 모든 게시물 목록을 받아오는 경우 (global)
- 페이지 변수를 이용하여 offset 을 계산한다
- 모든 게시물 목록 중 페이지에 해당하는 게시물 10개를 응답받는다
- 페이지 변수를 받아 내 피드 게시물 목록을 받아오는 경우 (feed)
- 페이지 변수를 이용하여 offset을 계산한다
- 내가 구독하는 작성자들의 게시물 목록 중 페이지에 해당하는 게시물 10개를 응답받는다
- 페이지 변수와 특정 값을 받아 게시물 목록을 필터링해서 받아오는 경우 (filtered)
- 필터링 경우의 수는 3가지가 있다: tag (태그), author (작성자), favorited (좋아요한 주체)
- 필터링된 게시물 목록 중 페이지에 해당하는 게시물 10개를 응답받는다
개별 게시물 객체를 받아오는 경우
- slug로 게시물을 판별하여 받아오는 경우 (slug)
- slug 를 인자로 받아, 이를 이용하여 게시물을 판별한다
- 단 하나의 게시물 객체를 응답받는다
쿼리 계층 구조 고민해보기
게시물 목록을 받아오는 경우
articles
: 가장 일반적인, 게시물 목록 전체에 대한 쿼리global
/feed
: 모든 게시물 목록 / 내 피드의 게시물 목록에 대한 쿼리articles
로부터 분기한다
favorited
/author
/tagged
: 필터링 범주- 게시물 전체에서 필터링해 오므로,
global
로부터 분기한다
- 게시물 전체에서 필터링해 오므로,
username
(변수) /tag
(변수) : 필터 값username
은favorited
/author
에서 분기한다tag
는tagged
에서 분기한다
page
: 페이지 번호- 모든 게시물 목록에 대해, 페이지별로 게시물을 가져오게끔 하는 키
- 가장 specific한 쿼리라고 볼 수 있다
개별 게시물 객체를 받아오는 경우
article
: 가장 일반적인, 임의의 게시물 하나에 대한 쿼리slug
(변수) : 게시물 구분 값- 특정 게시물을 구분하기 위한 키
- 가장 specific한 쿼리라고 볼 수 있다
쿼리 팩토리 만들어보기
export const articleKeys = {
// 게시글 목록에 관한 쿼리
lists: {
all: ['articles'] as const, // 전체 게시물 목록에 대한 쿼리
feed: {
all: () => [...articleKeys.lists.all, 'feeds'],
paged: (page: number) => [...articleKeys.lists.all, 'feeds', page] as const,
},
global: {
all: () => [...articleKeys.lists.all, 'global'],
paged: (page: number) => [...articleKeys.lists.all, 'global', page] as const,
},
filtered: {
favorited: (by: string, page: number) => [...articleKeys.lists.global.all(), 'favorited', by, page] as const, // 좋아요한 게시물 목록에 대한 쿼리
author: (by: string, page: number) => [...articleKeys.lists.global.all(), 'author', by, page] as const, // 작성자로 필터링한 게시물 목록에 대한 쿼리
tagged: (tag: string, page: number) => [...articleKeys.lists.global.all(), 'tagged', tag, page] as const, // 태그로 필터링한 게시물 목록에 대한 쿼리
},
},
// 게시물 각각에 관한 쿼리
article: {
only: ['article'] as const,
slug: (slug: string) => [...articleKeys.article.only, slug] as const, // 게시물에 대한 쿼리
},
};
계층 구조는 배열의 앞에 오는 값, 또는 객체의 구조로 구분할 수 있다
쿼리 키를 사용하고 싶을 때, articleKeys
객체를 이용하여 간단하게 특정 쿼리 키를 가져올 수 있게 되었다
또한 변경이 필요할 때도 중구난방으로 흩어진 쿼리 키들을 하나하나 변경할 필요 없이 단일 객체에서 수정하면 된다 (와〰️)
쿼리 팩토리 사용하기
export function useGetArticles(page: Ref<number>, params?: Params) {
return useQuery<ArticlesResponse>(
articleKeys.lists.global.paged(page.value), // 수정된 부분
() => articles.get({ ...params, offset: page.value * 10 - 10, limit: 10 }),
{
cacheTime: CACHE_TIME,
staleTime: STALE_TIME,
},
);
}
export function useGetArticle(slug: string | string[]) {
if (Array.isArray(slug)) slug = slug.join('');
return useQuery<ArticleData>(
articleKeys.article.slug(slug), // 수정된 부분
() => articles.getBySlug(slug as string),
{
cacheTime: CACHE_TIME,
staleTime: STALE_TIME,
}
);
}
팩토리를 사용할 때는, 원하는 쿼리 키를 불러오기 위해 객체의 함수를 호출하거나 변수를 호출하기만 하면 된다
매우 간단하지 않을 수 없다
참고 자료
'ClientSide > 라이브러리' 카테고리의 다른 글
tanstack-query Optimistic Update 적용하기 (0) | 2023.10.22 |
---|---|
Husky 사용해보기 (+ lint-staged) (0) | 2023.09.12 |
React 컴포넌트 또는 HTML 문서 일부를 pdf로 내보내기 (1) | 2023.08.17 |
Socket.io로 간단한 소켓 통신 (0) | 2023.07.22 |
Storybook, sass 붙이기 + 전역변수 사용하기 (0) | 2023.06.28 |