어서와, 개발은 처음이지?

express-graphql 시작하기(Hello World Guide) 본문

GraphQL

express-graphql 시작하기(Hello World Guide)

오지고지리고알파고포켓몬고 2018. 10. 14. 20:50
반응형



몇년전 등장한 GraphQL은 RESTful API의 여러 단점들을 커버할 수 있는 것으로 잘 알려져있습니다.

하지만 불행히도 개념적인 내용은 검색만으로 쉽게 접할 수 있지만 사용법에 대한 내용은 찾아보기 쉽지 않습니다.(심지어 GraphQL의 공식 사이트에서도 구조와 스키마같은 기본 설명만 있을 뿐 명확한 사용법은 쉽게 찾아볼 수 없습니다.)


그래서 저도 GraphQL을 많이 다뤄보지 않았지만, GraphQL을 처음 시작하시는 분들에게 조금이나마 가이드가 되기를 바라며 기본적인 사용법에 대해 작성하겠습니다.


이 글에 작성된 내용은 Github에서 확인할 수 있습니다.



1. 설치


GraphQL을 독립적으로 사용할 수도 있지만 express-graphql이라는 미들웨어를 사용하면 Express.js에 쉽게 적용할 수 있습니다.

npm install --save express express-graphql graphql

GraphQL을 사용하기 위해서 우리의 프로젝트에 express, express-graphql, graphql을 설치해줍니다.



2. Hello World


설치가 완료되었다면 app.js 파일을 만들고 아래 코드를 넣어줍니다.

var express = require('express');
var graphqlHTTP = require('express-graphql');
var { buildSchema } = require('graphql');

var schema = buildSchema(`
  type Query {
    hello: String
  }
`);

var root = { hello: () => 'Hello world!' };

var app = express();
app.use('/graphql', graphqlHTTP({
  schema: schema,
  rootValue: root,
  graphiql: true,
}));
app.listen(4000, () => console.log('Now browse to localhost:4000/graphql'));

이제 node명령을 사용하여 app.js를 실행한 뒤, localhost:4000/graqhql로 접속했을 때 아래와 같은 화면이 나온다면 GraphQL을 시작할 준비가 끝난 것 입니다.

<graphiql>


위 화면은 graphiql이라고 하며, 우리가 만든 스키마의 값을 쉽게 볼 수 있도록 도와주는 인터페이스 입니다.

필드명의 자동완성 기능을 가지고 있습니다.


여기까지가 공식 문서에 나와있는 내용입니다.



3. buildSchema로 스키마 만들기


GraphQL의 스키마를 만드는 방법은 공식 문서처럼 buildSchema()를 사용하여 String 형태로 명시하는 방법과 GraphQLSchema()를 사용하여 객체 형태로 명시하는 방법이 있습니다.


우선 감을 잡기위해 아래 코드로 스키마를 수정합니다.

var schema = buildSchema(`
  type Query {
    hello: String
    persons: [Person]
  }

  type Person {
    name: String
    age: Int
  }
`);

var root = {
  hello: () => 'Hello world!',
  persons: () => {
    return [
      {name:"kim", age: 20},
      {name:"lee", age: 30},
      {name:"park", age: 40},
    ];
  }
};

app.js를 재실행해보면 graphiql에서 persons 필드를 사용할 수 있게됩니다.

<persons 요청>


이미 문서를 보신분들은 resolver를 사용하여 실제 데이터에 접근한다는 것을 보셨을겁니다.


buildSchema로 정의한 패턴에서는 graqhplHTTP의 rootValue가 resolver의 집합이고 query와 매칭되는 rootValue가 실행됩니다. 실제 DB를 사용한다면 이 부분에 CRUD를 작성하게 됩니다.




4. GraphQLSchema로 스키마 만들기


GraphQLSchema를 사용하는 패턴은 객체 형태로 분리하여 관리할 수 있는 이점이 있지만 여러모로 명시적인 내용을 많이 작성해야합니다.

var express = require('express');
var graphqlHTTP = require('express-graphql');
var Graphql = require('graphql');

const personType = new Graphql.GraphQLObjectType({
  name: "Person",
  fields:{
    name: { type: Graphql.GraphQLString },
    age: { type: Graphql.GraphQLInt }
  }
});

const queryType = new Graphql.GraphQLObjectType({
  name: "Query",
  fields: {
    hello: {
      type: Graphql.GraphQLString,
      resolve: () => 'Hello world!',
    },
    persons: {
      type: new Graphql.GraphQLList(personType),
      resolve: () => {
        return [
          {name:"kim", age: 20},
          {name:"lee", age: 30},
          {name:"park", age: 40},
        ];
      }
    }
  }
});

const schema = new Graphql.GraphQLSchema({query: queryType});

var app = express();
app.use('/graphql', graphqlHTTP({
  schema: schema,
  graphiql: true,
}));
app.listen(4000, () => console.log('Now browse to localhost:4000/graphql'));

3번의 스키마 구조를 위와 같이 변경할 수 있습니다. 

두 코드를 직접 비교해보시기를 권장드립니다.


두 방식 모두 장단이 있지만 위 코드는 자료형(type)이 좀 더 까다로워 보입니다.

자료형의 종류는 이 곳에서 확인할 수 있습니다.



5. Context 사용


GraphQL의 resolver는 Context를 사용할 수 있습니다.

Context로 사용할 수 있는 주요 객체는 session이 있습니다.


우선 buildSchema를 기준으로 코드를 보겠습니다.

var app = express();
const session = {id: "1001", expires: 20000};

app.use('/graphql', graphqlHTTP({
  schema: schema,
  rootValue: root,
  context: session,
  graphiql: true,
}));

여기서는 임의로 session이라는 객체를 생성하여 graphqlHTTP안에 context 값에 넣어줍니다.(express-session 객체를 넣어줘도 됩니다.)

var root = {
  hello: () => 'Hello world!',
  persons: (args, context, info) => {
    console.log(context);

    return [
      {name:"kim", age: 20},
      {name:"lee", age: 30},
      {name:"park", age: 40},
    ];
  }
};

그리고 resolver에 해당하는 root 객체를 위와 같이 수정해줍니다.

persons의 resolver에 args, context, info 파라미터가 추가된 모습입니다.


args는 query에 쓰는 인자, info는 스키마의 정보(?)를 가지고 있습니다.


app.js를 재실행하고 persons쿼리를 요청하면 콘솔창에 세션이 나오는 모습을 볼 수 있습니다.

<persons query 요청>

<context로 session 객체가 넘겨지는 모습>


이를 사용하여 비인가된 사용자에게 API를 제공하지 않을 수 있습니다.

GraphQL의 문서에서는 resolver에서 세션검증을 하기 보다는 비즈니스 로직단에서 검증하는 것을 권장하고 있습니다.


<출처 : graphql.org>


이런 패턴에 대한 내용은 이 다른 글에서 작성하도록 하겠습니다.



GraphQLSchema에서 context 사용은 아래와 같습니다.

const queryType = new Graphql.GraphQLObjectType({
  name: "Query",
  fields: {
    hello: {
      type: Graphql.GraphQLString,
      resolve: () => 'Hello world!',
    },
    persons: {
      type: new Graphql.GraphQLList(personType),
      resolve: (post, args, context, info) => {
        console.log(context);
        
        return [
          {name:"kim", age: 20},
          {name:"lee", age: 30},
          {name:"park", age: 40},
        ];
      }
    }
  }
});

buildSchema의 resolver와 파라미터 순서가 다른점을 유의하시기 바랍니다.




6. Arguments 사용


GraphQL의 Query에 Arguments를 넣음으로써 조건을 넣을 수 있습니다.

이는 Schema에 명시해야 합니다.


먼저 buildSchema 입니다.

var schema = buildSchema(`
  type Query {
    hello: String
    persons(name: String, age: Int): [Person]
  }

  type Person {
    name: String
    age: Int
  }
`);

var root = {
  hello: () => 'Hello world!',
  persons: (args, context, info) => {
    console.log(context);
    console.log(args);
    const {name, age} = args;

    return [
      {name:"kim", age: 20},
      {name:"lee", age: 30},
      {name:"park", age: 40},
    ].filter((person) => {
      if(!name && !age){ return true; }
      if(!age && name && person.name === name){ return true; }
      if(!name && age && person.age === age){ return true; }
      if(name && age && person.name === name && person.age === age){ return true; }
      return false;
    });
  }
};

스키마에서 Query의 persons 부분에 입력받을 args의 정보를 설정합니다.

name: String!와 같은 방식으로 사용하면 name인자는 필수요소가 됩니다.


persons의 resovler가 조금 난잡해보이지만 DB access를 담당하는 로직이 별도로 있거나 원하는 결과를 나타낼 쿼리 문을 사용할 수 있는 경우 훨씬 깔끔하게 작성할 수 있습니다.


<name이 kim인 person을 요청>

<age가 1000인 person을 요청>

<name이 lee, age가 30인 person을 요청>

<args와 context의 로그>


GraphQLSchema에서의 사용은 아래와 같습니다.

const queryType = new Graphql.GraphQLObjectType({
  name: "Query",
  fields: {
    hello: {
      type: Graphql.GraphQLString,
      resolve: () => 'Hello world!',
    },
    persons: {
      type: new Graphql.GraphQLList(personType),
      args: {
        name: {type: Graphql.GraphQLString},
        age: {type: Graphql.GraphQLInt}
      },
      resolve: (post, args, context, info) => {
        console.log(context);
        console.log(args);
        const {name, age} = args;

        return [
          {name:"kim", age: 20},
          {name:"lee", age: 30},
          {name:"park", age: 40},
        ].filter((person) => {
          if(!name && !age){ return true; }
          if(!age && name && person.name === name){ return true; }
          if(!name && age && person.age === age){ return true; }
          if(name && age && person.name === name && person.age === age){ return true; }
          return false;
        });
      }
    }
  }
});



7. 마치며


여기까지 기본적인 사용 방법에 대해서 작성했습니다.



개인적인 느낌으로는 비동기 API 통신을 많이하는 SPA앱에 유용할 것 같습니다.


당연하게도 React의 필요에 의해 탄생한 패턴(프레임워크?)이기 때문일까요?

익히 알려진 장점 중 "API 포인트를 하나로 통합하고 중복 사용을 최소화 한다"라는 부분은 프론트 엔드 입장에서 좀 더 와닿습니다.


백엔드에서는 결국 쿼리를 받아서 DB에 접근하는 로직 개발이 필요하기 때문입니다.(저는 처음에 query만으로 DB에 직접 엑세스 한다고 이해했었기에 조금 실망했습니다.)


물론 고수님들은 최소에서 최대를 뽑아내는 로직을 만드시겠지만 그런분들이라면 REST에서도 뚝딱뚝딱 만들어 내시지않을까 싶습니다.

그래도 근래의 어플리케이션이 지향하는 스타일에 유용하게 사용할 수 있는 API 방식임은 확실한 것 같습니다.

 


그럼 계속해서 DB 사용(MongoDB)과 Session과 API 요청까지 단계별로 튜토리얼을 완성시키도록 하겠습니다.




'GraphQL' 카테고리의 다른 글

GraphQL Schema의 기본 문법  (1) 2018.10.24
express-graphql에 mongodb 사용하기  (8) 2018.10.22
Comments