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와의 싸움이 이루어졌고, 다음과 같은 해결 방법을 얻을 수 있었다.
- 중복 연결 로직이 있는지 확인하고 수정하기
- 중복 이벤트 처리 확인하고 수정하기
하지만 코드를 훑어보니
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번 연결되었던 것이었다.
해결하니 정말 행복하다 ㅎㅎ