<aside> <img src="/icons/bookmark_purple.svg" alt="/icons/bookmark_purple.svg" width="40px" />

목차

</aside>

본 글은 nest의 @WebSocketGateway`를 사용하며 '예외가 발생했을 때 클라이언트의 콜백으로 상태를 전달할 수는 없을까?'의 고민이 담겨 있습니다. 데코레이터의 역할과 실제 사용처를 먼저 찾아보고, 예외 발생 시 어디에서 어떻게 처리하면 좋을 지의 순서로 서술되었습니다.

@WebSocketGateway 데코레이터의 역할은?

nest에서 간단히 소켓 통신을 가능하게 해 주는 데코레이터입니다. 데코레이터의 역할 자체는 '메타데이터 등록'입니다. 하지만 진정한 가치는 nest가 실행될 때 발생합니다. nest는 등록된 메타데이터를 통해 해당 객체를 어떻게 관리할 지 결정합니다.

아래는 클래스 데코레이터인 @WebSocketGateway입니다.

import { GATEWAY_METADATA, GATEWAY_OPTIONS, PORT_METADATA } from '../constants';
import { GatewayMetadata } from '../interfaces';

/**
 * Decorator that marks a class as a Nest gateway that enables real-time, bidirectional
 * and event-based communication between the browser and the server.
 *
 * @publicApi
 */
export function WebSocketGateway(port?: number): ClassDecorator;
export function WebSocketGateway<
  T extends Record<string, any> = GatewayMetadata,
>(options?: T): ClassDecorator;
export function WebSocketGateway<
  T extends Record<string, any> = GatewayMetadata,
>(port?: number, options?: T): ClassDecorator;
export function WebSocketGateway<
  T extends Record<string, any> = GatewayMetadata,
>(portOrOptions?: number | T, options?: T): ClassDecorator {
  const isPortInt = Number.isInteger(portOrOptions as number);
  // eslint-disable-next-line prefer-const
  let [port, opt] = isPortInt ? [portOrOptions, options] : [0, portOrOptions];

  opt = opt || ({} as T);
  return (target: object) => {
    Reflect.defineMetadata(GATEWAY_METADATA, true, target);
    Reflect.defineMetadata(PORT_METADATA, port, target);
    Reflect.defineMetadata(GATEWAY_OPTIONS, opt, target);
  };
}

이벤트를 전달받는 @SubscribeMessage도 같이 살펴보겠습니다.

import { MESSAGE_MAPPING_METADATA, MESSAGE_METADATA } from '../constants';

/**
 * Subscribes to messages that fulfils chosen pattern.
 *
 * @publicApi
 */
export const SubscribeMessage = <T = string>(message: T): MethodDecorator => {
  return (
    target: object,
    key: string | symbol,
    descriptor: PropertyDescriptor,
  ) => {
    Reflect.defineMetadata(MESSAGE_MAPPING_METADATA, true, descriptor.value);
    Reflect.defineMetadata(MESSAGE_METADATA, message, descriptor.value);
    return descriptor;
  };
};

둘 다 공통적으로 메타데이터를 정의한다는 것을 확인할 수 있습니다.

어디서 메타데이터를 판단하는가

const app = await NestFactory.create(AppModule);

보통은 위와 같이 nest application을 생성합니다. packages/core/nest-factory.ts의 맨 마지막 코드를 살펴보면

export const NestFactory = new NestFactoryStatic();

이러한 코드가 작성되어 있습니다. NestFactoryStaticcreate 코드를 살펴보도록 하겠습니다(편의를 위해 오버라이딩 정의 코드는 제거하고 본문만 담았습니다).

public async create<T extends INestApplication = INestApplication>(
  moduleCls: any,
  serverOrOptions?: AbstractHttpAdapter | NestApplicationOptions,
  options?: NestApplicationOptions,
): Promise<T> {
  const [httpServer, appOptions] = this.isHttpServer(serverOrOptions)
    ? [serverOrOptions, options]
    : [this.createHttpAdapter(), serverOrOptions];

  const applicationConfig = new ApplicationConfig();
  const container = new NestContainer(applicationConfig);
  const graphInspector = this.createGraphInspector(appOptions, container);

  this.setAbortOnError(serverOrOptions, options);
  this.registerLoggerConfiguration(appOptions);

  await this.initialize(
    moduleCls,
    container,
    graphInspector,
    applicationConfig,
    appOptions,
    httpServer,
  );

  const instance = new NestApplication(
    container,
    httpServer,
    applicationConfig,
    graphInspector,
    appOptions,
  );
  const target = this.createNestInstance(instance);
  return this.createAdapterProxy<T>(target, httpServer);
}

해당 과정의 initialize도 살펴보도록 하겠습니다.

private async initialize(
  module: any,
  container: NestContainer,
  graphInspector: GraphInspector,
  config = new ApplicationConfig(),
  options: NestApplicationContextOptions = {},
  httpServer: HttpServer = null,
) {
  UuidFactory.mode = options.snapshot
    ? UuidFactoryMode.Deterministic
    : UuidFactoryMode.Random;

  const injector = new Injector({ preview: options.preview });
  const instanceLoader = new InstanceLoader(
    container,
    injector,
    graphInspector,
  );
  const metadataScanner = new MetadataScanner();
  const dependenciesScanner = new DependenciesScanner(
    container,
    metadataScanner,
    graphInspector,
    config,
  );
  container.setHttpAdapter(httpServer);

  const teardown = this.abortOnError === false ? rethrow : undefined;
  await httpServer?.init();
  try {
    this.logger.log(MESSAGES.APPLICATION_START);

    await ExceptionsZone.asyncRun(
      async () => {
        await dependenciesScanner.scan(module);
        await instanceLoader.createInstancesOfDependencies();
        dependenciesScanner.applyApplicationProviders();
      },
      teardown,
      this.autoFlushLogs,
    );
  } catch (e) {
    this.handleInitializationError(e);
  }
}

여기서 주목해야 할 부분은 await ExceptionsZone.asyncRun입니다.