주요 포인트 한눈에 보기
TanStack Query에서 queryKey는 단순한 캐시 이름이 아니라, 서버 데이터를 계층적으로 분류하기 위한 구조적 기준입니다. 이 글에서는 queryKey를 배열로 설계해야 하는 이유를 사용법이 아닌 구조·설계 관점에서 정리합니다.
- TanStack Query란 무엇인가
- queryKey란 무엇인가
- queryKey를 단순 식별자로 사용할 때의 문제
- 왜 배열 queryKey를 사용하는가
- 배열 queryKey로 가능한 동작들
- queryKey 설계를 코드로 관리하는 패턴
- 정리
- FAQ
TanStack Query란 무엇인가
TanStack Query는 서버에서 가져온 데이터를 단순한 응답 값이 아니라 상태로 관리되는 데이터로 취급하는 라이브러리입니다. 언제 데이터를 요청하고, 언제 재사용하며, 언제 다시 가져올지를 컴포넌트가 아닌 캐시 계층에서 결정하도록 설계되어 있습니다.
useQuery({
queryKey: ['products'],
queryFn: fetchProducts,
});
이때 서버 데이터는 컴포넌트 내부 상태에 저장되지 않고 queryKey를 기준으로 전역 캐시에 저장됩니다. 즉, queryKey는 서버 상태를 구분하는 기준점 역할을 합니다.
queryKey란 무엇인가
queryKey는 TanStack Query가 서버 데이터를 식별하고 캐시에 저장할 때 사용하는 기준값입니다. 많은 경우 이를 단순한 문자열 키로만 사용하게 되지만, 이는 구조적인 의도를 충분히 활용하지 못한 방식입니다.
queryKey: ['cart']
queryKey: ['cart-list']
queryKey: ['cart-lists']
위 예시는 queryKey를 모두 문자열 하나로만 설계한 경우입니다. 겉보기에는 각각 다른 데이터를 가리키는 것처럼 보이지만, 실제로는 이름만 다를 뿐 구조적인 차이는 전혀 없는 상태입니다.
문자열 네이밍으로 의미를 구분하려고 하면 캐시 간의 관계나 범위를 코드 수준에서 표현할 수 없게 됩니다. 이 방식은 데이터가 늘어날수록 TanStack Query 입장에서 캐시를 구조적으로 판단하기 어렵게 만듭니다.
queryKey를 단순 식별자로 사용할 때의 문제
queryKey를 단일 값으로만 사용하면 서로 다른 조건의 서버 데이터가 모두 같은 레벨에서 관리됩니다. 이 구조에서는 데이터 간의 관계나 범위를 표현할 수 없기 때문에, 특정 사용자나 조건에 해당하는 캐시만을 선택적으로 제어하기가 어려워집니다.
queryKey: ['cart']
queryKey: ['cart']
queryKey: ['cart']
위와 같이 설계하면 사용자별 장바구니, 상태가 다른 목록 데이터가 모두 동일한 캐시 키로 취급됩니다. 이 경우 캐시는 “장바구니”라는 이름으로만 존재할 뿐, 누가, 어떤 조건의 데이터인지 구분할 수 없게 됩니다.
이 구조에서는 TanStack Query가 “어디까지가 같은 데이터 범주인지”를 코드로 판단할 수 없게 됩니다.
올바른 접근은 queryKey를 데이터의 구조를 드러내는 기준으로 설계하는 것입니다. 즉, 하나의 이름이 아니라 도메인과 조건을 분리해 표현해야 합니다.
queryKey: ['cart']
queryKey: ['cart', 'lists']
queryKey: ['cart', 'list', id]
이처럼 배열 queryKey를 사용하면 모든 장바구니 데이터는 ['cart']라는 공통 범주 아래에 놓이고, 그 하위에서 목록, 개별 항목 데이터가 자연스럽게 구분됩니다.
왜 배열 queryKey를 사용하는가
배열 형태의 queryKey는 데이터를 하나의 이름이 아니라 의미 단위로 분해한 구조로 표현합니다. 각 요소는 도메인, 조건, 식별자 역할을 나눠 담당합니다.
이 배열 구조 덕분에 TanStack Query는 캐시를 단순 비교가 아닌 부분 일치 기반 계층 탐색으로 다룰 수 있습니다.
queryKey: ['cart', userId]
위 구조는 단순히 고유한 값을 만들기 위한 것이 아니라, 이후 캐시를 범위 단위로 관리하기 위한 전제 조건입니다. TanStack Query의 캐시 제어 API는 이 배열 구조를 기준으로 동작합니다.
배열 queryKey로 가능한 동작들
배열 queryKey를 사용하면 전체 캐시가 아닌 특정 범위의 캐시만 무효화할 수 있습니다.
queryClient.invalidateQueries({
queryKey: ['cart'],
});
TanStack Query는 배열 queryKey를 하나의 문자열이 아니라 앞에서부터 비교 가능한 구조로 저장하기 때문에, 부분 일치 기반 무효화가 가능합니다.
위 코드는 ['cart']로 시작하는 모든 캐시를 대상으로 하며, 사용자별로 분리된 여러 장바구니 캐시를 한 번에 stale 상태로 만들 수 있습니다.
배열 queryKey의 장점은 무효화 범위를 단계적으로 좁혀가며 제어할 수 있다는 점입니다.
queryClient.invalidateQueries({
queryKey: ['cart', userId],
});
위 예시는 특정 사용자에 해당하는 장바구니 캐시만 무효화합니다. 다른 사용자의 장바구니 캐시는 그대로 유지되며, 계층을 한 단계 더 내려서 캐시를 제어하는 방식입니다.
queryClient.invalidateQueries({
queryKey: ['cart', userId],
exact: true,
});
exact: true 옵션을 사용하면 ['cart', userId]와 정확히 일치하는 캐시만 stale 처리되며, ['cart', userId, 'items']와 같은 하위 키는 제외됩니다. 배열 구조와 옵션 조합을 통해 캐시 제어 범위를 더욱 정밀하게 설정할 수 있습니다.
이러한 배열 기반 제어 방식은 무효화(invalidate)뿐 아니라 제거(remove), 리셋(reset), 조회에도 동일하게 적용됩니다.
queryClient.removeQueries({ queryKey: ['cart'] });
queryClient.resetQueries({ queryKey: ['cart', userId] });
즉, 캐시를 어떻게 무효화할지뿐 아니라 언제 제거하고, 언제 초기화하며, 어떤 캐시를 대상으로 조회할지까지 모두 같은 배열 queryKey 구조를 기준으로 동작합니다.
TanStack Query Devtools에서 캐시가 계층 구조로 표시되는 이유 역시 queryKey를 배열 기반 구조로 취급하기 때문입니다.
cart
├─ userId: 1
├─ userId: 2
Devtools는 내부 캐시를 배열 queryKey의 계층 구조에 따라 탐색해 보여주며, 이 구조 덕분에 복잡한 캐시 상태도 시각적으로 파악할 수 있습니다.
queryKey 설계를 코드로 관리하는 패턴
배열 queryKey를 일관되게 사용하려면, 각 화면에서 직접 배열을 작성하기보다 공통 규칙을 코드로 묶어 관리하는 방식이 효과적입니다. 이를 흔히 queryKey factory 패턴이라고 부릅니다.
const cartKeys = {
all: ['cart'] as const,
lists: () => [...cartKeys.all, 'lists'] as const,
list: (userId: string) => [...cartKeys.all, 'list', userId] as const,
};
이 패턴을 사용하면 ['cart']라는 도메인 기준을 한 곳에서 정의하고, 그 하위 구조를 함수 형태로 파생시킬 수 있습니다. 그 결과 queryKey의 계층 구조가 코드 레벨에서 고정되고, 오타나 순서 불일치로 인한 캐시 오류를 방지할 수 있습니다.
또한 invalidateQueries(cartKeys.all)처럼 상위 범주를 기준으로 캐시를 제어할 수 있어, 배열 queryKey가 의도한 계층적 설계를 가장 안정적으로 유지할 수 있습니다.
정리
queryKey를 배열로 설계하는 이유는 단순한 스타일 가이드가 아니라, TanStack Query의 캐시 모델 자체가 구조적 키 설계를 전제로 만들어졌기 때문입니다. 배열 queryKey는 선택이 아니라 캐시 제어를 위한 기본 단위에 가깝습니다.
FAQ
Q. queryKey를 이렇게까지 구조적으로 나눌 필요가 있나요?
초반에는 차이가 크지 않지만, 데이터 종류와 조건이 늘어날수록 캐시 무효화와 재사용 전략에서 구조 설계 여부가 큰 차이를 만듭니다.
Q. queryKey는 문자열로 쓰면 안 되나요?
사용할 수는 있지만, 데이터 구조가 확장될수록 범위 단위 캐시 제어가 어려워집니다.
Q. queryKey 배열의 순서는 중요한가요?
중요합니다. 배열의 순서는 캐시 계층 구조와 일치해야 합니다.
Q. queryKey에 객체를 넣어도 되나요?
가능하지만, 항상 동일한 직렬화 결과를 보장해야 합니다.
Q. 배열 queryKey가 길어지면 성능에 영향은 없나요?
queryKey 비교는 문자열을 합쳐서 비교하는 방식이 아니라, 내부적으로 구조화된 키를 기준으로 관리됩니다. 일반적인 사용 범위에서는 배열 길이가 성능 병목이 되는 경우는 거의 없습니다.
Q. invalidateQueries는 캐시를 삭제하나요?
캐시를 즉시 삭제하지 않고 stale 상태로 만들어 재요청을 유도합니다.
테스트 입니다.