GraphQL 학습 - 3장 스키마(Schema) & 타입(Type)
GraphQL 타입 시스템은 API에서 어떤 데이터를 쿼리할 수 있는지 설명한다. 이러한 기능의 컬렉션을 서비스의 스키마라고 하며 클라이언트는 해당 스키마를 사용하여 예측 가능한 결과를 반환하는 API에 쿼리를 보낼 수 있다.
GraphQL은 모든 백엔드 프레임워크나 프로그래밍 언어와 함께 사용할 수 있으므로 구현에 따른 세부사항은 피하고 개념에 대해서만 이야기를 해보자
타입 시스템
GraphQL 쿼리 언어는 기본적으로 객체의 필드를 선택하는 것이다. 다음 예제를 보면
{
hero {
name
appearsIn
}
}
{
"data": {
"hero": {
"name": "R2-D2",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
]
}
}
}
- 특별한 “루트” 객체로 시작한다.
- hero 필드를 선택한다.
- hero에서 반환된 객체에 대해 name, appearsIn 필드를 선택한다.
쿼리와 결과의 모양이 거의 일치하기 때문에 쿼리가 무엇을 반환할지 예측할 수 있다. 다만, 요청할 수 있는 데이터에 대한 정확한 설명이 있으면 유용하다. 예를 들어, 어떤 필드를 선택할 수 있는지, 어떤 종류의 객체를 반환할지, 해당 하위 객체에서 어떤 필드를 사용할 수 있을지 등등.
특정 서비스에서 쿼리할 수 있는 가능한 데이터 셋을 완전히 설명하는 타입 셋을 정의한다. 그런 다음 요청이 들어오면 해당 스키마에 대해 검증되고 실행된다.
타입 언어
GraphQL 서비스는 어떤 언어로든 작성 가능, 스키마에서 타입을 정의할 때 취할 수 있는 다양한 접근 방식이 있음
- GraphQL 구현을 작성하는 데 사용된 것과 동일한 프로그래밍 언어를 사용하여 스키마 타입, 필드 및 확인자 함수를 함께 구성하도록 함
- 스키마 정의 언어(또는 SDL)라고 일반적으로 불리는 것을 사용하여 타입과 필드를 보다 인체공학적으로 정의한 다음 해당 필드에 대한 확인자 함수를 별도로 작성할 수 있도록 함
- 리졸버 함수를 작성하고 주석을 달고, 그로부터 스키마를 추론할 수 있음
- 기본 데이터 소스를 기반으로 타입과 확인자 함수를 모두 추론해 제공하기도 함
해당 가이드 문서에서는 SDL을 사용함. 쿼리 언어와 유사하며 언어에 구애 받지 않고 GraphQL 스키마에 대해 설명할 수 있다.
객체 타입 및 필드
GraphQL 스키마의 가장 기본적인 구성 요소는 Object 타입으로, 서비스에서 가져올 수 있는 객체의 종류와 해당 객체가 가진 필드를 나타낸다. SDL에서는 다음과 같이 표현한다.
type Character {
name: String!
appearsIn: [Episode!]!
}
String: 내장된 스칼라 타입 중 하나
!: Non-Null 타입, 값을 제공하겠다고 약속하는 것
Arguments
Object 타입의 모든 필드는 0개 이상의 인수를 가질 수 있다.
type Starship {
id: ID!
name: String!
length(unit: LengthUnit = METER): Float
}
현재 length 필드의 인수는 선택 사항이다. 인수가 만약 선택사항이라면 기본값을 정의할 수 있다. unit 인수가 전달되지 않으면 METER로 설정된다.
Query, Mutation 및 Subscription 타입
모든 GraphQL 스키마는 query 연산을 지원해야 한다. 이 루트 연산 타입의 진입점은 기본적으로 Query라는 일반 Object 타입이다.
{
droid(id: "2000") {
name
}
}
{
"data": {
"droid": {
"name": "C-3PO"
}
}
}
이는 GraphQL 서비스에 droid 필드가 있는 Query 타입이 필요하다는 것을 의미합니다:
type Query {
droid(id: ID!): Droid
}
스키마는 추가적인 Mutation과 Subscription 타입을 추가하고 해당 루트 연산 타입에 필드를 정의함으로써 뮤테이션과 구독 연산을 지원할 수 있습니다.
Query, Mutation, Subscription 타입이 스키마의 진입점이라는 특별한 상태를 제외하면 다른 GraphQL 객체 타입과 동일하며, 그들의 필드도 정확히 같은 방식으로 작동한다는 점을 기억하는 것이 중요합니다.
루트 연산 타입의 이름을 다르게 지정할 수도 있습니다. 만약 그렇게 하기로 선택했다면 schema 키워드를 사용하여 GraphQL에 새로운 이름을 알려줘야 합니다:
schema {
query: MyQueryType
mutation: MyMutationType
subscription: MySubscriptionType
}
스칼라 타입
GraphQL 객체 타입은 이름과 필드를 가지고 있지만 어느 시점에서는 그 필드가 구체적인 데이터로 해결되어야 한다. 여기서 스칼라 타입이 등장한다. 쿼리의 리프 값을 나타낸다.
리프 값이라는 뜻은 해당 필드에 하위 필드가 없다는 것이다.
GraphQL에는 기본적으로 제공되는 Scalar 타입 셋이 있다.
- Int: 부호 있는 32비트 정수.
- Float : 부호 있는 배정밀도 부동 소수점 값입니다.
- String : UTF‐8 문자 시퀀스.
- Boolean: T/F
- ID : 고유 식별자로, 종종 객체를 다시 가져오거나 캐시의 키로 사용됩니다. ID 타입은 String 같은 방식으로 직렬화되지만, ID로 정의하는 것은 사람들이 읽을 수 있도록 의도되지 않았음을 의미한다.
대부분의 GraphQL 서비스 구현에는 사용자 정의 Scalar 타입을 지정하는 방법도 있다.
scalar Date
그 다음 해당 타입이 어떻게 직렬화, 역직렬화 및 검증되어야 하는지 정의하는 것은 구현에 달려 있다.
Enum 타입
특정 허용 값 집합으로 제한되는 특수한 종류의 스칼라. 이를 통해 다음을 수행 가능
- 이 타입의 모든 인수가 허용된 값 중 하나인지 확인
- 필드는 항상 유한한 값 집합 중 하나임을 타입 시스템을 통해 전달
SLD에서 Enum 타입 정의는 다음과 같다
enum Episode {
NEWHOPE
EMPIRE
JEDI
}
즉, 스키마에서 해당 타입을 사용할 때마다 Episode 타입이 NEWHOPE, EMPIRE, JEDI 중에 반드시 하나여야 한다는 의미다.
타입 수정자
GraphQL에서 기본적으로 타입이 nullable이고 singular로 가정된다. 그러나 스키마에서 이러한 명명된 타입을 사용할 때 해당 값의 의미에 영향을 미치는 추가 타입 수정자를 적용할 수 있다.
Non-null
type Character{
name: String!
}
타입 이름 뒤에 !를 추가하여 Non-null 타입으로 표시한다. 서버는 항상 이 필드에 대해 null이 아닌 값을 반환할 것을 예상하고, 리졸버가 null 값을 생성하면 GraphQL 런타임 에러가 발생하여 클라이언트에게 무언가 잘못되었음을 알린다.
위의 예에서 보았듯이 Non-Null 타입 수정자는 필드에 대한 인수를 정의할 때도 사용할 수 있으며, 이 경우 해당 인수로 null 값이 전달되면 GraphQL 서버가 유효성 검사 오류를 반환한다.
List
목록은 비슷한 방식으로 작동합니다. 타입 수정자를 사용하여 타입을 List 타입으로 표시할 수 있으며, 이는 이 필드가 해당 타입의 배열을 반환함을 나타냅니다. SDL에서 이는 타입을 대괄호로 묶어서 표시한다. 인수의 경우에도 동일하게 작동하며, 검증 단계에서는 해당 값에 대한 배열을 예상한다.
type Character {
name: String!
appearsIn: [Episode]!
}
위에서 보듯이 Non-Null 및 List 수정자는 결합될 수 있다. 예를 들어, Non-Null String타입의 List를 가질 수 있다.
myField: [String!]
목록 자체는 null일 수 있으나, 목록이 null 멤버를 가질 수는 없다.
myField: null // valid
myField: [] // valid
myField: ["a", "b"] // valid
myField: ["a", null, "b"] // error
이제 다음과 같은 String 타입의 Non-Null 목록을 정의했다고 가정해보자.
myField: [String]!
목록 자체는 null이 될 수 없지만 null 값을 포함할 수 있습니다.
myField: null // error
myField: [] // valid
myField: ["a", "b"] // valid
myField: ["a", null, "b"] // valid
마지막으로, Non-Null String 타입의 Non-Null 목록을 가질 수도 있다.
myField: [String!]!
즉, 목록은 null이 될 수 없고 null 값을 포함할 수 없다.
myField: null // error
myField: [] // valid
myField: ["a", "b"] // valid
myField: ["a", null, "b"] // error
인터페이스 타입
많은 타입 시스템과 마찬가지로 GraphQL도 추상 타입을 지원한다. 우선 인터페이스 타입부터 살펴보자.
인터페이스 타입은 구체적인 Object 타입이나 다른 인터페이스 타입이 구현하기 위해 포함해야 하는 특정 필드 집합을 정의한다. (추상화)
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
}
두 타입 모두 Interface Character 타입의 모든 필드를 가지고 있으면서, 특정 문자 타입에만 해당하는 추가 필드 ( totalCredits, starships, primaryFunction ) 도 가지고 있는 것을 볼 수 있다.
인터페이스 타입은 객체나 객체 집합을 반환할 때 유용하지만, 여러 가지 다른 타입이 될 수 있습니다. 예를 들어, 다음 쿼리는 오류를 생성합니다.
{
hero(episode: JEDI) {
name
primaryFunction
}
}
hero type을 반환하는데, 이는 episode 인수에 따라 Character가 Human, Droid 중 하나가 될 수 있다. 따라서 위 쿼리에서 Interface 타입에 존재하는 필드만 요청할 수 있으며, 여기에는 primaryFunction이 포함되지 않는다.
특정 객체 타입의 필드를 요청하기 위해서는 인라인 fragment를 사용해야 한다.
{
hero(episode: JEDI) {
name
... on Droid {
primaryFunction
}
}
}
인터페이스 타입은 다른 인터페이스 타입도 구현할 수 있다.
interface Node {
id: ID!
}
interface Character implements Node {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
}
인터페이스 타입은 자기 자신을 구현할 수 없고 서로에 대한 순환 참조를 포함할 수 없다.
Union 타입
Union 타입은 interface 타입과 유사하지만, 구성 타입 간에 공유 필드를 정의 할 수 없다.
Union 타입은 멤버 Object 타입을 표시하여 정의된다.
union SearchResult = Human | Droid | Starship
SearchResult 스키마에서 타입을 반환하는 경우 Human, Droid 또는 Starship을 얻을 수 있다. Unoin 타입의 멤버는 구체적인 Object 타입이어야 한다. Interface 타입이나 다른 Union 타입을 멤버로 사용하여 정의할 수 없다.
이 경우에는 Union 타입을 반환하는 필드를 쿼리하는 경우 멤버 Object 타입에 정의된 모든 필드를 쿼리할 수 있도록 인라인 조각을 사용해야 한다.
search(text: "an") {
__typename
... on Human {
name
height
}
... on Droid {
name
primaryFunction
}
... on Starship {
name
length
}
}
}
{
"data": {
"search": [
{
"__typename": "Human",
"name": "Han Solo",
"height": 1.8
},
{
"__typename": "Human",
"name": "Leia Organa",
"height": 1.5
},
{
"__typename": "Starship",
"name": "TIE Advanced x1",
"length": 9.2
}
]
}
}
필드는 모든 Object 타입에 자동으로 존재하고 해당 타입의 이름으로 확인되는 __typename특수 메타 필드로, 클라이언트에서 데이터 타입을 구별하는 방법을 제공한다.
또한, 이 경우 Human및 Droid 는 공통 인터페이스(Character)를 공유하므로 한 곳에서 공통 필드를 쿼리해도 동일한 결과를 얻을 수 있습니다.
{
search(text: "an") {
__typename
... on Character {
name
}
... on Human {
height
}
... on Droid {
primaryFunction
}
... on Starship {
name
length
}
}
}
{
"data": {
"search": [
{
"__typename": "Human",
"name": "Han Solo",
"height": 1.8
},
{
"__typename": "Human",
"name": "Leia Organa",
"height": 1.5
},
{
"__typename": "Starship",
"name": "TIE Advanced x1",
"length": 9.2
}
]
}
}
주의할 점은 name 은 여전히 Starship에 지정되어 있어야 한다. 그렇지 않으면 Starship이 Character가 아니므로 결과에 나타나지 않습니다.
입력 객체 타입
필드 인수는 입력 타입을 지정할 수 있어야 한다.
명명된 타입인 Input Object 타입을 사용하여 복잡한 객체를 인수로 전달할 수 있다.
특히 뮤테이션의 경우, 생성될 전체 객체를 전달하고 싶을 수 있기에 가치가 있다. SDL에서 입력 객체 타입은 일반 객체 타입과 유사하지만 키워드가 input이다.
input ReviewInput {
stars: Int!
commentary: String
}
type Mutation {
createReview(episode: Episode, review: ReviewInput!): Review
}
뮤테이션에서 Input Object 타입을 사용하는 방법은 다음과 같다.
mutation {
createReview(
episode: JEDI,
review: {
stars: 5
commentary: "This is a great movie!"
}
) {
stars
commentary
}
}
{
"data": {
"createReview": {
"stars": 5,
"commentary": "This is a great movie!"
}
}
}
입력 객체 타입의 필드는 다른 입력 객체 타입을 참조할 수 있지만 스키마에서 입력 및 출력 타입을 혼합할 수 없다. 입력 객체 타입은 필드에 인수를 가질 수도 없다.
Directives
특정한 인스턴스에서 필드 인수가 부족하거나 특정 공통 동작을 여러 위치에 복제해야 하는 경우 지시문(Directives)을 사용하면 @ 문자 뒤에 지시문 이름을 사용하여 GraphQL 스키마나 작업의 일부를 수정할 수 있다. 타입 시스템 지시문을 사용하면 스키마의 타입, 필드, 인수에 주석을 달아서 이를 다르게 검증하거나 실행할 수 있다.
GraphQL 사양은 여러 내장 지시문을 정의한다. 예를 들어, SDL을 지원하는 구현의 경우 @deprecated 지시문을 사용하여 스카미의 더 이상 사용되지 않는 부분에 주석을 달 수 있다.
type User {
fullName: String
name: String @deprecated(reason: "Use `fullName`.")
}
SDL을 지원하는 GraphQL 구현을 사용하는 경우 스키마에 지시문을 명시적으로 정의할 필요는 없지만 @deprecated 기본 정의는 다음과 같다.
directive @deprecated(
reason: String = "No longer supported"
) on FIELD_DEFINITION | ENUM_VALUE
필드와 마찬가지로 지시문도 인수를 받을 수 있으며, 이러한 인수는 기본값을 가질 수 있습니다. @deprecated 지시문은 String을 입력 타입으로 받는 nullable한 reason 인수를 가지며, 기본값은 "No longer supported"입니다. 또한 지시문의 경우, 사용할 수 있는 위치를 지정해야 합니다. 예를 들어 @deprecated 지시문의 경우 FIELD_DEFINITION 또는 ENUM_VALUE에 사용될 수 있습니다.
GraphQL의 내장 지시문 외에도 자체적인 사용자 정의 지시문을 정의할 수 있습니다. 사용자 정의 스칼라 타입과 마찬가지로, 쿼리 실행 중 사용자 정의 지시문을 어떻게 처리할지는 선택한 GraphQL 구현에 따라 달라집니다.
Documentation
Descriptions
GraphQL을 사용하면 스키마의 타입, 필드 및 인수에 문서를 추가할 수 있다. 실제로 GraphQL 사양에서는 타입, 필드 또는 인수의 이름이 자체 설명적이지 않은 경우 모든 경우에 이를 수행하도록 권장한다. 스키마 설명은 Markdown 구문을 사용하여 정의되며 여러 줄 또는 한 줄로 작성할 수 있다.
"""
A character from the Star Wars universe
"""
type Character {
"The name of the character."
name: String!
}
"""
The episodes in the Star Wars trilogy
"""
enum Episode {
"Star Wars Episode IV: A New Hope, released in 1977."
NEWHOPE
"Star Wars Episode V: The Empire Strikes Back, released in 1980."
EMPIRE
"Star Wars Episode VI: Return of the Jedi, released in 1983."
JEDI
}
"""
The query type, represents all of the entry points into our object graph
"""
type Query {
"""
Fetches the hero of a specified Star Wars film.
"""
hero(
"The name of the film that the hero appears in."
episode: Episode
): Character
}
GraphQL API 스키마를 더 표현력 있게 만드는 것 외에도, 설명은 내부 검사 쿼리(introspection queries)에서 사용 가능하고 GraphiQL과 같은 개발자 도구에서 볼 수 있기 때문에 클라이언트 개발자에게 도움이 된다.
Comments
때때로 타입, 필드 또는 인수를 설명하지 않고 클라이언트에게 보여줄 필요가 없는 주석을 스키마에 추가해야 할 수 있다. 이런 경우에는 텍스트 앞에 # 문자를 붙여 SDL에 한 줄 주석을 추가할 수 있다:
# This line is treated like whitespace and ignored by GraphQL
type Character {
name: String!
}
Comment는 클라이언트 쿼리에서도 추가할 수 있다.
{
hero {
name
# Queries can have comments!
friends {
name
}
}
}
출처
https://graphql.org/learn/schema/
Schemas and Types | GraphQL
Copyright © 2024 The GraphQL Foundation. All rights reserved. For web site terms of use, trademark policy and general project policies please see https://lfprojects.org
graphql.org
https://graphql-kr.github.io/learn/schema/
GraphQL: API를 위한 쿼리 언어
GraphQL은 API에 있는 데이터에 대한 완벽하고 이해하기 쉬운 설명을 제공하고 클라이언트에게 필요한 것을 정확하게 요청할 수 있는 기능을 제공하며 시간이 지남에 따라 API를 쉽게 진화시키고
graphql-kr.github.io