Node.js

웹소켓(WebSocket) 6장 - socket.io를 이용한 여러 채팅방 구현

blackbearwow 2023. 7. 26. 06:36

웹소켓 4장에서는 websocket만을 사용해 여러 채팅방을 구현하였다. 이번에는 socket.io를 이용하여 쉽게 구현해보자.

 

이번에는 서버를 모듈화하여 기능을 분리하고, 알아보기 좋게 하였다.

 

1. 클라이언트 페이지

페이지 이름 설명
index 들어갈 수 있는 채팅방 리스트를 보여준다.
makeRoom 이름을 정해 새로운 채팅방을 만들 수 있다.
chat 채팅방에서 채팅을 할 수 있다. 없는 채팅방은 들어갈 수 없다.
<!--index.ejs-->
<!DOCTYPE html>
<html>
  <head>
    <title>Socket.IO chat</title>
    <style>
      #roomList { list-style-type: none; margin: 0; padding: 0; }
      #roomList > li { padding: 0.5rem 1rem; }
      #roomList > li:nth-child(odd) { background: #efefef; }
    </style>
    <script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.4/jquery.min.js"></script>
  </head>
  <body>
    <br>
    <button onclick="location.href='/makeRoom'">새로운 방 만들기</button>
    <h2>방 리스트</h2>
    <ul id="roomList">
      <li>방이 없습니다</li>
    </ul>
    <br>
    <script>
      let socket = io();

      socket.emit("indexPage");

      socket.emit("request room List");

      socket.on('response room List', function(list) {
        console.log(list);
        let temp = '';
        list.forEach((val)=>{
          temp += `<li onclick = "location.href='/chat/${val[0]}'">인원:${val[2]} 이름:${val[1]} </li>`;
        })
        if(list.length === 0) temp = `<li>방이 없습니다</li>`;
        $('#roomList').html(temp);
        window.scrollTo(0, document.body.scrollHeight);
      })
    </script>
  </body>
</html>
<!--makeRoom.ejs-->
<!DOCTYPE html>
<html>
  <head>
    <title>방 만들기</title>
  </head>
  <body>
    <h2>방 만들기</h2>
    <form method="post">
      채팅방이름:<input type="text" id="roomName" name="roomName" placeholder="채팅방이름">
      <button type="submit">만들기</button>
    </form>
  </body>
</html>
<!--chat.ejs-->
<!DOCTYPE html>
<html>
  <head>
    <title><%= title %></title>
    <style>
      #form { background: rgba(0, 0, 0, 0.15); padding: 0.25rem; position: fixed; bottom: 0; left: 0; right: 0; display: flex; height: 3rem; box-sizing: border-box; backdrop-filter: blur(10px); }
      #input { border: none; padding: 0 1rem; flex-grow: 1; border-radius: 2rem; margin: 0.25rem; }
      #input:focus { outline: none; }
      #form > button { background: #333; border: none; padding: 0 1rem; margin: 0.25rem; border-radius: 3px; outline: none; color: #fff; }

      #messages { list-style-type: none; margin: 0; padding: 0; }
      #messages > li { padding: 0.5rem 1rem; }
      #messages > li:nth-child(odd) { background: #efefef; }
    </style>
    <script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
  </head>
  <body>
    <button onclick="location.href='/'">home</button>
    <ul id="messages"></ul>
    <form id="form" action="">
      <input id="input" autocomplete="off" />
      <input type="submit">
    </form>
    <script>
      let socket = io();
      let key = window.location.pathname.split('/')[2];
      socket.emit("whatIsMyRoom", key);

      let messages = document.getElementById('messages');
      let form = document.getElementById('form');
      let input = document.getElementById('input');

      form.addEventListener('submit', function(e) {
        e.preventDefault();
        if (input.value) {
          socket.emit('chat message', key, input.value);
          input.value = '';
        }
      });

      socket.on('chat message', function(msg) {
        let item = document.createElement('li');
        item.textContent = msg;
        messages.appendChild(item);
        window.scrollTo(0, document.body.scrollHeight);
      })
    </script>
  </body>
</html>

2. 서버

모듈 이름 설명
index.js express라이브러리를 사용해 페이지를 클라리언트에 전송하는 역할을 한다.
room.js 채팅방 클래스. 클래스에서 채팅방 정보와 함수를 관리한다.
socket.js socket.io를 사용해 클라이언트와 웹소켓 통신하는 모듈.
//index.js
const express = require('express');
const app = express();
const http = require('http');
const server = http.createServer(app);

//room은 새로운 파일로 만들어 따로 관리하자.
const roomModule = require('./room');
const room = new roomModule.Room;

app.use(express.json());
app.use(express.urlencoded());
//위 2개를 추가해야 post body가 제대로 읽힌다.
app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');

app.get('/', (req, res) => {
    res.render('index');
});

app.get('/makeRoom', (req, res) => {
    res.render('makeRoom');
});
app.post('/makeRoom', (req, res) => {
    const roomName = req.body.roomName;
    const key = room.makeNewRoom(roomName);
    res.redirect(`/chat/${key}`);
});

app.get('/chat/:key', (req, res) => {
    const key = req.params.key;
    //없는 채팅방에 들어간다면 경고 표시후 /로 리다이렉트.
    if(room.keys.has(key) === false)
        res.send(`<script>alert('없는 채팅방입니다'); location.href='/';</script>`);
    else 
        res.render('chat', {title:room.getRoomName(key)});
});

server.listen(3000, () => {
    console.log('listening on *:3000');
});

require('./socket')(server, room);
//room.js
const crypto = require('crypto');

class Room {
    constructor() {
        //key는 방 접속id로, 방을 구분짓기 위한 것으로만 사용한다.
        this.keys = new Set();
        this.keyValuePopulation = new Array();
    }
    makeDistinctKey() {
        let randomString = crypto.randomBytes(5).toString("hex");
        //id가 겹친다면 다시 id를 만든다.
        if (this.keys.has(randomString))
            randomString = this.makeDistinctKey();
        return randomString;
    }
    makeNewRoom(name) {
        let key = this.makeDistinctKey();
        this.keys.add(key);
        this.keyValuePopulation.push([key, name, 0]);

        return key;
    }
    addPopulation(key) {
        for(let i=0; i<this.keyValuePopulation.length; i++) {
            if(this.keyValuePopulation[i][0] == key) {
                this.keyValuePopulation[i][2]++; break;
            }
        }
    }
    subPopulation(key) {
        for(let i=0; i<this.keyValuePopulation.length; i++) {
            if(this.keyValuePopulation[i][0] == key) {
                this.keyValuePopulation[i][2]--;
                if(this.keyValuePopulation[i][2] <= 0)
                    this.deleteRoom(key);
                break;
            }
        }
    }
    deleteRoom(key) {
        this.keys.delete(key);
        this.keyValuePopulation = this.keyValuePopulation.filter((v)=> v[0] != key);
    }
    getRoomList() {
        return this.keyValuePopulation;
    }
    getRoomName(key) {
        let keyValuePopulation = this.keyValuePopulation.find((val)=>val[0]===key);
        return keyValuePopulation[1];
    }
}

module.exports = {Room};
//socket.js
const {Server} = require("socket.io");

module.exports = (server, room) => {
    const io = new Server(server);

    io.on('connection', (socket) => {
        socket.on('chat message', (key, msg) => {
            io.to(key).emit('chat message', msg);
        });
        socket.on('request room List', () => {
            socket.emit('response room List', room.getRoomList());
        });
        socket.on("indexPage", () => {
            socket.join('index');
        });
        socket.on("whatIsMyRoom", (key) => {
            socket.join(key);
            room.addPopulation(key);
            io.to('index').emit('response room List', room.getRoomList());
        });
        socket.on('disconnecting', () => {
            for (const r of socket.rooms) {
                if(r != socket.id)
                    room.subPopulation(r);
            }
        });
        socket.on('disconnect', () => {
            io.to('index').emit('response room List', room.getRoomList());
        });
    });
};

 

결과: