본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout

내비게이션

  • Tech
  • Projects
  • Books
  • About
  • Tags

카테고리

  • AI / ML
  • 웹 개발
  • 프로그래밍
  • 개발 도구

연결

  • GitHub
  • Email
  • RSS
© 2026 Kreath Archive. All rights reserved.Built with Next.js + MDX
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 9장: 코드 생성과 타입 안전성
2026년 2월 22일·웹 개발·

9장: 코드 생성과 타입 안전성

GraphQL Code Generator를 활용한 TypeScript 타입 생성, React 훅 자동 생성, Fragment Colocation 패턴을 다룹니다. 코드 퍼스트 스키마 빌더와의 비교, CI 통합 전략도 학습합니다.

13분845자10개 섹션
graphqlapi-designfrontend
공유
graphql-architecture9 / 11
1234567891011
이전8장: 캐싱 전략다음10장: 프로덕션 운영과 모니터링

학습 목표

  • GraphQL Code Generator의 구조와 설정 방법을 이해한다
  • TypeScript 타입, React 훅, SDK를 자동 생성할 수 있다
  • Fragment Colocation 패턴을 적용하여 컴포넌트와 데이터 요구사항을 결합한다
  • 코드 퍼스트 스키마 빌더의 타입 안전성을 이해한다
  • CI 파이프라인에 코드 생성을 통합한다

왜 코드 생성인가

GraphQL의 강력한 타입 시스템은 스키마에만 존재합니다. TypeScript 코드에서는 이 타입 정보를 활용할 수 없습니다.

without-codegen.ts
typescript
// 코드 생성 없이: 타입 안전성 없음
const { data } = useQuery(GET_POST);
// data는 any 타입
// data.post.title — 오타가 있어도 컴파일러가 모름
// data.post.nonExistentField — 존재하지 않는 필드도 에러 없음

GraphQL Code Generator는 스키마와 쿼리 문서를 분석하여 TypeScript 타입을 자동 생성합니다. 이를 통해 스키마의 타입 시스템이 클라이언트 코드까지 관통합니다.

with-codegen.ts
typescript
// 코드 생성 후: 완전한 타입 안전성
import { useGetPostQuery } from './generated/graphql';
 
const { data } = useGetPostQuery({ variables: { id: '1' } });
// data?.post?.title — 자동 완성, 타입 추론
// data?.post?.nonExistentField — 컴파일 에러!

GraphQL Code Generator 설정

설치

install.sh
bash
pnpm add -D @graphql-codegen/cli \
  @graphql-codegen/typescript \
  @graphql-codegen/typescript-operations \
  @graphql-codegen/typescript-react-apollo \
  @graphql-codegen/near-operation-file-preset

설정 파일

codegen.ts
typescript
import 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;

실행

package.json
json
{
  "scripts": {
    "codegen": "graphql-codegen",
    "codegen:watch": "graphql-codegen --watch"
  }
}

생성되는 타입

스키마 타입

스키마의 모든 타입, 열거형, 입력 타입이 TypeScript로 변환됩니다.

schema.graphql
graphql
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!]
}
generated/schema-types.ts
typescript
// 자동 생성됨
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;
};

쿼리 작업 타입

각 쿼리/뮤테이션에 대한 입출력 타입이 생성됩니다.

src/features/posts/queries/GetPost.graphql
graphql
query GetPost($id: ID!) {
  post(id: $id) {
    id
    title
    content
    status
    author {
      id
      name
      avatar
    }
    tags {
      id
      name
    }
    createdAt
  }
}
src/features/posts/queries/GetPost.generated.ts
typescript
// 자동 생성됨
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 Colocation은 각 컴포넌트가 자신이 필요한 데이터를 Fragment로 선언하는 패턴입니다. 컴포넌트와 데이터 요구사항이 같은 파일에 위치합니다.

Fragment 정의

src/components/PostCard.tsx
typescript
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>
  );
}
src/components/AuthorAvatar.tsx
typescript
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 조합

상위 컴포넌트에서 Fragment를 조합하여 하나의 쿼리를 구성합니다.

src/pages/PostListPage.tsx
typescript
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>
  );
}
Info

Fragment Colocation의 핵심 이점은 데이터 요구사항의 지역성(locality) 입니다. 컴포넌트가 어떤 데이터를 필요로 하는지 한 파일에서 확인할 수 있고, 컴포넌트 변경 시 Fragment만 수정하면 타입이 자동으로 업데이트됩니다.


서버 리졸버 타입 생성

Code Generator는 클라이언트뿐 아니라 서버 리졸버의 타입도 생성할 수 있습니다.

codegen-server.ts
typescript
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;

생성된 타입을 사용하면 리졸버의 인자와 반환 타입이 강제됩니다.

typed-resolvers.ts
typescript
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 없이도 타입 안전성을 제공합니다.

Pothos의 타입 추론

pothos-type-safety.ts
typescript
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);
      },
    }),
  }),
});

스키마 퍼스트 vs 코드 퍼스트 비교

관점스키마 퍼스트 + Code Generator코드 퍼스트 (Pothos)
타입 안전성생성 후 안전즉시 안전
빌드 단계codegen 필수불필요
스키마 가독성SDL로 한눈에코드 속에 분산
IDE 지원생성 후 자동완성즉시 자동완성
러닝 커브낮음중간
Prisma 통합수동 매핑자동 연동
Tip

두 접근법 모두 프로덕션에서 검증된 방식입니다. 팀이 SDL에 익숙하고 프런트엔드와의 스키마 공유가 중요하면 스키마 퍼스트를, TypeScript 개발 경험을 극대화하려면 코드 퍼스트를 선택합니다.


SDK 생성

외부 서비스나 서버 간 통신을 위한 타입 안전한 SDK를 자동 생성할 수 있습니다.

codegen-sdk.ts
typescript
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,
      },
    },
  },
};
use-sdk.ts
typescript
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 통합

코드 생성을 CI 파이프라인에 통합하여 스키마 변경에 따른 타입 불일치를 조기에 감지합니다.

.github/workflows/graphql-check.yml
yaml
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-design#frontend

관련 글

웹 개발

10장: 프로덕션 운영과 모니터링

GraphQL API의 프로덕션 운영 전략을 다룹니다. 스키마 변경 관리, Breaking Change 감지, 스키마 레지스트리, 쿼리 제한, 관측 가능성, 에러 처리, 성능 모니터링을 학습합니다.

2026년 2월 24일·13분
웹 개발

8장: 캐싱 전략

GraphQL API의 다양한 캐싱 전략을 다룹니다. HTTP 캐싱의 한계, Persisted Queries를 통한 CDN 캐싱, @cacheControl 디렉티브, Redis 기반 리졸버 캐싱, 캐시 무효화 패턴을 학습합니다.

2026년 2월 20일·13분
웹 개발

11장: 실전 프로젝트 -- GraphQL API 구축

시리즈의 모든 개념을 종합하여 Apollo Server, Federation, DataLoader 기반의 프로덕션 수준 GraphQL API를 구축합니다. 스키마 설계부터 Docker 배포, 성능 테스트까지 전 과정을 다룹니다.

2026년 2월 26일·17분
이전 글8장: 캐싱 전략
다음 글10장: 프로덕션 운영과 모니터링

댓글

목차

약 13분 남음
  • 학습 목표
  • 왜 코드 생성인가
  • GraphQL Code Generator 설정
    • 설치
    • 설정 파일
    • 실행
  • 생성되는 타입
    • 스키마 타입
    • 쿼리 작업 타입
  • Fragment Colocation
    • Fragment 정의
    • Fragment 조합
  • 서버 리졸버 타입 생성
  • 코드 퍼스트와 타입 안전성
    • Pothos의 타입 추론
    • 스키마 퍼스트 vs 코드 퍼스트 비교
  • SDK 생성
  • CI 통합
  • 정리
    • 다음 장 미리보기