
Query Key Factory 패턴은 queryKey를 일관성있게 관리하기 위해 queryKey를 한 곳에서 생성하고 관리하는 패턴을 말한다.
즉, API별로 query key를 생성하는 “공장(factory)”를 만들어 사용하는 구조다.
Tanstack Query의 공식 문서에서는 queryKey는 'serializable' 하고 데이터 별로 'unique'해야 한다고 설명하며, 대규모의 어플리케이션에서의 조직화를 위한 팁으로 Query Key Factory 패키지를 참고하라고 안내한다.
cf. https://github.com/lukemorales/query-key-factory
// Key를 모아두는 폴더 하에 도메인 별로 파일을 관리
// 실무 프로젝트의 폴더 구조에 따라 Query Key Factory 폴더나 파일의 위치는 달라질 수 있음.
queryKeys/
products.ts
users.ts
// queryKeys/products.ts
export const productKeys = {
all: ['products'] as const,
list: (filters: { category?: string; sort?: string }) =>
[...productKeys.all, 'list', filters] as const,
detail: (id: string) =>
[...productKeys.all, 'detail', id] as const,
};
// 'products' 도메인 관련임을 명시하고(네임스페이스) 하위 api들을 통합 관리
// ...productKeys.all: 해당 도메인의 root key를 prefix로 재사용하게 해준다.
const filters = { category: 'shoes', sort: 'latest' };
useQuery({
queryKey: productKeys.list(filters),
queryFn: () => getProductList(filters),
});
이때, queryKey는 구조화되어 쉽게 사용하기 좋아졌지만, 결국 쿼리 호출 시 queryFn 및 기타 옵션들과 함께 사용되기 때문에 Query Key Factory와 실제 쿼리를 모두 유지보수해야하는 문제점이 생긴다.
이에 Tanstack Query 공식 문서에서도 기본 권장 패턴으로 'Query Options'를 언급하고 있다.
// https://tanstack.com/query/latest/docs/framework/react/guides/query-options
import { queryOptions } from '@tanstack/react-query'
function groupOptions(id: number) {
return queryOptions({
queryKey: ['groups', id],
queryFn: () => fetchGroups(id),
staleTime: 5 * 1000,
})
}
// usage:
useQuery(groupOptions(1))
useSuspenseQuery(groupOptions(5))
useQueries({
queries: [groupOptions(1), groupOptions(2)],
})
queryClient.prefetchQuery(groupOptions(23))
queryClient.setQueryData(groupOptions(42).queryKey, newGroups)
이렇게 queryOptions로 queryKey, queryFn, staleTime 등 쿼리의 모든 옵션을 하나의 객체로 묶어 관리하면 재사용이 편리해진다.
useQuery에서 쿼리 옵션을 매번 반복해서 작성할 필요가 없고, prefetchQuery, useInfiniteQuery, invalidateQueries 등 다른 쿼리 API에서 동일한 옵션 객체를 일관되게 사용할 수 있다.
또한 쿼리 정의를 실제 컴포넌트 밖으로 분리함으로써, 쿼리의 책임과 UI 로직 사이의 결합도를 낮추고 유지보수성을 향상시킬 수 있다.
즉, Query Key Factory 구조는 queryKey의 일관성을 높여주지만, 실제로 쿼리를 사용하는 시점에는 queryFn, staleTime, select 등 옵션을 함께 정의해야 한다.
결국 개발 시 Factory와 useQuery 옵션을 모두 관리해야 한다는 문제점을 보완하고자 'Query Factory'라는 구조가 등장했다.
다음은 Query Key Factory Package의 예시이다.
import { createQueryKeyStore } from "@lukemorales/query-key-factory";
// if you prefer to declare everything in one file
export const queries = createQueryKeyStore({
users: {
all: null,
detail: (userId: string) => ({
queryKey: [userId],
queryFn: () => api.getUser(userId),
}),
},
todos: {
detail: (todoId: string) => [todoId],
list: (filters: TodoFilters) => ({
queryKey: [{ filters }],
queryFn: (ctx) => api.getTodos({ filters, page: ctx.pageParam }),
contextQueries: {
search: (query: string, limit = 15) => ({
queryKey: [query, limit],
queryFn: (ctx) => api.getSearchTodos({
page: ctx.pageParam,
filters,
limit,
query,
}),
}),
},
}),
},
});
// Use throughout your codebase as the single source for writing the query keys, or even the complete queries for your cache management:
import { queries } from '../queries';
export function useUsers() {
return useQuery({
...queries.users.all,
queryFn: () => api.getUsers(),
});
};
export function useUserDetail(id: string) {
return useQuery(queries.users.detail(id));
};
이처럼 쿼리의 정의와 옵션은 Factory에서 모두 관리하고, 컴포넌트는 쿼리 호출만 할 수 있도록 모듈을 만드는 것이 'Query Factory' 구조이다.
해당 패키지를 사용하지 않더라도 간단히 Query Factory 구조를 실무에서 사용할 수 있다.
// product.queries.ts
export const productQueries = {
all: {
queryKey: ['product'] as const,
},
detail: (id: number) => ({
queryKey: ['product', 'detail', id] as const,
queryFn: () => api.product.getDetail(id),
staleTime: 60_000,
}),
list: (params: ProductListParams) => ({
queryKey: ['product', 'list', params] as const,
queryFn: () => api.product.getList(params),
}),
};
// 쿼리 호출 시,
const query = useQuery(productQueries.list({ category: "shoes" }));