Logo

[GraphQL] Apollo Server로 Subscription 구현

Subscription

GraphQL에는 query와 mutation 그리고 subscription 이렇게 총 3가지 operation type이 있습니다. 이 중에 query는 데이터 조회를 위해서 필수적으로 사용되고, mutation은 데이터 변경을 위해서 많이 사용되고 있습니다. query와 mutation 대비 다소 생소한 subscription은 주로 실시간(real-time) 애플리케이션을 구현하기 위해서 사용되는데요. subscription도 기본적으로 query처럼 데이터를 조회를 위해서 사용되지만 작동 방식에서 큰 차이가 있습니다.

query와 mutation은 전통적인 서버/클라이언트(server/client) 모델을 따르는 반면에, subscription은 발행/구독(pub/sub) 모델을 따릅니다. server/client 모델에서는 클라이언트에서 최신의 데이터를 받아오려면, 더 자주 서버를 호출하는 방법 밖에 없는데요. 접속자가 많은 서버에서 동시 다발적으로 변경이 발생하는 경우 클라이언트에서 아무리 자주 호출하더라도 완벽한 실시간을 달성하기는 어렵습니다. 또한, 변경이 자주 발생하지 않는 서버의 경우, 클라이언트에서 어렇게 자주 호출하는 것이 자체가 서버와 클라이언트 측 모두 낭비와 부담이 될 것입니다.

pub/sub 모델을 따르는 GraphQL의 subscription은 서버에서 발생하는 이벤트를 클라이언트에서 좀 더 효과적으로 인지할 수 있도록 해줍니다. query와 mutation이 HTTP 프로토콜을 사용하는 반면에, subscription은 Web Socket 프로토콜을 사용합니다. Web Socket을 사용하면 클라이언트는 서버와 연결 채널을 유지한체로, 서버에서 발생하는 이벤트를 실시간으로 수신받을 수 있습니다. 이번 포스팅에서는 Apollo Server를 사용하여 서버 측에서 subscription을 어떻게 구현할 수 있는지 살펴보겠습니다.

실습 프로젝트 기본 셋업

Apollo Server를 사용해서 서버 코드를 작성하려면 관련 포스팅를 참고하시어 사전에 기본적인 프로젝트 셋업을 해주셔야 합니다. 본 포스팅의 실습 코딩은 다음과 같이 최소한의 코드가 이미 작성되어 있다는 것을 전제하에 진행하도록 하겠습니다.

const { ApolloServer, gql } = require("apollo-server");

const typeDefs = gql`
  type Query {
    ping: String
  }
`;

const resolvers = {
  Query: {
    ping: () => "pong",
  },
};

const server = new ApolloServer({
  typeDefs,
  resolvers,
});

server.listen().then(({ url }) => {
  console.log(`Listening at ${url}`);
});

실습 프로젝트에서는 트위터와 같이 전 세계에서 실시간으로 메시지가 계속해서 올라올라오는 시나리오를 생각해보겠습니다. 모든 클라이언트에서 실시간 메시지에 대한 업데이트를 받기가 수월하도록 서버에서 Subscription을 제공한다고 가정하고 코드를 작성해보겠습니다.

PubSub 임포트 및 객체 생성

GraphQL 서버에서 subscription을 구현하려면 우선, Redis나 Google PubSub과 같은 pub/sub 엔진이 필요한데요. 다행히 Apollo Server는 자체적으로 pub/sub 엔진을 내장하고 있이며, ApolloServer, gql과 더불어 PubSub 클래스를 간단히 임포트해줄 수 있습니다.

const { ApolloServer, gql, PubSub } = require("apollo-server");

const pubsub = new PubSub();

임포트 한 PubSub 클래스로 객체를 생성하고 pubsub 변수에 할당합니다. 이 pubsub 변수에 할당된 PubSub 객체는 앞으로 계속 사용될 것입니다.

Subscription 타입 정의

query가 Query 타입, mutation이 Mutaiton 타입을 이용해서 스키마를 정의하는 것 처럼, subscription은 Subscription 타입으로 스키마를 정의합니다. 문자열 타입의 데이터를 반환하는 messageAdded이라는 subscription 하나를 Subscription 타입 내에 추가하겠습니다.

const typeDefs = gql`
  type Query {
    ping: String
  }
  type Subscription {
    messageAdded: String
  }
`;

Subscription 리졸버 구현

위에서 정의한 messageAdded subscription이 어떻게 작동할지에 대한 리졸버(resolver)를 추가합니다. query나 mutation에 대한 resolver를 작성할 때는 단순히 함수가 필요한 반면에, subscription은 subscribe 속성을 갖는 객체를 필요로 합니다. 위에서 생성한 PubSub 객체의 asyncIterator() 메서드에 messageAdded라는 이벤트명을 넘겨주면, 이 subscription은 messageAdded 이벤트가 발생할 때 마다 반응하게 됩니다.

const resolvers = {
  Query: {
    ping: () => "pong",
  },
  Subscription: {
    messageAdded: {
      subscribe: () => pubsub.asyncIterator("messageAdded"),
    },
  },
};

이벤트 임의 발생

현재 상태에서 위 subscription을 호출하면 서버에서는 아직 해당 이벤트가 발생할 일이 없으므로 아무런 데이터도 수신되지 않을 것입니다. 일반적으로 이벤트는 서버에서 데이터 변경이 일어날 때 발생하므로 어떤 mutation이 호출될 때 발생시키는 것이 일반적입니다. 하지만 본 포스팅에서는 최대한 간단한 실습 코드를 위해서 1초 간격으로 임의의 이벤트를 발생시켜보겟습니다.

먼저 임의의 메시지를 얻기 위해서 faker라는 외부 패키지를 설치하겠습니다.

$ npm i faker

이벤트를 발생시킬 때는 위에서 생성한 PubSub 객체의 publish() 메서드를 사용해서 이벤트 이름과, 이벤트 객체를 인자로 넘겨줍니다. 자바스크립트 내장 함수인 setInterval을 이용해서 1초 간격으로 위 코드가 실행되게 합니다.

const faker = require("faker");

setInterval(() => {
  pubsub.publish("messageAdded", {
    messageAdded: faker.lorem.sentence(),
  });
}, 1000);

setInterval() 함수에 대한 좀 더 자세한 내용은 관련 포스팅을 참고바랍니다.

서버 테스트

Apollo Server에 내장되어 있는 Playground라는 웹기반 도구를 이용하면 간단하게 GraphQL subscription 테스트를 해볼 수 있습니다. 브라우져에서 http://localhost:4000/를 열고 좌측 섹션에 다음과 같이 GraphQL 쿼리를 입력합니다.

subscription {
  messageAdded
}

그리고 중간에 실행 버튼을 실행하면 우측 섹션에 다음과 같이 1초마다 세로운 이벤트가 수신되는 것을 보실 수 있으실 것입니다.

{
  "data": {
    "messageAdded": "Laboriosam et impedit pariatur dignissimos et quidem in quibusdam."
  }
}

{
  "data": {
    "messageAdded": "Assumenda officiis voluptas harum."
  }
}

{
  "data": {
    "messageAdded": "Hic iure totam veniam est repellat cupiditate ad."
  }
}

전체 코드

const { ApolloServer, gql, PubSub } = require("apollo-server");
const faker = require("faker");

const pubsub = new PubSub();

const typeDefs = gql`
  type Query {
    ping: String
  }
  type Subscription {
    messageAdded: String
  }
`;

const resolvers = {
  Query: {
    ping: () => "pong",
  },
  Subscription: {
    messageAdded: {
      subscribe: () => pubsub.asyncIterator("messageAdded"),
    },
  },
};

setInterval(() => {
  pubsub.publish("messageAdded", {
    messageAdded: faker.lorem.sentence(),
  });
}, 1000);

const server = new ApolloServer({
  typeDefs,
  resolvers,
});

server.listen().then(({ url }) => {
  console.log(`Listening at ${url}`);
});

마치면서

이상으로 Apollo Server를 이용하여 서버 측에서 GraphQL subsription을 구현하는 방법에 대해서 간단히 알아보았습니다. 추후 포스팅에서는 클라이언트 측에서 이 subscription을 호출하는 방법에 대해서 알아보도록 하겠습니다.

관련 포스팅