본문으로 건너뛰기
Advertisement

13.4 GraphQL + TypeScript — graphql-codegen으로 타입 자동 생성

GraphQL + TypeScript의 핵심 문제

GraphQL 스키마를 직접 TypeScript 타입으로 변환하는 것은 번거롭고 오류가 발생하기 쉽습니다. graphql-code-generator는 이 과정을 자동화합니다.

GraphQL Schema (.graphql) → graphql-codegen → TypeScript 타입 (.ts)

graphql-codegen 설치 및 설정

npm install --save-dev @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-resolvers @graphql-codegen/typescript-operations
# codegen.yml
overwrite: true
schema: "src/schema/**/*.graphql"
documents: "src/**/*.graphql"
generates:
src/generated/graphql.ts:
plugins:
- typescript
- typescript-resolvers
- typescript-operations
config:
strictScalars: true
scalars:
DateTime: string
JSON: Record<string, unknown>
// package.json
{
"scripts": {
"codegen": "graphql-codegen --config codegen.yml",
"codegen:watch": "graphql-codegen --config codegen.yml --watch"
}
}

서버 사이드: 리졸버 타입

# schema/user.graphql
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
createdAt: DateTime!
}

type Query {
user(id: ID!): User
users(page: Int, limit: Int): [User!]!
}

type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
}

input CreateUserInput {
name: String!
email: String!
}

input UpdateUserInput {
name: String
email: String
}
// resolvers/user.resolver.ts
import { Resolvers } from '../generated/graphql'
import { UserService } from '../services/user.service'

const userService = new UserService()

export const userResolvers: Resolvers = {
Query: {
// id: string (자동 타입 적용)
user: async (_, { id }) => {
return userService.findById(id)
},
users: async (_, { page, limit }) => {
return userService.findAll({ page: page ?? 1, limit: limit ?? 10 })
},
},

Mutation: {
createUser: async (_, { input }) => {
// input: CreateUserInput (자동 타입)
return userService.create(input)
},
updateUser: async (_, { id, input }) => {
return userService.update(id, input)
},
},

User: {
// 필드 리졸버
posts: async (user) => {
return postService.findByUserId(user.id)
},
},
}

Apollo Server + TypeScript

// src/server.ts
import { ApolloServer } from '@apollo/server'
import { startStandaloneServer } from '@apollo/server/standalone'
import { readFileSync } from 'fs'
import { join } from 'path'
import { userResolvers } from './resolvers/user.resolver'
import { postResolvers } from './resolvers/post.resolver'
import type { Context } from './types/context'

// 스키마 파일 로드
const typeDefs = readFileSync(join(__dirname, 'schema/schema.graphql'), 'utf-8')

// Context 타입
export interface Context {
user: AuthUser | null
db: DatabaseClient
dataSources: {
userAPI: UserAPI
}
}

const server = new ApolloServer<Context>({
typeDefs,
resolvers: [userResolvers, postResolvers],
})

const { url } = await startStandaloneServer(server, {
context: async ({ req }) => {
const token = req.headers.authorization?.replace('Bearer ', '')
const user = token ? await verifyToken(token) : null

return {
user,
db,
dataSources: { userAPI: new UserAPI() },
}
},
listen: { port: 4000 },
})

console.log(`🚀 GraphQL 서버: ${url}`)

클라이언트 사이드: 쿼리 타입

# src/queries/user.graphql
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
createdAt
}
}

query GetUsers($page: Int, $limit: Int) {
users(page: $page, limit: $limit) {
id
name
email
}
}

mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
}
}
// 생성된 타입 사용 (React + Apollo Client)
import {
useGetUserQuery,
useGetUsersQuery,
useCreateUserMutation,
GetUserQuery,
GetUserQueryVariables,
} from '../generated/graphql'

function UserProfile({ userId }: { userId: string }) {
const { data, loading, error } = useGetUserQuery({
variables: { id: userId }, // GetUserQueryVariables 타입
})

if (loading) return <div>로딩 중...</div>
if (error) return <div>오류: {error.message}</div>

const user = data?.user // GetUserQuery['user'] 타입
if (!user) return <div>사용자 없음</div>

return <div>{user.name} - {user.email}</div>
}

function CreateUserForm() {
const [createUser, { loading }] = useCreateUserMutation({
onCompleted: (data) => {
console.log('생성됨:', data.createUser.id) // 타입 안전
},
})

const handleSubmit = async (name: string, email: string) => {
await createUser({
variables: { input: { name, email } }, // 타입 안전
})
}
}

urql + TypeScript

Apollo 대신 urql을 사용하는 경우:

import { useQuery, useMutation } from 'urql'
import {
GetUserDocument,
CreateUserDocument,
GetUserQuery,
GetUserQueryVariables,
} from '../generated/graphql'

function UserComponent({ id }: { id: string }) {
const [{ data, fetching, error }] = useQuery<GetUserQuery, GetUserQueryVariables>({
query: GetUserDocument,
variables: { id },
})

return <div>{data?.user?.name}</div>
}

고수 팁

1. 스칼라 타입 커스터마이징

# codegen.yml
config:
scalars:
DateTime: Date # string 대신 Date 객체로
UUID: string
JSON: Record<string, unknown>
Upload: File

2. Fragment로 타입 재사용

fragment UserBasic on User {
id
name
email
}

query GetUser($id: ID!) {
user(id: $id) {
...UserBasic
createdAt
}
}
// 생성된 Fragment 타입 재사용
import { UserBasicFragment } from '../generated/graphql'

function UserCard({ user }: { user: UserBasicFragment }) {
return <div>{user.name}</div>
}
Advertisement