본문 바로가기

배움의 즐거움/프로그래밍

(3) GraphQL - 스키마와 타입

반응형


Schemas and Types


이번 페이지에서는 GraphQL 타입 시스템에 대해 알아야 할 모든 것과 어떤 데이터가 요청될 수 있는지 설명하는 방법에 대해 알아본다. GraphQL 이 모든 벡엔드 프레임 워크나 프로그래밍 언어와 함께 사용될 수 있기 때문에 우리는 구체적인 적용보다는 오직 개념에 대해서 알아볼 것이다.

Type system


GraphQL 을 전에 본 적이 있다면 GraphQL 쿼리 언어는 기본적으로 객체에서 필드를 선택하는 것에 관한 것이라는 것을 알 수 있다. 예를 들어 다음 쿼리를 보자.

  1. 특별한 "root" 객체 에서 시작한다.
  2. 해당 객체에서 hero 필드를 선택한다.
  3. 리턴되는 hero 객체에 대해 우리는 name과 appearsIn 필드를 선택하였다.


GraphQL 쿼리가 결과와 굉장히 매치하기 때문에 우리는 서버에 대해서 알지 않아도 뭐가 리턴될 것인지 예측할 수 있다. 그러나 어떠한 필드를 선택할 수 있는지, 어떠한 데이터를 요청할 수 있는지, 어떠한 객체자 리턴될 것인지, 어떠한 필드가 하위 객체에서 선택 가능한지 등에 대한 설명이 있으면 유용할 것이다. 이게 스키마가 필요한 이유다.

각 GraphQL 서비스는 너가 해당 서비스에 쿼리할 수 있는 가능한 데이터 set 을 보여주는 set of types 을 정의한다. 그리고 쿼리가 들어오면, 그 서비스는 유효성을 검사하고 해당 스키마에 따라 실행한다.

Type language


GraphQL 서비스는 어떠한 언어로도 작성될 수 있다. GraphQL 스키마에 대해 설명하기 위하여 우리가 특정 프로그래밍 언어의 문법에 의존할 수 없기 때문에, 우리는 우리만의 간단한 언어를 정의할 것이다. 우리는 "GraphQL 스키마 언어" 를 사용할 것인데, 이것은 쿼리 언어와 비슷하고 또 언어에 상관 없이 GraphQL 스키마에 대해 설명할 수 있기 때문이다.

Object types and fields


가장 기본적인 GraphQL 스키마 구성 요소는 단순이 너가 너의 서비스에서 가져올 수 있는 객체와 그 객체가 가지고 있는 필드를 보여주는 object type 이다.  GraphQL 스키마 언어에서는 아마 이렇게 표현할 수 있다.

type Character {
  name: String!
  appearsIn: [Episode!]!
}

module Types UserType = GraphQL::ObjectType.define do name 'User' field :id, !ID field :email, !String end end


Types::ProjectType = GraphQL::ObjectType.define do name 'Project' field :id, !types.ID field :title, !types.String do resolve ->(project, args, ctx) { project.user.email + '-' + project.title } end field :user, !Types::UserType end


- 루비에서는 이렇게 오브젝트 타입을 보여줄 수 있다.(나는 루비스트이므로 내맘대로 추가함 데헷)

이 언어는 상당히 가독성이 좋으나 그래도 우리의 이해를 위해서 한번 살펴보자.

  • Character 는 GraphQL Object Type 이다. 무슨 말이냐하면, 몇 개의 필드를 가진 타입이라는 뜻이다. 스키마의 부분의 타입은 object type 일 것이다.
  • name 와 appearsIn 은  Character 타입의 필드들이다. 그것은 name 와 appearsIn 은 Character 타입에 요청된 쿼리에서만 나타날 수 있다는 것을 뜻한다.
  • String 은 내장된 스칼라 타입인데, 이는 단순 스칼라 객체를 위한 것으로, 쿼리에서 하위에 선택을 가질 수 없다. 우리는 스칼라 타입에 대해서 이따가 좀 더 알아볼 것이다.
  • String! 은 해당 필드가 null이 될 수 없다는 것을 뜻한다. 이것은 너가 해당 필드에 쿼리를 요청하였을 때  GraphQL 서비스가 항상 너에게 값을 제공해 줄 것이라는 것을 뜻한다. 타입 언어에서는 보통 이를 느낌표로 표현한다.
  • [Episode!]! 는 Episode 객체의 배열을 표현한다. 이것이 또한 null이 될 수 없기 때문에 appearsIn 필드에 쿼리를 요청하였을 때, 0개 또는 하나 이상의 아이템을 가진 배열을 예상할 수 있다. 


이제 당신은 GraphQL object type 이 어떻게 생겼는지, 그리고 GraphQL 타입 언어의 기본적인 것을 어떻게 읽는지 알게 되었다.

Arguments


GraphQL object type 에서 모든 필드는 0개 이상의 argument를 가질 수 있다. 예를 들어 아래 length 필드를 보자.

type Starship {
  id: ID!
  name: String!
  length(unit: LengthUnit = METER): Float
}

모든 argument 들은 이름이 있다. 자바스크립트나 파이썬같이 순서가 있는 argument 의 리스트를 받는 언어와는 달리, GraphQL에서 모든 argument 는 이름이 명시되어 전해진다. 이 경우에는 length  필드는 unit 이라는 하나의 정의된 argument가 있다.

argument 는 필수거나 선택이 될 수 있다. 만약 argument가 선택이라면, 우리는 디폴트 값을 정의할 수 있다. - 만약 unit argument 가 들어오지 않는다면, 우리는 METER 값을 디폴트로 설정하면 된다.

The Query and Mutation types


스키마에서 대부분의 타입은 일반적인 object type이다. 그러나 두 가지 특별한 타입이 있다.

schema {
  query: Query
  mutation: Mutation
}

모든 GraphQL service 는 query 타입과 mutation 타입(선택)이 있다. 이 두가지 타입은 일반적인 객체 타입과 같으나, 모든 GraphQL 쿼리에서 첫 부분에 정의 된다는 점에서 특별하다. 만약에 다음과 같은 쿼리를 본다면


이 것은 GraphQL service 가 droid와 드로이드 필드를 가진 Query 타입이 있어야 한다는 것을 뜻한다.

type Query {
  hero(episode: Episode): Character
  droid(id: ID!): Droid
}

module Types class QueryType < Types::BaseObject field :all_users, [Types::UserType], null: false, description: "all users" def all_users User.all end field :all_projects, [Types::ProjectType], null: false, description: "all projects" def projects Project.all end end end

- 쿼리 타입에 all_users 필드와 all_projects 필드 추가

Mutation도 비슷하게 동작한다. Mutation type에 필드를 정의하면 해당 필드들은 요청이 가능하게 된다.

query 와 mutation 타입은 스키마에서 "entry point" 가 된다는 특별한 점 외에도, 다른 GraphQL object tpye과 같으며 그들의 필드도 같은 방법으로 동작한다는 것을 기억하는 것은 중요하다. 

Scalar types


GraphQL object tpye은 이름과 필드가 있는데, 이 필드들 역시 데이터를 다뤄야 한다. 이 때 스칼라 타입이 쓰일 수 있다. 이들은 쿼리에서 잎(leaves)을 나타낸다.

다음 쿼리에서 name과 appearsIn 필드는 스칼라 타입을 다룬다.

이는 해당 필드들이 하위 필들르 가지고 있지 않기 때문이기에 알 수 있다. 이들은 쿼리의 잎(leaves)이다.

GraphQL 는 기본적인 디폴트 스칼라 타입을 가지고 있다.

  • Int: A signed 32‐bit integer.
  • Float: A signed double-precision floating-point value.
  • String: A UTF‐8 character sequence.
  • Booleantrue or false.
  • ID: ID 스칼라 타입은 unique identifier 를 나타내는데, 때때로 객체를 다시 가져오거나, 캐시의 키로써 사용된다. 이 ID 타입은 문자열과 같은 방법으로 serialized 되지만, ID 타입으로 지정한다는 것은 인간이 읽기 편하게 만들어진 것은 아니라는 것을 나타낸다.

대부분의 GraphQL 서비스 구현에서는 커스텀 스칼라 타입을 명시하는 방법이 있다. 예를 들어 Date 타입을 정의해보자.

scalar Date

이것은 어떻게 타입이 serialized/deserialized/validated 되어야 하는지를 정의하기 위한 우리의 구현에 달려있다. 예를 들어, Date 타입이 항상 integer 타임 스템프에 serialized 되도록 표현할 수 있고, 당신의 클라이언트는 이를 Date필드의 포멧에 대해 알아야 한다.

Enumeration types


Enums 이라고도 불리우는 enumeration 타입은 스칼라의 특별한 종류중 하나로, 허용된 값들의 집합에 제한된다. 다음과 같은 것을 할 수 있게끔 한다.

  1. 이 타입의 모든 arguments이 허용된 값 중 하나인지 유효성 검사를 한다.
  2. 어떠한 필드가 항상 유한한 값들의 집합중 하나가 될 것이라는 것을 타입 시스템을 통해 전달한다.

GraphQL 스키마 언어에서는 enum 정의가 다음과 같을 것이다.

enum Episode {
  NEWHOPE
  EMPIRE
  JEDI
}

이 것은 Episode 타입을 스키마에 사용할 때마다 이것은 정확히 NEWHOPEEMPIRE, 또는 JEDI중 하나라는 것을 예상할 수 있다는 것을 뜻한다.

다양한 언어에서 GraphQL 서비스 구현은 그 언어만의 구체적인 enum을 다루는 방식이 있을 것이다. enum을 first-class citizen 으로서 다루는 언어에서는, 구현 시 이 기능을 사용할 수 있다. 자바스크립트처럼 enum 지원이 없는 언어에서는 정수 집합으로 내부적으로 표현될 수 있을 것이다. 그러나 이러한 세부 사항은 클라이언트 측에서는 알 수 없으며 enum 값의 문자열 이름에 관해서만 동작할 수 있다.

Lists and Non-Null


object type, scalar, 그리고 enum 은 GraphQL 에서 정의할 수 있는 유일한 타입들이다. 그러나 스키마의 다른 파트에서 타입을 쓸 때에나 또는 쿼리 변수 선언에서 해당 값들의 유효성에 영향을 주는 추가적인 타입 modifier 들을 적용할 수 있다. 아래 예시를 보자.

type Character { name: String! appearsIn: [Episode]! }

여기서 보면 우리는 String 타입을 사용하고 있는데 타입 이름 다음에 느낌표(!)를 추가함으로써 Non-Null 값이라는 것을 명시해준다. 이것은 서버가 항상 해당 필드에 대해 non-null 값을 리턴할 것이며, null 값을 받을 시에는 클라이언트에게 무언가 잘못되었다는 것을 알려주며 GraphQL 실행 에러를 발생시킬 것이다.

non-null 타입 modifier 은 또한 필드에 argument 를 정의할 때에도 사용될 수 있다. 만약 GraphQL 문자열이나 변수에 null 값이 argument로 패스된다면, GraphQL 서버가 유효성 에러를 리턴할 것이다.


리스트도 비슷한 방식으로 동작한다. 우리는 어떤 타입을 List 타입을 정의하기 위해 type modifier을 사용할 수 있는데, 이는 해당 필드가 그 타입의 배열을 반환할 것이라는 것을 나타낸다. 스키마 언어에서는 [ type ] 이렇게 타입을 대괄호 안에 넣는 것으로 표현한다. argument 에서도 같은 방식으로 표현되는데, 유효성 검사에서 값의 배열을 예상하게 된다.


non-null 과 배열 modifier 은 합쳐서 사용될 수 있다. 예를 들면 non-null 문자열의 리스트을 나타낼 수 있다.

myField: [String!]

이것은 리스트 자체는 null 이 될 수 있으나, 리스트가 null 값을 가질 수 없다는 것을 의미한다. 예를 들면 JSON:

myField: null // valid
myField: [] // valid
myField: ['a', 'b'] // valid
myField: ['a', null, 'b'] // error

Now, let's say we defined a Non-Null List of Strings:

그리고 문자열의 non-null 리스트를 정의한다고 보자. (위에랑 다름)

myField: [String]!

이것은 리스트 자체는 null 이 될 수 없으나, 이 리스트가 가지고 있는 값은 null이 될 수 있음을 뜻한다.

myField: null // error
myField: [] // valid
myField: ['a', 'b'] // valid
myField: ['a', null, 'b'] // valid

필요에 따라 자율적으로 여러개의 non-null, 리스트 modifier을 사용할 수 있다.

Interfaces


많은 타입 시스템과 마찬가지로 GraphQL 은 인터페이스를 지원한다. 인터페이스란 인터페이스를 구현하기 위해 타입이 반드시 포함해야 하는 특정 필드 집합을 포함하는 추상 타입이다. (?.....무슨말)

예를 들어, 스타워즈 시리즈의 어떤 캐릭터든 표현하는 Character라는 인터페이스를 가질 수 있다.

interface Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
}

이 것은 인터페이스 Character 를 구현하는 어떤 타입이던 이 필드들 그리고 arguments 와 리턴 타입을 똑같이 가지고 있어야 한다는 것을 의미한다.


예를 들어 Character를 구현한 타입을 보자.

type Human implements Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
  starships: [Starship]
  totalCredits: Int
}

type Droid implements Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
  primaryFunction: String
}

두 가지 타입 모두 Character 인터페이스의 필드들을 가지고 있는 것을 볼 수 있다. 그러나 또한 각 캐릭터 타입에 따라 totalCreditsstarships, primaryFunction 과 같은 추가적인 필드가 있다.

인터페이스는 다른 타입일 수도 있는 객체나 객체의 셋을 리턴하고자 할 때 유용하다.


예를 들어, 아래 쿼리가 에러를 발생하는 걸 보자.


예를 들어 hero 필드는 Character 타입을 반환하는데, episode argument 에 따라 이는 Human 이 되거나 Droid 가 될 수 있다. 위의 쿼리에서 Character 인터페이스에만 존재하는 필드만 요청할 수 있는데, 이는 primaryFunction 를 포함하지 않는다.

특정 객체 타입에만 있는 필드를 요청하기 위해서는 inline fragment 를 사용해야 한다.

inline fragments 에 대해서는 여기서 더 배울 수 있다.

Union types


Union types 은  인터페이스와 굉장히 비슷하지만 타입간의 공통 필드를 명시하지는 않는다.

union SearchResult = Human | Droid | Starship

스키마에 있는 SearchResult 타입을 리턴할 때마다 HumanDroid, 또는 Starship 중 하나를 받게 될 것이다. 여기서 중요한 것은 union type 의 구성 요소들은 반드시 객체 타입이라는 것이다. 인터페이스나 다른 union 타입으로는 union 타입을 만들 수 없다.

이 경우에는, SearchResult union 타입을 리턴하는 필드에 쿼리를 넣는다면, 필드에 쿼리를 하기 위해서는 반드시 조건부 fragment 를 사용하여야 한다.


Input types


지금까지 우리는 enum이나 문자열과 같은 스칼라 값을 argument로 필드에 보내는 것에 대해서만 이야기 했다. 이 것은 생성이 될 전체 객체를 전달하는 것을 필요로 하는 mutation 의 경우에는 특히 중요하다. GraphQL 스키마 언어에서는, 키워드가 type이 아니라 input인 것만 제외하고 input 타입은 일반적인 객체 타입과 똑같이 보인다.

input ReviewInput {
  stars: Int!
  commentary: String
}

input 객체타입을 mutation에서 사용하는 방법은 다음과 같다.


input 객체타입의 필드는 input 객체타입을 참조할 수 있지만, 스키마에 input과 output 타입을 섞을 순 없다. input 객체타입은 또한 필드에 argument 를 가질 수 없다.


* 해당 글은 번역기 돌리다가 크롬 번역기 말도 안되는 해석에 지친 본인이 나중에 참고할 의도로 대충대충 발로 해석한 것이니 참고용으로만 사용하시길 바랍니다.

* 출처: https://graphql.github.io/learn/schema/


반응형

'배움의 즐거움 > 프로그래밍' 카테고리의 다른 글

(6) GraphQL - 인트로스펙션  (0) 2018.12.31
(5) GraphQL - 실행  (0) 2018.12.31
(4) GraphQL - 유효성 검사  (0) 2018.12.31
(2) GraphQL - 쿼리와 뮤테이션  (1) 2018.12.31
(1) GraphQL - 소개  (0) 2018.12.31