본문 바로가기
Server/NodeJs

[NodeJS] NestJs socket.io 로 실시간 채팅 구현

by kigo23 2023. 6. 13.
반응형

웹에서 일반적으로 쓰이는 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>

https://github.com/kigo1997/practice-socket.io-chat

'Server > NodeJs' 카테고리의 다른 글

[NodeJS] NestJs nodemailer 모듈로 메일 전송  (0) 2023.06.05
[NodeJS] NestJs MSSQL 연결하기  (0) 2023.03.24
[NodeJS] NestJs Oracle DB 연결하기  (4) 2023.03.23