GraphQL 사용법 알아보기

2017년 04월 01일


파라미터와 뮤테이션

지금까지 알아본 기능만으로는 쿼리에 파라미터를 명시하거나 데이터를 변경하는 작업을 할 수 없었습니다. GraphQL은 스키마 수준에서 파라미터를 명시할 수 있고, 데이터 변경을 위한 특별한 동작 뮤테이션을 제공합니다.

파라미터 추가하기

하나의 스키마에서 다양한 데이터를 얻기 위해선 파라미터를 이용합니다.

GraphQL을 요청하는 측에서는 간단하게 쿼리에 파라미터를 추가할 수 있습니다. 함수를 호출하는것 처럼 괄호를 열고, 파라미터의 이름과 값을 전달합니다.

{
  getCity(id: 42) {
    id
    name
    greeting
  }
  getCity(id: 17) {
    id
    name
    greeting
  }
  getCity(id: 61) {
    id
    name
    greeting
  }
}

이 쿼리를 그대로 요청하면 오류가 발생합니다. 아직 스키마에서 파라미터를 받을 준비가 되지 않았기 때문입니다.

우선 typeDefs의 getCity 필드에 파라미터를 추가하고, 파라미터를 resolver에서 이용할 수 있도록 Query.getCity를 변경합니다. 쿼리를 통해 전달된 파라미터는 resolver의 두 번째 파라미터 args에 전달됩니다.

schema.js

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

const Query = { 
  getCity(root, args, context) {
    const { id } = args;
    return context.db[id];
  }
};
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 { 
    # GraphQL의 주석은 #으로 작성합니다
    # 괄호로 파라미터를 명시하고 타입도 적습니다
    # 반환되는 값이 null일 수 있기 때문에 City!를 City로 변경합니다
    getCity(id: ID!): City
  }
  type City {
    id: ID!
    name: String!
    greeting: String!
  }
`;

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

쿼리도 변경할 필요가 있습니다. GraphQL의 반환 값은 JSON 객체이기 때문에 중복된 필드를 가질 수 없습니다. 이 경우 getCity가 3개나 있기 때문에 JSON 규칙에 어긋납니다.

다음과 같이 getCity의 alias를 추가합니다.

{
  bikiniCity: getCity(id: 17) {
    id
    name
    greeting
  }
  merongCity: getCity(id: 42) {
    id
    name
    greeting
  }
  voracity: getCity(id: 61) {
    id
    name
    greeting
  }
}

필드에 alias를 추가했기 때문에 결과도 alias로 얻습니다. 그리고 getCity의 타입을 City!에서 City로 변경했기 때문에 null을 반환할 수 있습니다.

{
  "data": {
    "bikiniCity": {
      "id": "17",
      "name": "bikini city"
    },
    "merongCity": {
      "id": "42",
      "name": "merong city"
    },
    "voracity": null
  }
}

뮤테이션 추가하기

지금까지 알아본 방법으로는 서버의 데이터를 변경할 수 없었습니다. 사실 Query에 대한 resolver에서 데이터를 변경하면 불가능한 일은 아닙니다.

하지만 이러한 방법은 추천드리지 않습니다. GraphQL은 Query가 데이터를 변경하지 않을 것이라 가정하기 때문에 병렬로 실행됩니다. 만약 Query에서 데이터를 변경한다면 레이스 컨디션이 발생하게 됩니다.

이러한 병렬 실행을 피하기 위한 것이 뮤테이션입니다. 뮤테이션은 항상 순차적으로 동작하기 때문에 레이스 컨디션이 발생하지 않습니다.

뮤테이션을 사용하는 방법에 대해 알아보기 전에, 지금까지 작성한 쿼리를 다시 살펴보겠습니다.

{
  getCity(id: 42) {
    id
    name
    greeting
  }
}

사실 이 쿼리는 축약된 형태입니다. 다음과 같이 장황하게 표현할 수 있습니다.

query AwesomeQueryNameForDebug {
  getCity(id: 42) {
    id
    name
    greeting
  }
}

눈여겨 볼 부분은 query라는 키워드입니다. 이 query 키워드를 mutation으로 바꾸기만 하면 뮤테이션을 사용할 수 있습니다.

mutation AwesomeMutationNameForDebug {
  updateCity(id: 42, name: "vora") {
    id
    name
  }
}

뮤테이션은 특별한 동작이기 때문에 서버에서도 특별하게 다룹니다. 기존의 스키마와는 다르게 따로 Mutation이라는 typeDefs를 작성하고, 동작도 Query 객체가 아닌 새로운 Mutation 객체에 명시해야합니다.

schema.js

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

const Query = { 
  getCity(root, args, context) {
    const { id } = args;
    return context.db[id];
  }
};
const Mutation = { 
  // 뮤테이션에 대한 resolver도 구조가 같습니다
  updateCity(root, args, context) {
    const { id, name } = args;
    let city = context.db[id];
    // 값을 직접 변경합니다
    // null이 반환되면 뮤테이션이 실패한 것을 알 수 있습니다
    if (city)
      return Object.assign(city, { name });
    else 
      return null;
  }
};
const City = {
  name(city, args, context) {
    return `${city.name} city`;
  },
  greeting(city, args, context) {
    return `Hello, ${city.name} city`;
  }
};
// resolvers에 Mutation을 추가합니다
const resolvers = { Query, Mutation, City };
const typeDefs = `
  type Query { 
    getCity(id: ID!): City
  }
  type Mutation { 
    # 쿼리와 비슷하게 뮤테이션을 명시합니다
    updateCity(id: ID!, name: String!): City
  }
  type City {
    id: ID!
    name: String!
    greeting: String!
  }
`;

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

이제 서버를 갱신하고 뮤테이션을 작성해봅니다.

mutation {
  firstMutaion: updateCity(id: 42, name: "velo") {
    id
    name
    greeting
  }
  secondMutaion: updateCity(id: 42, name: "vora") {
    id
    name
    greeting
  }
}

위의 뮤테이션을 요청하면 순차적으로 실행되기 때문에 merong city는 velo city로 변한 뒤 다시 vora city로 변경됩니다. 결과값으로 이 과정에 발생하는 값을 확인할 수 있습니다.

{
  "data": {
    "firstMutaion": {
      "id": "42",
      "name": "velo city",
      "greeting": "Hello, velo city"
    },
    "secondMutaion": {
      "id": "42",
      "name": "vora city",
      "greeting": "Hello, vora city"
    }
  }
}

이제 쿼리를 요청하면 변경된 값을 얻을 수 있습니다.

{
  getCity(id: 42) {
    id
    name
    greeting
  }
}

이 설계에서는 resolver의 동작방식을 설명하기 위해 name 필드 값에 city를 붙이고 있습니다. 때문에 뮤테이션에서 name으로 vora를 사용해야하는지, vora city를 사용해야 하는지 명확하지 못합니다.

서버를 설계할때는 항상 직관적으로 동작하도록 코드를 작성해야 합니다.

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

이제 서버를 작성하기 위한 핵심적인 부분은 전부 소개했습니다.