[실시간 채팅 구현] Socket.IO 사용

mason.98·2022년 11월 29일
0

JS

목록 보기
1/5

깃허브 링크

https://github.com/wannabeing/wa_zoom

프로젝트 생성

npm init -y

nodemon, babel 설치

npm i -D nodemon
touch babel.config.json
touch nodemon.json (nodemon 명령어 파일)
npm i -D @babel/core @babel/cli @babel/node @babel/preset-env

npm run dev 프로젝트 실행

// package.json
"scripts": {
  "dev": "nodemon" 
}
dev 명령어 입력 => nodemon 실행 => nodemon.json파일 참조 
=> exec 명령어 실행 => npm run babel-node src/server.js 실행

Express/PUG 설치

npm i express
npm i pug


[프로토콜] HTTP vs WebSocket

HTTP
stateless - 서버는 응답과 동시에 클라이언트와 연결을 끊는다.
서버는 오직 요청이 올 때만, 응답한다.


WebSocket
stateful - 서버와 클라이언트가 서로 요청과 응답을 한다.
서버는 클라이언트의 정보를 갖고 있는다.
한번 연결되면 지속적인 실시간 대화가 가능하다.


WS vs SocketIO

ws: WebSocket 프로토콜 사용을 도와주는 라이브러리

npm i ws
nodeJS에서 사용되는 WebSocket 프로토콜을 이용한 라이브러리이다.


SocketIO : 실시간, 양방향, 이벤트기반 통신 프레임워크

npm i socket.io
webSocket을 사용하지만, 이것만 사용하지 않는다.
webSocket API를 지원하지 않는 오래된 브라우저에서도 실시간, 양방향 통신이 가능하도록 도와준다.

  • 모든 브라우저는 서버와의 private room이 있다.
  • 각 브라우저의 해당하는 소켓ID가 있어 구별하기 쉽다.

참고 블로그 : https://inpa.tistory.com/category/Node.js/Socket.IO

👨🏻‍💻 클라이언트 (front/js/index.js)

// socketIO 사용하는 서버와 연결
const connectionServer = io();

// 닉네임 변수
const nameForm = document.querySelector("#nameForm");
const nameInput = nameForm.querySelector("input");
const useNameBtn = nameForm.querySelector("#useNameBtn");
const editNameBtn = nameForm.querySelector("#editNameBtn");

// 대기실 변수
const waitingRoom = document.querySelector("#waitingRoom");
const enterForm = waitingRoom.querySelector("form");
const enterInput = enterForm.querySelector("input");

// 채팅방 변수
const chatRoom = document.querySelector("#chatRoom");
const chatForm = chatRoom.querySelector("form");
const chatInput = chatForm.querySelector("input");
const chatDiv = document.querySelector("#chatDiv"); // ul태그 감싼 div
const exitRoomBtn = document.querySelector("#exitRoomBtn");

// 🚀 닉네임 폼 submit 이벤트
nameForm.addEventListener("submit", (event) => {
  event.preventDefault();

  connectionServer.emit("setName", nameInput.value, () => {
    nameInput.disabled = true;
    useNameBtn.style.display = "none";
    editNameBtn.style.display = "inline-block";
  });
});
// 🚀 닉네임 폼 click 이벤트
editNameBtn.addEventListener("click", (event) => {
  event.preventDefault();

  nameInput.disabled = false;
  useNameBtn.style.display = "inline-block";
  editNameBtn.style.display = "none";
});

// 🚀 대기실 폼 submit 이벤트: 대기실 -> 채팅방 입장 함수
enterForm.addEventListener("submit", (event) => {
  event.preventDefault();

  connectionServer.emit("enterRoom", enterInput.value, (roomName, count) => {
    chatRoom.style.display = "inline-block";
    waitingRoom.style.display = "none";
    setRoomTitle(roomName, count);
  });
});
// 🚀 채팅방 폼 usbmit 이벤트: 채팅방 채팅 함수
chatForm.addEventListener("submit", (event) => {
  event.preventDefault();

  connectionServer.emit(
    "sendChat",
    chatInput.value,
    enterInput.value,
    (msg) => {
      addMsg(msg, true);
    }
  );
  chatInput.value = "";
  chatInput.focus();
});
// 🚀 채팅방 나가기 버튼 이벤트
exitRoomBtn.addEventListener("click", () => location.reload());

/* 
    🚀 socketIO 함수
    - welcomeMsg : 입장 알림
    - byeMsg : 퇴장 알림
    - sendChat : 채팅 입력
    - editNameMsg : 닉네임 변경 알림
    - changeRoom : 공개방 조회 및 출력
*/
connectionServer.on("welcomeMsg", (name, count) => {
  addMsg(`${name}님이 입장하셨습니다.`, false);
  setRoomTitle(name, count);
});
connectionServer.on("byeMsg", (name, count) => {
  addMsg(`${name}님이 퇴장하셨습니다.`, false);
  setRoomTitle(name, count);
});
connectionServer.on("sendChat", (msg, name) => {
  addMsg(`[${name}]: ${msg}`, false);
});
connectionServer.on("editNameMsg", (prevName, editName) => {
  addMsg(`${prevName} -> ${editName} 이름변경`, false);
});
connectionServer.on("changeRoom", (rooms) => {
  const publicRoomUl = document.querySelector("#publicRoomList");
  publicRoomUl.innerHTML = "";
  if (rooms.length === 0) {
    publicRoomUl.innerHTML = "";
    return;
  }

  rooms.forEach((room) => {
    const publicRoomLi = document.createElement("li");
    publicRoomLi.innerText = room;
    publicRoomUl.appendChild(publicRoomLi);
  });
});
/*
    🚀 공통함수
        - addMsg() : 채팅 추가 함수
        - addMyMsg() : 나의 채팅 추가 함수
        - setRoomTitle() : 채팅방 이름 설정 함수
*/
function addMsg(msg, isMyMsg) {
  const chatList = chatRoom.querySelector("ul");
  const chat = document.createElement("li");

  if (isMyMsg) {
    chat.id = "myMsg";
  }
  chat.innerText = msg;
  chatList.appendChild(chat);
  chatDiv.scrollTop = chatDiv.scrollHeight; // 채팅 스크롤 항상 아래로
}
function setRoomTitle(roomName, count) {
  document.querySelector(
    "#roomTitle"
  ).innerText = `채팅방: ${roomName} (${count})`;
}

👨🏻‍💻 서버 (server.js)

// express http 서버
const httpServer = http.createServer(app);

// express http 서버 기반으로 생성한 socketIO 서버
const ioServer = new Server(httpServer, {
  cors: {
    origin: ["https://admin.socket.io"],
    credentials: true,
  },
});
// socketIO admin 페이지 설정
instrument(ioServer, {
  auth: false,
});

/* 
    🚀 getPublicRooms(): 공개방만 구하는 함수
        sids: 서버와 연결된 socket ID 리스트
        rooms: 생성된 채팅방 리스트 (개인방 포함)
        publicRooms : 유저가 직접 생성한 채팅방 리스트 (공개방만)

     🚀 getCount() : 해당 공개방의 인원수 구하는 함수
*/
function getPublicRooms() {
  const { sids, rooms } = ioServer.sockets.adapter;
  const publicRooms = [];
  rooms.forEach((_, key) => {
    if (sids.get(key) === undefined) {
      publicRooms.push(key);
    }
  });
  return publicRooms;
}
function getCount(roomName) {
  return ioServer.sockets.adapter.rooms.get(roomName)?.size;
}

/*
    📦 socketIO Server
    - connection : 서버와 브라우저가 연결되었을 때
    - disconnecting : 채팅방과 연결 끊기기 직전 이벤트
    - disconnect : 채팅방과 연결이 끊어진 이후 이벤트
*/
ioServer.on("connection", (frontSocket) => {
  frontSocket.onAny((event) => console.log(`🚀 [Event] ${event}`));
  frontSocket.name = "익명"; // 초기 닉네임 설정

  // 🚀 changeRoom()
  ioServer.sockets.emit("changeRoom", getPublicRooms());

  // 🚀 setName()
  frontSocket.on("setName", (editName, done) => {
    const prevName = frontSocket.name;
    frontSocket.name = editName;
    // 🚀 editNameMsg()
    frontSocket.rooms.forEach((eachRoom) => {
      frontSocket.to(eachRoom).emit("editNameMsg", prevName, editName);
    });
    done();
  });

  // 🚀 enterRoom()
  frontSocket.on("enterRoom", (roomName, done) => {
    frontSocket.join(roomName); // 해당 채팅방 입장
    done(roomName, getCount(roomName)); // 입장 이후 브라우저에게 채팅방 이름을 포함한 제어권 전달

    // 🚀 welcomeMsg()
    frontSocket
      .to(roomName)
      .emit("welcomeMsg", frontSocket.name, getCount(roomName)); // 본인 이외에 같은 채팅방 유저에게 id전달
    // 🚀 changeRoom()
    ioServer.sockets.emit("changeRoom", getPublicRooms());
  });

  // 🚀 sendChat()
  frontSocket.on("sendChat", (msg, roomName, done) => {
    frontSocket.to(roomName).emit("sendChat", msg, frontSocket.name);
    done(msg);
  });

  frontSocket.on("disconnecting", () => {
    // 🚀 byeMsg()
    frontSocket.rooms.forEach((eachRoom) => {
      frontSocket
        .to(eachRoom)
        .emit("byeMsg", frontSocket.name, getCount(eachRoom) - 1);
    });
  });
  frontSocket.on("disconnect", () => {
    // 🚀 changeRoom()
    ioServer.sockets.emit("changeRoom", getPublicRooms());
  });
});

👨🏻‍💻 PUG (index.html)

    body
        header
            h1 WA_ZOOM 😎
       
            h2#roomTitle
            div#waitingRoom
                form#enterForm(action="", method="get")
                    h3 와줌 채팅방 리스트
                    #publicRoomList 
                        ul
                    input(type="text", name="enterInput", placeholder="채팅방 이름 입력", maxLength="10" required)
                    button 채팅방 입장하기
            div#nameDiv
                form#nameForm(action="", method="get")
                    h3 닉네임 설정
                    input(type="text", name="nameInput", placeholder="사용할 닉네임 입력", maxLength="7", required)
                    button#useNameBtn 사용하기
                    button#editNameBtn 수정하기
           
            div#chatRoom
                #chatDiv
                    ul
                form#chatForm(action="", method="get")
                    h3
                    input(type="text", name="msgInput", placeholder="메시지 입력", required, maxLength="20")
                    #chatBtnDiv
                        button 메시지 전송
                        button#exitRoomBtn 나가기
    script(src="/socket.io/socket.io.js") <!-- 브라우저에 socketIO 설치 -->      
    script(src="/static/js/index.js")
profile
wannabe---ing

0개의 댓글