이번엔 웹소켓까지?

HanSH·2024년 2월 14일

NestJS

목록 보기
16/29

기본 게시판 CRUD를 만들고 파일 업로드까지 마쳤다.
지금 할 수 있는 것 중 쉬운 것은 "웹소켓"

웹소켓을 이용하여 채팅을 구현해보자.

게이트웨이

NestJS에서 웹소켓은 Gateway를 통해 이뤄진다. 공식 문서에 따르면, Gateway는 Provider처럼 제공된다.

웹소켓을 연결하게 할 수 있게 해주는 것을 이르는 말 정도로 할 수 있겠다.

게이트웨이 생성

먼저 모듈을 설치해주자.

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

yarn add @nestjs/websockets @nestjs/platform-socket.io --save

그 다음 모듈과 게이트웨이를 만들어주자.

nest g mo socket
nest g ga socket // nest g gateway socket

게이트웨이를 만들었다면 아래와 같이 나타날 것이다.

@WebSocketGateway()
export class AsdfGateway {
  @SubscribeMessage('message')
  handleMessage(client: any, payload: any): string {
    return 'Hello world!';
  }
}

웹소켓으로 하면 왜인지 모르겠지만 테스트 코드에서도 웹소켓 연결조차 되지 않는다.
CORS 문제일까 생각해서 origin을 모든 곳에서 받는 것으로 했는데도 안되고...

따라서 Socket.IO를 사용하였다.

import { Server, Socket } from 'socket.io';
@WebSocketGateway({
  cors: {
    origin: '*',
  },
}) // 기본 포트로 설정(3000)
export class EventsGateway {
  // socket.io 이용하기 위한 방법
  @WebSocketServer() // ① SocketIO로 사용하겠다 설정
  server: Server;
  
  @SubscribeMessage('events')
  handleMessage( // ②
    @MessageBody() data: any, 
    // 클라이언트 정보를 확인하기 위해서는 ConnectedSocket 어노테이션 사용
    @ConnectedSocket() sender: Socket
  ) : void {
    ...
  }
}
  1. @WebSocketServer()을 이용해 Socket.IO로 웹소켓 통신을 하도록 한다.
  2. handleMessage : Socket.IO를 사용하기 위해 메소드 수정

웹소켓으로 헤더를 전송?

웹소켓은 소켓 연결 시 헤더를 넘겨줄 수 없다. 헤더를 전달하기 위해서는
1. 소켓 연결 전 http handshake로 헤더 전달
2. 헤더 검증을 마치고 소켓 연결
이렇게 해야한다.

socket.io에서는 extraHeaders를 통해 헤더를 전달할 수 있다.

  1. 클라이언트
socket = io('http://localhost:3000', {
    extraHeaders: {
        Authorization: "Bearer asdf.zxcv.qwer",
        roomName: "test",
        ...
    }
});
  1. 서버
// @SubscribeMessage('events')
handleConnection(client: Socket, ...args: any[]) {
  const headers = client.handshake.headers;
  const token = headers.authorization as string;
  const roomName = headers.roomName as string;
  ...
}

소켓 연결 시 헤더로 방 정보를 보낼 수 있다는 장점 때문에 WebSocket 대신 Socket.IO를 사용하였다.

연결, 연결 해제 시 채팅방 관리

Socket.IO를 이용하여 구현하였다.

위를 구현하기 위해 아래의 구조의 형식으로 만들 예정이다.

rooms = {
  "room1": [
    user1,  
    user2,
    user3,
    ...
  ],
  "room2": [ 
    user1,
    user4,
    ...
  ],
  ...
}
// Map<string, Set<Socket>>
// Map : 딕셔너리처럼 관리 가능
// Set : 중복을 제거하는 Set 사용. 혹시라도 모를 재진입 방지

Map.get({ name })이 Call-By-Value가 아닌 Call-By-Reference 방식으로 동작해서 너무 다행이다. get으로 얻어온 객체를 수정해도 원본의 데이터가 수정이 된다!

웹소켓 연결 시

  1. 방이 있는지 확인한다.
    1.1. 방이 없다면 방을 생성한다.
  2. 방에 접속한 유저를 넣는다.
  3. 접속한 유저에게 연결됐음을 알린다.
handleConnection(client: Socket, ...args: any[]) {
  const headers = client.handshake.headers;
  const roomName = headers.roomName as string
  if (headers.authorization !== "Bearer asdf.zxcv.qwer") {
    client.send("disconnected");
    client.disconnect();
    return;
  }
  if (!this.rooms.get(roomName)) {
    this.rooms.set(roomName, new Set<Socket>());
  }
  const room = this.rooms.get(roomName);
  room.add(client);
  client.send("conneted");
}

웹소켓 연결 해제 시

  1. 방에서 유저를 삭제한다.
  2. 남은 유저가 없다면 해당 방을 없앤다.
handleDisconnect(client: Socket) {
  const headers = client.handshake.headers;
  const roomName = headers.roomName as string

  const room = this.rooms.get(roomName);
  room.delete(client); // 유저 삭제
  if (room.size === 0) { // 채팅방에 유저가 없다면
    this.rooms.delete(roomName); // 채팅방 삭제
  }
}

보낸 사람을 제외한 모든 사람에게 메시지 전달

현재 채팅방에 있는 모든 사람 중 sender가 아닌 사람에게만 보내면 된다.

@SubscribeMessage('events')
handleMessage(
  @MessageBody() data: any, 
  // 클라이언트 정보를 확인하기 위해서는 ConnectedSocket 어노테이션 사용
  @ConnectedSocket() sender: Socket
): Observable<WsResponse<number>> {
  const headers = sender.handshake.headers;
  const room = this.rooms.get(headers.roomName as string);
  room.forEach( (client) => { // forEach로 탐색 후 메시지 전달
    if (client !== sender) {
      client.send(data);
    }
  });
  return;
}

결론

웹소켓까지 성공적으로 구현하였다. Socket.IO를 이용하여서 간단하게 소켓 통신까지 구현하였다.

해결되지 않은 부분 또는 구현해야할 내용은 아래와 같다.

  1. WebSocket으로 연결 시 연결 되지 않음
  2. WebSocket으로 연결한다면 handshake때 토큰 전송 시도
  3. 데이터베이스에 채팅방 정보 저장
  4. Guard로 연결 시 토큰 검증 ← 이게 될까?

출처

채팅 아이콘 - https://www.flaticon.com/kr/free-icon/chatting_3249451?term=%EC%B1%84%ED%8C%85&page=1&position=16&origin=tag&related_id=3249451

profile
저는 말하는 싹 난 감자입니다

0개의 댓글