NestJS)GraphQL 서브스크립션

이정훈·2024년 12월 16일

GraphQL

목록 보기
12/13

Subscription

GraphQL은 데이터를 가져오는 쿼리, 데이터를 조작하는 뮤테이션에 더하여 3번째 연산자인 서브스크립션을 지원합니다.
서브스크립션은 데이터에 대한 실시간 변경사항을 받을 수 있게 해주는 기능입니다.

Subscription을 사용하기 위한 셋팅

먼저 graphQL의 서브스크립션 기능을 지원하는 웹소켓 패키지를 설치해야 합니다.

npm install graphql-ws graphql-subscriptions

이후 GraphQLModule의 forRoot메서드에서 subscriptions옵션을 설정해줘야 합니다.

import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { join } from 'path';
import { AppResolver } from './app.resolver';
import { AuthorsModule } from './authors/authors.module';

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
      subscriptions: {
        'graphql-ws': true,
      },
    }),
    AuthorsModule,
  ],
  providers: [AppResolver],
})
export class AppModule {}

서브스크립션 등록

서브스크립션을 등록하고 사용하기 위해서는 PubSub클래스 객체와 @Subscription()데코레이터를 사용해야 합니다.

import {
  Args,
  Int,
  Mutation,
  Parent,
  ResolveField,
  Resolver,
  Subscription,
} from '@nestjs/graphql';
import { Author } from './models/author.model';
import { Query } from '@nestjs/graphql';
import { Post } from 'src/posts/models/post.model';
import { AuthorsService } from './authors.service';
import { PostsService } from 'src/posts/posts.service';
import { PubSub } from 'graphql-subscriptions';

const pubSub = new PubSub();

@Resolver(() => Author)
export class AuthorsResolver {
  constructor(
    private authorService: AuthorsService,
    private postsService: PostsService,
  ) {}

  @Query(() => Author)
  async author(@Args('id', { type: () => Int }) id: number) {
    return this.authorService.findOneById(id);
  }

  @ResolveField('posts', () => [Post])
  async posts(@Parent() author: Author) {
    const { id } = author;
    return this.postsService.findAll(id);
  }

  @Mutation(() => Post)
  async upvotePost(@Args({ name: 'postId', type: () => Int }) postId: number) {
    return this.postsService.upvoteById(postId);
  }

  @Subscription(() => Post)
  postVoted() {
    return pubSub.asyncIterableIterator('postVoted');
  }
}

@Subscriptions()데코레이터를 이용해 어떤 스키마를 반환할지 설정하고 PubSub객체를 이용해 이벤트를 구독합니다.
위에서는 Post를 반환하는 postVoted이벤트를 구독하고 있습니다.

퍼블리싱

이벤트를 구독하였으니 특정 상황에서 이벤트를 발생시켜야 합니다.
이렇게 이벤트를 발생시키는 것을 퍼블리싱이라고 합니다.
퍼블리싱은 pubsub객체를 통해 가능합니다.

...

  @Mutation(() => Post)
  async upvotePost(@Args({ name: 'postId', type: () => Int }) postId: number) {
    const result = this.postsService.upvoteById(postId);
    pubSub.publish('postVoted', { postVoted: result });
    return result;
  }
  
...

PubSub객체의 publish메서드를 통해 이벤트를 발행합니다.
첫 번째 인자는 발행할 이벤트의 이름이며 두 번째 이름은 이벤트와 함께 보내는 데이터 입니다. 두 번째 인자는 키-밸류 형태를 가지는데 가능하면 키 값이 이벤트 이름과 같은 것이 좋습니다.

서브스크립션 실행

참고로 playground에서는 graphql-ws와 호환되지 않아 아래와 같이 구 버전을 이용해서 서브스크립션을 테스트 해야 합니다.

import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { join } from 'path';
import { AppResolver } from './app.resolver';
import { AuthorsModule } from './authors/authors.module';

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
      installSubscriptionHandlers: true, // WebSocket 설정 활성화
      subscriptions: {
        'graphql-ws': true,
        'subscriptions-transport-ws': {
          path: '/graphQL', // 명시적으로 경로를 지정할 수도 있습니다.
        },
      },
    }),
    AuthorsModule,
  ],
  providers: [AppResolver],
})
export class AppModule {}

서브스크립션 필터링

서브스크립션에서 결과를 필터링해서 가져올 수 있습니다.
이를 위해서는 먼저 @Subscription()데코레이터에서 filter옵션을 설정해줘야 합니다.

@Subscription(() => Post, {
    filter: async (payload, variables) => {
      const promisePostVoted = payload.postVoted
      console.log(variables);
      return (await promisePostVoted).id === variables.id;
    },
  })
  postVoted(@Args('id') id: number) {
    return pubSub.asyncIterableIterator('postVoted');
  }

filter옵션은 콜백함수를 인자로 받습니다.
이때 콜백 함수의 첫 번째 인자는 pubsub객체가 이벤트를 발생시키면서 같이 보낸 키-값 객체를 가지고 있으며 variables는 클라이언트가 GraphQL에서 Subscription을 하면서 보낸 인자를 가집니다.
위에서는 클라이언트가 보낸 인자 id 값과 같을 때만 해당 클라이언트에게 데이터를 보내는 것입니다.

서브크립션 뮤테이팅

서브스크립션을 통해 반환되는 데이터를 중간에 조작하여 클라이언트에게 줄 수 있습니다.
@Subscription()데코레이터에서 resolve옵션을 통해 가능합니다.

@Subscription(() => Post, {
    filter: async (payload, variables) => {
      const promisePostVoted = payload.postVoted;
      return (await promisePostVoted).id === variables.id;
    },
    resolve: async (value) => {
      const promisePostVoted = value.postVoted;
      value = await promisePostVoted;
      console.log(value);
      return { ...value, title: value.title + 'hehehe' };
    },
  })
  postVoted(@Args('id') id: number) {
    return pubSub.asyncIterableIterator('postVoted');
  }

value는 filter때의 payload인자와 똑같이 이벤트를 발생시키면서 보낸 키-값 객체가 있습니다.
잘 보면 반환하는 값은 키-값 객체에서 값 부분만을 반환하는데 이것이 필수적입니다.
중간에서 이렇게 조작을 가할 때는 키 부분을 제거하고 값 부분만 반환해줘야 합니다.

profile
기록으로 흔적을 남깁니다.

0개의 댓글