1. InMemoryCache의 typePolicies
기본 개념: Apollo Client의 캐싱 메커니즘
Apollo Client는 GraphQL 응답을 메모리에 캐싱해서 같은 데이터를 다시 요청할 때 네트워크 요청 없이 즉시 반환.
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
campaign_application: {
keyArgs: ['where', 'order_by'],
merge(existing, incoming) {
return incoming
},
},
},
},
},
})
상세 분석
typePolicies란?
캐시가 각 타입과 필드를 어떻게 처리할지 정의하는 규칙.
typePolicies: {
Query: { // Query 타입에 대한 정책
fields: { // Query 타입의 필드들
campaign_application: { // 이 필드에 대한 정책
// ...
}
}
}
}
keyArgs: ['where', 'order_by']
캐시 키를 만들 때 어떤 인자를 사용할지 결정.
예시로 이해하기:
// 쿼리 1
useQuery(GET_APPLICATIONS, {
variables: {
where: { campaign_id: { _eq: "abc-123" } },
order_by: { created_at: "desc" },
limit: 10
}
})
// 쿼리 2
useQuery(GET_APPLICATIONS, {
variables: {
where: { campaign_id: { _eq: "abc-123" } },
order_by: { created_at: "desc" },
limit: 20 // limit만 다름
}
})
keyArgs가 없다면:
캐시 키 1: campaign_application({"where":{...},"order_by":{...},"limit":10})
캐시 키 2: campaign_application({"where":{...},"order_by":{...},"limit":20})
→ 다른 키로 인식, 별도로 캐싱
keyArgs: ['where', 'order_by']로 설정하면:
캐시 키 1: campaign_application({"where":{...},"order_by":{...}})
캐시 키 2: campaign_application({"where":{...},"order_by":{...}})
→ 같은 키! limit는 무시됨
결과:
- 쿼리 2는 네트워크 요청 없이 쿼리 1의 캐시를 재사용
- limit은 클라이언트에서 필터링 (배열 slice)
merge(existing, incoming)
같은 캐시 키에 새 데이터가 들어올 때 어떻게 병합할지 결정.
merge(existing, incoming) {
return incoming // 기존 데이터 버리고 새 데이터로 교체
}
동작 시나리오:
// 1차 쿼리 실행
const { data } = useQuery(GET_APPLICATIONS, {
variables: { where: { status: { _eq: "pending" } } }
})
// 캐시에 저장: [app1, app2, app3]
// 2차 쿼리 실행 (같은 where, order_by)
refetch()
// 새 데이터: [app1, app2, app4]
// merge 함수 호출됨
merge(
existing: [app1, app2, app3], // 기존 캐시
incoming: [app1, app2, app4] // 새로 받은 데이터
)
// return incoming → [app1, app2, app4]로 덮어씀
다른 merge 전략 예시:
// 1. 배열 합치기 (무한 스크롤)
merge(existing = [], incoming) {
return [...existing, ...incoming]
}
// 2. 중복 제거하며 합치기
merge(existing = [], incoming) {
const existingIds = new Set(existing.map(item => item.id))
const newItems = incoming.filter(item => !existingIds.has(item.id))
return [...existing, ...newItems]
}
// 3. 특정 조건으로 병합
merge(existing, incoming, { args }) {
if (args.offset === 0) {
return incoming // 첫 페이지면 교체
}
return [...existing, ...incoming] // 아니면 추가
}
실제 사용 예시
// lib/apollo-client.tsx
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
// 무한 스크롤용
campaign_application: {
keyArgs: ['where', 'order_by'],
merge(existing = [], incoming, { args }) {
const offset = args?.offset ?? 0
const merged = existing.slice(0)
for (let i = 0; i < incoming.length; ++i) {
merged[offset + i] = incoming[i]
}
return merged
},
},
// 단순 교체 (실시간 데이터)
campaign_by_pk: {
keyArgs: ['id'],
merge(existing, incoming) {
return incoming
},
},
},
},
},
})
결과:
// 컴포넌트 1
const { data } = useQuery(GET_APPLICATIONS, {
variables: { where: { status: { _eq: "pending" } }, limit: 10 }
})
// 컴포넌트 2 (같은 where, order_by)
const { data } = useQuery(GET_APPLICATIONS, {
variables: { where: { status: { _eq: "pending" } }, limit: 5 }
})
// → 네트워크 요청 없이 캐시에서 가져옴! ⚡
2. ApolloLink - 미들웨어 체인
기본 개념
Apollo Link는 GraphQL 요청의 미들웨어 체인이에요. Express의 미들웨어와 비슷.
요청 → Link 1 → Link 2 → Link 3 → 서버
응답 ← Link 1 ← Link 2 ← Link 3 ← 서버
Logger Link 상세 분석
const loggerLink = new ApolloLink((operation, forward) => {
console.log(`GraphQL Request: ${operation.operationName}`)
const start = Date.now()
return forward(operation).map(response => {
console.log(`Took ${Date.now() - start}ms`)
return response
})
})
매개변수 설명
(operation, forward) => {
// operation: 현재 GraphQL 작업 정보
// forward: 다음 링크로 전달하는 함수
}
operation 객체:
{
operationName: "GetCampaigns", // 쿼리 이름
query: DocumentNode, // GraphQL 쿼리 AST
variables: { id: "abc-123" }, // 변수들
extensions: {}, // 확장 정보
getContext: () => {}, // 컨텍스트
setContext: () => {} // 컨텍스트 설정
}
실행 흐름
// 1. 요청 시작
console.log(`GraphQL Request: ${operation.operationName}`)
// 출력: "GraphQL Request: GetCampaigns"
// 2. 시작 시간 기록
const start = Date.now() // 예: 1640000000000
// 3. 다음 링크로 전달 (서버로 요청)
return forward(operation)
// forward(operation)는 Observable을 반환
.map(response => {
// 4. 응답 받았을 때 실행
console.log(`Took ${Date.now() - start}ms`)
// 출력: "Took 234ms"
// 5. 응답 그대로 반환 (변경 안 함)
return response
})
Observable 패턴
Apollo Link는 Observable을 사용 (RxJS와 비슷).
// Observable의 개념
const observable = forward(operation)
observable.map(response => {
// 응답을 변환
return response
})
observable.subscribe({
next: (value) => console.log('받음:', value),
error: (err) => console.error('에러:', err),
complete: () => console.log('완료')
})
실전 활용 예시
1. 인증 헤더 추가
const authLink = new ApolloLink((operation, forward) => {
// 토큰 가져오기
const token = localStorage.getItem('token')
// 헤더에 추가
operation.setContext({
headers: {
authorization: token ? `Bearer ${token}` : '',
}
})
return forward(operation)
})
2. 에러 핸들링
import { onError } from '@apollo/client/link/error'
const errorLink = onError(({ graphQLErrors, networkError, operation }) => {
if (graphQLErrors) {
graphQLErrors.forEach(({ message, locations, path }) => {
console.error(
`[GraphQL error]: Message: ${message}, Path: ${path}`
)
})
}
if (networkError) {
console.error(`[Network error]: ${networkError}`)
// 로그인 페이지로 리다이렉트 등
}
})
3. 재시도 로직
import { RetryLink } from '@apollo/client/link/retry'
const retryLink = new RetryLink({
delay: {
initial: 300,
max: 5000,
jitter: true
},
attempts: {
max: 3,
retryIf: (error) => !!error && error.statusCode !== 400
}
})
4. 로딩 상태 추적
import { makeVar } from '@apollo/client'
export const loadingVar = makeVar(false)
const loadingLink = new ApolloLink((operation, forward) => {
loadingVar(true)
return forward(operation).map(response => {
loadingVar(false)
return response
})
})
// 컴포넌트에서 사용
function GlobalLoader() {
const isLoading = useReactiveVar(loadingVar)
return isLoading ? <Spinner /> : null
}
5. 성능 모니터링 (고급)
const performanceLink = new ApolloLink((operation, forward) => {
const start = performance.now()
return forward(operation).map(response => {
const duration = performance.now() - start
// 분석 서비스로 전송
analytics.track('GraphQL Query', {
operationName: operation.operationName,
duration,
variables: operation.variables,
// 느린 쿼리 경고
slow: duration > 1000
})
return response
})
})
링크 체인 구성
import { ApolloClient, from, HttpLink } from '@apollo/client'
const httpLink = new HttpLink({
uri: process.env.NEXT_PUBLIC_HASURA_URL,
})
// 체인 순서가 중요!
const client = new ApolloClient({
cache,
link: from([
loggerLink, // 1. 로깅
errorLink, // 2. 에러 처리
authLink, // 3. 인증 헤더 추가
retryLink, // 4. 재시도
performanceLink, // 5. 성능 추적
httpLink, // 6. 실제 HTTP 요청 (마지막!)
]),
})
실행 순서:
요청 → logger → error → auth → retry → performance → HTTP → 서버
↓
응답 ← logger ← error ← auth ← retry ← performance ← HTTP ← 서버
조건부 링크 (분기 처리)
import { split } from '@apollo/client'
import { getMainDefinition } from '@apollo/client/utilities'
import { GraphQLWsLink } from '@apollo/client/link/subscriptions'
import { createClient } from 'graphql-ws'
const wsLink = new GraphQLWsLink(
createClient({ url: 'ws://localhost:8080/v1/graphql' })
)
// subscription은 WebSocket, 나머지는 HTTP
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query)
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
)
},
wsLink, // true면 WebSocket
httpLink // false면 HTTP
)
전체 통합 예시
// lib/apollo-client.tsx
'use client'
import { ApolloClient, ApolloLink, from, HttpLink, InMemoryCache } from '@apollo/client'
import { onError } from '@apollo/client/link/error'
import { ApolloNextAppProvider } from '@apollo/experimental-nextjs-app-support/ssr'
// 캐시 설정
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
campaign_application: {
keyArgs: ['where', 'order_by'],
merge(existing, incoming, { args }) {
if (args?.offset === 0) {
return incoming // 첫 페이지면 교체
}
return [...(existing ?? []), ...incoming] // 추가
},
},
},
},
},
})
// 로거 링크
const loggerLink = new ApolloLink((operation, forward) => {
console.log(`🚀 ${operation.operationName}`)
const start = Date.now()
return forward(operation).map(response => {
console.log(`✅ ${operation.operationName} (${Date.now() - start}ms)`)
return response
})
})
// 에러 링크
const errorLink = onError(({ graphQLErrors, networkError, operation }) => {
if (graphQLErrors) {
console.error(`❌ [${operation.operationName}]:`, graphQLErrors)
}
if (networkError) {
console.error(`🔌 Network error:`, networkError)
}
})
// 인증 링크
const authLink = new ApolloLink((operation, forward) => {
const token = localStorage.getItem('token')
operation.setContext({
headers: {
authorization: token ? `Bearer ${token}` : '',
}
})
return forward(operation)
})
// HTTP 링크
const httpLink = new HttpLink({
uri: process.env.NEXT_PUBLIC_HASURA_URL,
})
function makeClient() {
return new ApolloClient({
cache,
link: from([
loggerLink,
errorLink,
authLink,
httpLink,
]),
})
}
export function ApolloWrapper({ children }: { children: React.ReactNode }) {
return (
<ApolloNextAppProvider makeClient={makeClient}>
{children}
</ApolloNextAppProvider>
)
}
콘솔 출력 예시:
🚀 GetCampaigns
✅ GetCampaigns (234ms)
🚀 GetApplications
✅ GetApplications (156ms)
🚀 UpdateCampaign
❌ [UpdateCampaign]: [
{
message: "permission denied",
extensions: { code: "validation-failed" }
}
]
요약 정리
InMemoryCache typePolicies
- 목적: 캐시 동작 커스터마이징
- keyArgs: 캐시 키 생성 기준
- merge: 데이터 병합 전략
- 효과: 불필요한 네트워크 요청 감소 ⚡
ApolloLink
- 목적: GraphQL 요청/응답 미들웨어
- 로거: 성능 모니터링
- 인증: 헤더 추가
- 에러: 중앙 집중식 에러 처리
- 효과: 관심사 분리, 재사용성 증가 🎯
'library' 카테고리의 다른 글
[250929 TIL] ApolloLink (0) | 2025.09.29 |
---|---|
[250929 TIL] tanstack vs apollo 쿼리캐시 비교 (0) | 2025.09.29 |
[250929 TIL] Hasura의 특별한 점 (0) | 2025.09.29 |
[250927 TIL] Hasura(v2) + Next.js + Apollo 기본 (0) | 2025.09.27 |
[250907 TIL] 실전적인 axios client 구성 (0) | 2025.09.07 |