웹에서 일반적으로 쓰이는 http 통신에서는 클라이언트가 서버에 request를 보내고 서버가 response를 주는 방식입니다. 하지만 서버-클라이언트의 양방향 통신이 필요한 경우에 무한하게 http request를 보내는 방식을 사용할 순 있겠지만 매우 비효율적일 것입니다. 서버와 클라이언트를 연결하여 실시간으로 데이터를 전달받는 tcp/ip 소켓 통신 방식을 사용하여아합니다. socket.io는 서버와 클라이언트를 연결하고 실시간 양방향 통신이 가능하도록 도와주는 JS 라이브러리입니다. socket.io를 사용하여 실시간 채팅 어플리케이션을 간단하게 구현해보았습니다.
작업환경
- node : 16.19.1
- nest : 9.2.0
- @nestjs/platform-socket.io: ^9.4.2
- @nestjs/websockets: ^9.4.2
- socket.io: ^4.6.2
NestJS 프로젝트에 socket.io 관련 패키지를 설치합니다
npm i @nestjs/platform-socket.io @nestjs/websockets socket.io
chat.gateway.ts를 생성합니다.
nest g ga chat
chat.gateway.ts에서는 클라이언트와 서버간에 통신할 수 있도록 지원합니다.
//chat.gateway.ts
import {
WebSocketGateway,
WebSocketServer,
SubscribeMessage,
OnGatewayConnection,
OnGatewayDisconnect,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
@WebSocketGateway()
export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect {
@WebSocketServer()
server: Server;
connectedClients: { [socketId: string]: boolean } = {};
clientNickname: { [socketId: string]: string } = {};
roomUsers: { [key: string]: string[] } = {};
handleConnection(client: Socket): void {
if (this.connectedClients[client.id]) {
client.disconnect(true);
return;
}
this.connectedClients[client.id] = true;
}
handleDisconnect(client: Socket): void {
delete this.connectedClients[client.id];
// 클라이언트 연결이 종료되면 해당 클라이언트가 속한 모든 방에서 유저를 제거
Object.keys(this.roomUsers).forEach((room) => {
const index = this.roomUsers[room]?.indexOf(
this.clientNickname[client.id],
);
if (index !== -1) {
this.roomUsers[room].splice(index, 1);
this.server
.to(room)
.emit('userLeft', { userId: this.clientNickname[client.id], room });
this.server
.to(room)
.emit('userList', { room, userList: this.roomUsers[room] });
}
});
// 모든 방의 유저 목록을 업데이트하여 emit
Object.keys(this.roomUsers).forEach((room) => {
this.server
.to(room)
.emit('userList', { room, userList: this.roomUsers[room] });
});
// 연결된 클라이언트 목록을 업데이트하여 emit
this.server.emit('userList', {
room: null,
userList: Object.keys(this.connectedClients),
});
}
@SubscribeMessage('setUserNick')
handleSetUserNick(client: Socket, nick: string): void {
this.clientNickname[client.id] = nick;
}
@SubscribeMessage('join')
handleJoin(client: Socket, room: string): void {
// 이미 접속한 방인지 확인
if (client.rooms.has(room)) {
return;
}
client.join(room);
if (!this.roomUsers[room]) {
this.roomUsers[room] = [];
}
this.roomUsers[room].push(this.clientNickname[client.id]);
this.server
.to(room)
.emit('userJoined', { userId: this.clientNickname[client.id], room });
this.server
.to(room)
.emit('userList', { room, userList: this.roomUsers[room] });
this.server.emit('userList', {
room: null,
userList: Object.keys(this.connectedClients),
});
}
@SubscribeMessage('exit')
handleExit(client: Socket, room: string): void {
// 방에 접속되어 있지 않은 경우는 무시
if (!client.rooms.has(room)) {
return;
}
client.leave(room);
const index = this.roomUsers[room]?.indexOf(this.clientNickname[client.id]);
if (index !== -1) {
this.roomUsers[room].splice(index, 1);
this.server
.to(room)
.emit('userLeft', { userId: this.clientNickname[client.id], room });
this.server
.to(room)
.emit('userList', { room, userList: this.roomUsers[room] });
}
// 모든 방의 유저 목록을 업데이트하여 emit
Object.keys(this.roomUsers).forEach((room) => {
this.server
.to(room)
.emit('userList', { room, userList: this.roomUsers[room] });
});
// 연결된 클라이언트 목록을 업데이트하여 emit
this.server.emit('userList', {
room: null,
userList: Object.keys(this.connectedClients),
});
}
@SubscribeMessage('getUserList')
handleGetUserList(client: Socket, room: string): void {
this.server
.to(room)
.emit('userList', { room, userList: this.roomUsers[room] });
}
@SubscribeMessage('chatMessage')
handleChatMessage(
client: Socket,
data: { message: string; room: string },
): void {
// 클라이언트가 보낸 채팅 메시지를 해당 방으로 전달
this.server.to(data.room).emit('chatMessage', {
userId: this.clientNickname[client.id],
message: data.message,
room: data.room,
});
}
}
@SubscribeMessage() 는 클라이언트에서 이벤트를 수신하였을 때 실행하는 메소드를 정의합니다.
클라이언트에서 아래의 스크립트를 통해 입력한 채팅 메시지를 전달합니다.
'chatMessage'란 이름의 이벤트를 보내고 데이터는 object형식으로 전달합니다.
socket.emit('chatMessage', { message, room });
Nest 서버에서는 위 이벤트를 수신하고 해당 room에 접속한 클라이언트들에게 다시 chatMessage 이벤트를 전달합니다.
@SubscribeMessage('chatMessage')
handleChatMessage(
client: Socket,
data: { message: string; room: string },
): void {
// 클라이언트가 보낸 채팅 메시지를 해당 방으로 전달
this.server.to(data.room).emit('chatMessage', {
userId: this.clientNickname[client.id],
message: data.message,
room: data.room,
});
}
아래는 클라이언트의 스크립트 부분 입니다. 참고하셔서 봐주셔도 될 것 같습니다.
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.5.0/socket.io.js"></script>
<script>
const socket = io();
const animalNames = [
'곰',
'호랑이',
'사자',
'원숭이',
'기린',
'코끼리',
'여우',
'늑대',
'팬더',
'얼룩말',
'코뿔소',
'하마',
'고릴라',
'캥거루',
'악어',
'사슴',
'앵무새',
'치타',
'카멜레온',
'펭귄',
];
const randomAnimalName =
animalNames[Math.floor(Math.random() * animalNames.length)] +
Math.floor(Math.random() * 100).toString();
document.getElementById('random-animal-name').innerHTML =
randomAnimalName;
socket.emit('setUserNick', randomAnimalName);
const roomUsers = {
room1: document.getElementById('room1Users'),
room2: document.getElementById('room2Users'),
};
const roomChatList = {
room1: document.getElementById('room1-chat-list'),
room2: document.getElementById('room2-chat-list'),
};
const roomChatContainer = {
room1: document.getElementById('room1-chat-container'),
room2: document.getElementById('room2-chat-container'),
};
const roomMessageInput = {
room1: document.getElementById('room1-messageInput'),
room2: document.getElementById('room2-messageInput'),
};
const inputMessageRoom1 = document.getElementById('room1-messageInput');
const inputMessageRoom2 = document.getElementById('room2-messageInput');
function joinRoom(room) {
socket.emit('join', room);
}
function exitRoom(room) {
if (!room) return;
roomUsers[room].innerHTML = '';
roomChatList[room].innerHTML = '';
socket.emit('exit', room);
}
function sendRoomMessage(room) {
const message = roomMessageInput[room].value;
if (message.trim() !== '') {
socket.emit('chatMessage', { message, room });
roomMessageInput[room].value = '';
}
}
socket.on('userList', ({ room, userList }) => {
if (!room) return;
const usersElement = roomUsers[room];
usersElement.innerHTML = '';
console.log({ room, userList });
userList.forEach((userId) => {
const p = document.createElement('p');
p.textContent = userId;
usersElement.appendChild(p);
});
});
socket.on('userJoined', ({ userId, room }) => {
const message = `${userId} joined the room.`;
appendMessage(room, message);
});
socket.on('userLeft', ({ userId, room }) => {
const message = `${userId} left the room.`;
appendMessage(room, message);
});
socket.on('chatMessage', ({ userId, message, room }) => {
appendMessage(room, `${userId} : ${message}`);
});
function appendMessage(room, message) {
const chatList = roomChatList[room];
const li = document.createElement('li');
li.className = 'chat-item';
const p = document.createElement('p');
p.textContent = message;
li.appendChild(p);
chatList.appendChild(li);
// 스크롤 아래로 이동
roomChatContainer[room].scrollTop =
roomChatContainer[room].scrollHeight;
}
inputMessageRoom1.addEventListener('keyup', function (event) {
if (event.key === 'Enter') {
sendRoomMessage('room1');
}
});
inputMessageRoom2.addEventListener('keyup', function (event) {
if (event.key === 'Enter') {
sendRoomMessage('room2');
}
});
</script>
'Server > NodeJs' 카테고리의 다른 글
[NodeJS] NestJs nodemailer 모듈로 메일 전송 (0) | 2023.06.05 |
---|---|
[NodeJS] NestJs MSSQL 연결하기 (0) | 2023.03.24 |
[NodeJS] NestJs Oracle DB 연결하기 (4) | 2023.03.23 |