성장, 그리고 노력

부족하더라도 어제보다 더 잘해지자. 노력은 절대 배신하지 않는다.

GraphQL

[GraphQL] 4. GraphQL 스키마 정의

제이콥(JACOB) 2019. 12. 9. 00:38

Schema-First Development

스키마 우선주의는 디자인 방법론의 일종이다. 개발시 스키마를 우선 개발하는 것이다. 여기서 스키마(Schema)란 데이터 타입의 집합이다. 이를 미리 정의해 두면, 스키마 정의는 API 문서 같은 역할을 하며, 프론트엔드 개발자와 백엔드 개발자가 많은 의사소통에 대한 비용을 줄이고 빠른 개발을 할 수 있다는 장점이 있다. 백엔드 개발자는 어떤 데이터를 전달해야 하는지, 프론트엔드 개발자는 인터페이스 작업 작업을 할 때 필요한 데이터를 정의할 수 있는 것이다.

하지만, 당연히 장점만 존재하지 않다. 단점은 이 글의 취지와는 멀기 때문에 링크를 첨부한다.

 

GraphQL Schema 설계하기

GraphQL의 API을 설계하기 전에 항상 사용할 스키마(Schema)를 먼저 정의해야 한다. 왜냐하면 GraphQL 쿼리의 형태는 리턴되는 값과 거의 일치한다. 어떤 필드를 선택할지 어떤 종류의 객체를 반환할지, 하위 객체에서 필요한 사용할 수 있는 필드는 무엇인지 알기 위해선 스키마의 존재가 필연적이다.

GraphQL schema language

GraphQL은 스키마 정의를 위해 SDL(Schma Definition Language, 스키마 정의 언어) 를 지원한다. SDL 역시 프로그래밍 언어나 프레임워크와 상관없이 사용법이 항상 동일하다. 책이나 글이서 GraphQL SDL이라고 명명하더라도 동일한 의미로 받아드리면 된다.

Schema 문서화

schema에는 타입 정의를 모아 둔다. 확장자는 .graphql 이다. GraphQL 스키마를 작성할 때는 옵션으로 각 필드에 대한 설명을 적을 수 있다. 문서화에 대한 필요성은 너무 많기 때문에 다른 글을 참고 부탁드린다.

주석 위, 아래로 인용 부호를 붙여서 추가하면 된다.

image


잘 정의된 스키마는 GraphQL의 핵심이다.

 

Type

GraphQL 스키마의 핵심 단위이다. GraphQL에서 타입은 커스텀 객체이며 이를 보고 애플리케이션의 핵심 기능을 알 수 있다. 아래는 간단한 예시이다.

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

 

  • Character는 GraphQL의 객체 타입이다. 스키마는 대부분 객체 타입이라고 보면 된다. 이런 타입은 애플리케이션의 데이터를 상징한다. 그리고 곧 팀에서 도메인 객체에 대해 이야기할 때 사용할 공통의 언어*(Ubiquitous Language) 를 정의하는 것과 같다. 따라서 이를 보고 애플리케이션의 핵심 기능을 알 수 있도록 짓는 것이 중요하다.
  • ID!는 GraphQL의 스칼라 타입이며, 고유 식별자 값이 반환되어야 하는 곳이다.
  • name, appearsInCharacter 타입의 필드(field) 이다. 필드는 각 객체의 데이터와 관련이 있으며 각각의 필드는 특정 종류의 데이터를 반환한다. 이때 문자열, 커스텀 객체 타입, 여러 타입을 리스트로 묶어 반환하기도 한다.
  • String은 GraphQL에 내장된 스칼라 타입(기본 타입) 중 하나이다.
  • String!은 필드가 non-nullable임을 의미한다. 다시 말해 필수값을 정의했다고 보며, 이 값은 쿼리할 때 항상 값을 반환한다는 것을 의미한다. 타입 언어에서는 ! 느낌표로 나타낸다. (non-nullable이란 null 값을 허용하지 않는다는 것을 의미한다.) !값이 없다면 null 값을 허용하는 옵셔널(nullable) 값이다.
  • [Episode]!Episode 객체의 배열(Array) 를 나타낸다. 또한 !이기 때문에 appearsIn 필드를 쿼리할 때 항상 0개 이상의 아이템을 가진 배열을 기대할 수 있다.

커스텀 스칼라 타입

스칼라 타입은 기본적으로 Int, Float, String, Boolean, ID 이지만, GraphQL에서는 커스텀 스칼라 타입을 지정하는 것도 가능하다.

scalar DateTime! # 스칼라 타입 정의

type Character {
  id: ID!
  name: String!
  appearsIn: [Episode]!
  created: DateTime! # 사용
}

 

열거 타입(Enumeration type)

열거 타입 또한 스칼라 타입에 속하며, 특별한 종류의 스칼라이다.

(혹시 잊어버렸을까봐 다시 말하지만, 개발 언어가 열거 타입을 지원하는냐는 중요하지 않다. 언어와 상관없이 사용할 수 있다.)

열거형 타입은 항상 허용된 값 중 하나임을 검증한다. 그리고 enum 키워드를 이용해 타입을 만들 수 있다.

enum Episode { # 열거 타입 정의
  NEWHOPE
  EMPIRE
  JEDI
}

type Character {
  id: ID!
  name: String!
  appearsIn: [Episode]! # 반환되는 Episode 타입은 반드시 NEWHOPE, EMPIRE, JEDI 중 하나이다.
}

리스트

appearsIn: [Episode]!

위에서 봤듯이 리스트를 리턴 받을 수 있다. 문자열 리스트([String])이나 커스텀 리스트([Episode]) 등 다양한 리스트를 정의할 수 있다. 리스트에 여러 개의 타입을 담는 것 또한 가능하다. 위에서 언급했지만 !(느낌표)에 따라 의미가 달라져 헷갈릴 수 있기 때문에 다시 한번 언급한다.

  • [String] - 리스트 안에 담긴 문자열 값은 null이 될 수 있다.
  • [String!] - 리스트 안에 담긴 문자열 값은 null이 될 수 없다.
  • [String]! - 리스트 안에 담긴 문자열 값은 null이 될 수 있으나, 리스트도 null이 될 수 없다.
  • [String!]! - 리스트 안에 담긴 문자열 값은 null이 될 수 없고, 리스트도 null이 될 수 없다.

 

인터페이스(interface)

GraphQL은 다른 타입 언어와 동일하게 인터페이스를 지원하며, 인터페이스는 이를 구현하기 위해 타입이 포함해야하는 특정 필드들을 포함하는 추상 타입이다.

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

인터페이스를 구현은 다른 타입 언어와 비슷하다.

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
}

HumanDroid인 인터페이스에서 요구한 부분을 모두 가지고 있고, 추가 필드도 가질 수 있기 때문에 위와 같이 구현되었다.

 

유니온 타입(union)

유니온 타입은 인터페이스와 유사하지만, 차이점은 타입 간에 공통 필드를 특정하지 않으며, 파이프(|)를 통해 타입을 원하는만큼 연결할 수 있다.

union SearchResult = Human | Droid | Starship

유니온 타입을 반환하는 필드를 쿼리하면, 인라인 프래그먼트를 사용하면 된다.

{
  search(text: "an") {
    ... on Human {
      name
      height
    }
    ... on Droid {
      name
      primaryFunction
    }
    ... on Starship {
      name
      length
    }
  }
}
{
  "data": {
    "search": [
      {
        "name": "Han Solo",
        "height": 1.8
      },
      {
        "name": "Leia Organa",
        "height": 1.5
      },
      {
        "name": "TIE Advanced x1",
        "length": 9.2
      }
    ]
  }
}

인터페이스에서도 다른 필드를 쿼리한다면 똑같이 인라인 프래그먼트를 사용하면 된다. 인라인 프래그먼트에 대해서는 앞 글에서 설명했기 때문에 내용을 생략한다.

 

input 타입

input 타입은 object 타입과 일치하지만, type 대신 input을 사용한다. 특히 뮤테이션에서 유용하다.

input ReviewInput { # input 타입 정의
  stars: Int!
  commentary: String
}
mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
  createReview(episode: $ep, review: $review) {
    stars
    commentary
  }
}

특별히 다른거 없어보이지만, input 타입을 사용하면 모든 필드에서 인자로 사용할 수 있고 정렬 및 필터링 필드와 관련된 코드 구조를 체계화하고 재사용할 수 있다.

input PhotoFilter {
  category: PhotoCategory
  createdBetween: DateRange
  taggedUsers: [ID!]
  searchText: String
}

input DateRage {
  start: DateTime!
  end: DateTime!
}

input DataPage {
  first: Int = 25
  start: Int = 0
}

input DataSort {
  sort: SortDirection = DESCENDING
  sortBy: SortablePhotoField = created
}

# 복잡한 인풋 데이터 받기 쿼리
query getPhotos ($filter: PhotoFilter, $page: DataPage, $sort: DataSort) {
  allPhotos(filter: $filter, paging: $page, sorting: $sort) {
    id
    name
    url
  }
}

음 유용한 타입에는 틀림없으나 많이 사용해 보며 감을 더 익혀야 할거 같다;;

 

스키마에 인자 정의

만약 Query 타입에 allUsers와 allPhotos를 목록으로 반환하는 필드가 있는데, User 한명, 혹은 Photo 한 장만 선택하고 싶을때는 어떻게 해야할까? 간단히 생각해도 쿼리문 인자에 정보만 넣으면 될거같다. 이걸 스키마에서는 어떻게 정의해야 할까?

type Query {
  ...
  User(githubLogin: ID!): User!
  Photo(id: ID!) Photo!
}

스키마에서 사용할 수 있는 스칼라 타입이나 객체 타입으로 인자의 타입을 정해주면 된다. 사용할 때는 앞에서 했던거와 동일하다.

query {
  User(githubLogin: "jacob") {
    name
    avatar
  }
}

 

이렇게하면 한 명의 유저 정보만 가져온다. 그럼 많은 유저 중에 데이터를 끊어서 가져오고 싶을 때는 어떻게 해야할까? 예를 들면 페이지가 나눠져 있어 1번 페이지에서는 0~50번째까지의 유저 정보만 필요한 경우를 의미한다.

type Query {
  ...
  allUsers(first: Int=50 start: Int=0): [User!]!
}

두 인자를 추가하였고 기본값을 설정해 주었다. 만약 클라이언트에서 인자를 따로 넣어주지 않는다면 기본 값을 인자로 사용할 것이다.

query {
  allUsers(first: 10 start: 30) { # 10~30명만 보내주세여!
    name
    avatar
  }
}

 

스키마에 뮤테이션(mutation) 정의

스키마의 루트 mutation 타입에 추가하여 클라이언트에서 사용할 수 있도록 하면 된다.

type Mutation {
  postPhoto(
    name: String!
    description: String
    category: PhotoCategory = PORTRAIT
  ): Photo!
}

schema {
  query: Query
  mutation: Mutation
}

이미 다뤘던 내용에서 정의만 바뀐거라 그냥 넘어간다.

 

리턴 타입

기존에 정의한 타입이랑 다른 타입을 리턴하고 싶은 경우가 생긴다. 위에서는 대부분 User나 Photo 타입을 반환하고 있다. 하지만 로그인 했을 경우 토큰을 같이 넘긴다는 경우든지 커스텀 리턴 타입이 필요한 경우가 있다. 그럴 경우 타입을 새로 만들어서 리턴 타입을 설정해 주면 된다.

type AuthPayload {
  user: User!
  token: String!
}

type Mutation {
  ...
  githubAuth(code: String!): AuthPayload!
}

 

참고자료

https://graphql-kr.github.io

https://landscape.graphql.org/

https://graphql.org/

Learing GraphQL by Eve Porello and Alex Banks(O'Reilly). Copyright 2018 Moon Highway, LLC, 978-1-492-03071

반응형