배경
최근 Apollo GraphQL 기반 express 서버와 Nestjs 서버를 통합하는 작업을 진행했다. 서로 다른 두 서버의 GraphQL 스키마 또한 통합하기 위해 graphql-tools/schema 패키지의 mergeSchemas 함수를 사용했다.
이 과정에서 printSchema를 호출하고, 이 때
- 리졸버(GraphQL의 API 개념)의 스키마를 검증 시 쿼리의 기본 값이 있으면서(Nestjs Graphql Resolver의 Enum 타입 인풋 @Arg 데코레이터에 defaultValue 옵션을 설정)
- enum 타입인 경우 그 Enum 타입이 키와 다른 내부 값을 가진다면
graphql/type 패키지의 deifinition.js 의 serialize 단계에서 오류가 발생했다.
- serialize 목적 : 서버 내부 데이터 값을 클라이언트에게 반환 가능한 GraphQL 응답 데이터로 변환(및 이를 검증)하는 것
아래는 재구성한 코드 스니펫이다. 아래 코드와 상응하는 오류 메시지는 "Enum SortOrderEnum cannot represent value: ASC" 이다.
// TypeScript enum & NestJS register
export enum SortOrderEnum {
ASC = 'asc',
DESC = 'desc',
}
registerEnum('sortOrder', SortOrderEnum);
// NestJS resolver
@Resolver()
export class ItemsResolver {
@Query(() => [String])
items(@Args('order', { type: () => SortOrder, defaultValue: SortOrder.ASC }) order: SortOrder) {
console.log(order); // 'asc' (문자열)
return ['item1', 'item2'];
}
}
// app.module.ts
@Module({
imports: [
GraphQLModule.forRootAsync<ApolloFederationDriverConfig>({
driver: ApolloFederationDriver,
useFactory: () => ({
transformSchema: nestSchema => {
const expressSchema = buildGraphQLSchema()
let mergedSchema = mergeSchemas({
schemas: [expressSchema, nestSchema],
})
return mergedSchema
}
})
})
],
// ...
})
export class AppModule { }
직접적인 원인
라이브러리 내부에 로그를 심어 테스트 한 결과 발견한 원인은 다음과 같다. graphql 패키지는 Enum 타입을 Enum value 를 키로 한 맵 자료구조(GraphQLEnumType 클래스의 valueLookup 필드)로 관리하는데, 이 데이터에서 Enum key를 맵의 키로 하여 값을 찾으려다 실패한 것이다.
- valueLookup 데이터
Map(1) {
'a' => {
name: 'A',
description: undefined,
value: 'a',
deprecationReason: undefined,
extensions: [Object: null prototype] {},
astNode: undefined
}
}
따라서 해결을 위해서는 다음의 세 가지 방법이 있다.
- default value 를 데코레이터가 아니라 코드로 구현
- => 데코레이터를 사용하지 않아 serialize 대상에서 제외하는 단순한 우회 전략이다.
- Nestjs 패키지의 enum 타입(valueLookup) 처리 로직을 변경 -> (모호)
- Graphql 패키지의 Enum 타입 관리 방식을 변경 -> (좋지 않음)
빠른 통합이 목적이었기에 팀에서는 우선적으로 1번의 방법을 선택하였지만 관련 부분을 더 조사하기로 했다.
근본적인 원인 분석
TypeScript 은 Enum type의 내부 값을 허용하며, 문자열 기반 Enum type은 숫자 기반 Enum과 달리 단방향 맵핑만 가능하다.
GraphQL은 Enum을 제한된 상수 값의 집합이라고 정의한다. 각 언어가 Enum 을 처리하는 방식에 상관 없이, 클라이언트와는 문자열만을 주고 받는다.
=> 즉 타입스크립트와 GraphQL 간 Enum 처리 방식 차이로 발생하는 충돌은 개발자가 명시적으로 대응해야 한다.

고찰
그렇다면 Nestjs/graphql 라이브러리가 registerEnum을 처리할 때 역맵핑도 지원하면 어떨까? 그런데 왜 Nestjs 팀은 그렇게 하지 않았을까? GraphQL 명세에 기반한 예측가능성을 해칠 수 있다.
Nestjs의 명시성과 단순성 ~ 개발자에게 명확한 Enum 설계 책임이 trade-off 관계인 것 같다.
그래서 옵션으로 제어하는 것은 어떨지 이슈를 올려보았다.
회고
enum 타입과 nestjs - apollo - graphql 간 동작에 대해 알아보는 계기가 되었다.
추후 개선작업이 가능하다면
- NestJS의 Enum 등록 과정에서 value 기반 defaultValue를 자동 설정하도록 커스텀 Decorator를 구현하거나,
- GraphQL Enum 타입을 생성할 때 TypeScript Enum을 value 중심 구조로 변환하는 래퍼를 작성할 수 있을 것이다.
참고
- 코드 https://github.com/graphql/graphql-js/blob/16.x.x/src/type/definition.ts#L1415
- GraphQL - Enum types https://graphql.org/learn/schema/#enum-type
- 오류 메시지 스택 전문
GraphQLError: Enum "EnumTypeName" cannot represent value: "EnumKey"
at GraphQLEnumType.serialize (.../graphql/type/definition.js:1121:13)
at astFromValue (.../graphql/utilities/astFromValue.js:123:29)
at astFromValue (.../graphql/utilities/astFromValue.js:45:22)
at astFromValue (.../graphql/utilities/astFromValue.js:100:26)
at astFromArg (.../@graphql-tools/merge/node_modules/@graphql-tools/utils/cjs/print-schema-with-directives.js:219:89)
at .../@graphql-tools/merge/node_modules/@graphql-tools/utils/cjs/print-schema-with-directives.js:378:42
at Array.map (<anonymous>)
at astFromField (.../@graphql-tools/merge/node_modules/@graphql-tools/utils/cjs/print-schema-with-directives.js:378:31)
at .../@graphql-tools/merge/node_modules/@graphql-tools/utils/cjs/print-schema-with-directives.js:239:62
at Array.map (<anonymous>)
at astFromObjectType (.../@graphql-tools/utils/cjs/print-schema-with-directives.js:239:49)
at getDocumentNodeFromSchema (.../@graphql-tools/merge/node_modules/@graphql-tools/utils/cjs/print-schema-with-directives.js:30:30)
at visitTypeSources (.../@graphql-tools/merge/cjs/typedefs-mergers/merge-typedefs.js:44:72)
at visitTypeSources (.../@graphql-tools/merge/cjs/typedefs-mergers/merge-typedefs.js:40:17)
at mergeGraphQLTypes (.../@graphql-tools/merge/cjs/typedefs-mergers/merge-typedefs.js:71:41)
at mergeTypeDefs (.../@graphql-tools/merge/cjs/typedefs-mergers/merge-typedefs.js:13:22)
at makeExecutableSchema (.../@graphql-tools/schema/cjs/makeExecutableSchema.js:72:58)
at mergeSchemas (.../@graphql-tools/schema/cjs/merge-schemas.js:32:63)
at transformSchema (.../src/app.module.ts:75:42)
at GraphQLFederationFactory.generateSchema (.../@nestjs/graphql/dist/federation/graphql-federation.factory.js:38:22)
at async GraphQLModule.onModuleInit (.../@nestjs/graphql/dist/graphql.module.js:109:27)
at async callModuleInitHook (.../@nestjs/core/hooks/on-module-init.hook.js:51:9)
'기술 조사 > 기타' 카테고리의 다른 글
| Elasticsearch 에 인덱싱할 때 CPU 이슈 (2) | 2026.02.24 |
|---|---|
| prisma의 마이그레이션 히스토리 관리와 개발 생산성 사이의 고민 (0) | 2026.02.08 |
| reflect-metadata 와 Nest.js (4) | 2025.02.15 |
| DataLoader 는 어떻게 GraphQL의 1+N 문제를 해결하는지 (7) | 2024.11.07 |