GraphQL의 스키마(Schema)

이번 포스트는 apollo docs 의 schema 부분의 일부를 번역 한 포스트 입니다.

원글: https://www.apollographql.com/docs/apollo-server/schema/schema/

Schema

  • Scalar types
  • Object types
  • The Query type
  • The Mutation type
  • Input types

Scalar types

스칼라 타입은 다른 프로그래밍 언어에서 이야기 하는 기본형 (primitive type)과 유사 합니다.

GraphQL의 default scalar types

  • Int: A signed 32-bit integer
  • Float: A signed double-precision floating-point value
  • String: A UTF-8 character sequence
  • Boolean: true or false
  • ID (serialized as a String): A unique identifier that’s often used to refetch an object or as the key for a cache. Although it’s serialized as a String, an ID is not intended to be human-readable.

Default 스칼라 타입으로 대부분의 경우 커버 가능 하지만, 그렇지 않은 경우에는 Custom scalar types 을 만들어서 사용하는것도 가능 합니다.

Object types

GraphQL 스카마에 정의된 대부분의 타입은 object 타입 입니다.
object는 필드의 컬렉션을 포함 하고 있고, 각 필드는 scalar 타입 이거나, 다른 object 타입 입니다.
예를 들면

1
2
3
4
5
6
7
8
9
type Book {
title: String
author: Author
}

type Author {
name: String
books: [Book]
}

The Query type

Query 타입은 client에서 호출 할 GraphQL의 정확한 query를 정의 합니다. (read query)
Query 의 필드는 쿼리 이름과 리턴타입으로 구성 되어 있습니다.

예를 들면

1
2
3
4
type Query {
getBooks: [Book]
getAuthors: [Author]
}

위의 query 타입은 getBooksgetAuthors 라는 이름의 두 쿼리를 정의 하고, 각 쿼리는 Book과 Author 의 리스트를 각각 리턴 하는걸 알 수 있습니다.

이게 REST-based API 였다면 아마도 두개의 다른 endpoints로 만들었겠지요 (예를 들면 /api/books and /api/authors). 하지만 GraphQL은 하나의 endpoint에서 두개의 리소스에 접근 할 수 있는 유연성을 제공 합니다.

Structuring a query

client에서 서버의 data graph에 실행할 쿼리를 만들때, 그 쿼리는 서버의 schema에 정의 된 object types과 일치 해야 합니다.

지금까지 예로 들은 몇개의 스키마를 예를 들면, client는 다음과 같이 모든 book 과 모든 author 의 이름을 가져오는 쿼리를 할 수 있습니다.

1
2
3
4
5
6
7
8
query {
getBooks {
title
}
getAuthors {
name
}
}

우리 서버에서는 아래와 비슷하게 응답 해 줄 것 입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"data": {
"getBooks": [
{
"title": "Jurassic Park"
},
...
],
"getAuthors": [
{
"name": "Michael Crichton"
},
...
]
}
}

이렇게 두개의 전혀 다른 리스트를 가져오는게 쓸모있는 경우가 있긴 하겠지만,

client에서는 아마도 author의 이름이 포함 되어있는 하나의 books 리스트를 받기를 선호할것 같습니다.

Book 타입이 Author 타입의 author 필드를 가지고 있으므로, client는 아마 아래와 같이 쿼리 하는것이 더 낫겠네요.

1
2
3
4
5
6
7
8
query {
getBooks {
title
author {
name
}
}
}

그러면 서버에서는 아래와 같이 더 깔끔한 결과를 보내 줄 것 입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"data": {
"getBooks": [
{
"title": "Jurassic Park",
"author": {
"name": "Michael Crichton"
}
},
...
]
}
}

The Mutation type

Mutation 타입은 Query 타입과 구조적으로 비슷하게 생겼습니다. Query 타입이 데이터를 읽어오는(read) operation 을 정의 한다면,
Mutation 타입은 데이터를 쓰는 (write) operation을 정의 합니다.

각 필드는 시그니처와 리턴 타입을 정의 하고, 예를 들면 아래와 같습니다.

1
2
3
type Mutation {
addBook(title: String, author: String): Book
}

위 예의 Mutaion타입은 addBook 이라는 하나의 mutation을 정의 했습니다.

addBooktitleauthor 두개의 인자값을 가지고 있으며 Book 타입의 object를 리턴해 준다고 정의 되어 있습니다.

Book 타입은 물론 위의 obejct types의 스키마에 정의된 그 Book 입니다.

Structuring a mutation

쿼리와 마찬가지로, 뮤테이션도 스키마에 정의된 타입과 일치하는 구조를 갖고 있습니다.

아래 예는 새로운 Book 오브젝트의 생성을 요청하고, 리턴값으로는 방금 생성한 오브젝트의 특정 필드를 반환해 달라고 합니다.

1
2
3
4
5
6
7
8
mutation {
addBook(title: "Fox in Socks", author: "Dr. Seuss") {
title
author {
name
}
}
}

쿼리의 결과와 마찬가지로 서버에서는 아래와 같이 뮤테이션에서 요청한 필드들을 반환해 줍니다.

1
2
3
4
5
6
7
8
9
10
{
"data": {
"addBook": {
"title": "Fox in Socks",
"author": {
"name": "Dr. Seuss"
}
}
}
}

한개의 client 요청에 여러개의 뮤테이션이 들어 있는 경우, 경합을 방지하기 위해 뮤테이션은 순차적으로 처리 됩니다.

Input types

인풋 타입은 특별한 오브젝트 타입 입니다.

쿼리와 뮤테이션에 정의된 operation은 인자로 스칼라 타입만 가질 수 있는데, 인풋 타입은 스칼라가 아니지만 인자로 사용할 수 있습니다.

그래서 인풋 타입은 operation의 시그니처를 깔끔한 상태로 유지할 수 있게 도와 줍니다.

이건 java에서 paramMap을 넘기거나, javascript에서 options object를 인자로 넘기는게 각 인자를 쭉~ 나열하는것 보다 보기에 깔끔하고, 추후에 인자로 추가 하고 싶은것이 생겼을때

함수의 시그니처를 변경하지 않아도 되는 장점이 있는것과 동일 합니다.

아래 뮤테이션이 블로그 포스트를 생성하는 뮤테이션이라고 가정 합시다.

1
2
3
4
5
type Mutation {

createPost(title: String, body: String, mediaUrls: [String]): Post

}

위와 같이 세개의 인자를 받는게 아니라 하나의 인풋 타입의 인자를 받게 바꿔 보겠습니다.

이렇게 하면 추후에 인자를 더 추가하기로 결정 한 경우, 예를 들면 author같은, 훨씬 간편 합니다.

1
2
3
4
5
6
7
8
9
10
type Mutation {
createPost(post: PostAndMediaInput): Post

}

input PostAndMediaInput {
title: String
body: String
mediaUrls: [String]
}

이렇게 전달 하면 client에서 PostAndMediaInput은 도대체 뭐 하는 놈인데? 라고 생각 할 수 있는데,

이런걸 대비해서 GraphQL은 각 필드를 설명하는 annotating fields를 사용 할 수 있게 했습니다.

GraphQL-enabled 된 툴에서는 해당 설명이 바로 보일 수 있게 말이죠.

1
2
3
4
5
6
7
8
input PostAndMediaInput {
"A main title for the post"
title: String
"The text body of the post."
body: String
"A list of URLs to render in the post."
mediaUrls: [String]
}

인풋타입은 가끔 완벽히 동일한 인자를 요구하는 쿼리나 뮤테이션에서 재 사용 하기 위해 사용 되기도 합니다.

그런데 이런 재사용은 최소한으로 해야 합니다. 왜냐하면 지금은 완벽히 동일한 인자가 필요 하지만 나중에는 아닐수도 있으니까요.

** 쿼리와 뮤테이션에서 동일한 인풋타입을 사용하지 마세요 **

뮤테이션에는 required 인 필드가 쿼리에서는 optional인 경우가 굉장히 많기 때문 입니다.

Share