본문 바로가기
graphql

[GraphQL] custom scalar type 사용하기

by 박연호의 개발 블로그 2022. 10. 9.

scalar라는 단어는 선형대수학에서 처음 나왔는데, 행렬과 벡터에서 하나의 수를 구분하기 위해 사용되었다고 합니다. 의미는 프로그래밍에서 사용하는 것과 유사한데, 자료구조에서 원소를 구분할 있는 single value scalar라고 합니다.

 

[12, 6, true, "a", "b"] 배열에 있다면 각각의 원소값은 number(12,6), string(a, b), boolean(true)로 구분할 수 있으며 이때 number, string, boolean이 scalar 값을 의미합니다.

 

scalar와 대조되는 개념은 compound이며 compound는 array, map, set, struct 처럼 여러 scalar 값을 포함하는 것을 의미합니다. 

종종 scalar와 primitive를 헷갈리는데, primitive는 reference와 대조되는 개념입니다. 

 

scalar를 말할 때 어떤 언어에서 scalar를 말하느냐에 따라 그 값이 scalar인지 compound인지 조금씩 다릅니다.

 

예를 들어 문자열의 경우 자바스크립트의 경우 하나의 value이며, 불변값입니다. 즉 scalar값입니다. 하지만 c에서 문자열은 문자(character)의 배열이기 때문에 compound가 됩니다.


GraphQL scalar types

GraphQL에서도 Scalar type이 존재하며 공식 문서에서는 다음과 같이 설명하고 있습니다.

Scalar types represent primitive leaf values in a GraphQL type system. GraphQL responses take the form of a hierarchical tree; the leaves on these trees are GraphQL scalars.

scalar types는 GraphQL type system에서 기본적인 leaf value이며, GraphQL 트리 구조에서 이러한 leaf들이 scalar type 입니다.즉 데이터를 표현하는 가장 최소한의 값을 의미하게 됩니다.

  type Profile {
    id: Int!
    url: String!
  }

  type User {
    id: Int!
    name: String
    profile: Profile!
  }

User의 id, name와 Profile의 id는 더 이상 분리할 수 없는 최소한의 값이기 때문에 scalar type입니다. 반면에 User의 profile은 다시 Profile을 표현하기 때문에 Object type이 됩니다. 

 

GraphQL 공식문서에서 지원하는 default scalar type은 다음과 같습니다.


custom scalar type

기본적으로 5가지 scalar type을 지원해주긴 하지만, 경우에 따라 좀 더 다양한 scalar type이 필요할 수도 있습니다.

 

email의 경우 String scalar type으로 해 놓으면 어떤 문자열이든 다 들어갈 수 있습니다. 하지만 email scalar type을 만들어 놓으면 해당 field의 값이 email 형태라는 것을 쉽게 알 수가 있으며, 이메일 형태가 맞는지 검증까지 할 수 있습니다. 

 

실제로 github graphql api, shopify graphql api에서 다양한 custom scalar type을 사용하고 있습니다.

 

이제 우리가 원하는 scalar type을 직접 정의해 봅시다.

const { GraphQLScalarType, Kind } = require('graphql');

const ThreeLetterName = new GraphQLScalarType({
    name: "ThreeLetterName",
    description: "ThreeLetterName custom scalar type",
    serialize(value) {
      if (typeof value !== "string")
        throw new Error("ThreeLetterName cannot represent non-string value")
      if (value.length !== 3)
        throw new Error("ThreeLetterName must be three letter")
      return value
    },
    parseValue(value) {
      if (typeof value !== "string")
        throw new Error("ThreeLetterName cannot represent non-string value")
      if (value.length !== 3)
        throw new Error("ThreeLetterName must be three letter")
      return value
    },
    parseLiteral(ast) {
      if (ast.kind !== Kind.STRING)
        throw new Error("ThreeLetterName cannot represent non-string value")

      if (ast.value.length !== 3)
        throw new Error("ThreeLetterName must be three letter")
      return null
    },
  }),

위에서 name에 들어가는 값이 실제 SDL에서 사용하는 scalar type이며, code-first하게 만들었다면 자동으로 SDL에 명시 되겠지만, schema-first하게 만들었다면 ThreeLetterName를 resolver에 추가한 후, SDL에 추가해주어야 사용할 수 있습니다.

scalar ThreeLetterName

scalar type을 만들 때 사용하는 메서드에는 총 3개가 있습니다.

  • serialize : 서버에서 클라이언트로 데이터를 보낼 때 실행
  • parseValue : 클라이언트에서 서버로 데이터를 보낼때 실행
  • parseLiteral : 클라이언트에서 서버로 데이터를 보낼때 실행

위의 3개 메서드는 해당 scalar type으로 정의한 필드를  클라이언트에서서버로 데이터를 보낼 때(parseValue, parseLiteral), 서버에서 클라이언트로 데이터를 보낼 때(serialize) 실행되게 됩니다.

클라이언트 -> parseValue, parseLiteral -> 서버
서버 -> serialize -> 클라이언트

각 메서드는 서버 - 클라이언트 사이에서 우리가 정의한 scaalr type에 대한 검증을 하게 됩니다. data type, 문자열 길이, 정규식...등등 어떤 scalar type을 정의하는지에 따라 메서드에서 하는 일이 다릅니다.

 

위에서 만든 scalar type은 길이가 3인 문자열으로 정의했습니다. SDL 상의 scalar type은 껍데기 일 뿐이며, 위의 3개 메서드에서 런타임에서 실제로 어떻게 동작하는지 정의를 해주어야 합니다.

-- SDL
type User {
  id: Int!
  name: ThreeLetterName
}

type Query {
  user(name: ThreeLetterName): User
}
  
  
-- Query  
query{
 user(name:"pyh") {
   name
  }
}

serialize : 서버에서 클라이언트로 데이터를 보내줄 때 실행

value값은 서버의 User.name resolver함수에서 반환한 값입니다. 여기에는 어떠한 타입의 값이 들어올 수 있습니다. 만약 value가 boolean값이고 어떠한 검증이 없다면 클라이언트가 받는 User.name은 boolean이 됩니다. 말이 안되는 경우죠. 그렇기 때문에 우리는 여기서 잘~검증을 해주어야 합니다.

 

parseValue, parseLiteral : 클라이언트에서 서버로 데이터를 보내줄 때 실행

serialize와 마찬가지로 클라이언트에서 서버로 데이터를 보낼 때 데이터에 대한 우리가 원하는 데이터가 맞는지 검증을 하게 됩니다. 

두개의 메서드가 존재하는 이유는 클라이언트에서 서버로 데이터를 보내는 방식의 차이가 있기 때문입니다.

- parseValue
query($name:ThreeLetterName){
  user(name:$name) {
    name
  }
}
variable : { name: "pyh" }

- parseLiteral
query{
  user(name:"pyh") {
    name
  }
}

클라이언트에서 서버로 값을 보낼 때 variable에 넣거나, query에 문자열 그대로 넘기는 경우가 있습니다. 전자의 경우 parseValue 메서드가 실행되며, 후자의 경우 parseLiteral이 실행됩니다. 

 

parseLiteral 메서드가 실행되는 경우 클라이언트의 넘겨주는 값은 query 문자열에 포함되어 있습니다. 이는 Json형태가 아니기 때문에 AST(Abstract Syntax Tree)로 변환되고 이것을 해석한 값을 사용하게 됩니다. 반면에 parseValue의 경우 variable이 Json 형태이기 때문에 바로 사용할 수 있습니다. 

 


참고

- https://www.apollographql.com/docs/apollo-server/schema/custom-scalars/#serialize

- https://stackoverflow.com/questions/41510880/whats-the-difference-between-parsevalue-and-parseliteral-in-graphqlscalartype

- https://www.npmjs.com/package/graphql-scalars