coding/Node.js

socket.io - 실시간 채팅 / 채팅방 구현

JIN_Coder 2022. 9. 7. 01:03

프로젝트를 하면서 핵심기능 중 하나인 실시간 채팅, 채팅방을 구현해보았다.

socket.io를 사용했고, 전에 알림이나, 소캣을 이용해 같은 페이지에 몇 명이 같이 보고 있는지를 보여주기 위해 소캣을 사용해 본 적은 있었다.(강의 내용) 하지만, 실시간 채팅은 한 번도 해본 적이 없었는데 생각보다 간단한 것 같다.

간단하게 소캣을 프론트와 백 둘 다 연결하고, 소캣 안에서 메시지를 주면 서버에서 받아서 모두에게 뿌려주면 끝이다.

말은 간단하지만, 적용이 정말 쉬운 건 아니다. 나 같은 경우 파일을 분리하는 곳에서 조금 애를 먹었고, 채팅방처럼 방에서 말한 건 방 사람들끼리 공유가 되어야 하고, 채팅 내용을 저장해서 잠깐 나갔다 들어와도 전에 나누었던 채팅들을 볼 수 있게 해야 했기 때문에 쉽지만은 않았다.

그래도 이번 기회에 배운 거를 정리해서 내 것으로 완벽하게 만들어 보려고 한다.

 

실제 배포과정에서는 프론트는 리액트와 연결해서 사용했지만, 나는 혼자 바닐라스크립트로 연습 정도만 했기 때문에 완벽하게 호환이 되지는 않는 점 참고해야 한다.

또한, 디비를 사용해서 프라이머리 키를 사용해야 해서 바닐라스크립트엔 하드코딩도 들어가 있으니 참고해야 한다.

 

 

실시간 채팅 / 채팅방 구현

1. socket.io 설치

npm install socket.io

2. app.js / socket.js / index.html 전체 코드

// app.js

const express = require('express');
const Router = require('./routes/index');

const webSocket = require('./socket');

require('dotenv').config();
const port = process.env.PORT;

const app = express();

const path = require('path');
app.use(express.static(path.join(__dirname, 'src')));

app.use(express.json());
app.use('/api', Router);
app.get('/', (req, res) => {
  res.status(200).json({ massage: '연동 잘 됨.' });
});

const server = app.listen(port, () => {
  console.log(port, '포트로 서버가 열렸어요!');
});

webSocket(server, app);

module.exports = app;
// socket.js

// const app = require('./app');
const socket = require('socket.io');
// const http = require('http');
const { Room, Chat, User, Participant } = require('./models');
// require('socket.io-client')('http://localhost:3000');
// const server = http.createServer(app);

module.exports = (server, app) => {
  const io = socket(server, {
    cors: {
      origin: '*',
      credentials: true,
    },
  });
  app.set('socket.io', io);

  // 소캣 연결
  io.on('connection', (socket) => {
    console.log('a user connected');

    // 채팅방 목록? 접속(입장전)
    socket.on('join-room', async (data) => {
      let { roomKey, userKey } = data;
      const enterUser = await Participant.findOne({
        where: { roomKey, userKey },
        include: [
          { model: User, attributes: ['nickname'] },
          { model: Room, attributes: ['title'] },
        ],
      });

      // 해당 채팅방 입장
      socket.join(enterUser.Room.title);
	  const enterMsg = await Chat.findOne({
        where: {
          roomKey,
          userKey: 12,
          chat: `${enterUser.User.nickname}님이 입장했습니다.`,
        },
      });

      // 처음입장이라면 환영 메세지가 없을테니
      if (!enterMsg) {
        await Chat.create({
          roomKey,
          userKey: 12, // 관리자 유저키
          chat: `${enterUser.User.nickname}님이 입장했습니다.`,
        });

        // 관리자 환영메세지 보내기
        let param = { nickname: enterUser.User.nickname };
        io.to(enterUser.Room.title).emit('welcome', param);
      } else {
        // 재입장이라면 아무것도 없음
      }
    });

    // 채팅 받아서 저장하고, 그 채팅 보내서 보여주기
    socket.on('chat_message', async (data) => {
      let { message, roomKey, userKey } = data;
      const newChat = await Chat.create({
        roomKey,
        userKey,
        chat: message,
      });
      const chatUser = await Participant.findOne({
        where: { roomKey, userKey },
        include: [
          { model: User, attributes: ['nickname'] },
          { model: Room, attributes: ['title'] },
        ],
      });

      // 채팅 보내주기
      let param = {
        message,
        roomKey,
        nickname: chatUser.User.nickname,
        time: newChat.createdAt, // (9시간 차이나는 시간)
      };

      io.to(chatUser.Room.title).emit('message', param);
    });

    // 채팅방 나가기(채팅방에서 아에 퇴장)
    socket.on('leave-room', async (data) => {
      let { roomKey, userKey } = data;
      const leaveUser = await Participant.findOne({
        where: { roomKey, userKey },
        include: [
          { model: User, attributes: ['nickname'] },
          { model: Room, attributes: ['title', 'userKey'] },
        ],
      });

      // 호스트가 나갔을 때
      if (userKey === leaveUser.Room.userKey) {
        let param = { nickname: leaveUser.User.nickname };
        socket.broadcast.to(leaveUser.Room.title).emit('byeHost', param);
      } else {
        // 일반유저가 나갔을 때(호스트X)
        await Chat.create({
          roomKey,
          userKey: 12, // 관리자 유저키
          chat: `${leaveUser.User.nickname}님이 퇴장했습니다.`,
        });
        
        let param = { nickname: leaveUser.User.nickname };
        io.to(leaveUser.Room.title).emit('bye', param);
      }
    });
  });
};
// ./src/index.html

<!DOCTYPE html>
<html>
  <head>
    <style>
      * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
      }

      body {
        font: 13px Helvetica, Arial;
      }

      form {
        background: #000;
        padding: 3px;
        position: fixed;
        bottom: 0;
        width: 100%;
      }

      form input {
        border: 0;
        padding: 10px;
        width: 90%;
        margin-right: 0.5%;
      }

      form button {
        width: 9%;
        background: rgb(130, 224, 255);
        border: none;
        padding: 10px;
      }

      #messages {
        list-style-type: none;
        margin: 0;
        padding: 0;
      }

      #messages li {
        padding: 5px 10px;
      }

      #messages li:nth-child(odd) {
        background: #eee;
      }
    </style>
  </head>
  <body>
    <select>
      <option value="Room1">Room1</option>
      <option value="Room2">Room2</option>
    </select>
    <ul id="messages"></ul>
    <form action="">
      <input id="m" autocomplete="off" />
      <button>Send</button>
    </form>
    <script src="/socket.io/socket.io.js"></script>
    <script src="http://code.jquery.com/jquery-1.11.1.js"></script>
    <script>
      $(() => {
        const name = prompt('What your name');
        const socket = io();
        let room = ['room1', 'room2'];
        let num = 0;
        // let param = { roomKey: 2, userKey: 5 };

        socket.emit('join-room', (param = { roomKey: 2, userKey: 1 }));

        $('select').change(() => {
          socket.emit('leave-room', (param = { roomKey: 2, userKey: 1 }));
          num++;
          num = num % 2;
          socket.emit('join-room', (param = { roomKey: 6, userKey: 1 }));
        });

        $('form').submit(() => {
          param = { message: $('#m').val(), roomKey: 2, userKey: name };
          socket.emit('chat_message', param);
          $('#m').val('');
          return false;
        });

        socket.on('message', (data) => {
          console.log(data);
          $('#messages').append(
            $('<li>').text(data.nickname + '  :  ' + data.message)
          );
        });

        socket.on('bye', (num, name) => {
          $('#messages').append(
            $('<li>').text(name + '    leaved ' + room[num] + ' :(')
          );
        });

        socket.on('byeHost', (data) => {
          $('#messages').append(
            $('<li>').text(data.nickname + '    leaved ' + ' :(')
          );
        });

        socket.on('welcome', (num, name) => {
          $('#messages').append(
            $('<li>').text(name + '    joined ' + room[num] + ':)')
          );
        });
      });
    </script>
  </body>
</html>

index.html의 script부분의 첫 번째 줄은 socket.io, 두 번째 줄은 jquery를 사용하기 위해서 적어주는 코드이다.

 

 

전체 코드를 기반으로 하나씩 정리해보겠다.

솔직히 말해서 아직 socket.js로 파일 분리해서 모듈을 불러와서 사용하고 있는 것 같은데 이 부분은 정확히 어떻게 동작이 되고 있는지는 잘 모르겠다. 여러 블로그를 보면서 시행착오를 거치면서 서버가 정상 동작하고, 배포했을 때 문제가 없어서 이대로 사용하고 있다.

 

 

소캣이 실행되는 부분보다는 로직과 메서드, 이벤트에 대해서 정리해보려고 한다.

일단 소캣 통신의 기본은 통신을 보낸다. 받는다 그걸 또 보낸다. 받는다

ex) 클라든 서버든 socket.emit.("event")로 보내면 socket.on."event"로 받는다.

 

기본 메서드

io.emit() : 연결되어있는 모든 클라이언트에게 전송한다.

socket.broadcast.emit() : 메시지를 전송한 클라이언트를 제외하고 나머지 모든 클라이언트에게 전송한다.

socket.emit() : 서버에 메시지를 전송한 클라이언트에게만 전송한다.

io.to(id). emit() : 귓속말(채팅방)

 

기본 이벤트

connection, disconnect처럼 기본 이벤트가 존재한다.

message처럼 직접 만든 이벤트도 존재한다.

message이벤트의 경우 이 이벤트가 발생했을 시 해당 이벤트에 속하는 부분들이 실행된다.

이벤트 명은 서버와 프론트가 같은 것을 사용해야 서로 통신이 가능하다.(api URL과 비슷한 느낌)

 

 

내가 구현한 socket.js의 경우

io.on('connection', (socket) => {}
소캣 연결을 한다.
모든 소캑들이 이 안에서 정의되고 통신 받고 보내면서 실행 된다.
 
socket.on('join-room', async (data) => {}
소캣 연결 후 채팅방에 들어가기 위해 연결된 소캣이다.
이 안에서 원하는 채팅방에 처음 입장하는지, 재입장하는지 확인 후 채팅방에 입장하게 된다.
 
socket.join(enterUser.Room.title);
.join을 통해 특정한 채팅방에 들어갈 수 있다. 이제 해당 채팅방에서 이뤄지는 채팅은 안에 있는 사람들끼리만 통신이 된다.
 
io.to(enterUser.Room.title).emit('welcome', param);
처음 입장이라면 환영 메세지를 보내기 위해서 welcome이라는 이벤트로 정의했다.
welcome으로 메세지를 보내면 프로늩에서 welcome으로 메세지를 받아 뷰에 관리자 메세지로 보여준다.
 
socket.on('chat_message', async (data) => {}
프론트에서 사용자가 채팅을 보내면 서버에서 chat_message이라는 이벤트로 받고, 채팅을 디비에 저장한다.
 
io.to(chatUser.Room.title).emit('message', param);
받은 채팅을 저장하고, 같은 방에 있는 사람들(나포함)에게 보여주어야 하기때문에 message 메세지를 보낸다.
프론트에선 message라는 곳으로 받아서 채팅을 뷰로 보여준다.
현재 채팅방끼리 대화를 하는거기 때문에 특정 채팅방에게만 통신을 보내야한다. io.to(chatUser.Room.title).emit을 통해 내가 보낸 채팅을 내가 속한 채티방 사람들에게만 보여지고, 다른 방에선 보여지지 않는다.
만약 io.emit을 사용하면 소캣에 연결된 모든 채팅방, 사람들에게 메세지가 가게 할 수도 있다.
 
 
socket.on('leave-room', async (data) => {}
채팅방을 아에 나간다면 leave-room 이벤트로 통신이 올거고, 여기서는 나가는 사람이 일반 유저인지, 방을 만든 호스트 인지 판별한다.
 
socket.broadcast.to(leaveUser.Room.title).emit('byeHost', param);
호스트가 나간다면 방이 사라지기 때문에 호스트를 제외한 사람들에게 alert을 보내기 위해 broadcast라는 자신을 제외한 나머지에게 메세지를 보내는 메서드를 사용했다.
 
io.to(leaveUser.Room.title).emit('bye', param);
일반유저가 나간다면 ㅇㅇ님이 퇴장했습니다. 같은 퇴장문구를 관리자 채팅으로 보여주기 위해 bye이벤트에서 통신한다.
 

 

이렇게 채팅방을 구현하고, 그 안에서만 통신이 되게 했으며, 채팅 내용도 저장해서 api를 이용해 채팅방에 들어오면 지금까지 있었던 채팅 내용들을 불러와서 카카오톡처럼 최대한 구현해보았다. 아직 이해가지 않는 부분도 많지만, 실시간 채팅 자체만은 어렵지 않은 것 같다.

 

 

 

 

Node.js와 Socket.io를 이용한 채팅 구현 (3)

socket.io에서는 Namespace와 Room 두가지 방법으로 채팅방을 구현 할 수 있는데요, 저는 Room을 이용해서 채팅방을 만들어 보겠습니다. 저번 시간에도 설명했지만 Room은 서버에서만 join과 leave가 가능합

berkbach.com

 

소켓 통신 활용을 위한 변수 설정 및 미들웨어 구성하기

앞서 가장 기본적인 핑퐁 서버에서는 다음과 같이 작성한 바 있습니다만 더 다양한 기능을 부여하기에는 부족했습니다. const webSocket = require("./socket"); // listen const server = app.listen(process.env..

darrengwon.tistory.com

 

[Node.js] Socket.io 모듈 ( join으로 채팅 )

[Node.js] Socket.io 모듈 ( join으로 채팅 ) join메서드를 이용해서 채팅 시스템을 만들어봤다. DB를 ...

blog.naver.com

 

'coding > Node.js' 카테고리의 다른 글

로그 남기기 2 - morgan 사용  (0) 2022.09.10
로그 남기기 1 - winston 사용  (0) 2022.09.09
소켓 / 웹소켓  (0) 2022.09.05
에러 핸들러  (0) 2022.09.03
Sequelize Op like - 검색 기능  (0) 2022.08.23