본문 바로가기

# GraphQL/GraphQL.js

타입스크립트 GraphQL Object Type 클래스


오브젝트 타입

GraphQL Object는 필드라는 단위로 구성되어 있으며, 클래스와 유사한 성질을 가지고 있습니다. 예를 들어 bookType 이라는 오브젝트 타입을 정의하는 스키마 정의 문법(SDL)을 생각해보겠습니다.

이것은 다음 타입스크립트 문법으로 표현될 수 있습니다.

// TypeScript.
const bookType: GraphQLObjectType = new GraphQLObjectType({
    name: "bookType",
    fields: {
        title: { 
            type: GraphQLString 
        },
        author: { 
            type: GraphQLString 
        }
    }
});

bookType 이라는 오브젝트 타입은 생성되었지만 아직은 사용할 수 없습니다. 이것은 말 그대로 타입에 불과하기 때문에 실제 데이터가 주어져야 각 필드의 값을 결정(resolve)할 수 있기 때문입니다. 클라이언트가 bookTypetitle이 있다는 것을 알고 제목을 가져오려 했지만, 실제 데이터가 없다면 어떤 정보도 얻을 수 없겠죠.


데이터를 결정하는 방법에는 2가지가 있습니다.

  • 타입에서 리졸브
  • 객체에서 리졸브

타입에서 결정 :

아래의 코드는 Type 수준에서 각 필드의 값을 resolve를 통해 결정하고 있습니다. title의 값은 동물농장으로 고정되었으며, author의 값도 조지오웰로 고정되었습니다. 타입이 결정한 것이기에 이외의 다른 값을 가질 수 없습니다.

//Typescript.
const bookType: GraphQLObjectType = new GraphQLObjectType({
    name: "bookType",
    fields: {
        title: { 
            type: GraphQLString,
            resolve: () => "동물농장"
        },
        author: { 
            type: GraphQLString,
            resolve: () => "조지 오웰"
        }
    }
});

객체에서 결정 :

Type이 직접 필드값을 결정하지 않았다면, 반환된 데이터에서 동일한 프로퍼티의 값으로 리졸브합니다. 아래의 코드에서 libraryTypebookType[]을 반환한다고 선언하고 동일한 구조의 데이터를 반환했기 때문에, 제목이 각각 동물농장1984bookType이 반환됩니다.

//Typescript
const bookType: GraphQLObjectType = new GraphQLObjectType({
    name: "bookType",
    fields: {
        title: {
            type: GraphQLString
        },
        author: {
            type: GraphQLString
        }
    }
});

const libraryType: GraphQLObjectType = new GraphQLObjectType({
    name: "libraryType",
    fields: {
        books: {
            type: new GraphQLList(bookType),
            resolve: () => {
                return [
                    { title: "동물농장", author: "조지 오웰" },
                    { title: "1984년", author: "조지 오웰" }
                ];
            }
        }
    }
});

루트 오브젝트

사용자가 처음으로 접근할 수 있는 오브젝트 타입을 루트 오브젝트라고 부릅니다. 예를 들어, 내 방의 문을 열기 위해서는 내 집의 문을 먼저 열어야 하죠? 또 그 전에는 아파트 정문 또는 아파트 후문을 먼저 열어야 할겁니다.

내 방의 문에 바로 갈 수 없기 때문에, 아파트 정문 또는 아파트 후문부터 시작해야 하죠. 이런 시작지점의 역할을 하는 오브젝트를 루트 오브젝트라고 부릅니다.


루트의 필드는 특별하다

루트 오브젝트는 말 그대로 서버의 시작점(Entry-Point)이기 때문에, 루트 오브젝트의 필드는 오퍼레이션의 이름을 갖습니다.

const root: GraphQLObjectType = new GraphQLObjectType({
    name: "root",
    fields: {
        allBooks: {
            ...
        },
        findBook: {

        },
        addBook: {
            ...
        },
        delBook: {
            ...
        },
        editBook: {
            ...
        },
    }
});

쿼리와 뮤테이션

루트 오브젝트에서 각 필드의 resolve는 데이터베이스를 수정하거나 외부변수를 조작하는 등의 사이드 이펙트(Side-Effect)를 발생시킬 수 있습니다. 이 때, 사이드 이펙드가 없는 필드를 쿼리라고 부르고, 사이드 이펙트를 동반하는 필드를 뮤테이션이라고 부릅니다.

데이터를 조회하는 필드 (Query) :

단순히 데이터를 조회하는 것은 사이드 이펙트가 없으므로, 이 필드는 쿼리로 생각할 수 있습니다. allBooksfindBook 같은 필드가 이에 해당합니다.


데이터를 수정하는 필드 (Mutation) :

데이터베이스를 수정, 추가, 삭제하는 것은 사이드 이펙트이므로, 이 필드는 뮤테이션으로 생각할 수 있습니다. addBook, delBook, editBook이 이에 해당합니다.


쿼리 오브젝트, 뮤테이션 오브젝트

다만 GraphQL.JS는 쿼리만 모아놓은 오브젝트와, 뮤테이션만 모아놓은 오브젝트를 나누어 설계하는 방식을 취합니다.

//Typescript
const schemaObject: GraphQLSchema = new GraphQLSchema({
    query: queryObject,  // 필수
    mutation: mutationObject // 선택
});

Express에 부착하기

위에서 생성된 스키마를 express-graphql 모듈에 넘겨서 graphql middleware를 생성할 수 있으며, 이것을 express에 넘길 수 있습니다.

//Typescript
import express from "express";
import expressGraphQL from "express-graphql";
import querySchema from "./querySchema";

const app = express();
app.use(
    "/",
    expressGraphQL({
        schema: schemaObject,
        graphiql: true
    })
);

graphiql: true로 설정하면 playground페이지도 함께 만들어집니다.