2023-08-31 21:11:30

WebSocket이란?

WebSocket은 웹 페이지와 웹 서버간의 지속적이고 양방향 통신을 가능하게 하는 프로토콜을 말하며 일반적인 HTTP와 달리 WebSocket은 연결이 한번 수립되면 지속적으로 유지됩니다. 이를 통해 서버나 클라이언트가 실시간으로 데이터를 주고 받을 수 있게 됩니다. 실시간으로 데이터를 주고 받기 때문에 채팅방, 가상화폐 거래소 같은 곳에 많이 쓰입니다.

 

  • 실시간 웹 통신 : WebSocket은 웹 브라우저와 서버 간의 실시간 양방향 통신을 가능하게 하여 웹 브라우저 상에서 사용자의 동작에 따라 실시간으로 반응하거나 서버의 상태를 즉시 반영할 수 있습니다.
  • 성능 향상 : 일반적인 HTTP 요청과 응답은 각 요청마다 header 정보가 포함되어 전송되는 반면 WebSocket 연결은 초기 handshake 이후에는 추가적인 header 정보 없이 데이터만 전송되므로 네트워크 오버헤드가 줄어듭니다.
  • 지속적인 연결 : WebSocket 연결은 한번 수립되면 지속적으로 유지되므로 지속적인 통신이 필요한 애플리케이션에 효율적입니다.
  • 서버 자원의 효율성 : WebSocket은 지속적인 연결을 통해 서버 자원의 효율적인 사용이 가능하게 합니다. 일반적인 HTTP 통신에서는 주기적인 폴링이 필요하지만 WebSocket을 사용하면 이러한 폴링의 필요성이 줄어들어 서버의 부담을 덜어줍니다.
  • 프로토콜 호환성 : WebSocket은 초기 연결을 수렵하기 위해 HTTP/HTTPS 프로토콜을 사용하므로 기존 웹 인프라와 호환성이 좋습니다.
  • 방화벽 및 프록시 호환성 : WebSocket은 기본적으로 80 및 443 포트를 사용하여 통신하기 때문에 대부분의 방화벽이나 프록시를 통과하는데 문제가 없습니다.

 

socket.io

socket.io는 WebSocket을 포함한 여러 통신 기술을 추상화하여 실시간 웹 애플리케이션을 쉽게 구축 할 수 있도록 도와주는 JavaScript 라이브러리로 WebSocket을 지원하지 않는 브라우저나 환경에서도 작동하도록 폴백 메커니즘을 제공합니다.

 

WebSocket을 활용한 채팅방 구현

프로젝트 디렉토리 구조

 

app.js

// 채팅방 만들기
// 유저간의 귓속말
const express = require("express");
const path = require("path");
const socketIo = require("socket.io");
const app = express();

app.set("views", path.join(__dirname, "page"));
app.set("view engine", "ejs");

const server = app.listen(8080, () => {
  console.log("Server On");
});

app.get("/", (req, res) => {
  res.render("main");
});

let userId = [];
let userList = [];
let userTab1 = [];
let userTab2 = [];

const io = socketIo(server);

// 유저들의 소켓이 생성되면
io.sockets.on("connection", (socket) => {
  console.log("user connect");
  // 유저 접속시 배열에 추가
  // 유저의 socket.id를 배열에 저장
  // user가 방에 들어오면
  socket.on("joinRoom", (room, name) => {
    socket.join(room);

    // 같은 방에 있는 유저에게 이벤트 푸쉬
    // 누가 방에 들어왔는지 알려줌
    if (room == "room1") {
      userTab1.push({ name, id: socket.id });
      io.to(room).emit("joinRoom1", room, name, userTab1);
    }
    if (room == "room2") {
      userTab2.push({ name, id: socket.id });
      io.to(room).emit("joinRoom2", room, name, userTab2);
    }
  });

  // 유저가 방을 떠날 때
  socket.on("leaveRoom", (room, name) => {
    // 유저가 채팅방을 떠날때 이벤트 푸쉬
    socket.leave(room);

    if (room == "room1") {
      userTab1 = userTab1.filter((value) => value.name != name);
      // 누가 나갔는지 같은 방에 있는 유저에게 푸쉬
      io.to(room).emit("leaveRoom", room, name, userTab1);
    }
    if (room == "room2") {
      userTab2 = userTab2.filter((value) => value.name != name);

      io.to(room).emit("leaveRoom", room, name, userTab2);
    }
  });

  // 같은 방에 있는 유저끼리 채팅보이기
  socket.on("chat", (room, name, msg) => {
    io.to(room).emit("chat", name, msg);
  });

  socket.on("chat2", (id, name, msg) => {
    io.to(id).emit("chat2", name, " 귓속말 " + msg);
  });

  socket.on("whisper", (room, username) => {
    if (room == "room1") {
      const whisperId = userTab1.filter((value) => value.name == username);
      io.sockets.emit("whisper", whisperId, username);
    }
    if (room == "room2") {
      const whisperId = userTab2.filter((value) => value.name == username);
      io.sockets.emit("whisper", whisperId, username);
    }
  });

  // 유저의 접속이 끊어지면
  socket.on("disconnect", () => {
    console.log("user logout");
    // 유저 배열에서 삭제
    // filter로 해당 대상의 이름을 찾아 제거
    // 해당 userId가 아니면 배열에 다시 저장
    // userList = userList.filter((value) => value != socket.id);
    userTab1 = userTab1.filter((value) => value.id != socket.id);
    userTab2 = userTab2.filter((value) => value.id != socket.id);
    userId = userId.filter((value) => value != socket.id);
  });
});

 

page/main.ejs

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="/socket.io/socket.io.js"></script>
    <style>
      body {
        position: relative;
        height: 100vh;
      }
      .content {
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        width: 500px;
        height: 500px;
        border: 1px solid;
        position: relative;
        box-sizing: border-box;
      }

      #send {
        position: fixed;
        bottom: 0;
        width: 100%;
        border: 1px solid;
        box-sizing: border-box;
        display: flex;
        left: 0;
      }

      #send #msg {
        border: 0;
        box-sizing: border-box;
        padding: 10px;
        width: 90%;
      }

      #send #sendBtn {
        background-color: antiquewhite;
        border: none;
        box-sizing: border-box;
        padding: 10px;
        width: 10%;
      }

      #messages {
        margin: 0;
        padding: 0;
      }

      #messages li {
        list-style: none;
      }

      #login {
        width: 300px;
        height: 300px;
        display: flex;
        align-items: center;
        justify-content: center;
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
      }

      .join_text {
        background-color: white;
      }

      .leave_text {
        background-color: gray;
        border: 1px solid gray;
      }

      #main {
        display: none;
      }

      .userlist {
        width: 100px;
        height: 500px;
        border: 1px solid;
        position: absolute;
        top: 0;
        right: -100px;
        box-sizing: border-box;
      }

      #userlisttab {
        list-style: none;
      }
    </style>
  </head>
  <body>
    <div class="content">
      <div>chat</div>
      <div id="login">
        <p>login</p>
        <input type="text" id="username" />
        <button id="loginBtn">connect</button>
      </div>
      <div id="main">
        <select name="" id="rooms">
          <option value="room1">room 1</option>
          <option value="room2">room 2</option>
        </select>
        <ul id="messages">
          <div id="send">
            <input type="text" id="msg" />
            <button id="sendBtn">send</button>
          </div>
          <div id="send2">
            <input type="text" id="msg2" />
            <button id="sendBtn2">whispering</button>
          </div>
        </ul>
      </div>
      <div class="userlist">
        <ul id="userlisttab"></ul>
      </div>
    </div>
  </body>

  <script>
    window.onload = () => {
      // 로그인 버튼을 누르면
      loginBtn.onclick = () => {
        login.style.display = "none";
        main.style.display = "block";

        const name = username.value;
        // 선택한 select의 value 값을 가져옴
        let room = rooms.options[rooms.selectedIndex].value;
        // socket에 연결 시도
        const socket = io.connect();
        socket.emit("joinRoom", room, name);

        // select 태그의 선택이 바뀌었을때
        rooms.onchange = function (e) {
          let el = e.target;
          // 다른 채팅방을 선택하여 유저가 떠남
          socket.emit("leaveRoom", room, name);
          room = rooms.options[el.selectedIndex].value;

          // 기존에 있던 방을 떠난 후 새로운 방에 조인
          socket.emit("joinRoom", room, name);
        };

        socket.on("joinRoom1", (room, name, userTab1) => {
          let chatLi = document.createElement("li");

          let chatText = `${name} 님이 ${room} 에 입장하셧습니다.`;
          chatLi.append(chatText);
          messages.appendChild(chatLi);
          userlisttab.innerHTML = "";

          userTab1.forEach((el) => {
            let chatLi2 = document.createElement("li");
            chatLi2.classList.add("userArr");

            chatLi2.append(el.name);
            userlisttab.append(chatLi2);
          });
        });

        socket.on("joinRoom2", (room, name, userTab2) => {
          let chatLi = document.createElement("li");

          let chatText = `${name} 님이 ${room} 에 입장하셧습니다.`;
          chatLi.append(chatText);
          messages.appendChild(chatLi);
          userlisttab.innerHTML = "";

          userTab2.forEach((el) => {
            let chatLi2 = document.createElement("li");

            chatLi2.append(el.name);
            userlisttab.append(chatLi2);
          });
        });
        // 채팅방을 떠나면
        socket.on("leaveRoom", (room, name, userTab) => {
          let chatLi = document.createElement("li");
          let chatText = `${name} 님이 ${room} 에 나갔습니다.`;
          chatLi.append(chatText);
          messages.append(chatLi);

          userlisttab.innerHTML = "";

          userTab.forEach((el) => {
            let chatLi2 = document.createElement("li");

            chatLi2.append(el.name);
            userlisttab.append(chatLi2);
          });
        });

        // 같은 채팅방에 있는 사람만 채팅이 보임
        socket.on("chat", (name, msg) => {
          let chatLi = document.createElement("li");
          let chatText = `${name} : ${msg}`;
          chatLi.append(chatText);
          messages.append(chatLi);
        });

        socket.on("chat2", (name, msg) => {
          let chatLi = document.createElement("li");
          let chatText = `${name} : ${msg}`;
          chatLi.append(chatText);
          messages.append(chatLi);
        });

        sendBtn.onclick = () => {
          socket.emit("chat", room, name, msg.value);
          msg.values = "";
        };
        sendBtn2.onclick = () => {
          socket.emit("whisper", room, msg2.value);
          socket.on("whisper", ([whisperId], name) => {
            const { id } = whisperId;
            socket.emit("chat2", id, name, msg.value);
          });
          msg.values = "";
        };

        userlisttab.onclick = function (e) {
          const username = e.target.innerHTML;
          msg2.value = username;
        };
      };
    };
  </script>
</html>

 

  • 3명의 사용자가 채팅방에 입장하였고 각각 채팅을 치고 있는 상황
  • test1의 유저가 test3에게 귓속말을 하였고 test2는 귓속말에 대한 내용이 보이지 않음
  • test2 유저 혼자 room2 방으로 이동하였고 우측 사용자 탭에서 빠진걸 볼 수 있음
  • room2에 입장한 유저가 채팅을 입력하여도 room1에 있는 유저에게 보이지 않음
728x90