쿠팡이츠, Subscriptions를 활용한 실시간 애플리케이션1

shooting star·2024년 5월 10일
1
post-thumbnail

들어가며

GraphQL은 REST API와 달리 한 번의 요청으로 필요한 데이터만 효율적으로 가져올 수 있는 장점이 있습니다. 그러나 실시간 애플리케이션에서는 데이터 변화를 클라이언트로 즉시 전달하는 것이 중요합니다. 이때 GraphQL Subscriptions를 사용하면 효율적으로 실시간 데이터를 전송할 수 있습니다. 이번 블로그에서는 GraphQL Subscriptions와 NestJS를 활용한 실시간 애플리케이션을 구축하는 방법을 소개하겠습니다.

GraphQL Subscriptions란?

GraphQL Subscriptions는 pubsub 시스템(예: Redis)과 GraphQL을 연결하여 실시간 데이터 전송을 구현하는 npm 패키지입니다. 이 패키지는 모든 GraphQL 클라이언트 및 서버(Apollo 포함)와 함께 사용할 수 있습니다.

설치

먼저 필요한 패키지를 설치합니다.

npm install graphql-subscriptions

PubSub 설정

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 통합

이제 NestJS 프로젝트에서 GraphQL Subscriptions를 활성화하기 위해 필요한 설정을 알아보겠습니다.

GraphQLModule 설정

GraphQLModule을 구성하여 GraphQL Subscriptions를 활성화하려면 graphql-ws 프로토콜을 지원해야 합니다. 이를 위해 ApolloDrivergraphql-ws를 함께 사용합니다.

  1. Driver 설정: ApolloDriver를 사용하여 GraphQL 서버를 구동합니다.
  2. Schema 자동 생성: autoSchemaFile 옵션을 true로 설정하면 GraphQL 스키마 파일을 자동으로 생성합니다.
  3. 구독 설정:
    • 웹소켓 프로토콜 사용: 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 상수 및 의존성 주입

PubSub 인스턴스를 모든 Resolver에서 사용할 수 있도록 상수와 의존성 주입을 설정합니다. 이를 위해 CommonModule을 작성하고, 이 모듈을 전역으로 사용하여 어디서나 쉽게 PubSub를 사용할 수 있도록 구성합니다.

  1. 상수 정의: 이벤트 이름과 의존성 주입을 위한 토큰을 common.constants.ts에 정의합니다.
// common.constants.ts
export const PUB_SUB = 'PUB_SUB';
export const NEW_PENDING_ORDER = 'NEW_PENDING_ORDER';
  1. 글로벌 모듈 구성: 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 {}
  1. AppModuleCommonModule 포함: 글로벌 모듈로 설정되었기 때문에 명시적으로 가져올 필요는 없지만, 명확성을 위해 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 {}

GraphQL Subscriptions를 이용한 주문 서비스 구현

이제 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를 이용한 테스트를 살펴보도록 하겠습니다.

NestJS GraphQL Subscriptions 공식 문서

0개의 댓글