오브젝트 타입
ObjectType
은 GraphQL
에서 데이터 객체를 표현하는 단위이며, 데이터베이스로 비유하면 Relation
의 개념에 가깝습니다. ObjectType
은 서로다른 ObjectType
와 관계를 맺을 수 있기 때문에, 수 많은 오브젝트 타입들이 엉키고설키면 마치 그래프와 같은 모양새가 나오게 됩니다. GraphQL
이라는 이름이 붙은 이유도 이와 비슷할 것 입니다.
@ObjectType
클래스 위에 @ObjectType()
데코레이터를 붙이면 클래스를 ObjectType
으로 만들 수 있습니다. 마찬가지로 필드 위에 @Field()
데코레이터를 붙이면 Field of ObjectType
로 만들 수 있습니다.
아래의 SDL
을 클래스로 표현하면 다음과 같습니다.
type Book {
title: String!
price: Int!
}
import { ObjectType, Int } from "type-graphql";
@ObjectType()
class Book {
@Field() // 암묵적 타입 선언
title !: string;
@Field(()=>Int) // 명시적 타입 선언
@price !: number;
}
@Field()
에 () => GraphQLType
을 넘겨주면 명시적으로 타입을 선언할 수 있습니다. 넘겨주지 않은 경우 다음 규칙에 의해 자료형이 결정됩니다.
- string → String
- number → Float
- boolean → Boolean
string
과 boolean
은 그냥 넘어가도 괜찮지만 number
는 조심해야 합니다. GraphQL
에는 Float
만이 아니라 Int
도 있기 때문이죠. 비트 개수도 다르므로 적어도 number
는 명시적으로 타입을 선언해주는 것이 좋습니다.
기본 자료형
GraphQL
에는 여러가지 자료형이 있지만 여기서는 Primitive
와 Array
만 다뤄보도록 하겠습니다.
Primitive
Int
부호있는 32bit
정수입니다.
Float
IEEE754
로 널리 알려진 배정밀도 부동 소수점입니다. 전체는 64bit
이지만 유효숫자는 53bit
이므로 안전하게 저장할 수 있는 가장 큰 숫자는 2**53-1
입니다.
String
UTF-8
문자의 시퀀스입니다.
Boolean
true
또는 false
입니다.
ID
String
타입과 완벽하게 같지만, 각 오브젝트를 유일하게 식별할 수 있는 Key-String
라는 의미를 내포하고 있습니다. 데이터를 식별할 수 있지만 사람이 편하게 읽을 수 없는 문자열로 생각하면 편합니다. ex) ek54nakq72sbq
nullable
어떤 필드가 null
값을 가질 수 있다면 @Field()
에 { nullable: true }
옵션을 함께 넘겨주면 됩니다.
@ObjectType()
class DataBox {
@Field({ nullable: true })
nullableStr ?: string;
@Field(()=>Int, { nullable :true })
nullableInt ?: number;
}
Array
T[]
() => [T]
와 같이 자료형을 배열첨자로 감싸주면 됩니다.
@ObjectType()
class DataBox {
//
// [Int!]!
@Field(() => [Int])
intArray !: number[];
}
nullable
Array
에 적용할 수 있는 nullable
옵션은 프리미티브보다 더 많습니다. 배열 그 자체
와 배열의 요소
에 각각 nullable
속성을 부여할 수 있기 때문입니다.
nullable : true
배열만 null
일 수 있음. [T!]
nullable : "items"
요소만 null
일 수 있음. [T]!
nullable : "itemsAndList"
배열과 요소가 둘 다 null
일 수 있음. [T]
코드로 표현하면 다음과 같습니다.
@ObjectType()
class ArrayType {
//
// [Int!]!
@Field(() => [Int]) // = { nullable: false }
list !: number[];
//
// [Int!]
@Field(() => [Int], { nullable: true })
nullableList !: number[] | undefined;
//
// [Int]!
@Field(() => [Int], { nullable: "items" })
nullableItemsList !: (number | undefined)[]
//
// [Int]
@Field(() => [Int], { nullable: "itemsAndList" })
nullableItemsAndList !: (number | undefined)[] | undefined;
}
()=>T를 사용하는 이유
왜 굳이 자료형을 함수로 감싸서 넘겨주는 걸까요? @Field(String)
처럼 작성하는게 훨씬 직관적인데 말이죠. 사실 이러한 설계는 Circular References
를 해결하기 위해 도입되었습니다.
서로다른 두 개의 ObjectType
의 필드가 반대쪽 ObjectType
을 지목하게 되면 누구를 먼저 컴파일해도 에러가 발생합니다. 닭이 먼저냐 달걀이 먼저냐 하는 문제와 비슷합니다.
하지만 우회법은 있습니다. TypeScript
는 함수에 대해서는 Argument
와 ReturnType
, 내부 Variables
에 대한 타입 검사만 진행하고, 문법적으로만 올바르면 값이 무엇인지는 신경쓰지 않기 때문입니다. 따라서 함수로 한번 감싸면 서로를 가르키는 것을 감출 수 있고, 이 함수를 호출하면 런타임 시점에서 닭과 달걀을 둘 다 얻을 수 있습니다.
런타임 시간에는 닭과 달걀이 전부 컴파일이 완료된 상태이기 때문에 가능한 편법입니다.
예제 다운로드
이 포스팅에 사용된 전체 코드는 여기에서 확인할 수 있습니다.
'# GraphQL > TypeGraphQL' 카테고리의 다른 글
[TypeGraphQL] 상속, Inheritance (0) | 2020.07.04 |
---|---|
[TypeGraphQL] @EnumType (0) | 2020.06.18 |
[TypeGraphQL] Scalar (0) | 2020.06.14 |
[TypeGraphQL] @InputType, @ArgsType (0) | 2020.06.13 |
ApolloServer + TypeScript + TypeGraphQL 조합으로 GraphQL 서버 시작하기 (0) | 2020.06.07 |