캐시 키 비교
TanStack Query
// 명시적으로 캐시 키 지정
useQuery({
queryKey: ['campaigns', { status: 'open', page: 1 }],
queryFn: () => fetchCampaigns({ status: 'open', page: 1 })
})
// 캐시 구조
{
'["campaigns",{"status":"open","page":1}]': { data: [...], ... },
'["campaigns",{"status":"open","page":2}]': { data: [...], ... },
}
Apollo Client
// 캐시 키 자동 생성!
useQuery(GET_CAMPAIGNS, {
variables: { status: 'open', page: 1 }
})
// 캐시 구조 (자동 생성)
{
'Query': {
'campaigns({"status":"open","page":1})': [...],
'campaigns({"status":"open","page":2})': [...],
}
}
Apollo Client는 GraphQL 쿼리를 분석해서 자동으로 캐시 키를 만듭니다.
자동 캐시 키 생성 원리
1. 기본 규칙: 필드 이름 + 모든 인자
// GraphQL 쿼리
query GetCampaigns($status: String, $page: Int) {
campaigns(status: $status, page: $page) {
id
title
}
}
// 자동 생성되는 캐시 키
'campaigns({"status":"open","page":1})'
'campaigns({"status":"open","page":2})'
// → TanStack Query의 queryKey와 동일한 개념!
2. keyArgs로 캐시 키 제어
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
campaigns: {
keyArgs: ['status'], // page는 무시!
},
},
},
},
})
// 결과 캐시 키
'campaigns({"status":"open"})' // page 1이든 2든 같은 키!
// TanStack Query로 비유하면:
useQuery({
queryKey: ['campaigns', { status: 'open' }], // page 제외
// ...
})
즉, keyArgs는 "어떤 인자로 캐시를 구분할지" 선택.
실제 동작 비교
TanStack Query
// 쿼리 1
const query1 = useQuery({
queryKey: ['campaigns', { status: 'open', page: 1 }],
queryFn: fetchCampaigns
})
// 쿼리 2
const query2 = useQuery({
queryKey: ['campaigns', { status: 'open', page: 1 }], // 동일!
queryFn: fetchCampaigns
})
// → 쿼리 2는 캐시 hit! 네트워크 요청 안 함 ✅
Apollo Client (keyArgs 없을 때)
// 쿼리 1
const query1 = useQuery(GET_CAMPAIGNS, {
variables: { status: 'open', page: 1 }
})
// 캐시 키: 'campaigns({"status":"open","page":1})'
// 쿼리 2
const query2 = useQuery(GET_CAMPAIGNS, {
variables: { status: 'open', page: 1 } // 동일!
})
// 캐시 키: 'campaigns({"status":"open","page":1})'
// → 캐시 hit! 네트워크 요청 안 함 ✅
Apollo Client (keyArgs 있을 때)
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
campaigns: {
keyArgs: ['status'], // page 무시
},
},
},
},
})
// 쿼리 1
const query1 = useQuery(GET_CAMPAIGNS, {
variables: { status: 'open', page: 1 }
})
// 캐시 키: 'campaigns({"status":"open"})'
// 캐시 내용: [item1, item2, ..., item10]
// 쿼리 2
const query2 = useQuery(GET_CAMPAIGNS, {
variables: { status: 'open', page: 2 } // page만 다름
})
// 캐시 키: 'campaigns({"status":"open"})' // 같은 키!
// → 캐시 hit하지만, merge 함수가 실행됨!
// merge 함수
merge(existing, incoming, { args }) {
// existing: [item1, ..., item10] (page 1)
// incoming: [item11, ..., item20] (page 2)
// args: { status: 'open', page: 2 }
return [...existing, ...incoming] // 무한 스크롤!
// 결과: [item1, ..., item10, item11, ..., item20]
}
TanStack Query로 비유하면:
// 이런 느낌!
useInfiniteQuery({
queryKey: ['campaigns', { status: 'open' }], // page 제외
queryFn: ({ pageParam }) => fetchCampaigns(pageParam),
getNextPageParam: (lastPage) => lastPage.nextPage,
})
InMemory 저장 방식
TanStack Query
// 메모리 구조
const queryCache = {
queries: [
{
queryKey: ['campaigns', { status: 'open' }],
state: {
data: [...],
status: 'success',
fetchStatus: 'idle',
}
},
{
queryKey: ['campaign', { id: 'abc-123' }],
state: { data: {...}, ... }
}
]
}
Apollo Client
// 메모리 구조 (정규화됨!)
const cache = {
ROOT_QUERY: {
'campaigns({"status":"open"})': [
{ __ref: 'Campaign:1' },
{ __ref: 'Campaign:2' },
],
'campaign({"id":"abc-123"})': { __ref: 'Campaign:abc-123' }
},
'Campaign:1': {
__typename: 'Campaign',
id: '1',
title: 'Summer Sale',
status: 'open'
},
'Campaign:2': {
__typename: 'Campaign',
id: '2',
title: 'Winter Sale',
status: 'open'
},
'Campaign:abc-123': {
__typename: 'Campaign',
id: 'abc-123',
title: 'Spring Sale',
status: 'closed'
}
}
핵심 차이: Apollo Client는 객체를 정규화해서 참조로 저장!
정규화의 장점
TanStack Query (정규화 없음)
// 쿼리 1: 캠페인 목록
useQuery({
queryKey: ['campaigns'],
queryFn: () => [
{ id: '1', title: 'Summer Sale', status: 'open' },
{ id: '2', title: 'Winter Sale', status: 'open' }
]
})
// 쿼리 2: 특정 캠페인
useQuery({
queryKey: ['campaign', '1'],
queryFn: () => ({ id: '1', title: 'Summer Sale', status: 'open' })
})
// 쿼리 3: 캠페인 업데이트
useMutation({
mutationFn: updateCampaign,
onSuccess: (data) => {
// 수동으로 모든 관련 캐시 업데이트 필요!
queryClient.setQueryData(['campaign', '1'], data)
queryClient.setQueryData(['campaigns'], (old) =>
old.map(c => c.id === '1' ? data : c)
)
// 😰 실수하기 쉽고 번거로움
}
})
Apollo Client (정규화됨)
// 쿼리 1: 캠페인 목록
useQuery(GET_CAMPAIGNS)
// 캐시: Campaign:1, Campaign:2 객체 생성
// 쿼리 2: 특정 캠페인
useQuery(GET_CAMPAIGN, { variables: { id: '1' } })
// 캐시: Campaign:1 재사용! (이미 있음)
// 쿼리 3: 캠페인 업데이트
useMutation(UPDATE_CAMPAIGN, {
variables: { id: '1', title: 'New Title' }
})
// Apollo가 자동으로 Campaign:1 업데이트
// → 모든 관련 쿼리가 자동으로 리렌더링! 🎉
Apollo Client는 같은 객체(id가 같으면)를 한 곳에만 저장하고 참조를 사용.
구체적인 캐시 동작 예시
시나리오: 캠페인 목록 → 상세 → 업데이트
// 1. 캠페인 목록 조회
const { data: campaigns } = useQuery(gql`
query GetCampaigns {
campaigns {
id
title
status
}
}
`)
// Apollo 캐시 상태
{
ROOT_QUERY: {
'campaigns': [
{ __ref: 'Campaign:1' },
{ __ref: 'Campaign:2' }
]
},
'Campaign:1': { id: '1', title: 'Summer Sale', status: 'open' },
'Campaign:2': { id: '2', title: 'Winter Sale', status: 'open' }
}
// 2. 상세 조회 (추가 필드 포함)
const { data: campaign } = useQuery(gql`
query GetCampaign($id: ID!) {
campaign(id: $id) {
id
title
status
description # 새로운 필드!
budget # 새로운 필드!
}
}
`, { variables: { id: '1' } })
// Apollo 캐시 상태 (병합됨!)
{
ROOT_QUERY: {
'campaigns': [{ __ref: 'Campaign:1' }, { __ref: 'Campaign:2' }],
'campaign({"id":"1"})': { __ref: 'Campaign:1' }
},
'Campaign:1': {
id: '1',
title: 'Summer Sale',
status: 'open',
description: 'Amazing summer deals!', // 추가됨
budget: 10000 // 추가됨
},
'Campaign:2': { id: '2', title: 'Winter Sale', status: 'open' }
}
// 3. 업데이트 mutation
const [updateCampaign] = useMutation(gql`
mutation UpdateCampaign($id: ID!, $title: String!) {
updateCampaign(id: $id, title: $title) {
id
title
status
}
}
`)
await updateCampaign({
variables: { id: '1', title: 'Super Summer Sale!' }
})
// Apollo 캐시 상태 (자동 업데이트!)
{
'Campaign:1': {
id: '1',
title: 'Super Summer Sale!', // 자동으로 변경됨!
status: 'open',
description: 'Amazing summer deals!',
budget: 10000
},
// ...
}
// 결과: campaigns 쿼리와 campaign 쿼리 모두 자동으로 리렌더링! ✨
캐시 키 생성 규칙 상세
1. 기본 규칙: 타입명 + id (또는 _id)
// GraphQL 응답
{
campaign: {
__typename: "Campaign",
id: "abc-123",
title: "Summer Sale"
}
}
// 자동 생성되는 캐시 키
"Campaign:abc-123"
// 커스터마이즈 가능
const cache = new InMemoryCache({
typePolicies: {
Campaign: {
keyFields: ['id'], // 기본값
// 또는
keyFields: ['customId'],
// 또는 복합 키
keyFields: ['businessId', 'campaignId'],
}
}
})
2. 복합 키 예시
// GraphQL 응답
{
campaignApplication: {
__typename: "CampaignApplication",
campaignId: "camp-1",
influencerId: "inf-1",
status: "approved"
}
}
// 캐시 설정
const cache = new InMemoryCache({
typePolicies: {
CampaignApplication: {
keyFields: ['campaignId', 'influencerId'],
}
}
})
// 생성되는 캐시 키
"CampaignApplication:camp-1:inf-1"
3. 키가 없는 객체 (리스트 아이템 등)
// keyFields: false → 캐시 키 없이 인라인 저장
const cache = new InMemoryCache({
typePolicies: {
CampaignStats: {
keyFields: false, // 캐시 키 생성 안 함
}
}
})
// 결과: 부모 객체 안에 직접 저장
{
'Campaign:1': {
id: '1',
title: 'Summer Sale',
stats: { // 인라인으로 저장 (참조 없음)
__typename: 'CampaignStats',
views: 1000,
clicks: 100
}
}
}
TanStack Query vs Apollo Client 정리
특징 | TanStack Query | Apollo Client |
---|---|---|
캐시 키 | 수동 지정 필수 | 자동 생성 (커스터마이즈 가능) |
저장 방식 | queryKey별로 독립 | 정규화 (객체 단위) |
중복 제거 | 없음 (같은 데이터 여러 번 저장) | 자동 (참조 사용) |
캐시 업데이트 | 수동 (setQueryData) | 자동 (같은 id면 자동 업데이트) |
복잡도 | 단순 | 복잡 (학습 곡선) |
사용 케이스 | REST API | GraphQL |
실무 팁
Apollo Client에서 TanStack Query 패턴 사용하기
// 정규화 비활성화 (TanStack Query처럼)
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
searchResults: {
keyArgs: ['query'], // 검색어로만 구분
merge(existing, incoming) {
return incoming // 단순 교체
},
},
},
},
SearchResult: {
keyFields: false, // 정규화 안 함
},
},
})
// 결과: TanStack Query처럼 동작
디버깅 팁
// Apollo DevTools 없이 캐시 확인
import { useApolloClient } from '@apollo/client'
function DebugCache() {
const client = useApolloClient()
const showCache = () => {
console.log(client.cache.extract())
}
return <button onClick={showCache}>Show Cache</button>
}
출력 예시:
{
"ROOT_QUERY": {
"campaigns({\"status\":\"open\"})": [
{"__ref": "Campaign:1"},
{"__ref": "Campaign:2"}
]
},
"Campaign:1": {
"__typename": "Campaign",
"id": "1",
"title": "Summer Sale"
}
}
결론
- ✅ 캐시 키 = TanStack Query의 queryKey (개념적으로 동일)
- ✅ 자동 생성됨 (GraphQL 쿼리 기반)
- ✅ InMemory = 메모리에 저장 (TanStack Query와 동일)
- ✨ 추가로 정규화 기능 (Apollo만의 강점!)
keyArgs는 "어떤 변수로 캐시를 구분할지" 제어
merge는 "같은 캐시 키에 데이터가 들어올 때 어떻게 합칠지" 제어
'TIL' 카테고리의 다른 글
[250929 TIL] ApolloLink Next.js 설정 (0) | 2025.09.29 |
---|---|
[250929 TIL] ApolloLink (0) | 2025.09.29 |
[250929 TIL] (cache)typePolicies, Apollo Link (0) | 2025.09.29 |
[250929 TIL] Hasura의 특별한 점 (0) | 2025.09.29 |
[250927 TIL] Hasura(v2) + Next.js + Apollo 기본 (0) | 2025.09.27 |