2025년 08월 14일
Tanstack Query
프로젝트에서 Tanstack Query를 사용하면서 queryKey
를 문자열로 직접 관리할 때, 사소한 오타 등으로 인해 캐시가 공유되지 않거나 쿼리 무효화가 제대로 동작하지 않는 이슈를 겪었습니다. 그래서 사소하지만, 이에 대한 고민에 대해 글을 작성합니다.
Tanstack Query에서 queryKey
는 데이터 캐싱과 관리를 위한 중요한 식별자입니다. 이 키를 별도 규칙 없이 문자열로 사용하면 다음과 같은 문제들이 발생할 수 있습니다.
queryKey
사용 - 휴먼에러서로 다른 컴포넌트에서 동일한 데이터를 조회한다고 가정할 때, 개발자마다 다른 문자열을 queryKey
로 사용할 수 있습니다.
1// A 컴포넌트 2useQuery({ queryKey: ['posts', postId], ... }); 3 4// B 컴포넌트 5useQuery({ queryKey: ['post', postId], ... }); // 'posts'가 아닌 'post'
만약 API 명세가 변경되어 queryKey
일부를 변경해야 할 때, 문자열로 작성된 모든 키를 개발자가 직접 찾아 수정해야 합니다. 이 과정에서 수정이 하나라도 누락되면 해당 쿼리는 더이상 올바르게 무효화되거나 갱신되지 않는 버그로 이어질 수 있습니다.
언급한 문제들을 사전에 방지하기 위해 queryKey
의 핵심이 되는 문자열을 Enum
으로 정의하여 중앙에서 관리하는 방법을 적용할 수 있습니다.
queryKeys.ts
파일에 Enum 정의먼저 프로젝트에 queryKey
를 모아둘 파일을 생성하고 Enum
을 정의합니다.
1export enum QueryKey { 2 Posts = 'POSTS', 3 Users = 'USERS', 4 Products = 'PRODUCTS', 5}
예를 들어 USER
에 종속된 USER_DETAIL
, USER_PLAN
과 같은 하위 쿼리키들은 별도 객체로 묶어서 관리합니다.
단순히 Enum에 USER_DETAIL
을 추가하는 것만으로는 USER
와의 연관성을 표현하기도 어렵고 동적인 값 (예를 들면 userId)을 처리하기 애매할것입니다.
그리고, 만약 Enum에 UserDetail = 'USER_DETAIL'이라고만 정의하면, 이 키가 USER에 속해있다는 관계를 코드상으로 표현할 수 없고, 동적인 ID '123'을 결합하는 로직이 각 컴포넌트에 흩어지게 됩니다.
1export enum QueryKey { 2 Users = 'USERS', 3 // 다른 도메인들... 4 // Posts = 'POSTS', 5}
USERS와 관련된 모든 하위 키들을 생성하는 userKeys 객체를 만듭니다. USER_PLAN은 특정 유저(userId)의 상세 정보(detail)에 속한 하위 리소스로 모델링하는 것이 구조적으로 명확합니다.
1import { QueryKey } from './queryKeys'; 2 3export const userKeys = { 4 /** 5 * User 전체에 대한 기본 키 6 * 결과: ['USERS'] 7 */ 8 all: [QueryKey.Users] as const, 9 10 /** 11 * 필터링된 유저 리스트에 대한 키 12 * 결과: ['USERS', 'list', { role: 'admin' }] 13 */ 14 list: (filters: { role: string }) => 15 [...userKeys.all, 'list', filters] as const, 16 17 /** 18 * 특정 유저의 상세 정보 (USER_DETAIL) 19 * 결과: ['USERS', 'detail', 'user-123'] 20 */ 21 detail: (userId: string) => [...userKeys.all, 'detail', userId] as const, 22 23 /** 24 * 특정 유저의 요금제 정보 (USER_PLAN) 25 * 결과: ['USERS', 'detail', 'user-123', 'plan'] 26 */ 27 plan: (userId: string) => [...userKeys.detail(userId), 'plan'] as const, 28};
이제 useQuery나 useMutation을 사용할 때, 문자열 대신 정의한 Enum을 가져와 사용합니다.
1export const usePostsQuery = (postId: string) => { 2 return useQuery({ 3 // 문자열 'POSTS' 대신 Enum을 사용 4 queryKey: [QueryKey.Posts, postId], 5 queryFn: () => fetchPostById(postId), 6 }); 7};
이 방식의 가장 큰 장점이라고 생각되는 부분입니다. queryClient로 캐시를 직접 조작할 때 입니다.
1// 게시글 수정 후, 관련 목록을 갱신할 때 2const queryClient = useQueryClient(); 3 4// Enum을 사용하면 자동 완성의 도움을 받을 수 있고, 오타 가능성이 사라짐 5queryClient.invalidateQueries({ queryKey: [QueryKey.Posts] });
queryKey
Enum이 프로젝트의 모든 쿼리 키에 대한 유일한 참조가 됩니다. 키를 변경해야 할 경우, 이 Enum 파일 하나만 수정하면 이를 참조하는 모든 코드에 일괄적으로 반영됩니다.queryKey
를 작성할 때마다 문자열을 기억해낼 필요 없이 자동 완성 기능을 통해 정확하게 키를 입력할 수 있습니다.처음에는 queryKey를 문자열로 사용하는것이 간단해보였지만, 프로젝트 규모가 커지면서 모든 queryKey를 기억할 수도 없었습니다. Enum을 도입하는 과정 자체는 매우 간단했지만, 그 결과로 얻은 장점이 매우 많았습니다.
이번 경험을 통해서 라이브러리의 핵심 기능을 사용할 때 약간와 규칙과 추상화를 더하는 것만으로도 수많은 오류를 예방하고 코드 품질을 높일 수 있다는 경험을 얻게 된 것 같습니다.