GraphQL 사용법 알아보기

2017년 04월 01일


서버 로직 작성하기

기본적인 모양은 잡혔습니다. 이제 GraphQL의 기능을 사용해서 다양한 데이터를 제공하는 방법에 대해 알아봅니다.

타입 정의하기

서버를 작성하기에 앞서, 서버가 어떤 데이터를 제공할지 결정해야합니다. 지금 만든 서버는 hello와 world라는 null이 될 수 없는 String을 제공하고 있습니다.

제공할 데이터는 스키마를 설계하는 것으로 결정합니다. 우선 typeDefs에 제공할 타입을 명시하고, 실제로 타입에 대한 값을 다루는 로직은 resolver에 작성합니다.

schema.js

const { makeExecutableSchema } = require('graphql-tools');

const Query = { 
  getCity(root, args, context) {
    return { id: 42, name: 'merong' };
  }
};
const City = {
  name(city, args, context) {
    return `${city.name} city`;
  },
  greeting(city, args, context) {
    return `Hello, ${city.name} city`;
  }
};
const resolvers = { Query, City };
const typeDefs = `
  type Query { 
    getCity: City!
  }
  type City {
    id: ID!
    name: String!
    greeting: String!
  }
`;

module.exports = makeExecutableSchema({ typeDefs, resolvers });

이제 서버를 갱신하고 GraphiQL에서 쿼리를 실행해보겠습니다. getCity는 필드를 갖기 때문에 중첩된 쿼리를 작성합니다.

{
  getCity {
    id
    name
    greeting
  }
}

결과는 다음과 같습니다.

{
  "data": {
    "getCity": {
      "id": "42",
      "name": "merong city",
      "greeting": "Hello, merong city"
    }
  }
}

getCity에서 제공하는 데이터는 { id: 42, name: 'merong' }지만, 질의의 결과로 얻은 데이터는 조금 다르게 생겼습니다.

이러한 동작은 GraphQL이 쿼리를 실행한 뒤, 결과값을 다시 타입에 해당하는 resolver로 실행하기 때문에 발생한 결과입니다. 코드를 다시 살펴보겠습니다.

schema.js

const { makeExecutableSchema } = require('graphql-tools');

const Query = { 
  getCity(root, args, context) {
    return { id: 42, name: 'merong' };
  }
};
const City = {
  name(city, args, context) {
    return `${city.name} city`;
  },
  greeting(city, args, context) {
    return `Hello, ${city.name} city`;
  }
};
const resolvers = { Query, City };
const typeDefs = `
  type Query { 
    getCity: City!
  }
  type City {
    id: ID!
    name: String!
    greeting: String!
  }
`;

module.exports = makeExecutableSchema({ typeDefs, resolvers });

우선 getCity 쿼리가 들어오면 Query의 resolver의 필드 getCity를 실행시킵니다. 여기서 반환된 { id: 42, name: 'merong' }는 typeDefs에 의해서 City 타입이기 때문에 City의 resolver로 호출됩니다.

City의 resolver에는 id 필드가 없기 때문에 Query.getCity에서 반환한 객체의 id를 그대로 사용합니다. 하지만 name 필드는 있기 때문에, 기존의 merong을 그대로 반환하지 않고, City.name를 실행합니다.

이 과정에서 City.name의 첫 번째 인자 city로 { id: 42, name: 'merong' }가 전달됩니다. 이 값을 이용해 반환할 name을 작성합니다.

마지막으로 City.greeting 필드입니다. 이 필드는 Query.getCity의 결과값에는 없는 필드입니다. City.name과 마찬가지로 City.greeting의 resolver가 실행되고, 첫 번째 인자로 전달된 city값을 이용해 반환할 greeting을 작성합니다.

이 과정은 몇 번이고 재귀적으로 반복할 수 있습니다. resolver를 통해 값을 만들고 필요하다면 타입에 대한 resolver를 다시 호출합니다. resolver가 실행되는 모양이 마치 그래프를 순회하는 것과 비슷하다 하여 이름부터 GraphQL입니다.

의존성 추가하기

위 코드는 필요한 데이터를 직접 함수에서 생성하고 있습니다. 실제 개발을 할때는 데이터베이스를 이용하거나, 기존의 API에 요청하는 등, 외부의 데이터를 이용해 요청에 응답합니다.

때문에 resolver 안에서 외부 데이터에 접근할 수단이 필요합니다. 이럴때 사용할 수 있는 것이 resolver의 세 번째 인자 context입니다.

context는 다음과 같이 graphqlExpress를 호출하는 과정에 넘겨줄 수 있습니다.

index.js

const express = require('express');
const bodyParser = require('body-parser');
const { graphqlExpress, graphiqlExpress } = require('apollo-server-express');
const schema = require('./schema');
const app = express();

const context = {
  db: { 
    42: { id: 42, name: 'merong' },
    17: { id: 17, name: 'bikini' }
  }
};
// graphqlExpress에 context를 추가하면 resolver에 context가 넘어갑니다
app.use('/graphql', bodyParser.json(), graphqlExpress({ schema, context }));
app.use('/graphiql', graphiqlExpress({ endpointURL: '/graphql' }));
app.listen(7777, () => console.log("Hello GraphQL!"));

context에는 어떠한 데이터라도 올 수 있습니다. DB 커넥션이나 질의를 요청한 사용자의 정보 혹은 auth에 대한 토큰등이 올 수 있습니다. 위 코드에서는 간단하게 모킹 DB를 전달했습니다.

context에 넘겨진 모킹 DB는 resolver에서 직접 접근할 수 있습니다.

schema.js

const { makeExecutableSchema } = require('graphql-tools');

const Query = { 
  getCity(root, args, context) {
    return context.db[42];
  }
};
const City = {
  name(city, args, context) {
    return `${city.name} city`;
  },
  greeting(city, args, context) {
    return `Hello, ${city.name} city`;
  }
};
const resolvers = { Query, City };
const typeDefs = `
  type Query { 
    getCity: City!
  }
  type City {
    id: ID!
    name: String!
    greeting: String!
  }
`;

module.exports = makeExecutableSchema({ typeDefs, resolvers });

GraphiQL에서 확인해보면 이전과 같은 결과를 얻을 수 있는 것을 확인할 수 있습니다. 동작은 같지만 resolver의 내부에서 context를 이용하고 있습니다.