Backend/Nest.js

[Nest.js] socket.io 중복 연결 문제 해결

okojin 2024. 10. 9. 01:20

nest.js를 활용하여 프로젝트를 진행하던 도중 1:1 채팅방을 구현하기 위해 socket.io 라이브러리를 채택하였다.

그러나, 내가 알던 socket.io 동작방식은 클라이언트가 연결하면 실시간으로 1회 연결된다는 것인데 개발 도중에 다음과 같은 문제가 발생하였다.

그 문제는 바로...

클라이언트 연결 서버 로그

클라이언트에서 소켓을 연결하면 2번 연결된다는 문제가 발생하였다.

이러한 문제를 해결하기 위해서 2시간의 구글링과 2시간의 챗gpt와의 싸움이 이루어졌고, 다음과 같은 해결 방법을 얻을 수 있었다.

  1. 중복 연결 로직이 있는지 확인하고 수정하기
  2. 중복 이벤트 처리 확인하고 수정하기

하지만 코드를 훑어보니

import {
  SubscribeMessage,
  WebSocketGateway,
  WebSocketServer,
  OnGatewayInit,
  OnGatewayConnection,
  OnGatewayDisconnect,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { ChatService } from './chat.service';
import { CreateRoomDto } from './dto/createRoom.dto';
import { SendMessageDto } from './dto/sendMessage.dto';
import { UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@WebSocketGateway({
  cors: {
    origin: '*',
  },
})
export class ChatGateway
  implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect
{
  @WebSocketServer()
  server: Server;

  // 클라이언트 목록을 관리하는 Map
  private connectedClients: Map<string, Socket> = new Map();
  private rooms: Map<string, string[]> = new Map();

  constructor(private readonly chatService: ChatService) {}

  afterInit() {
    console.log('WebSocket 서버 초기화');
  }

  handleConnection(client: Socket) {
    // 클라이언트의 고유 사용자 ID 가져오기 (예: 핸드쉐이크에서 userId 전달)
    const userId = client.handshake.headers['x-user-id'] as string;

    if (!userId) {
      console.log('사용자 ID를 찾을 수 없음');
      client.disconnect();
      return;
    }

    // 중복 연결 확인
    if (this.connectedClients.has(userId)) {
      console.log(`중복 연결 시도 차단: ${userId}`);
      client.disconnect();
      return;
    }

    // 클라이언트 연결 등록
    this.connectedClients.set(userId, client);
    console.log(`클라이언트 연결됨: ${userId}`);
  }

  handleDisconnect(client: Socket) {
    // 클라이언트의 고유 사용자 ID 가져오기
    const userId = client.handshake.headers['x-user-id'] as string;

    if (userId && this.connectedClients.has(userId)) {
      // 연결 해제된 클라이언트 제거
      this.connectedClients.delete(userId);
      console.log(`클라이언트 연결 끊김: ${userId}`);
    } else {
      console.log('연결된 클라이언트를 찾을 수 없음');
    }
  }

  @SubscribeMessage('createRoom')
  async handleCreateRoom(client: Socket, createRoomDto: CreateRoomDto) {
    const room = await this.chatService.findOrCreateRoom(createRoomDto);
    this.rooms.set(room.chat_room_id, [
      ...(this.rooms.get(room.chat_room_id) || []),
      client.id,
    ]);
    client.join(room.chat_room_id);
    console.log(`새 대화방 생성: ${room.chat_room_id}`);
    client.emit('create', 'd');
    return room;
  }

  @SubscribeMessage('sendMessage')
  async handleSendMessage(client: Socket, sendMessageDto: SendMessageDto) {
    console.log(sendMessageDto);
    const message = await this.chatService.sendMessage(
      JSON.stringify(sendMessageDto),
    );
    console.log(`메시지 전송: ${message.content}`);
    if (this.rooms.has(sendMessageDto.join_room)) {
      this.rooms.get(sendMessageDto.join_room)?.forEach((id) => {
        if (id !== client.id) {
          this.connectedClients.get(id)?.emit('newMessage', message);
        }
      });
    }
    client.emit('newMessage', message);
    return message;
  }

  @SubscribeMessage('joinRoom')
  async handleJoinRoom(client: Socket, roomId: string) {
    // 이미 접속한 방이면 무시
    if (client.rooms.has(roomId)) {
      return;
    }
    const room = await this.chatService.findRoomById(roomId);
    if (room) {
      this.rooms.set(roomId, [...(this.rooms.get(roomId) || []), client.id]);
      client.join(roomId);
      console.log('방 참가');
      const messages = await this.chatService.getMessages(roomId);
      console.log(messages);
      client.emit(
        'roomMessages',
        messages.sort((a: any, b: any) => a.created_at - b.created_at),
      );
    }
    console.log(`대화방 입장: ${room}`);
    console.log(`현재 참가자: ${this.rooms.get(roomId)}`);
  }

  @SubscribeMessage('leaveRoom')
  async handleLeaveRoom(client: Socket, roomId: string) {
    client.leave(roomId);
    this.rooms.set(
      roomId,
      this.rooms.get(roomId)?.filter((id) => id !== client.id),
    );
    if (this.rooms.get(roomId)?.length === 0) {
      this.rooms.delete(roomId);
    }
    console.log(`대화방 퇴장: ${roomId}`);
    console.log(`현재 참가자: ${this.rooms.get(roomId)}`);
  }
}

그 어디에도 소켓을 중복으로 연결한다던가 이벤트를 중복으로 발생시키는 로직이 존재하지 않았다.

 

혹시 모듈이 문제인가 해서 살펴봤더니

// app.module.ts
// ...
  providers: [ChatGateway],
})
export class AppModule {}
// chat.module.ts
// ...
  providers: [ChatService, ChatGateway],
  exports: [ChatService],
})
export class ChatModule {}

chat 모듈과 app 모듈 2가지 모듈에서 provider에 ChatGateway를 넣어줘서 2번 연결되었던 것이었다.

 

해결하니 정말 행복하다 ㅎㅎ