Zoom Clone Coding

zoom을 클론 코딩하면서 nodejs, websockets, 등에 대해 익혀보자.

Development Environment Configurations

  1. 우선 Node.js을 설치해준다.
  2. npm을 이용해서 nodejs 프로젝트 폴더를 생성해준다.
    npm init -y
    
  3. npm을 이용해서 필요한 package들을 설치한다.
    #nodejs을 이용해서 서버를 개발할 때, 파일이 변경되면 알아서 서버를 재기동해주는 package
    npm i nodemon
    #웹 브라우져에서 ES 버전 간의 문제점을 해결해주는 package
    npm i @babel/core @babel/cli @babel/node @babel/preset-env
    #http server을 구현하기 위한 package
    npm i express
    #WebSocket을 이용하기 위한 package
    npm i ws
    #html template engine
    npm i pug
    
  4. 위와 같이 설치한 후 몇가지 설정 파일들을 생성해준다.

package.json

"scripts": {
    "dev": "nodemon"
  },

nodemon.json

{
   //ignore 설정을 통해 파일 수정으로 인한 무분별한 서버 재기동을 막는다.
    "ignore":["src/public/*"],
    "exec":"babel-node src/server.js"
}

babel.config.json

{
    "presets":["@babel/env"]
}

/.gitignore

/node_modules

  1. server 실행
    nodemon -L
    

Basic Routing

기본적인 웹 경로에 대해 라우팅해주기 위해 express을 활용한다.

server.js

import express from "express";
//app을 생성해준다.
const app=express();
//html engine에 대한 설정
app.set("view engine","pug");
app.set("views",__dirname +"/views");
//사용자에게 static 폴더를 공유하기 위한 설정
app.use("/public",express.static(__dirname+ "/public"));

// '/'와 나머지 모든 경로에 대한 기본적인 라우팅을 설정해준다.
app.get("/",(req,res) => res.render("home"));
app.get("/*",(req,res) => res.redirect("/"));

Websockets

HTTP Server

HTTP의 경우 사용자의 request에 대해 서버에서 response해주는 방식이다,또한 http는 client에 대한 정보를 사이트에 저장하지 않는 stateless이며, 오직 request만 있을 때 response을 보낼 수 있다. 따라서 http을 이용해서 real-time program을 구현하기는 어렵다. 이를 위해 Websocket을 이용한다. Websocket

Websocket의 경우 먼저 client와 server간에 connection을 설정하는 hand-shaking 과정을 거치며, 서버가 먼저 client에 data를 보낼 수 있다.또한, Websocket은 외부 package가 아니라, 모든 브라우져에서 제공하는 것으로 호환성에 문제가 되지 않는다.

//http server => to get access to server
const server=http.createServer(app);
//websocket server
const wss=new WebSocket.Server({server});

이런식으로 http server와 wss server을 동시에 운영하는 것 또한 가능하다.

Realtime Chatting

Websocket방식에서도 아래와 같은 event들이 존재한다, 따라서 이러한 event들에 대해 eventHandler을 등록해서 event가 발생했을 떄 실행하는 function들을 구현할 수 있다.

Events

connection:event에는 사용자가 연결하는

message: 사용자가 메세지를 전송하였다.

close: 사용자가 접속을 종료하였다.

websocket event handling

socket.on(event_type, event_handler)

위의 socket.on function을 이용해서 해당 event에 대한 event handling function을 설정할 수 있다.

websocket message sending

socket.send(msg)

위의 socket.send function을 이용해서 메세지를 전송할 수 있다.

server.js

const sockets=[]
//wss=> websocket server
wss.on("connection",(socket)=>{
  //event handler for message event
   socket.on("message",(msg)=>{
       const message=JSON.parse(msg.toString('utf-8'));
       console.log(message);
       switch(message.type){
            case "new_message":
                sockets.forEach(aSocket=>aSocket.send(`${socket.nickname}: ${message.payload}`));
       }
       //socket.send(message);
       
   })
    //socket.send("hello!!!");
});

위의 server.js는 백엔드 서버를 구현한다. Websocket에 대한 event handler을 설정하는 부분이다. wss.on 모든 connection에 대해 작동하는 event handler이며, 내부의 함수들은 각각의 socket에 대해 작동하게 된다. 또한, msg 형태를 JSON 형식으로 관리하여서 사용자로부터 받은 message의 종류를 구분할 수 있다.

추가로, 각각의 브라우저에 대한 연결을 유지하기 위해, socket을 저장하는 sockets 배열을 사용하며, 이 socket은 아래와 같이 object 형태를 지니기 때문에 property을 추가하여 socket 별로 값을 저장할 수 있다.

socket

<ref *1> WebSocket {
  _events: [Object: null prototype] { close: [Function (anonymous)] },
  _eventsCount: 1,
  _maxListeners: undefined,
  _binaryType: 'nodebuffer',
  _closeCode: 1006,
  _closeFrameReceived: false,
  _closeFrameSent: false,
  _closeMessage: <Buffer >,
  _closeTimer: null,
  _extensions: {},
  _paused: false,
  _protocol: '',
  _readyState: 1,
  _receiver: Receiver {
    _writableState: WritableState {
      objectMode: false,
      highWaterMark: 16384,
      finalCalled: false,
      needDrain: false,
      ending: false,
      ended: false,
      finished: false,
      destroyed: false,
      decodeStrings: true,
      defaultEncoding: 'utf8',
      length: 0,
      writing: false,
      corked: 0,
      sync: true,
      bufferProcessing: false,
      onwrite: [Function: bound onwrite],
      writecb: null,
      writelen: 0,
      afterWriteTickInfo: null,
      buffered: [],
      bufferedIndex: 0,
      allBuffers: true,
      allNoop: true,
      pendingcb: 0,
      constructed: true,
      prefinished: false,
      errorEmitted: false,
      emitClose: true,
      autoDestroy: true,
      errored: null,
      closed: false,
      closeEmitted: false,
      [Symbol(kOnFinished)]: []
    },
    
    .
    .
    .

  _isServer: true,
  [Symbol(kCapture)]: false
}

app.js

//socket => represents a connection between front and back
const socket=new WebSocket(`ws://${window.location.host}`);
const messageList=document.querySelector("ul");
const messageForm=document.querySelector("#message");
// Recieve message => (Events)

//make JSON Converter
const makeMessage=function(type,payload){
    const msg={type,payload}
    return JSON.stringify(msg);
}

//Connected to Server
socket.addEventListener("open",()=>{
    console.log("Connected to Server");
});
//Recieving message
socket.addEventListener("message",(message)=>{
    console.log("Just Got This: ",message.data,"from the server");
    const li=document.createElement("li");
    li.innerText=message.data;
    messageList.appendChild(li);
});
//Disconnected to Server
socket.addEventListener("close",()=>{
    console.log("Connection Closed");
});

//Event Handler for Messsage submits
function handleSubmit(event){
    event.preventDefault();
    const input=messageForm.querySelector("input")
    socket.send(makeMessage("new_message",input.value));
    
    const li=document.createElement("li");
    li.innerText=`You: ${input.value}`;
    messageList.appendChild(li);

    input.value="";
}

messageForm.addEventListener("submit",handleSubmit);

위의 app.js 파일은 프론트엔드 역할을 수행하며, 백엔드와 프론트엔드 간에 연결 중가는 WebSocket가 존재한다. 각각의 connection을 위해 WebSocket가 생성되며, 프론트엔드에서 또한 socket의 event에 대한 eventHandler을 설정한다. 여기서 주의해할 점은 Websocket가 HTTP는 프로토콜 자체가 다르기 때문에 아래와 같이 protocol 형식이 다름을 인지하자.

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

Nickname Configuration

어떤 사용자가 채팅을 했는지에 대해 구분을 해주기 위해 nickname을 입력받는다. nickname을 등록하는 부분은 message와 유사한 부분이 많다.

server.js

 sockets.push(socket);
    socket["nickname"]="Anonymous";
   socket.on("message",(msg)=>{
       const message=JSON.parse(msg.toString('utf-8'));
       console.log(message);
       switch(message.type){
            case "nickname":
                console.log(message.payload);
                socket["nickname"]=message.payload
       }
    }
   )

app.js

const nickForm=document.querySelector("#nickname");
//Event Handler for nickname submits
function handleNickSubmit(event){
    event.preventDefault();
    const input=nickForm.querySelector("input");
    socket.send(makeMessage("nickname",input.value));
    input.value="";
}
nickForm.addEventListener("submit",handleNickSubmit);

References

link: nomadcoders

babel 관련 참고 자료

link: babel

link: babel1

link: babel2

express 관련 참고 자료

link: express

websocket 관련 자료

link: websocket

댓글남기기