GraphQL에서는 query
와 mutation
그리고 subscription
총 3가지의 operation type이 있습니다.
이 중 실시간(real-time) 어플리케이션을 구현하기 위해 사용하는 subscription에 대해 이야기 해 볼 예정입니다. 😄
Subscription은 발행/구독(pub/sub)의 모델을 따릅니다.
pub/sub 모델을 따르는 GraphQL의 subscription은 서버에서 발생하는 이벤트를 클라이언트에서 좀 더 효과적으로 인지할 수 있도록 해준다고 합니다.
HTTP 프로토콜을 사용하는 query와 mutation과 달리 Subscription은 Web Socket 프로토콜을 사용한다고 합니다.
Web Socket Protocol
이벤트 방식으로 서버에서 발생하는 이벤트를 실시간으로 수신받을 수 있습니다. 따라서 빠른 데이터 전송이 필요한 앱에 사용하는데 적합합니다.
DB Schema에 대해 간단하게 말씀드리고 시작하려 합니다.
본 예제는 채팅방에 채팅 내역을 실시간으로 확인하기 위해 공부 한 내용을 정리한 내용입니다.
따라서 Message, User, Room(채팅 방)의 구성으로 db table을 설계했다고 생각하시고 봐 주시면 감사하겠습니다! 😄
import { GraphQLServer, PubSub } from 'graphql-yoga';
import schema from './schema'; //typedefs 와 resolver의 schema
const PORT = process.env.PORT || 4000;
const pubsub = new PubSub();
const server = new GraphQLServer({ schema, context: { pubsub } });
server.start({ port: PORT, cors: { origin: '*' } }, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
GraphQL 서버에서 subscription을 구현할려면 Redis나 Google PubSub과 같은 pub/sub엔진이 필요합니다. 이를 graphql-yoga에서 PubSub엔진을 내장하고 있기 때문에 간단하게 pubsub 클래스를 임포트를 해줄 수 있습니다.
import { GraphQLServer, PubSub } from 'graphql-yoga';
const pubsub = new PubSub();
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export default {
Mutation: {
createMessage: async (_, args, { pubsub }) => {
const { text, userId, roomId } = args;
const newMessage = await prisma.message.create({
data: {
text,
user: {
connect: {
id: userId,
},
},
room: {
connect: {
id: roomId,
},
},
},
include: {
user: true,
},
});
pubsub.publish(TRIGGER.NEW_MESSAGE, {
newMessage,
});
return true;
},
},
};
context로 넘겨준 pubsub을 이용해서 새로 생성된 message를 publish합니다.
이벤트를 발생시킬 때 pubsub의 publish() 메서드를 사용해서 이벤트 이름과 이벤트 객체(newMessage)를 인자로 넘겨줍니다.
type Subscription {
newMessage(roomId: Int!): Message
}
subscription의 타입으로 스키마를 정의합니다. 구독하려는 채팅방의 roomId를 데이터로 받아 해당 채팅방의 Message를 반환타입으로 추가하였습니다.
import { withFilter } from 'graphql-subscriptions';
export default {
Subscription: {
newMessage: {
subscribe: withFilter(
(_: any, __: any, { pubsub }) => pubsub.asyncIterator('NEW_MESSAGE'),
(payload, variables) => {
return payload.newMessage.roomId === variables.roomId;
},
),
},
},
};
subscription은 subscribe라는 속성을 갖는 객체를 필요로 합니다.
Server에서 context를 통해 넘겨준 pubsub을 통해 asyncIterator()메서드에 해당 이벤트를 넘겨주면 subscription은 해당 이벤트('NEW_MESSAGE')가 발생할 때마다 반응하게 됩니다.
때때로 클라이언트는 컨텍스트와 인수를 기반으로 특정 이벤트를 필터링하기를 원할 것입니다.
이를 위해이 패키지의 withFilter 도우미를 사용하면 AsyncIterator를 필터 함수로 래핑하고 각 데이터를 선별할 수 있습니다. - Apollo 공식 문서
payload : 이벤트가 발생한 시점에서 넘겨준 data 즉 newMessage를 가지고 있습니다.
variables : typeDefs에서 넘겨 준 data 이 예제에서는 roomId를 가지고 있습니다.
return이 true일 경우만 해당 구독한 내용을 반환해주게 됩니다! 참 쉽죠? 😄
실시간 채팅을 위해 서버를 구현하면서 느낀점은 subscription API를 설계하는 데 사전 지식만 있다면 아주 빠르고 쉽게 API를 생성할 수 있다는 생각이 들었습니다.
또한 Apollo 공식문서의 친절한 예제와 설명을 통해 filter 기능까지 쉽게 구현할 수 있었습니다.😄
아직 프로젝트를 진행하면서 기능 구현을 하는 과정에서 찾아보고 배운 내용을 정리하면서 작성하는 블로그이기 때문에 내용이 부족하거나 잘못된 내용이 있을 수 있습니다! 이점을 고려해서 너그럽게 봐주시면 감사하겠습니다. 🤭
다음에는 GraphQL의 각 resolver에서 Authentication을 구현한 것을 정리 해 볼 예정입니다.🔥