GraphQL은 REST API와 달리 한 번의 요청으로 필요한 데이터만 효율적으로 가져올 수 있는 장점이 있습니다. 그러나 실시간 애플리케이션에서는 데이터 변화를 클라이언트로 즉시 전달하는 것이 중요합니다. 이때 GraphQL Subscriptions를 사용하면 효율적으로 실시간 데이터를 전송할 수 있습니다. 이번 블로그에서는 GraphQL Subscriptions와 NestJS를 활용한 실시간 애플리케이션을 구축하는 방법을 소개하겠습니다.
GraphQL Subscriptions는 pubsub
시스템(예: Redis)과 GraphQL을 연결하여 실시간 데이터 전송을 구현하는 npm 패키지입니다. 이 패키지는 모든 GraphQL 클라이언트 및 서버(Apollo 포함)와 함께 사용할 수 있습니다.
먼저 필요한 패키지를 설치합니다.
npm install graphql-subscriptions
graphql-subscriptions
패키지에서 제공하는 PubSub
클래스를 사용하여 이벤트를 발행하고 구독하는 로직을 간단하게 구현할 수 있습니다.
import { PubSub } from 'graphql-subscriptions';
const pubsub = new PubSub();
asyncIterator
메서드를 사용하여 특정 이벤트를 구독하고, publish
메서드를 사용하여 이벤트를 발행합니다.
// 이벤트 발행
pubsub.publish('SOMETHING_CHANGED', { somethingChanged: { id: '123' } });
// 이벤트 구독
const resolvers = {
Subscription: {
somethingChanged: {
subscribe: () => pubsub.asyncIterator('SOMETHING_CHANGED'),
},
},
};
이제 NestJS 프로젝트에서 GraphQL Subscriptions를 활성화하기 위해 필요한 설정을 알아보겠습니다.
GraphQLModule
을 구성하여 GraphQL Subscriptions를 활성화하려면 graphql-ws
프로토콜을 지원해야 합니다. 이를 위해 ApolloDriver
와 graphql-ws
를 함께 사용합니다.
ApolloDriver
를 사용하여 GraphQL 서버를 구동합니다.autoSchemaFile
옵션을 true
로 설정하면 GraphQL 스키마 파일을 자동으로 생성합니다.graphql-ws
프로토콜을 사용하여 웹소켓 기반 구독을 활성화합니다.onConnect
콜백: 웹소켓 연결 시 실행되는 콜백 함수로, 연결된 클라이언트의 connectionParams
를 통해 JWT 토큰 등 필요한 정보를 전달받습니다.context
옵션: 각 요청에 대한 context
를 설정합니다. 웹소켓 연결 시에는 extra
에 저장된 토큰을 사용하며, 일반 HTTP 요청의 경우에는 req.headers
에서 토큰을 가져옵니다.// app.module.ts
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { Context } from 'apollo-server-core';
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: true,
subscriptions: {
'graphql-ws': {
onConnect: (context: Context<any>) => {
const { connectionParams, extra } = context;
extra.token = connectionParams['x-jwt'];
},
},
},
context: ({ req, extra }) => {
return { token: req ? req.headers['x-jwt'] : extra.token };
},
}),
],
})
export class AppModule {}
PubSub
인스턴스를 모든 Resolver에서 사용할 수 있도록 상수와 의존성 주입을 설정합니다. 이를 위해 CommonModule
을 작성하고, 이 모듈을 전역으로 사용하여 어디서나 쉽게 PubSub를 사용할 수 있도록 구성합니다.
common.constants.ts
에 정의합니다.// common.constants.ts
export const PUB_SUB = 'PUB_SUB';
export const NEW_PENDING_ORDER = 'NEW_PENDING_ORDER';
CommonModule
을 글로벌 모듈로 설정하여 PubSub 인스턴스를 제공하고 공유합니다.// common.module.ts
import { Global, Module } from '@nestjs/common';
import { PubSub } from 'graphql-subscriptions';
import { PUB_SUB } from 'src/common.common.constants';
const pubsub = new PubSub();
@Global()
@Module({
providers: [
{
provide: PUB_SUB,
useValue: pubsub,
},
],
exports: [PUB_SUB],
})
export class CommonModule {}
AppModule
에 CommonModule
포함: 글로벌 모듈로 설정되었기 때문에 명시적으로 가져올 필요는 없지만, 명확성을 위해 AppModule
에서 CommonModule
을 가져옵니다.// app.module.ts
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { Context } from 'apollo-server-core';
import { CommonModule } from './common.module';
@Module({
imports: [
CommonModule,
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: true,
subscriptions: {
'graphql-ws': {
onConnect: (context: Context<any>) => {
const { connectionParams, extra } = context;
extra.token = connectionParams['x-jwt'];
},
},
},
context: ({ req, extra }) => {
return { token: req ? req.headers['x-jwt'] : extra.token };
},
}),
],
})
export class AppModule {}
이제 PubSub
인스턴스를 통해 주문 상태 업데이트를 실시간으로 구독하는 기능을 구현하겠습니다.
OrderService
에서는 PubSub
를 의존성 주입하여 신규 주문 생성 시 이벤트를 발행합니다.
// orders.service.ts
import { Injectable, Inject } from '@nestjs/common';
import { PubSub } from 'graphql-subscriptions';
import { PUB_SUB, NEW_PENDING_ORDER } from 'src/common.common.constants';
@Injectable()
export class OrderService {
constructor(@Inject(PUB_SUB) private readonly pubSub: PubSub) {}
async createOrder(): Promise<void> {
const order = { id: '123', status: 'Pending' }; // 가상의 주문 데이터
await this.pubSub.publish(NEW_PENDING_ORDER, {
pendingOrders: { order, ownerId: 'owner-id' },
});
}
}
설명:
createOrder
메서드에서 가상의 주문 데이터를 생성한 후 NEW_PENDING_ORDER
이벤트를 발행합니다.pubSub.publish
메서드의 첫 번째 매개변수는 이벤트 이름이며, 두 번째 매개변수는 이벤트에 담을 데이터 객체입니다.OrderResolver
에서는 Subscription
데코레이터를 사용하여 pendingOrders
이벤트를 구독합니다.
// orders.resolver.ts
import { Resolver, Subscription } from '@nestjs/graphql';
import { Inject } from '@nestjs/common';
import { PubSub } from 'graphql-subscriptions';
import { PUB_SUB, NEW_PENDING_ORDER } from 'src/common.common.constants';
@Resolver()
export class OrderResolver {
constructor(@Inject(PUB_SUB) private readonly pubSub: PubSub) {}
@Subscription(returns => String, {
filter: ({ pendingOrders: { ownerId } }, _, { user }) => {
return ownerId === user.id;
},
resolve: ({ pendingOrders: { order } }) => order,
})
pendingOrders() {
return this.pubSub.asyncIterator(NEW_PENDING_ORDER);
}
}
GraphQL Subscriptions는 실시간 데이터를 필요로 하는 애플리케이션에서 강력한 기능을 제공합니다. 이번 포스팅에서는 NestJS와 함께 GraphQL Subscriptions를 활성화하고, PubSub를 사용하여 간단한 주문 상태 업데이트 시스템을 구축하는 방법을 다뤘습니다. 다음편에서는 오늘의 내용을 이어서 필터링 및 페이로드 변경
그리고 Altair GraphQL Client를 이용한 테스트
를 살펴보도록 하겠습니다.