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

2022. 6. 8. 21:06·Node.js/실험실
반응형

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"));

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

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

 

반응형
저작자표시 비영리 변경금지 (새창열림)

'Node.js > 실험실' 카테고리의 다른 글

[Node.js] Express ORM 세팅해보기  (0) 2022.10.09
[Node.js] Express에 Webpack 구현하기  (1) 2022.09.28
[Node.js] Express set "views"  (2) 2022.09.26
[Node.js] Zoom 클론코딩 - 채팅방편  (1) 2022.06.21
[Node.js] Zoom 클론코딩 - 세팅편  (0) 2022.06.07
'Node.js/실험실' 카테고리의 다른 글
  • [Node.js] Express에 Webpack 구현하기
  • [Node.js] Express set "views"
  • [Node.js] Zoom 클론코딩 - 채팅방편
  • [Node.js] Zoom 클론코딩 - 세팅편
잉여개발자
잉여개발자
풀스택 개발자를 목표로 잉여롭게 개발 공부도 하면서 다양한 취미 생활도 즐기고 있는 잉여 개발자입니다.
  • 잉여개발자
    잉여로운 개발일지
    잉여개발자
    • 분류 전체보기 (789)
      • 개발정보 (36)
      • 개발환경 (7)
      • 개발생활 (19)
      • React (141)
        • 이론 (23)
        • 기능 (12)
        • 실험실 (88)
        • 버그 (6)
        • 패스트캠퍼스 (9)
        • Npm (3)
      • React Native (28)
        • 공통 (6)
        • TypeScript (3)
        • JavaScript (18)
        • 버그 (1)
      • Next.js (30)
        • 이론 (13)
        • 실험실 (13)
        • 버그 (3)
      • Web (35)
      • 알고리즘 (202)
        • 풀이 힌트 (39)
      • JavaScript (47)
      • TypeScript (29)
        • 기초 (27)
        • 실험실 (2)
      • Node.js (13)
        • 이론 (0)
        • 기능 (3)
        • 실험실 (9)
        • 버그 (1)
      • 도커 (4)
      • CCNA (22)
        • 이론 (4)
        • 문제 (18)
      • 취미생활 (167)
        • 잉여로운 칵테일 (2)
        • 잉여의 식물키우기 (130)
        • 잉여로운 여행기 (11)
        • 잉여의 제2외국어 (21)
        • 잉여로운 책장 (2)
      • Java (1)
        • Java의 정석 (1)
      • 꿀팁 공유 (3)
  • 태그

    바질
    typescript
    리얼학습일기
    redux
    ChatGPT
    다이소
    알고리즘
    덤프
    리액트
    타입스크립트
    ReactNative
    프로그래머스
    CCNA
    식물
    네이버 부스트캠프
    javascript
    Babel
    Node.js
    타일러영어
    네트워크
    react
    바질 키우기
    리얼클래스
    영어독학
    자바스크립트
    영어회화
    webpack
    CSS
    next.js
    Docker
  • hELLO· Designed By정상우.v4.10.1
잉여개발자
[Node.js] Zoom 클론코딩 - 채팅편
상단으로

티스토리툴바