Timpossible history

[Express JS] Socket.IO 이용해서 실시간 채팅 어플리케이션 구성 본문

백엔드/Node JS

[Express JS] Socket.IO 이용해서 실시간 채팅 어플리케이션 구성

팀파서블 2024. 1. 3. 14:20

Socket.IO는 웹 소켓(Websocket)을 쉽게 다룰 수 있도록 도와주는 라이브러리로, 실시간 양방향 통신을 구현하는데 아주 유용하다.

 

1. 프로젝트 초기화

mkdir mychatting
cd mychatting
npm init -y

 

먼저 프로젝트를 초기화 해주고, 필요한 패키지를 설치해준다.

npm install express socket.io pug

 

Node js에는 View Engine이 존재하는데, 이는 서버에서 얻은 결과값을 정적 페이지(html)에 표시 할 수 있게 해준다. 물론 자동으로 생성되는 것이 아니라, 기본 템플릿을 만들어놔야 하는데, 기본 html 형식으로 하지 않도록 도와주는 것이 pug이다.

(ejs도 많이 쓰임. pug든 ejs든 동적으로 정적 페이지 렌더링 가능함.)

여기서 pug는 간단하고 직관적으로 보기가 편해서 정적 페이지 구성이 쉬운 장점이 있으니 pug를 쓸 것이다.

 

2. 서버 사이드 설정

// src/server.js

import express from "express";
import socketIO from "socket.io";
import http from "http";

const app = express();

app.set("view engine", "pug"); // view engine은 pug로 설정
app.set("views", __dirname + "/views"); // 정적 페이지 파일들은 __dirname + '/views' 경로에 있는 폴더에 있다고 설정.
app.use("/public", express.static(__dirname + "/public")); // '/public' 경로로 들어오는 요청을 
app.get("/", (req, res) => res.render("home")); // 유저가 메인 페이지('/')에 들어오면 views 폴더의 home.pug가 보여짐.

app.get("/*", (req, res) => res.redirect("/")); // 일단 '/'외에 다른 페이지가 없으므로 기타 다른 루트를 입력해도 home.pug가 렌더링 되도록 설정.

const handleListen = () => console.log(`Listening on http://localhost:3000`);

const httpServer = http.createServer(app);

const wsServer = socketIO(httpServer);

// user가 메인페이지에 들어올 시, 웹소켓 서버와 연결
wsServer.on("connection", (socket) => {
	// user의 소켓 객체에 임의적으로 'Anon' 닉네임 property 추가.
  socket["nickname"] = "Anon";
  
   // user가 roomName이라는 이름의 방에 들어올 때  
  socket.on("enter_room", (roomName) => {
    socket.join(roomName);
	
    // roomName이름의 방에 있는 전체 user들에게 'welcome'이라는 신호를 보내고, 'nickname joined' 문자열과, roomName 변수를 메세지로 보냄.
    socket
      .to(roomName)
      .emit(
        "welcome",
        `${socket.nickname} joined!!`,
        roomName,
      );
  });
  
  //'nickname'을 입력했을 때, socket 객체의 nickname property 수정
  socket.on("nickname", (nickname, callback) => {
    socket["nickname"] = nickname;
    callback();
  });
  
  // 메세지를 입력했을 때, 방에 있는 유저들에게 'nickname: msg'라는 문자열을 전달
  socket.on("new_message", (msg, roomName, callback) => {
    socket.to(roomName).emit("new_message", `${socket.nickname} : ${msg}`);
    callback();
  });

  socket.on("disconnecting", () => {
    socket.rooms.forEach((room) =>
      socket.to(room).emit("bye", `${socket.nickname} left..`)
    );
  });

  socket.on("disconnect", () => {
	console.log('user left');
  });

});

httpServer.listen(3000, handleListen);

3. 클라이언트 사이드 설정

// src/views/home.pug

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 Noom
        
        // 간단한 스타일링을 위해서 mvp.css를 연결해주었다.
        link(rel="stylesheet", href="https://unpkg.com/mvp.css@1.12/mvp.css")
    body 
        header 
            h1 Noom
        main 
            h2 Welcome to Noom!
            div#welcome
                form
                    input(type="text", placeholder="Room name", required)
                    button Enter room
                ul
            div#room
                h3#roomName
                form#name
                    input(type="text", placeholder="nickname")
                    button Save
                ul
                form#msg
                    input(type="text", placeholder="message", required)
                    button Send
        script(src="/socket.io/socket.io.js")
        // 클라이언트 사이드에서 실행시킬 javascript 코드
        script(src="/public/js/app.js")

 

'/' 루트의 메인 정적 페이지 home.pug를 보기처럼 작성해준다.

클라이언트 사이드에서 실행시키는 js 코드는 /public/js/app.js 루트에 작성했다.

const socket = io();

const welcome = document.querySelector("#welcome");
const form = welcome.querySelector("form");
const room = document.querySelector("#room");
room.hidden = true;

let roomName;

// 유저 입장, 유저 메세지 등을 보여주는 DOM 만듦
const addMessage = (message) => {
  const ul = room.querySelector("ul");
  const li = document.createElement("li");
  li.innerText = message;
  ul.appendChild(li);
};

form.addEventListener("submit", (e) => {
  e.preventDefault();
  welcome.hidden = true;
  const roomNameInput = form.querySelector("input");
  form.hidden = true;
  room.hidden = false;
  roomName = roomNameInput.value;
  const h3 = room.querySelector("h3");
  h3.innerText = `Room ${roomName}`;
  roomNameInput.value = "";
  const nickname = room.querySelector("#name");
  const msg = room.querySelector("#msg");
  msg.hidden = true;

  nickname.addEventListener("submit", (e) => {
    e.preventDefault();
    const nicknameInput = room.querySelector("#name input");

	// 입력한 nickname을 웹소켓 서버에 보내준다.
    // 세번째 인자는 함께 실행시킬 콜백함수.
    // nickname을 입력하면 enter_room이라는 신호를 웹소켓 서버에 보내게 됨고 웹소켓 서버에서 on('enter_room') 실행. 
    socket.emit("nickname", nicknameInput.value, () => {
      socket.emit("enter_room", roomName);
    });

    msg.hidden = false;
    nickname.hidden = true;

    msg.addEventListener("submit", (e) => {
      e.preventDefault();
      const msgInput = room.querySelector("#msg input");
      const value = msgInput.value;
		
      // msgInput에 입력한 메세지를 보냄.
      socket.emit("new_message", msgInput.value, roomName, () =>
        addMessage(`You: ${value}`)
      );
      msgInput.value = "";
    });
  });
});

 

4. 실행

node src/server.js

 

실행해주면, 웹 브라우저 'http://localhost:3000'으로 접속하게됨. 여러 윈도우 탭을 열어 localhost:3000으로 들어가서 실습해볼 수 있다.