본문 바로가기

# GraphQL/TypeGraphQL

[TypeGraphQL] 기본 자료형과 @ObjectType

오브젝트 타입

ObjectTypeGraphQL에서 데이터 객체를 표현하는 단위이며, 데이터베이스로 비유하면 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

stringboolean은 그냥 넘어가도 괜찮지만 number는 조심해야 합니다. GraphQL에는 Float만이 아니라 Int도 있기 때문이죠. 비트 개수도 다르므로 적어도 number는 명시적으로 타입을 선언해주는 것이 좋습니다.


기본 자료형

GraphQL에는 여러가지 자료형이 있지만 여기서는 PrimitiveArray만 다뤄보도록 하겠습니다.


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는 함수에 대해서는 ArgumentReturnType, 내부 Variables에 대한 타입 검사만 진행하고, 문법적으로만 올바르면 값이 무엇인지는 신경쓰지 않기 때문입니다. 따라서 함수로 한번 감싸면 서로를 가르키는 것을 감출 수 있고, 이 함수를 호출하면 런타임 시점에서 닭과 달걀을 둘 다 얻을 수 있습니다.


런타임 시간에는 닭과 달걀이 전부 컴파일이 완료된 상태이기 때문에 가능한 편법입니다.


예제 다운로드

이 포스팅에 사용된 전체 코드는 여기에서 확인할 수 있습니다.