<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();
이러한 코드가 작성되어 있습니다.
NestFactoryStatic
의 create
코드를 살펴보도록 하겠습니다(편의를 위해 오버라이딩 정의 코드는 제거하고 본문만 담았습니다).
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
입니다.
dependenciesScanner.scan(module)
: 주어진 module
내의 모든 클래스와 프로바이더를 스캔합니다. 이 과정에서 데코레이터 메타데이터를 읽고 @WebSocketGateway
나 다른 데코레이터가 지정된 클래스들을 식별합니다.instanceLoader.createInstancesOfDependencies()
: 스캔된 클래스들을 기반으로 의존성을 주입하고 인스턴스를 생성합니다. 즉, @WebSocketGateway
가 붙은 클래스의 인스턴스가 이 단계에서 생성됩니다.