GraphQL Code Generator를 활용한 TypeScript 타입 생성, React 훅 자동 생성, Fragment Colocation 패턴을 다룹니다. 코드 퍼스트 스키마 빌더와의 비교, CI 통합 전략도 학습합니다.
GraphQL의 강력한 타입 시스템은 스키마에만 존재합니다. TypeScript 코드에서는 이 타입 정보를 활용할 수 없습니다.
// 코드 생성 없이: 타입 안전성 없음
const { data } = useQuery(GET_POST);
// data는 any 타입
// data.post.title — 오타가 있어도 컴파일러가 모름
// data.post.nonExistentField — 존재하지 않는 필드도 에러 없음GraphQL Code Generator는 스키마와 쿼리 문서를 분석하여 TypeScript 타입을 자동 생성합니다. 이를 통해 스키마의 타입 시스템이 클라이언트 코드까지 관통합니다.
// 코드 생성 후: 완전한 타입 안전성
import { useGetPostQuery } from './generated/graphql';
const { data } = useGetPostQuery({ variables: { id: '1' } });
// data?.post?.title — 자동 완성, 타입 추론
// data?.post?.nonExistentField — 컴파일 에러!pnpm add -D @graphql-codegen/cli \
@graphql-codegen/typescript \
@graphql-codegen/typescript-operations \
@graphql-codegen/typescript-react-apollo \
@graphql-codegen/near-operation-file-presetimport type { CodegenConfig } from '@graphql-codegen/cli';
const config: CodegenConfig = {
// 스키마 소스 (로컬 파일 또는 원격 URL)
schema: 'http://localhost:4000/graphql',
// 쿼리 문서 위치
documents: ['src/**/*.graphql', 'src/**/*.tsx'],
generates: {
// 서버 스키마 타입 생성
'src/generated/schema-types.ts': {
plugins: ['typescript'],
config: {
strictScalars: true,
scalars: {
DateTime: 'string',
JSON: 'Record<string, unknown>',
},
},
},
// 클라이언트 쿼리별 타입 + 훅 생성
'src/': {
preset: 'near-operation-file',
presetConfig: {
extension: '.generated.ts',
baseTypesPath: 'generated/schema-types.ts',
},
plugins: [
'typescript-operations',
'typescript-react-apollo',
],
config: {
withHooks: true,
withComponent: false,
withHOC: false,
},
},
},
hooks: {
afterAllFileWrite: ['prettier --write'],
},
};
export default config;{
"scripts": {
"codegen": "graphql-codegen",
"codegen:watch": "graphql-codegen --watch"
}
}스키마의 모든 타입, 열거형, 입력 타입이 TypeScript로 변환됩니다.
type Post {
id: ID!
title: String!
content: String!
status: PostStatus!
author: User!
tags: [Tag!]!
createdAt: DateTime!
}
enum PostStatus {
DRAFT
PUBLISHED
ARCHIVED
}
input CreatePostInput {
title: String!
content: String!
tags: [String!]
}// 자동 생성됨
export type Post = {
__typename?: 'Post';
id: string;
title: string;
content: string;
status: PostStatus;
author: User;
tags: Array<Tag>;
createdAt: string;
};
export enum PostStatus {
Draft = 'DRAFT',
Published = 'PUBLISHED',
Archived = 'ARCHIVED',
}
export type CreatePostInput = {
title: string;
content: string;
tags?: Array<string> | null;
};각 쿼리/뮤테이션에 대한 입출력 타입이 생성됩니다.
query GetPost($id: ID!) {
post(id: $id) {
id
title
content
status
author {
id
name
avatar
}
tags {
id
name
}
createdAt
}
}// 자동 생성됨
export type GetPostQueryVariables = {
id: string;
};
export type GetPostQuery = {
__typename?: 'Query';
post: {
__typename?: 'Post';
id: string;
title: string;
content: string;
status: PostStatus;
author: {
__typename?: 'User';
id: string;
name: string;
avatar: string | null;
};
tags: Array<{
__typename?: 'Tag';
id: string;
name: string;
}>;
createdAt: string;
} | null;
};
// React Apollo 훅
export function useGetPostQuery(
baseOptions: Apollo.QueryHookOptions<GetPostQuery, GetPostQueryVariables>,
) {
return Apollo.useQuery<GetPostQuery, GetPostQueryVariables>(
GetPostDocument,
baseOptions,
);
}Fragment Colocation은 각 컴포넌트가 자신이 필요한 데이터를 Fragment로 선언하는 패턴입니다. 컴포넌트와 데이터 요구사항이 같은 파일에 위치합니다.
import { gql } from '@apollo/client';
import type { PostCardFragment } from './PostCard.generated';
export const POST_CARD_FRAGMENT = gql`
fragment PostCard on Post {
id
title
summary
author {
...AuthorAvatar
}
createdAt
}
`;
interface PostCardProps {
post: PostCardFragment;
}
export function PostCard({ post }: PostCardProps) {
return (
<article>
<h2>{post.title}</h2>
<p>{post.summary}</p>
<AuthorAvatar author={post.author} />
<time>{post.createdAt}</time>
</article>
);
}import { gql } from '@apollo/client';
import type { AuthorAvatarFragment } from './AuthorAvatar.generated';
export const AUTHOR_AVATAR_FRAGMENT = gql`
fragment AuthorAvatar on User {
id
name
avatar
}
`;
interface AuthorAvatarProps {
author: AuthorAvatarFragment;
}
export function AuthorAvatar({ author }: AuthorAvatarProps) {
return (
<div>
<img src={author.avatar ?? '/default-avatar.png'} alt={author.name} />
<span>{author.name}</span>
</div>
);
}상위 컴포넌트에서 Fragment를 조합하여 하나의 쿼리를 구성합니다.
import { gql } from '@apollo/client';
import { POST_CARD_FRAGMENT, PostCard } from '../components/PostCard';
import { AUTHOR_AVATAR_FRAGMENT } from '../components/AuthorAvatar';
import { useListPostsQuery } from './PostListPage.generated';
const LIST_POSTS = gql`
${POST_CARD_FRAGMENT}
${AUTHOR_AVATAR_FRAGMENT}
query ListPosts($first: Int!, $after: String) {
posts(first: $first, after: $after) {
edges {
node {
...PostCard
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
`;
export function PostListPage() {
const { data, loading, fetchMore } = useListPostsQuery({
variables: { first: 10 },
});
if (loading) return <p>로딩 중...</p>;
return (
<div>
{data?.posts.edges.map((edge) => (
<PostCard key={edge.node.id} post={edge.node} />
))}
</div>
);
}Fragment Colocation의 핵심 이점은 데이터 요구사항의 지역성(locality) 입니다. 컴포넌트가 어떤 데이터를 필요로 하는지 한 파일에서 확인할 수 있고, 컴포넌트 변경 시 Fragment만 수정하면 타입이 자동으로 업데이트됩니다.
Code Generator는 클라이언트뿐 아니라 서버 리졸버의 타입도 생성할 수 있습니다.
import type { CodegenConfig } from '@graphql-codegen/cli';
const config: CodegenConfig = {
schema: './schema.graphql',
generates: {
'src/generated/resolvers-types.ts': {
plugins: ['typescript', 'typescript-resolvers'],
config: {
contextType: '../context#Context',
mapperTypeSuffix: 'Model',
mappers: {
User: '../models#UserModel',
Post: '../models#PostModel',
},
},
},
},
};
export default config;생성된 타입을 사용하면 리졸버의 인자와 반환 타입이 강제됩니다.
import type { Resolvers } from './generated/resolvers-types';
const resolvers: Resolvers = {
Query: {
// args.id는 자동으로 string 타입
// 반환 타입은 Post | null로 강제
post: async (_parent, args, context) => {
return context.dataSources.posts.findById(args.id);
},
},
Post: {
// parent는 PostModel 타입
// 반환 타입은 User로 강제
author: async (parent, _args, context) => {
return context.loaders.userById.load(parent.authorId);
},
},
};2장에서 소개한 코드 퍼스트 접근법(Pothos, Nexus)은 Code Generator 없이도 타입 안전성을 제공합니다.
import SchemaBuilder from '@pothos/core';
import PrismaPlugin from '@pothos/plugin-prisma';
import type PrismaTypes from '@pothos/plugin-prisma/generated';
const builder = new SchemaBuilder<{
PrismaTypes: PrismaTypes;
Context: Context;
}>({
plugins: [PrismaPlugin],
prisma: { client: prisma },
});
// Prisma 모델과 자동 연동
builder.prismaObject('Post', {
fields: (t) => ({
id: t.exposeID('id'),
title: t.exposeString('title'),
content: t.exposeString('content'),
// 관계 필드도 자동 타입 추론
author: t.relation('author'),
tags: t.relation('tags'),
// 계산 필드
readingTime: t.int({
resolve: (parent) => {
// parent는 자동으로 Prisma Post 타입
return Math.ceil(parent.content.split(/\s+/).length / 200);
},
}),
}),
});| 관점 | 스키마 퍼스트 + Code Generator | 코드 퍼스트 (Pothos) |
|---|---|---|
| 타입 안전성 | 생성 후 안전 | 즉시 안전 |
| 빌드 단계 | codegen 필수 | 불필요 |
| 스키마 가독성 | SDL로 한눈에 | 코드 속에 분산 |
| IDE 지원 | 생성 후 자동완성 | 즉시 자동완성 |
| 러닝 커브 | 낮음 | 중간 |
| Prisma 통합 | 수동 매핑 | 자동 연동 |
두 접근법 모두 프로덕션에서 검증된 방식입니다. 팀이 SDL에 익숙하고 프런트엔드와의 스키마 공유가 중요하면 스키마 퍼스트를, TypeScript 개발 경험을 극대화하려면 코드 퍼스트를 선택합니다.
외부 서비스나 서버 간 통신을 위한 타입 안전한 SDK를 자동 생성할 수 있습니다.
const config: CodegenConfig = {
schema: 'https://api.example.com/graphql',
documents: 'src/sdk/**/*.graphql',
generates: {
'src/generated/sdk.ts': {
plugins: [
'typescript',
'typescript-operations',
'typescript-graphql-request',
],
config: {
rawRequest: false,
},
},
},
};import { GraphQLClient } from 'graphql-request';
import { getSdk } from './generated/sdk';
const client = new GraphQLClient('https://api.example.com/graphql', {
headers: { Authorization: `Bearer ${token}` },
});
const sdk = getSdk(client);
// 완전한 타입 안전성
const { post } = await sdk.GetPost({ id: '123' });
console.log(post?.title); // string | undefined코드 생성을 CI 파이프라인에 통합하여 스키마 변경에 따른 타입 불일치를 조기에 감지합니다.
name: GraphQL Schema Check
on:
pull_request:
paths:
- 'schema.graphql'
- 'src/**/*.graphql'
- 'codegen.ts'
jobs:
codegen-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
# 코드 생성 실행
- run: pnpm codegen
# 생성된 파일에 변경이 있으면 실패
- name: Check for uncommitted changes
run: |
if [[ -n $(git diff --name-only) ]]; then
echo "Generated files are out of date. Run 'pnpm codegen' and commit."
git diff --name-only
exit 1
fi
# TypeScript 컴파일 검증
- run: pnpm tsc --noEmit이번 장에서는 GraphQL Code Generator를 활용한 타입 안전한 개발 워크플로를 학습했습니다. TypeScript 타입, React 훅, SDK의 자동 생성을 다루었고, Fragment Colocation을 통한 컴포넌트와 데이터 요구사항의 결합 패턴을 살펴보았습니다. 코드 퍼스트 빌더와의 비교, CI 통합 전략도 함께 다루었습니다.
코드 생성은 "있으면 좋은 것"이 아니라 "필수"입니다. GraphQL의 타입 시스템이 제공하는 안전성을 TypeScript 코드까지 관통시켜야 그 가치를 온전히 누릴 수 있습니다.
10장에서는 프로덕션 운영과 모니터링을 다룹니다. 스키마 변경 관리와 Breaking Change 감지, 스키마 레지스트리, 쿼리 복잡도 제한, 관측 가능성(Apollo Studio/트레이싱), 에러 처리 전략을 살펴보겠습니다.
이 글이 도움이 되셨나요?
관련 주제 더 보기
GraphQL API의 프로덕션 운영 전략을 다룹니다. 스키마 변경 관리, Breaking Change 감지, 스키마 레지스트리, 쿼리 제한, 관측 가능성, 에러 처리, 성능 모니터링을 학습합니다.
GraphQL API의 다양한 캐싱 전략을 다룹니다. HTTP 캐싱의 한계, Persisted Queries를 통한 CDN 캐싱, @cacheControl 디렉티브, Redis 기반 리졸버 캐싱, 캐시 무효화 패턴을 학습합니다.
시리즈의 모든 개념을 종합하여 Apollo Server, Federation, DataLoader 기반의 프로덕션 수준 GraphQL API를 구축합니다. 스키마 설계부터 Docker 배포, 성능 테스트까지 전 과정을 다룹니다.