타입 안전성과 효율적인 데이터 캐싱을 위한 GraphQL 아키텍처
npm install @apollo/client graphql npm install -D @graphql-codegen/cli @graphql-codegen/typescript npm install -D @graphql-codegen/typescript-operations npm install -D @graphql-codegen/typescript-react-apollo
먼저 캐시를 확인하고, 없을 때만 서버에 요청합니다. 빠른 응답 속도를 제공하지만 오래된 데이터를 표시할 수 있습니다.
fetchPolicy: "cache-first"{
"post1": {
"id": "1",
"title": "GraphQL Basics",
"author": {
"id": "user1",
"name": "Alice",
"email": "alice@example.com"
}
},
"post2": {
"id": "2",
"title": "Advanced GraphQL",
"author": {
"id": "user1",
"name": "Alice",
"email": "alice@example.com"
}
}
}{
"User:user1": {
"id": "user1",
"name": "Alice",
"email": "alice@example.com"
},
"Post:1": {
"id": "1",
"title": "GraphQL Basics",
"author": { "__ref": "User:user1" }
},
"Post:2": {
"id": "2",
"title": "Advanced GraphQL",
"author": { "__ref": "User:user1" }
}
}# schema.graphql
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
comments: [Comment!]!
createdAt: DateTime!
}
type Comment {
id: ID!
text: String!
author: User!
post: Post!
createdAt: DateTime!
}
type Query {
user(id: ID!): User
users: [User!]!
post(id: ID!): Post
posts(limit: Int, offset: Int): [Post!]!
}
type Mutation {
createPost(title: String!, content: String!): Post!
updatePost(id: ID!, title: String, content: String): Post!
deletePost(id: ID!): Boolean!
}
type Subscription {
postAdded: Post!
commentAdded(postId: ID!): Comment!
}// codegen.ts
import type { CodegenConfig } from '@graphql-codegen/cli';
const config: CodegenConfig = {
schema: 'http://localhost:4000/graphql', // GraphQL 서버 URL
documents: ['src/**/*.graphql', 'src/**/*.tsx'], // 쿼리가 있는 파일
generates: {
'./src/__generated__/': {
preset: 'client',
plugins: [],
presetConfig: {
gqlTagName: 'gql',
}
},
'./src/__generated__/graphql.ts': {
plugins: [
'typescript',
'typescript-operations',
'typescript-react-apollo'
],
config: {
withHooks: true, // useQuery, useMutation Hook 자동 생성
withComponent: false,
withHOC: false,
}
}
},
};
export default config;실행 명령어:
npm run codegenimport { ApolloClient, InMemoryCache, createHttpLink } from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
// HTTP 연결 설정
const httpLink = createHttpLink({
uri: 'http://localhost:4000/graphql',
});
// 인증 헤더 추가
const authLink = setContext((_, { headers }) => {
const token = localStorage.getItem('token');
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : "",
}
};
});
// Apollo Client 생성
export const client = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
posts: {
// 페이지네이션 병합 전략
keyArgs: false,
merge(existing = [], incoming) {
return [...existing, ...incoming];
},
},
},
},
Post: {
fields: {
comments: {
// 댓글 병합 전략
merge(existing = [], incoming) {
return incoming;
},
},
},
},
},
}),
defaultOptions: {
watchQuery: {
fetchPolicy: 'cache-and-network',
errorPolicy: 'all',
},
query: {
fetchPolicy: 'cache-first',
errorPolicy: 'all',
},
mutate: {
errorPolicy: 'all',
},
},
});
// App에서 사용
import { ApolloProvider } from '@apollo/client';
function App() {
return (
<ApolloProvider client={client}>
<YourApp />
</ApolloProvider>
);
}// queries/posts.graphql
query GetPosts($limit: Int, $offset: Int) {
posts(limit: $limit, offset: $offset) {
id
title
content
createdAt
author {
id
name
email
}
comments {
id
text
author {
id
name
}
}
}
}
query GetPost($id: ID!) {
post(id: $id) {
id
title
content
createdAt
author {
id
name
email
}
}
}
mutation CreatePost($title: String!, $content: String!) {
createPost(title: $title, content: $content) {
id
title
content
createdAt
}
}
// ✅ Codegen이 자동으로 생성하는 Hook:
// - useGetPostsQuery
// - useGetPostQuery
// - useCreatePostMutationimport { useGetPostsQuery, useCreatePostMutation } from './__generated__/graphql';
function PostList() {
// ✅ 자동 생성된 Hook 사용 (타입 안전)
const { data, loading, error, refetch } = useGetPostsQuery({
variables: { limit: 10, offset: 0 },
fetchPolicy: 'cache-and-network',
});
const [createPost, { loading: creating }] = useCreatePostMutation({
// 캐시 업데이트 전략
update(cache, { data: newPost }) {
cache.modify({
fields: {
posts(existingPosts = []) {
const newPostRef = cache.writeFragment({
data: newPost?.createPost,
fragment: gql`
fragment NewPost on Post {
id
title
content
}
`,
});
return [newPostRef, ...existingPosts];
},
},
});
},
// Optimistic Response (즉시 UI 업데이트)
optimisticResponse: {
createPost: {
__typename: 'Post',
id: 'temp-id',
title: newTitle,
content: newContent,
createdAt: new Date().toISOString(),
author: {
__typename: 'User',
id: currentUser.id,
name: currentUser.name,
email: currentUser.email,
},
},
},
});
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
{data?.posts.map((post) => (
<div key={post.id}>
<h2>{post.title}</h2>
<p>{post.content}</p>
<span>by {post.author.name}</span>
</div>
))}
</div>
);
}import { useApolloClient } from '@apollo/client';
function PostActions({ postId }: { postId: string }) {
const client = useApolloClient();
// 1. 캐시에서 읽기
const readCache = () => {
const post = client.readFragment({
id: `Post:${postId}`,
fragment: gql`
fragment PostData on Post {
id
title
content
}
`,
});
console.log('Cached post:', post);
};
// 2. 캐시에 쓰기
const updateCache = () => {
client.writeFragment({
id: `Post:${postId}`,
fragment: gql`
fragment UpdatePost on Post {
title
}
`,
data: {
title: 'Updated Title',
},
});
};
// 3. 특정 쿼리 무효화
const invalidateQuery = () => {
client.refetchQueries({
include: ['GetPosts'],
});
};
// 4. 캐시 초기화
const clearCache = () => {
client.cache.reset();
};
// 5. 특정 필드 업데이트
const updateField = () => {
client.cache.modify({
id: `Post:${postId}`,
fields: {
title(cachedTitle) {
return cachedTitle + ' (Updated)';
},
},
});
};
return <div>...</div>;
}캐시를 먼저 확인하고 없을 때만 서버 요청
캐시 데이터 즉시 반환 + 백그라운드 서버 요청
항상 서버에서 최신 데이터 가져옴
서버에서 가져오고 캐시에 저장하지 않음