NestJS에서 사용하는 WebSocket에 대해 알아보자

WebSocket이란?

💡 채팅 기능 구현을 시작하기에 앞서 NestJS의 WebSocket에 대해 알아보자. (NestJS 공식 문서 참고)

✔︎ 종속성 주입, 데코레이터, 예외 필터, 파이프, 가드 및 인터셉트와 같은 Nest에서 사용되는 대부분의 개념은 게이트웨이에 동일하게 적용된다.

✔︎ 가능한 경우 Nest는 동일한 구성 요소가 HTTP 기반 플랫폼, WebSocket 및 마이크로서비스에서 실행될 수 있도록 구현 세부 정보를 추상화한다.

✔︎ Nest에서 게이트웨이는 단순히 @WebSocketGateway() 데코레이터로 주석이 달린 클래스이다.

✔︎ 기술적으로, 게이트웨이는 플랫폼에 구애받지 않으므로 (platform-agnostic) 접속 소켓(adapter)가 생성되면 모든 WebSockets 라이브러리와 호환된다.

✔︎ 기본적으로 지원되는 두 가지 플랫폼으로는 socket.iows가 있다.

설치

$ npm i --save @nestjs/websockets @nestjs/platform-socket.io

Gateway

✔︎ 일반적으로 앱이 웹 애플리케이션이 아니거나 포트를 수동으로 변경하지 않는 한 각 게이트웨이는 HTTP server와 동일한 포트에서 수신 대기한다.

✔︎ 이 기본 동작은 80이 선택된 포트 번호인 @WebSocketGateway(80) 데코레이터에 인수를 전달함으로써 수정할 수 있다.

다음과 같이 namespace를 설정할 수 있다.

@WebSocketGateway(80, { namespace: 'events' })

다음과 같이 @WebSocketGateway() 데코레이터의 두 번째 인수를 통해 소켓 생성자에 해당하는 옵션을 전달할 수 있다.

@WebSocketGateway(81, { transports: ['websocket'] })

✔︎ 현재, 게이트웨이가 현재 수신 대기 중이지만 아직 어느 수신 메시지도 구독하지 않았다.

event 메시지들을 구독하고 정확한 동일 데이터로 유저에게 응답하는 핸들러를 만들어보자.

// event.gateway.ts
import { SubscribeMessage, MessageBody } from '@nestjs/websockets';

@SubscribeMessage('events')
handleEvent(@MessageBody() data: string): string {
	return data;
}

생성된 게이트웨이를 다음과 같이 모듈에 등록한다.

// event.module.ts

@Module({
	providers: [EventsGateway]
})
export class EventsModule {}

들어오는 메시지 본문을 추출하기 위해 속성키를 데코레이터에 넣을 수 있다.

// events.gateway.ts

@SubscribeMessage('events')
handleEvent(@MessageBody('id') id: numebr): number {
	// id === messageBody.id
	return id;
}

데코레이터 없이 다음과 같이 쓸 수 있다. (권장 x)

// events.gateway.ts

@SubscribeMessage('events')
handleEvent(client: Socket, data: string): string {
	return data;
}

✔︎ 위에서 사용된 handleEvent() 함수는 두 개의 인수를 받는다.

  1. 플랫폼별 소켓 인스턴스
  2. 클라이언트에서 받은 데이터

⚠️ 이 방식은 각 단위 테스트에서 소켓을 가짜 값으로 만들어야 해서 권장되는 방식은 아니다.

✔︎ event 메시지 수신시 핸들러는 네트워크를 통해 전송된 것과 동일한 데이터로 승인을 보낸다.

✔︎ client.emit() 메서드를 이용하는 것처럼 라이브러리별 접근 방식을 사용하여 메시지를 내보낼 수 있다.

연결된 소켓 인스턴스에 접근하려면 @ConnectedSocket() 데코레이터를 사용하면 된다.

// events.gateway.ts
import { ..., ConnectedSocket } from '@nestjs/websockets';

@SubscribeMessage('events')
handleEvent(
	@MessageBody() data: string,
	@ConnectedSocket() client: Socket,
): string {
	return data;
}

✔︎ 위의 경우 인터셉터를 활용할 수 없다.

✔︎ 유저에게 응답하지 않으려면 return 명령문을 건너뛰면 된다. (또는, undefined과 같은 falsy한 값을 명시적으로 리턴하면 된다.)

이제 클라이언트가 메시지를 내보낼 때 다음과 같이 작성하면 된다.

socket.emit('events', { name: 'Nest' });

✔︎ handleEvent() 메소드가 실행될 것이다.

위의 핸들러 내에서 보내진 메시지들을 수신하려면, 클라이언트는 해당 리스너에 접근해야 한다.

socket.emit('events', { name: 'Nest' }, data => console.log(data));

다중 응답 (Multiple responses)

✔︎ 승인(acknowledgment)은 한번만 발송되며 기본 WebSocket 구현에서는 지원되지 않는다.

✔︎ 이 제한을 해결하려면, 두 개의 속성으로 이루어진 객체를 리턴하면된다.

event는 보내진 이벤트의 이름이고 데이터는 클라이언트에게 전달되어야 한다.

// events.gateway.ts

@SubscribeMessage('events')
handleEvent(@MessageBody() data: unknown): WsResponse<unknown> {
	const event = 'events';
	return { event, data };
}

들어오는 응답을 수신하려면 클라이언트가 또 다른 이벤트 리스너를 적용해야 한다.

socket.on('events', data => console.log(data));

비동기 응답 (Asynchronous responses)

✔︎ 메시지 핸들러는 동기 또는 비동기로 응답할 수 있다. ( → async 메서드 지원)

✔︎ 메시지 핸들러는 Observable 스트림이 완료될 때까지 결과 값을 내보내는 경우 반환활 수도 있다.

3번 응답하는 메시지 핸들러

// events.gateway.ts

@Bind(MessageBody())
@SubscribeMessage('events')
onEvent(data) {
	const event = 'events';
	const response = [1, 2, 3];

	return from(response).pipe(
		map(data => ({ event, data})),
	);
}

Lifecycle hooks

3가지 유용한 Lifecycle hooks가 있다.

  • **OnGatewayInit**

    ✔︎ afterInit() 메서드를 구현하도록 강제한다.

    ✔︎ 라이브버리별 서버 인스턴스를 인수로 사용하고 필요시 나머지를 퍼뜨린다.

  • **OnGatewayConnection**

    ✔︎ handleConnection() 메서드를 구현하도록 강제한다.

    ✔︎ 라이브러리별 클라이언트 소켓 인스턴스를 인수로 사용한다.

  • **OnGatewayDisconnect**

    ✔︎ handleDisconnect() 메서드를 구현하도록 강제한다.

    ✔︎ 라이브러리별 클라이언트 소켓 인스턴스를 인수로 사용한다.

서버 (Server)

✔︎ 경우에 따라 플랫폼별 서버 인스턴스에 직접 접근할 수 있다.

✔︎ 이 객체애 대한 참조는 afterIntit() 메서드 (OnGateWayInit 인터페이스)에 대한 인수로 전달된다.

✔︎ 다른 옵션으로는 @webSocketServer()를 사용하는 것이 있다.

@webSocketServer()
server: Server;

Exception Filter

throw new WsException('Invalid credentials.');

WsException 구조

{
  status: 'error',
  message: 'Invalid credentials.'
}

사용 예시

@UseFilters(new WsExceptionFilter())
@SubscribeMessage('events')
onEvent(client, data: any): WsResponse<any> {
  const event = 'events';
  return { event, data };
}

핵심 예외 필터를 확장하고 특정 요인에 따라 동작을 재정의 하려는 경우 다음과 같이 상속을 사용할 수 있다.

import { Catch, ArgumentsHost } from '@nestjs/common';
import { BaseWsExceptionFilter } from '@nestjs/websockets';@Catch()
export class AllExceptionsFilter extends BaseWsExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    super.catch(exception, host);
  }
}