본문 바로가기

Node.js/실험실

[Node.js] Zoom 클론코딩 - 채팅편

0. 들어가며

앞서 세팅편을 보지 않으셨다면 먼저 보고 오시는 것을 추천합니다.

1. [Node.js] Zoom 클론코딩 - 세팅편

 

1. 서버 설정하기

npm i ws

Node.js에서 webSocket의 핵심 패키지ws이다. 

 

express는 기본적으로 http를 지원하기 때문에 ws는 지원하지 않는다. 

그래서 서버에 ws 기능을 추가로 설치할 예정이다. 

 

import express from "express";
import http from "http";

const app = express();

app.set("view engine", "pug");
app.set("views", __dirname + "/views");
app.use("/public", express.static(__dirname + "/public"));

app.get("/", (req, res) => res.render("home"));
app.get("/*", (req, res) => res.redirect("/"));

const server = http.createServer(app)

Node.js에 내장되어 있는 http 패키지를 사용해서 서버를 실행할 것이다. 

 

http.createServer(app)

createServer를 사용해서 Express Application으로부터 서버를 만들었다. 

 

... 

import WebSocket from "ws";

const app = express();

...

const server = http.createServer(app);
const wss = new WebSocket.Server({ server });

http.createServer를 사용해서 http 서버를 만들고 ws 패키지의 WebSocket을 이용해서 WebSocket 서버를 만들었다. 

 

new WebSocket.Server({ server });

Server()의 매개변수로 서버를 넘겨줘서 http 서버와 webSocket 서버 둘 다 실행이 가능해졌다. 

만약 http 서버가 필요 없다면 매개변수로 넘겨주지 않아도 상관없다. 

 

import express from "express";
import http from "http";
import WebSocket from "ws";

const app = express();

app.set("view engine", "pug");
app.set("views", __dirname + "/views");
app.use("/public", express.static(__dirname + "/public"));

app.get("/", (req, res) => res.render("home"));
app.get("/*", (req, res) => res.redirect("/"));

const server = http.createServer(app);
const wss = new WebSocket.Server({ server });

server.listen(3000, () => console.log("Listening on http://localhost:3000")); // -- *

최종으로 listion()을 통해서 http 서버와 webSocket 서버 모두 실행하였다. 

 

2. 메시지 주고받기

import express from "express";
import http from "http";
import WebSocket from "ws";

const app = express();

app.set("view engine", "pug");
app.set("views", __dirname + "/views");
app.use("/public", express.static(__dirname + "/public"));

app.get("/", (req, res) => res.render("home"));
app.get("/*", (req, res) => res.redirect("/"));

const server = http.createServer(app);
const wss = new WebSocket.Server({ server });

wss.on("connection", (socket) => console.log(socket));               // -- *

server.listen(3000, () => console.log("Listening on http://localhost:3000"));

webSocketon 함수를 통해서 여러 가지 이벤트가 발생했을 때 추가적인 작업이 가능하다. 

JavaScript에서 element.addEventListener("click", () =>...)와 비슷한 역할이라고 생각하면 된다. 

 

즉, 위 코드는 connection 이벤트가 발생하면 socket을 콘솔 로그에 띄운다는 뜻이다

 

// src/public/js/app.js

const socket = new WebSocket(`ws://${window.location.host}`);

FrontEnd브라우저가 이미 WebSocket 클라이언트에 대한 패키지를 가지고 있다. 

그래서 서버의 webSocket과 연결하고 싶다면 자바스크립트를 통해서 바로 연결이 가능하다. 

 

한 가지 다른 점이 흔히 서버와 통신할 때는 주소가 "http://~~~.~~" 같이 앞에 http가 붙는데, webSocket은 

" ws://주소 "를 통해서 webSocket 서버와 연결할 수 있다. 

 

import express from "express";
import http from "http";
import WebSocket from "ws";

const app = express();

app.set("view engine", "pug");
app.set("views", __dirname + "/views");
app.use("/public", express.static(__dirname + "/public"));

app.get("/", (req, res) => res.render("home"));
app.get("/*", (req, res) => res.redirect("/"));

const server = http.createServer(app);
const wss = new WebSocket.Server({ server });

wss.on("connection", (socket) => {
  socket.send("hello!!");     // -- *
});

server.listen(3000, () => console.log("Listening on http://localhost:3000"));

전달받은 socket을 이용하면 FrontEnd에게 메시지를 보낼 수 있다. 

 

const socket = new WebSocket(`ws://${window.location.host}`);

socket.addEventListener("open", () => {
  console.log("Conneted to Server");
});

socket.addEventListener("message", (message) => {
  console.log("message : ", message);
});

socket.addEventListener("close", () => {
  console.log("Disconneted to Server");
});

FrontEnd에서는 socket에게 addEventListener를 통해 webSocket 연결 시

필요한 이벤트 ( open : 연결 완료, message : 서버로부터 메시지가 올 경우, close : 연결 종료 ) 때 적절한 작업을 

수행할 수 있다. 

브라우저에서 확인 가능한 콘솔

// ...

wss.on("connection", (socket) => {
  //   console.log(socket);
  socket.send("hello!!");
  socket.on("close", () => console.log("DIsconnected from the Browser"));
});

// ...

서버에서도 마찬가지로 " close " 이벤트가 있어서 연결이 종료되면 추가 작업이 가능하다.

 

// ...

wss.on("connection", (socket) => {
  //   console.log(socket);
  socket.send("hello!!");
  socket.on("close", () => console.log("Disconnected from the Browser"));
  socket.on("message", (message) => {
    console.log(message.toString("utf8"));
  });
});

// ...

서버에서도 마찬가지로 " message " 이벤트를 받아서 FrontEnd로부터 메시지를 받을 수 있다. 

 

const socket = new WebSocket(`ws://${window.location.host}`);

socket.addEventListener("open", () => {
  console.log("Conneted to Server");
});

socket.addEventListener("message", (message) => {
  console.log("message : ", message);
});

socket.addEventListener("close", () => {
  console.log("Disconneted to Server");
});

setTimeout(() => {
  socket.send("hello from the browser!");
}, 10000);

FrontEnd도 마찬가지로 " send " 이벤트를 통해서 서버에게 메시지를 보낼 수 있다. 

 

3. 다수의 FrontEnd와 통신하기 

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 Zoom Clone
        link(rel="stylesheet", href="https://unpkg.com/mvp.css")
    body 
        header
            h1 Zoom

        main 
            ul
            form
                input(type="text", name="write a msg", required)
                button Send 
        script(src="/public/js/app.js")

pug에서 form 안에 텍스트를 입력할 수 있는 input과 button을 만들어주고, ul은 메시지 리스트를 나타낼 것이다. 

 

const ul = document.querySelector("ul");
const form = document.querySelector("form");

const socket = new WebSocket(`ws://${window.location.host}`);

// ... 

function handleSubmit(event) {
  event.preventDefault();

  const input = form.querySelector("input");

  socket.send(input.value);
  input.value = "";
}

form.addEventListener("submit", handleSubmit);

form을 querySelector로 가져온 뒤 submit 이벤트를 통해서 서버로 작성한 내용을 전달한다. 

 

wss.on("connection", (socket) => {
  socket.send("hello!!");
  socket.on("close", () => console.log("Disconnected from the Browser"));
  socket.on("message", (message) => {
    socket.send(message.toString());               // -- *
  });
});

서버에서는 다시 전달 발은 메시지를 FrontEnd로 넘겨준다. 

하지만 아직까지는 FrontEnd와 서버 간의 1: 1 메시지만 가능하다. 우리는 많은 FrontEnd끼리의 통신을 목표로 만들고

있다. 

 

const sockets = [];

wss.on("connection", (socket) => {
  //   console.log(socket);
  sockets.push(socket);
  socket.send("hello!!");
  socket.on("close", () => console.log("Disconnected from the Browser"));
  socket.on("message", (message) => {
    sockets.forEach((client) => {
      client.send(message.toString());
    });
  });
});
wss.on("connetion" () => {})

 

connection 이벤트는 FrontEnd와 서버가 webSocket 통신이 성공될 때마다 호출된다. 

 

그렇기 때문에 임의의 데이터베이스 sockets 배열을 만들어 안에 socket을 저장한다면 서버와 통신하는 모든 socket을 

저장할 수 있고, message가 오면 모든 sokect에게 전달해서 다중 통신이 가능하게 만들었다. 

 

// ...

socket.addEventListener("message", (message) => {
  const li = document.createElement("li");
  li.innerHTML = message.data;

  ul.append(li);
});

// ...

message가 오면 li에 전달받은 메시지를 넣어 화면에 나오게 만들었다. 

 

4. 닉네임 나오게 하기

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 Zoom Clone
        link(rel="stylesheet", href="https://unpkg.com/mvp.css")
    body 
        header
            h1 Zoom

        main 
            form#nick
                input(type="text", name="choose a nickname", required)
                button Save 
            ul
            form#messge
                input(type="text", name="write a msg", required)
                button Send 
        script(src="/public/js/app.js")

메시지는 완벽하게 전달이 되지만, 익명으로 메시지가 나오기 때문에 FrontEnd별 닉네임을 정하는 기능을 

추가하려고 한다. 

 

먼저, 새롭게 form 태그를 하나 만들고 안에 닉네임용 input과 button을 만들어준다. 

const ul = document.querySelector("ul");
const nickForm = document.querySelector("#nick");
const meesageForm = document.querySelector("#message");

// ...

function handleSubmit(event) {
  event.preventDefault();

  const input = meesageForm.querySelector("input");

  socket.send(input.value);
  input.value = "";
}

function handleNickSumit(event) {
  event.preventDefault();

  const input = nickForm.querySelector("input");

  socket.send(input.value);
  input.value = "";
}

meesageForm.addEventListener("submit", handleSubmit);
nickForm.addEventListener("submit", handleNickSumit);

form이 하나 추가돼서 querySelector를 수정해주고 각각 나눠서 sumit 함수를 만들어주었다. 

 

// ...

function makeMessage(type, payload) {
  const msg = { type, payload };

  return JSON.stringify(msg);
}

function handleSubmit(event) {
  event.preventDefault();

  const input = meesageForm.querySelector("input");

  socket.send(makeMessage("new_message", input.value));
  input.value = "";
}

function handleNickSumit(event) {
  event.preventDefault();

  const input = nickForm.querySelector("input");

  socket.send(makeMessage("nickname", input.value));
  input.value = "";
}

meesageForm.addEventListener("submit", handleSubmit);
nickForm.addEventListener("submit", handleNickSumit);

 

현재 필요한 것은 하나의 webSocket에서 2개의 유형으로 나뉘어서 처리가 돼야 한다. 

가장 좋은 방법은 JSON을 서버에게 보내주는 건데, webSocket으로 서버에게 메시지를 보낼 때는 String 자료형으로만 보낼 수 있다. 

 

그래서 JSON.strignify를 사용해 JSON 데이터를 String으로 변환해서 서버에 보내고 서버에서 다시

JSON.parse로 JSON 데이터로 바꿔서 처리하는 방식을 사용할 것이다. 

 

wss.on("connection", (socket) => {
  socket["nickname"] = "Anon";
  sockets.push(socket);
  socket.send("hello!!");
  socket.on("close", () => console.log("Disconnected from the Browser"));
  socket.on("message", (message) => {
    const data = JSON.parse(message.toString());
    switch (data.type) {
      case "new_message":
        sockets.forEach((client) => {
          client.send(`${socket.nickname}: ${data.payload}`);
        });
        break;
        
      case "nickname":
        socket["nickname"] = data.payload;
        break;
    }
  });
});

서버에서 message를 JSON.parse로 다시 JSON 데이터로 바꿔서 각 타입마다 처리하게 작업했다. 

 

socket ["nickname"]은 socket도 객체이기 때문에 위와 같이 선언이 가능하며, 그럴 경우 최초 연결 시 닉네임이 

지정되지 않기 때문에 초기화를 시켜야 했다. 

그래서 처음 연결 시 socket ["nickname"]을 Anon으로 초기화시켰다. 

 

5. 깃허브 

https://github.com/SeoJaeWan/zoom-clone

 

GitHub - SeoJaeWan/zoom-clone

Contribute to SeoJaeWan/zoom-clone development by creating an account on GitHub.

github.com

 

반응형