📝

Custom Scalar Type 사용하기

Created
2022/12/05 04:05
Tags
안녕하세요. Bold9 서버 개발팀에서 일하고 있는 박연호입니다.
이번 글의 주제는 [GraphQL에서 Custom Scalar Type 제대로 알고 써보기!] 입니다. 글의 주제인 Custom Scalar Type은 Scalar Type을 직접 정의한다는 의미인 것 같은데… 여기서 말하는 Scalar Type은 무엇을 의미하는 걸까요?
[1, true, “apple”, 2, “banana”] 값을 가지는 배열이 있다고 가정하겠습니다. 여기서 각 원소 데이터를 다음과 같이 분류할 수 있습니다.
number : [1, 2]
string : [”apple”, “banana”]
boolean : [true]
각각의 원소 값은 number, string, boolean으로 구분할 수 있으며, 여기서 number, string, boolean을 Scalar Type이라고 합니다. Scalar Type은 한 번에 하나의 값만 저장할 수 있는 single value를 의미합니다.
Scalar와 대조되는 개념은 compound으로 위의 배열처럼, 여러개의 Scalar Type 데이터를 저장할 수 있습니다.
GraphQL 스펙에서는 Scalar Type을 다음과 같이 설명하고 있습니다.

Scalar types represent primitive leaf values in a GraphQL type system.

leaf value라고 표현한 것이 인상적인데, 클라이언트가 아래처럼 질의했을 때 더 이상 최하단의 노드를 leaft value라고 합니다. 이 경우 Scalar Type을 반환하게 됩니다.
query{ users{ email - leaf value age - leaf value } }
JavaScript
복사
GraphQL에서 기본적으로 지원하는 Scalar Type은 다음과 같으며 스펙에 맞게 구현되어 있습니다.

Int

The Int scalar type represents a signed 32‐bit numeric non‐fractional value. Response formats that support a 32‐bit integer or a number type should use that type to represent this scalar.

Float

The Float scalar type represents signed double‐precision fractional values as specified by IEEE 754. Response formats that support an appropriate double‐precision number type should use that type to represent this scalar.

String

The String scalar type represents textual data, represented as UTF‐8 character sequences. The String type is most often used by GraphQL to represent free‐form human‐readable text. All response formats must support string representations, and that representation must be used here.

Boolean

The Boolean scalar type represents true or false. Response formats should use a built‐in boolean type if supported; otherwise, they should use their representation of the integers 1 and 0.

ID

The ID scalar type represents a unique identifier, often used to refetch an object or as the key for a cache. The ID type is serialized in the same way as a String; however, it is not intended to be human‐readable. While it is often numeric, it should always serialize as a String.
하지만 개발을 하다 보면 기본 Scalar Type으로만 SDL을 표현하는데 조금 부족한 부분이 있으며, 상황에 따라 좀 더 세부적으로 표현하고 싶은 경우가 있을 수 있습니다. 예를 들어, 다음과 같은 경우입니다.
 개발자 1 : 유저의 email 필드를 정의해야 하는데, String 타입을 사용해도 문제는 없지만 해당 필드는 email이 라고 명시하고 싶어.
 개발자 2 : 하루를 24시간을 표현하고 싶어. Int는 24보다 더 다양한 숫자를 표현할 수 있기 때문에 적절하지가 않아.
위처럼 SDL은 좀 더 expressive 하게 사용하기 위해 기본 타입 이외의 Scalar Type을 정의할 수 있어야 합니다.
위의 개발자 2번의 요구사항을 충족시킬 수 있는 “24시간을 표현하는 정수값” Scalar Type을 정의하면 다음과 같습니다.
const nychthemeron = new GraphQLScalarType({ name: 'Nychthemeron', description: '24시간을 표현하는 정수값', serialize(value) { if (typeof value !== 'number') throw new Error('Nychthemeron는 정수값이여야 합니다.') if (value < 1 || value > 24) throw new Error('Nychthemeron의 범위는 1 ~ 24 입니다.') }, parseValue(value) { if (typeof value !== 'number') throw new Error('Nychthemeron는 정수값이여야 합니다.') if (value < 1 || value > 24) throw new Error('Nychthemeron의 범위는 1 ~ 24 입니다.') return value }, parseLiteral(ast) { if (ast.kind !== Kind.INT) throw new Error('Nychthemeron는 정수값이여야 합니다.') const num = parseInt(ast.value) if (num < 1 || num > 24) throw new Error('Nychthemeron의 범위는 1 ~ 24 입니다.') return num }, })
JavaScript
복사
Scalar Type은 단지 껍데기 일뿐이며, 실제 런타임에 어떻게 동작하는지에 대해서는 개발자가 직접 정의해 줘야 합니다. 어떻게 동작하는지에 대해서는 정의할 수 있게 Scalar Type은 3가지 메서드를 제공하고 있습니다.
serialize : 서버에서 클라이언트로 데이터를 보낼 때 실행
parseValue : 클라이언트에서 서버로 데이터를 보낼 때 실행
parseLiteral : 클라이언트에서 서버로 데이터를 보낼 때 실행
각각의 메서드는 하나의 “문”입니다. 서버 → 클라이언트, 클라이언트 → 서버로 데이터가 오갈 때 해당 메서드를 거쳐가며 내부에서 내가 정의한 Scalar Type의 성격에 맞게 검증할 수 있습니다.
위에서는 24시간을 표현하는 Nychthemeron Scalar Type을 정의했으며, Nychthemeron type의 필드 데이터는 반드시 “1~24 범위를 가지는 정수값”이여야 하며, 각 메서드에서 이를 검증하고 있습니다.
신기한 점은 클라이언트에서 서버로 데이터를 보낼 때 사용할 수 있는 메서드가 2개가 존재하는데, 이는 클라이언트가 어떻게 쿼리의 변수를 보내는지에 따라 나뉩니다.

parseValue

query($hour: Nychthemeron){ testQuery(hour: $hour){ ... } } - Variables { "hour": 24 }
JavaScript
복사

parseLiteral

query{ testQuery(hour:23){ ... } }
JavaScript
복사
parseLiteral의 경우 변수가 코드상에서 Graphql Query에 들어간 상태로 서버로 보내지게 되지만, parseValue는 변수가 Json 형태이기 때문에 서버에서 이 둘을 파싱 하는 방법이 다릅니다.
parseLiteral의 경우 서버에서 AST(Abstract Syntax Tree) 형태로 파싱 후 사용하게 됩니다. 하지만 사용한 변수를 객체에 담아 전달하는 경우 그 자체로 JSON이며, 이미 JSON 값을 가지고 있기 때문에 별다른 파싱 과정 없이 바로 사용할 수 있으며 이때 parseValue를 사용하게 됩니다.
parseLiteral 메서드의 타입은 다음과 같으며 첫 번째 인자인 ValueNode의 타입은 여기에서 확인할 수 있습니다.
export type GraphQLScalarLiteralParser<TInternal> = ( valueNode: ValueNode, variables: Maybe<{ [key: string]: any }>, ) => Maybe<TInternal>;
JavaScript
복사
위에서 만든 Scalar Type의 경우 INT 값이기 때문에 사용하는 ValueNode 타입은 아래와 같습니다.
export interface IntValueNode { readonly kind: 'IntValue'; readonly loc?: Location; readonly value: string; }
JavaScript
복사
value : 클라이언트에서 보낸 변수값
kind : value의 타입
loc : AST 상의 영역을 식별하는 UTF-8 문자 오프셋 및 토큰을 참조
여기서 kind는 AST node가 가질 수 있는 값을 의미하는데, 그 종류는 여기서 확인할 수 있습니다.
GraphQL을 사용하면서 얻을 수 있는 장점 중 하나는 직관적인 SDL이며, SDL 개발자 뿐만 아니라 비 개발자도 보고 이해할 수 있을 정도로 충분히 expressive 하게 작성해야 합니다. 그 방법 중 하나가 Custom Scalar Type이며 적절하게 사용했을 때는 더 풍부한 SDL이 될 수 있다고 생각합니다.
뿐만 아니라, Scalar Type의 각각 메서드에서 적절한 validation을 할 수 있는 것 또한 장점이라고 생각합니다.

볼드나인과 함께해요