본문 바로가기
Programming

WebSocket 사용으로 실시간 채팅 애플리케이션 구현 Project

by 하하호호 2022. 4. 12.
반응형

 

 

 

 

 

Node.js Express를 서버로 사용하고, pug로 프론트엔드를 구성해서 WebSocket을 사용하는 Zoom 클론 프로젝트다. 

 

 

 

프로젝트 초기 설정


NPM 초기화

$ npm init -y

 

package.json 설정

{
  "name": "01_zoom",
  "version": "1.0.0",
  "description": "Zoom Clone + WebRTC + Websockets + Node.js",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

 

README.md 작성

# Noom

Zoom Clone Project using WebRTC, Websocket, Node.js, Express

 

Nodemon 설치

$ npm i nodemon -D

 

Git repository 초기화

$ git init .

 

Git ignore 파일 설정

$ touch .gitignore
/node_modules

 

Babel 설치

$ npm i @babel/core @babel/cli @babel/node @babel/preset-env -D

 

nodemon.json 설정

{
    "exec" : "babel-node src/server.js"
}

 

babel.config.json 설정

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

 

package.json 설정

scripts key 값에 dev 설정값을 입력해준다. nodemon.json을 참고해서 코드를 실행 시키는 부분이다. package.json에서 nodemon을 실행하고, nodemon에서는 babel을 presets으로 설정해서 실행.

{
  "name": "01_zoom",
  "version": "1.0.0",
  "description": "Zoom Clone + WebRTC + Websockets + Node.js",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "scripts":{
    "dev" : "nodemon"
  },
  "devDependencies": {
    "@babel/cli": "^7.17.6",
    "@babel/core": "^7.17.9",
    "@babel/node": "^7.16.8",
    "@babel/preset-env": "^7.16.11",
    "nodemon": "^2.0.15"
  }
}

 

Express Server 설치

$ npm i express

 

pub 설치

$ npm i pug

 

server.js 설정

Express 모듈을 import하고 app을 만들어준다. 3000 port 에서 서버가 실행된다.

import express from "express";

const app = express();

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

app.listen(3000, handleListen);

 

서버 실행

$ npm run dev

 

 

 

 

 

 

 

 

 

 

서버 재구성


view engine으로 pug를 설정해주고, views에 home.pug가 포함된 views 디렉토리를 설정해줌. request, resonse에 home.pug 사용하도록 라우터를 구성해줌. template를 사용하기 위해 static 설정을 public으로 해줌.

import express from "express";

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

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

app.listen(3000, handleListen);

 

nodemon 재설정

public 아래의 소스들이 수정될 때 서버가 재시작 되지 않는다. 

{
    "ignore" : ["src/public/*"],
    "exec" : "babel-node src/server.js"
}

 

mvp css 설정

 

 

MVP.css - Minimalist stylesheet for HTML elements

A decent MVP in no time More building and less designing with "set and forget" styling. "Uber for X" brainstorming session

andybrewer.github.io

 

<link rel="stylesheet" href="https://unpkg.com/mvp.css">

 

 

 

 

 

 

백엔드 / 프론트 엔드 구성

src/server.js는 백엔드서버 역할을 하게 된다. 즉, 사용자가 코드를 열어볼 수는 없음. public 디렉토리 내의 js, views 등의 static 파일들은 사용자에게 렌더링을 위해서 공개된다. 사용자에게 공개할 소스와 숨길 소스를 구분하는 것은 security issue에서 굉장히 중요한 문제임

 

 

 

실시간 채팅 구현 + WebSocket


WebSocket

http에서 사용자가 서버에 request를 보내면 서버에서는 response를 보낸다. http는 stateless기 때문에 사용자가 누군지 특정이 불가능하다. 그래서 사용하는게 auth관련 token을 생성해서 서버가 응답하고 request에 정보를 태워서 요청해야 한다. 

 

wss(web security socket)은 http와는 완전히 다른 프로토콜이다. 사용자가 WebSocket request를 보내면 서버는 거절하거나 appept 하게 된다. 사용자와 서버간의 서버의 accept가 있으면 연결이 형성된다. 서버와 사용자가 연결되어 있는 상태에서는 서버는 사용자가 누군지 특정이 가능하다. 또한 bi-directional이기 때문에 서버가 먼저 사용자에게 메시지를 보낼 수 있게 된다. 

 

WebSocket 프로토콜을 사용하면 웹 브라우저와 서버간의 통신 뿐만 아니라 백엔드와 백엔드간의 통신이 가능하다. javascript 마을 위한 라이브러리가 아니라 프로토콜 이기 때문에 API를 지원하는 모든 언어에서 사용이 가능하다. 

 

ws : a Node.js WebSocket library

 

 

GitHub - websockets/ws: Simple to use, blazing fast and thoroughly tested WebSocket client and server for Node.js

Simple to use, blazing fast and thoroughly tested WebSocket client and server for Node.js - GitHub - websockets/ws: Simple to use, blazing fast and thoroughly tested WebSocket client and server for...

github.com

 

ws 설치

$ npm i ws

 

ws 서버 생성

http 서버를 생성하고, 그 위에 ws 서버를 생성에서 올려준다. http와 ws 프로토콜을 모두 이해하는 서버가 생성된다. static, views, js를 사용하기 위해서 http 서버를 만들고 그 위에 WebSocket 프로토콜을 이해하는 서버를 생성하는 것이다. http 서버와 ws 서버 모두 3000 port를 공유하게 된다. 

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("/", (_, res) => res.render("home"));
app.get("/*", (_, res) => res.redirect("/"));

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

const server = http.createServer(app);

// WebSocket server crate
const wsServer = new WebSocket.Server({ server });

server.listen(3000, handleListen);

 

 

connection 생성

웹 브라우저는 WebSocket()을 가지고 있다. ws 서버를 생성하고 on 메소드를 사용해서 connection을 만들고 socket(연결된 사람의 정보)를 확인할 수 있게 된다. 

 

server.js

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("/", (_, res) => res.render("home"));
app.get("/*", (_, res) => res.redirect("/"));

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

const server = http.createServer(app);

// WebSocket server crate
const wsServer = new WebSocket.Server({ server });

function handleConnection(socket){
    console.log(socket);
}

wsServer.on("connection", handleConnection);


server.listen(3000, handleListen);

 

app.js

const socket = new WebSocket("http://localhost:3000");

 

이제 http://localhost:3000 으로 접속하면 에러가 발생한다.

 

 

URL의 프로토콜은 ws로 보내야 WebSocket()이 생성된다는 것이다.

 

ws 프로토콜을 사용한 WebSocket을 사용하면 정상적으로 브라우저와 서버간의 Socket 정보를 얻어올 수가 있다. socket은 연결된 상대방을 의미하기 때문에, server.js에서의 socket은 웹 브라우저를 의미하고, app.js에서의 socket은 서버를 의미한다.

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

 

 

 

 

 

 

소켓정보

<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)]: []
    },
    _events: [Object: null prototype] {
      conclude: [Function: receiverOnConclude],
      drain: [Function: receiverOnDrain],
      error: [Function: receiverOnError],
      message: [Function: receiverOnMessage],
      ping: [Function: receiverOnPing],
      pong: [Function: receiverOnPong]
    },
    _eventsCount: 6,
    _maxListeners: undefined,
    _binaryType: 'nodebuffer',
    _extensions: {},
    _isServer: true,
    _maxPayload: 104857600,
    _skipUTF8Validation: false,
    _bufferedBytes: 0,
    _buffers: [],
    _compressed: false,
    _payloadLength: 0,
    _mask: undefined,
    _fragmented: 0,
    _masked: false,
    _fin: false,
    _opcode: 0,
    _totalPayloadLength: 0,
    _messageLength: 0,
    _fragments: [],
    _state: 0,
    _loop: false,
    [Symbol(kCapture)]: false,
    [Symbol(websocket)]: [Circular *1]
  },
  _sender: Sender {
    _extensions: {},
    _socket: Socket {
      connecting: false,
      _hadError: false,
      _parent: null,
      _host: null,
      _readableState: [ReadableState],
      _events: [Object: null prototype],
      _eventsCount: 4,
      _maxListeners: undefined,
      _writableState: [WritableState],
      allowHalfOpen: true,
      _sockname: null,
      _pendingData: null,
      _pendingEncoding: '',
      server: [Server],
      _server: [Server],
      parser: null,
      on: [Function (anonymous)],
      addListener: [Function (anonymous)],
      prependListener: [Function: prependListener],
      setEncoding: [Function: socketSetEncoding],
      _paused: false,
      timeout: 0,
      [Symbol(async_id_symbol)]: 97,
      [Symbol(kHandle)]: [TCP],
      [Symbol(kSetNoDelay)]: true,
      [Symbol(lastWriteQueueSize)]: 0,
      [Symbol(timeout)]: null,
      [Symbol(kBuffer)]: null,
      [Symbol(kBufferCb)]: null,
      [Symbol(kBufferGen)]: null,
      [Symbol(kCapture)]: false,
      [Symbol(kBytesRead)]: 0,
      [Symbol(kBytesWritten)]: 0,
      [Symbol(RequestTimeout)]: undefined,
      [Symbol(websocket)]: [Circular *1]
    },
    _firstFragment: true,
    _compress: false,
    _bufferedBytes: 0,
    _deflating: false,
    _queue: []
  },
  _socket: <ref *2> Socket {
    connecting: false,
    _hadError: false,
    _parent: null,
    _host: null,
    _readableState: ReadableState {
      objectMode: false,
      highWaterMark: 16384,
      buffer: BufferList { head: null, tail: null, length: 0 },
      length: 0,
      pipes: [],
      flowing: true,
      ended: false,
      endEmitted: false,
      reading: true,
      constructed: true,
      sync: false,
      needReadable: true,
      emittedReadable: false,
      readableListening: false,
      resumeScheduled: true,
      errorEmitted: false,
      emitClose: false,
      autoDestroy: true,
      destroyed: false,
      errored: null,
      closed: false,
      closeEmitted: false,
      defaultEncoding: 'utf8',
      awaitDrainWriters: null,
      multiAwaitDrain: false,
      readingMore: false,
      dataEmitted: false,
      decoder: null,
      encoding: null,
      [Symbol(kPaused)]: false
    },
    _events: [Object: null prototype] {
      end: [Array],
      close: [Function: socketOnClose],
      data: [Function: socketOnData],
      error: [Function: socketOnError]
    },
    _eventsCount: 4,
    _maxListeners: undefined,
    _writableState: WritableState {
      objectMode: false,
      highWaterMark: 16384,
      finalCalled: false,
      needDrain: false,
      ending: false,
      ended: false,
      finished: false,
      destroyed: false,
      decodeStrings: false,
      defaultEncoding: 'utf8',
      length: 0,
      writing: false,
      corked: 0,
      sync: false,
      bufferProcessing: false,
      onwrite: [Function: bound onwrite],
      writecb: null,
      writelen: 0,
      afterWriteTickInfo: [Object],
      buffered: [],
      bufferedIndex: 0,
      allBuffers: true,
      allNoop: true,
      pendingcb: 1,
      constructed: true,
      prefinished: false,
      errorEmitted: false,
      emitClose: false,
      autoDestroy: true,
      errored: null,
      closed: false,
      closeEmitted: false,
      [Symbol(kOnFinished)]: []
    },
    allowHalfOpen: true,
    _sockname: null,
    _pendingData: null,
    _pendingEncoding: '',
    server: Server {
      maxHeaderSize: undefined,
      insecureHTTPParser: undefined,
      _events: [Object: null prototype],
      _eventsCount: 5,
      _maxListeners: undefined,
      _connections: 3,
      _handle: [TCP],
      _usingWorkers: false,
      _workers: [],
      _unref: false,
      allowHalfOpen: true,
      pauseOnConnect: false,
      httpAllowHalfOpen: false,
      timeout: 0,
      keepAliveTimeout: 5000,
      maxHeadersCount: null,
      maxRequestsPerSocket: 0,
      headersTimeout: 60000,
      requestTimeout: 0,
      _connectionKey: '6::::3000',
      [Symbol(IncomingMessage)]: [Function: IncomingMessage],
      [Symbol(ServerResponse)]: [Function: ServerResponse],
      [Symbol(kCapture)]: false,
      [Symbol(async_id_symbol)]: 8
    },
    _server: Server {
      maxHeaderSize: undefined,
      insecureHTTPParser: undefined,
      _events: [Object: null prototype],
      _eventsCount: 5,
      _maxListeners: undefined,
      _connections: 3,
      _handle: [TCP],
      _usingWorkers: false,
      _workers: [],
      _unref: false,
      allowHalfOpen: true,
      pauseOnConnect: false,
      httpAllowHalfOpen: false,
      timeout: 0,
      keepAliveTimeout: 5000,
      maxHeadersCount: null,
      maxRequestsPerSocket: 0,
      headersTimeout: 60000,
      requestTimeout: 0,
      _connectionKey: '6::::3000',
      [Symbol(IncomingMessage)]: [Function: IncomingMessage],
      [Symbol(ServerResponse)]: [Function: ServerResponse],
      [Symbol(kCapture)]: false,
      [Symbol(async_id_symbol)]: 8
    },
    parser: null,
    on: [Function (anonymous)],
    addListener: [Function (anonymous)],
    prependListener: [Function: prependListener],
    setEncoding: [Function: socketSetEncoding],
    _paused: false,
    timeout: 0,
    [Symbol(async_id_symbol)]: 97,
    [Symbol(kHandle)]: TCP {
      reading: true,
      onconnection: null,
      _consumed: true,
      [Symbol(owner_symbol)]: [Circular *2]
    },
    [Symbol(kSetNoDelay)]: true,
    [Symbol(lastWriteQueueSize)]: 0,
    [Symbol(timeout)]: null,
    [Symbol(kBuffer)]: null,
    [Symbol(kBufferCb)]: null,
    [Symbol(kBufferGen)]: null,
    [Symbol(kCapture)]: false,
    [Symbol(kBytesRead)]: 0,
    [Symbol(kBytesWritten)]: 0,
    [Symbol(RequestTimeout)]: undefined,
    [Symbol(websocket)]: [Circular *1]
  },
  _isServer: true,
  [Symbol(kCapture)]: false
}

 

 

웹 브라우저를 확인해보면 MesageEvent 오브젝트가 정상적으로 연결되어 출력되는 것을 확인할 수 있다.

 

서버와 브라우저간 메시지 전달은 socket.addEventListener() 메소드와 on 메소드를 이용해서 구현이 가능하다.

 

server.js (SERVER)

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("/", (_, res) => res.render("home"));
app.get("/*", (_, res) => res.redirect("/"));

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

const server = http.createServer(app);

// WebSocket server crate
const wsServer = new WebSocket.Server({ server });

wsServer.on("connection", (socket)=>{
	// socket의 메소드
    // Browser에게 Message를 보냄
    socket.send("hello web browser");
    // Browser가 보낸 메세지 출력
    socket.on("message", (message)=>{
        console.log(message);
    })
    // 연결이 끊어질 때 메시지 출력
    socket.on("close", ()=> console.log("Disconnected from the Brower"));
});

server.listen(3000, handleListen);

 

app.js(BROWSER)

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

// Connection이 연결될 때 Event
socket.addEventListener("open", ()=>{
    console.log("Connected to Server");
});

// Message를 받을 때 Event
socket.addEventListener("message", (message)=>{
    console.log("The message got is : ", message.data);
});

// Error가 발생할 때 Event
socket.addEventListener("error", ()=>{
    console.log("Error occured");
});

// Connection이 종료될 때 Event
socket.addEventListener("close", ()=>{
    console.log("Disconnected from server");
});

setTimeout(()=>{
    socket.send("The Message from the Browser");
}, 3000);

 

서버와 브라우저 간 통신은 구성되었지만, 브라우저와 브라우저간 통신을 만들기 위해서는 소켓 정보를 담을 배열이 필요하다. 서버에서는 새로운 소켓이 생성되면 배열에 사용자 정보들을 담아서 1개의 브라우저에서 message를 보내면 배열에 담긴 소켓들에게 모두 message를 전달하게 된다.

 

브라우저 간 통신을 할 때 적합한 데이터 모델을 Json 오브젝트를 사용하는 것이다. JSON은 stringify로 JSON 오브젝트를 String으로 변환해주고, 수신할 때는 JSON.parse() 메소드를 사용해서 String을 JSON 오브젝트로 변환해줘야 한다. 

 

서버에서 데이터를 수신할 때 Blob 오브젝트로 수신한다면 string으로 변환해줘야 제대로 된 데이터값을 확인할 수 있다. 

 

 

 

 

 

 

Server.js

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("/", (_, res) => res.render("home"));
app.get("/*", (_, res) => res.redirect("/"));

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

const server = http.createServer(app);

// WebSocket server crate
const wsServer = new WebSocket.Server({ server });

// 서버에 연결된 소켓들을 담을 배열을 생성해준다.
const sockets = [];

// 새로운 사용자가 연결될 때 마다 소켓을 생성한다.
wsServer.on("connection", (socket)=>{
	// 소켓 배열에 추가해주고,
    sockets.push(socket);
    // 사용자의 nickname을 Anoymous로 초기화해준다.
    socket["nickname"] = "Anonymous";
    // 서버에 연결시 안내 메시지를 서버에 띄워준다.
    console.log("Connected to the Browser");
    // socket의 메소드 , close시 작동
    socket.on("close", ()=> console.log("Disconnected from the Brower"));
    // message를 받을 때 동작한다.
    socket.on("message", (data, isBinary)=>{     
    	// Blob 오브젝트로 받은 데이터를 String으로 변환해준다.
        const message = isBinary ? data : data.toString();
        // 브라우저에서 받은 JSON 오브젝트를 파싱한다.
        const parsedMessage = JSON.parse(message);
        // message와 nickname 로직을 구성한다.
        if(parsedMessage.type === "message"){
        	// message가 수신되면 {Nickname : message} 형태로 연결된 모든 브라우저에 데이터를 전달
            // socket 배열에 담긴 socket들을 순회하면서 message를 보내준다.
            sockets.forEach((singleSocket)=>{
                singleSocket.send(`${socket.nickname} : ${parsedMessage.payload}`);
            })
        }else if(parsedMessage.type === "nickname"){
        	// nickname이 입력되면 저장된 소켓의 nickname 필드값을 변경해준다.
            socket["nickname"] = parsedMessage.payload;
        };
    })
});

server.listen(3000, handleListen);

 

 

화면을 구성하고 Nickname을 입력할 부분과 Message를 입력할 부분을 생성한다. 두개의 form은 각자 nickname과 message id를 가진다.

 

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 Joom Clone Coding
        link(rel="stylesheet", href="https://unpkg.com/mvp.css")
    body 
        header 
            h1 Zoom Clone Coding
            button click
            h2 Project gets started
        main 
            ul
            form#nick
                input(type="text", placeholder="Select Nickname", required)
                button Save
            form#message 
                input(type="text", placeholder="write a message", required)
                button Send
            
            
        script(src="/public/js/app.js")

 

app.js(Browser)

const messageList = document.querySelector("ul");
const messageForm = document.querySelector("#message");
const nickForm = document.querySelector("#nick");
const socket = new WebSocket(`ws://${window.location.host}`);

// JSON 오브젝트 직렬화
function configMessage(type, payload){
    const msg = {type, payload};
    return JSON.stringify(msg);
}

// message를 수신할 때 작동
socket.addEventListener("message", (message)=>{   
	// li Element 생성
    const li = document.createElement("li");
    // li Element에 수신한 message 데이터 삽입
    li.innerText = message.data;
    // ul 태그의 자식 노드로 붙여줌
    messageList.append(li)
});

// messageForm submit 시 작동
// input 태그의 value값을 가져와서 JSON 직렬화 데이터를 서버에 전송함
messageForm.addEventListener("submit", (e)=>{
    e.preventDefault();
    const input = messageForm.querySelector("input");
    socket.send(configMessage("message", input.value));
    input.value="";
})

// nicknameForm submit 시 작동
// input 태그의 value값을 가져와서 JSON 직렬화 데이터를 서버에 전송함
nickForm.addEventListener("submit", (e)=>{
    e.preventDefault();
    const input = nickForm.querySelector("input");
    socket.send(configMessage("nickname", input.value));
    input.value="";
})

// 서버 연결시 작동
socket.addEventListener("open", ()=>{
    console.log("Connected to Server");
});

// error 발생시 작동
socket.addEventListener("error", ()=>{
    console.log("Error occured");
});

socket.addEventListener("close", ()=>{
    console.log("Disconnected from server");
});

 

테스팅

브라우저를 두개 오픈한 다음 동일한 주소(localhost:3000)으로 접속한다. 닉네임을 설정하고 메시지를 보내면 각자 지정한 닉네임으로 브라우저 2개와 서버에서 해당 메시지를 확인할 수 있다. 각 브라우저는 Nickname 데이터와 Message 데이터를 서버로 보내고, 서버에서 socket의 nickname과 message를 설정해서 각 브라우저로 전송하게 되는 것이다.

 

 

 

 

 

 

 

 

 

 

SocketIO


socketIO는 WebSocket의 부가기능이 아니다. WebSocket을 사용하기 위한 framework이다. SocketIO는 real-time과 양방향 메시지 전달 그리고 event를 기반으로 통신하는 프로그램을 만들도록 지원하는 기능을 담고 있다. socketIO는 WebSocket을 베이스로 작동하지만 만약 사용중인 브라우저에서 WebSocket을 지원하지 않는다면 HTTP long-polling 등을 이용해서 계속 구동이 가능하다.

 

 

상당히 많은 카지노 사이트들이 실시간 채팅, 배팅, 환전 기능을 작동시키기 위해서 socketIO를 사용하고 있다. 그만큼 출시된지는 오래되었지만 믿을만한 기능을 제공하고 있다는 반증이다.

 

또한 socketIO의 큰 장점 중 하나는 서버와의 연결이 끊어진다 하더라도 지속적인 연결요청을 자동으로 생성한다는 점이다. socketIO를 사용해서 프론트엔드와 백엔드를 구성한 후 서버를 kill 하면 아래 에러메세지가 2~3초당 한번씩 발생한다. 

 

자동연결을 시도하는 socketIO

 

SocketIO 설치

웹 브라우저는 WebSocket은 설치해서 가지고 있지만 SocketIO는 설치되어 있지 않다. 따라서 백엔드와 프론트엔드 양쪽에 SocketIO를 따로 설치해줘야 한다.

$ npm i socket.io
// views template에 추가
script(src="/socket.io/socket.io.js")

 

 

SocketIO 서버 구성

WebSocket 서버를 구성할 때는 먼저 express로 app(server)를 생성하고, HTTP 서버를 생성해서 그 위에 WebSocket 서버를 올려서 사용한다. SocketIO 서버 또한 이와 유사한 로직으로 생성한다.

 

server.js

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

const app = express();

app.set("view engine", "pug");
app.set("views", __dirname + "/views/");
app.use("/public", express.static(__dirname+"/public"));
app.get("/", (_, res) => res.render("home"));
app.get("/*", (_, res) => res.redirect("/"));


const server = http.createServer(app);

// SocketIO Server create
const socketIOServer = SocketIO(server);

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

 

이제 http://localhost:3000/socket.io/socket.io.js URL로 접속하면 정상적으로 SocketIO가 작동하는 모습을 볼 수 있다. SocketIO에서는 문자열이 가득담긴 페이지를 반환하게 되는데 이는 SocketIO가 WebSocket 뿐만 아니라 다양한 기능을 가지고 있기 때문이다.

 

 

 

 

SocketIO 초기 화면

 

프론트엔드에서는 SocketIO를 설치했을 때 사용가능한 io() function을 연결해준다.

const socket = io();

 

이제 서버에서 socketIO 서버가 연결되었을 때 socket을 출력해보면 WebSocket과는 조금 다른 Socket을 볼 수 있게 된다.

 

 

socket.emit() 메소드 사용

socketIO를 사용하면 message, send() 메소드를 사용할 필요가 없다. 그냥 socket.emit() 메소드를 사용해서 메시지를 보낼 수 있다. 먼저 프론트엔드에서 Room 이름을 적어줄 폼을 작성하고, socket.emit() 메소드를 사용해서 서버로 보낼 로직을 짜준다. 또한 socketIO를 사용하면 JSON.stringify(), JSON.parse() 메소드를 사용하지 않아도 서버와 프론트 엔드 간 javascript object를 데이터로 주고 받을 수 있게 된다. 

 

views Template

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 Joom Clone Coding
        link(rel="stylesheet", href="https://unpkg.com/mvp.css")
    body 
        header 
            h1 Zoom Clone Project
        main 
            center
                div#introRoom
                    form
                        input(placeholder="room name", required, type="text")
                        button Enter Room
            
        script(src="/socket.io/socket.io.js")
        script(src="/public/js/app.js")

 

Frontend(app.js)

프론트엔드에서 서버로 메시지를 보낼 때 socket.emit() 메소드를 사용한다. 사용자 정의 이벤트 이름을 지정할 수 있고, arguments로는 데이터, function을 보낼 수 있다.

 

socket.emit("사용자 정의 이벤트 이름", 데이터, function);
// 서버의 socketIO를 자동으로 찾아줌.
const socket = io();

const introRoom = document.getElementById("introRoom");
const form = introRoom.querySelector("form");

form.addEventListener("submit", (e)=>{
    e.preventDefault();
    const input = form.querySelector("input");
    socket.emit("room", {payload : input.value}, ()=>{
        console.log("Server is connected to Browser");
    });
    input.value="";
})

 

Backend(Server.js)

프론트엔드에서 socket.emit() 메소드로 보낸 함수를 받을 때는 on() 메소드를 사용한다. 익명함수에서 데이터와 function을 매개변수로 받아 로직을 구성할 수 있다.

import express from "express";
import http from "http";
// import WebSocket from "ws";
import SocketIO from "socket.io";

const app = express();

app.set("view engine", "pug");
app.set("views", __dirname + "/views/");
app.use("/public", express.static(__dirname+"/public"));
app.get("/", (_, res) => res.render("home"));
app.get("/*", (_, res) => res.redirect("/"));


// app.listen(3000, handleListen);

const server = http.createServer(app);

// SocketIO Server create
const socketIOServer = SocketIO(server);

socketIOServer.on("connection", (socket)=>{
    socket.on("room", (msg, done)=>{
        console.log(msg);
        setTimeout(()=>{
            done();
        }, 1000)
    })

})

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

 

 

socketIO의 emit() 메소드는 arguments를 무한대로 받아올 수 있는 장점이 있다. 또한 어떤 타입의 데이터도 argument로 받을 수 있다. 하지만 주의해야 할 점은 function은 항상 가장 마지막에 위치해야 한다는 점이다. 프론트엔드에서 emit() 메소드의 마지막 argument로 backendDone() 메소드를 보낸다. 매개변수 msg를 받아서 출력하는 기능을 수행한다. 

 

백엔드에서는 검증되지 않은 메소드를 실행할 수 없다. 보안상 DB를 건드리는 메소드 일 수 있기 때문이다. 따라서 socket.on() 메소드가 받은 done() 메소드가 실행되는 지점은 백엔드가 아닌 프론트엔드에서 실행된다. "Backend is done" 데이터는 브라우저 콘솔창에 출력되는 것을 확인할 수 있다.

 

Frontend(app.js)

function backendDone(msg){
    console.log("Backend is done", msg);
}

form.addEventListener("submit", (e)=>{
    e.preventDefault();
    const input = form.querySelector("input");
    // function은 가장 마지막 argument로 던져줘야 한다.
    // 매개변수로 전달된 function은 Frontend에서 실행된다.
    // Backend에서 신뢰할 수 없는 function을 실행해서는 안됨.
    socket.emit("room", input.value,backendDone);
    input.value="";
})

 

Backend(server.js)

socketIOServer.on("connection", (socket)=>{
    socket.on("room", (roomName,done)=>{
        console.log(roomName);
        setTimeout(()=>{
            done("Message from the backend");
        }, 3000);
    });
})

 

 

 

Room 생성

프론트엔드에서 socket.emit("room") 메소드를 사용해서 room을 생성해준다. 이제 서버에서 socket.join() 메소드로 room에 접속하게 되고, room에 접속한 모든 유저들에게 동일한 메시지를 던져줄 수 있게 된다.

 

view template

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 Joom Clone Coding
        link(rel="stylesheet", href="https://unpkg.com/mvp.css")
    body 
        header 
            h1 Zoom Clone Project
        main 
            center
                div#introRoom
                    h3
                    form
                        center
                            input(placeholder="room name", required, type="text")
                            button Enter Room
            center 
                div#room
                    h3#title
                    ul 
                    form
                        center
                            input(placeholder="message", required, type="text")
                            button Send


        script(src="/socket.io/socket.io.js")
        script(src="/public/js/app.js")

 

Frontent(app.js)

// 서버의 socketIO를 자동으로 찾아줌.
const socket = io();

const introRoom = document.getElementById("introRoom");
const form = introRoom.querySelector("form");
const room = document.getElementById("room");
room.hidden = true;
var roomName;

function enterRoom(){
    introRoom.hidden = true;
    room.hidden = false;
    const title = document.getElementById("title");
    title.innerText = `Room Title : ${roomName}`;
}

function appendMessage(msg){
    const ul = room.querySelector("ul");
    const li = document.createElement("li");
    li.innerText = msg;
    ul.appendChild(li);
}

form.addEventListener("submit", (e)=>{
    e.preventDefault();
    const input = form.querySelector("input");
    // function은 가장 마지막 argument로 던져줘야 한다.
    // 매개변수로 전달된 function은 Frontend에서 실행된다.
    // Backend에서 신뢰할 수 없는 function을 실행해서는 안됨.
    socket.emit("room", input.value, enterRoom);
    roomName = input.value;
    input.value="";
})

socket.on("welcome", ()=>{
    appendMessage("Someone Joined");
})

 

Backend(server.js)

import express from "express";
import http from "http";
// import WebSocket from "ws";
import SocketIO from "socket.io";

const app = express();

app.set("view engine", "pug");
app.set("views", __dirname + "/views/");
app.use("/public", express.static(__dirname+"/public"));
app.get("/", (_, res) => res.render("home"));
app.get("/*", (_, res) => res.redirect("/"));


// app.listen(3000, handleListen);

const server = http.createServer(app);

// SocketIO Server create
const socketIOServer = SocketIO(server);

socketIOServer.on("connection", (socket)=>{
    socket.on("room", (roomName, enterRoom)=>{
        socket.join(roomName);
        setTimeout(()=>{
            enterRoom();
        }, 500);
        socket.to(roomName).emit("welcome");
    });
})



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

 

브라우저를 4개를 실행했고, 동일하게 localhost:3000으로 접속했다. 123이라는 Room을 새로 생성하고 Join을 하면 동일한 메시지가 전송되는 것을 확인할 수 있다. 프론트엔드에서 form이 submit 될 때의 이벤트를 활용해서 Room을 생성하고, 서버에서 생성된 Room에 Join을 한 후 접속해 있는 유저들에게 일괄적으로 메시지를 전달할 수 있다. 

 

 

 

 

브라우저 톡 기능 구현하기

프론트엔드에서 Room에 접속 한 후 메시지 전송 버튼을 클릭하면 form 태그 아래의 input에 담긴 value 값과, button의 submit이 작동해서 eventListener가 작동한다. room 페이지의 submit이 작동하면 socket.emit("사용자 정의 이벤트", 메시지 텍스트, Room name, function) 코드를 통해서 해당 Room name에 메시지를 전달하는 event를 서버로 보낸다. 

 

서버에서는 프론트에서받은 이벤트를 socket.on() 메소드로 받아서 socket.to().emit() 메소드로 모든 Room에 참여한 사용자들에게 특정 유저한테서 받은 메시지를 전송하게 된다. 다시 프론트의 다른 유저들은 서버에서 보낸 socket.emit() 메소드를 받아서 메시지를 처리하게 된다.

 

view Template

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 Joom Clone Coding
        link(rel="stylesheet", href="https://unpkg.com/mvp.css")
    body 
        header 
            h1 Zoom Clone Project
        main 
            center
                div#introRoom
                    h3
                    form
                        center
                            input(placeholder="room name", required, type="text")
                            button Enter Room
            //- In the Room
            center 
                div#room
                    h3#title
                    ul 
                    form
                        center
                            input(placeholder="message", required, type="text")#message
                            button Send


        script(src="/socket.io/socket.io.js")
        script(src="/public/js/app.js")

 

Frontend(app.js)

 

// 서버의 socketIO를 자동으로 찾아줌.
const socket = io();

const introRoom = document.getElementById("introRoom");
const form = introRoom.querySelector("form");
const room = document.getElementById("room");

room.hidden = true;
var roomName;

function enterRoom(){
    introRoom.hidden = true;
    room.hidden = false;
    const title = document.getElementById("title");
    title.innerText = `Room Title : ${roomName}`;
}

function appendMessage(msg){
    const ul = room.querySelector("ul");
    const li = document.createElement("li");
    li.innerText = msg;
    ul.appendChild(li);
}

form.addEventListener("submit", (e)=>{
    e.preventDefault();
    const input = form.querySelector("input");
    // function은 가장 마지막 argument로 던져줘야 한다.
    // 매개변수로 전달된 function은 Frontend에서 실행된다.
    // Backend에서 신뢰할 수 없는 function을 실행해서는 안됨.
    socket.emit("room", input.value, enterRoom);
    roomName = input.value;
    input.value="";
})

room.addEventListener("submit", (e)=>{
    e.preventDefault();
    const message = room.querySelector("#message");
    const messageValue = message.value;
    socket.emit("message", messageValue, roomName, ()=>{
        appendMessage(`You : ${messageValue}`);
    });
})

socket.on("welcome", ()=>{
    appendMessage("Someone Joined");
})

socket.on("bye", ()=>{
    appendMessage("Someone close connection");
})

socket.on("messageFromServer", (e)=>{
    appendMessage(`Others : ${e}`)
})

 

Backend(server.js)

import express from "express";
import http from "http";
// import WebSocket from "ws";
import SocketIO from "socket.io";

const app = express();

app.set("view engine", "pug");
app.set("views", __dirname + "/views/");
app.use("/public", express.static(__dirname+"/public"));
app.get("/", (_, res) => res.render("home"));
app.get("/*", (_, res) => res.redirect("/"));


// app.listen(3000, handleListen);

const server = http.createServer(app);

// SocketIO Server create
const socketIOServer = SocketIO(server);

socketIOServer.on("connection", (socket)=>{
    socket.on("room", (roomName, enterRoom)=>{
        socket.join(roomName);
        setTimeout(()=>{
            enterRoom();
        }, 500);
        socket.to(roomName).emit("welcome");
    });
    socket.on("disconnecting", ()=>{
        socket.rooms.forEach((room) => socket.to(room).emit("bye"));
    })
    socket.on("message", (msg, roomName, done)=>{
        socket.to(roomName).emit("messageFromServer", msg);
        done();
    })
})



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

 

 

 

Room 내에서 누가 메시지를 보냈는지 확인하기 위해서 nickname을 붙여줘야 한다. 프론트엔드에서 nickname을 설정하고, 서버에 socket event를 날릴 때 nickname input 값을 함께 보내준다. 서버에서는 소켓 내의 "nickname" 필드를 새로 생성해서 프론트에서 넘겨받은 nickname 정보를 담아 다른 유저들에게 emit() 메소드를 보내준다.

 

 

 

 

view template

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 Joom Clone Coding
        link(rel="stylesheet", href="https://unpkg.com/mvp.css")
    body 
        header 
            h1 Zoom Clone Project
        main 
            center
                div#introRoom
                    h3
                    //- enterRoomForm
                    form
                        center
                            input(placeholder="room name", required, type="text")
                            button Enter Room
            //- In the Room
            center 
                //- room
                div#room
                    h3#title
                    ul 
                    form#nickname
                        center
                            input(placeholder="Enter you nickname", required, type="text")
                            button Save
                    form#message
                        center
                            input(placeholder="message", required, type="text")
                            button Send


        script(src="/socket.io/socket.io.js")
        script(src="/public/js/app.js")

 

 

Frontend(app.js)

// 서버의 socketIO를 자동으로 찾아줌.
const socket = io();

const introRoom = document.getElementById("introRoom");
const enterRoomForm = introRoom.querySelector("form");
const room = document.getElementById("room");
const nicknameSaveBtn = room.querySelector("#nickname button");

room.hidden = true;
var roomName;

function enterRoom(){
    introRoom.hidden = true;
    room.hidden = false;
    const title = document.getElementById("title");
    title.innerText = `Room Title : ${roomName}`;
    const nicknameForm = room.querySelector("#nickname");
    const messageForm = room.querySelector("#message");
    nicknameForm.addEventListener("submit", nicknameEventHandler);
    messageForm.addEventListener("submit", messageEventHandler);
}

function appendMessage(msg){
    const ul = room.querySelector("ul");
    const li = document.createElement("li");
    li.innerText = msg;
    ul.appendChild(li);
}

function messageEventHandler(e){
    e.preventDefault();
    const input = room.querySelector("#message input");
    const value = input.value;
    socket.emit("message", value, roomName, ()=>{
        appendMessage(`You : ${value}`);
    });
    input.value = "";
}

function nicknameEventHandler(e){
    e.preventDefault();
    const input = room.querySelector("#nickname input");
    socket.emit("nickname", input.value);
}

enterRoomForm.addEventListener("submit", (e)=>{
    e.preventDefault();
    const input = enterRoomForm.querySelector("input");
    // function은 가장 마지막 argument로 던져줘야 한다.
    // 매개변수로 전달된 function은 Frontend에서 실행된다.
    // Backend에서 신뢰할 수 없는 function을 실행해서는 안됨.
    socket.emit("room", input.value, enterRoom);
    roomName = input.value;
    input.value="";
})

socket.on("welcome", (e)=>{
    appendMessage(`${e} Joined`);
})

socket.on("bye", (e)=>{
    appendMessage(`${e} close connection`);
})

socket.on("messageFromServer", (msg, nickname)=>{
    appendMessage(`${nickname} : ${msg}`)
})

 

Backend(server.js)

 

import express from "express";
import http from "http";
// import WebSocket from "ws";
import SocketIO from "socket.io";

const app = express();

app.set("view engine", "pug");
app.set("views", __dirname + "/views/");
app.use("/public", express.static(__dirname+"/public"));
app.get("/", (_, res) => res.render("home"));
app.get("/*", (_, res) => res.redirect("/"));


// app.listen(3000, handleListen);

const server = http.createServer(app);

// SocketIO Server create
const socketIOServer = SocketIO(server);

socketIOServer.on("connection", (socket)=>{
    socket.on("room", (roomName, enterRoom)=>{
        socket["nickname"] = "Anonymous";
        socket.join(roomName);
        enterRoom();
        socket.to(roomName).emit("welcome", socket["nickname"]);
    });
    socket.on("disconnecting", ()=>{
        socket.rooms.forEach((room) => socket.to(room).emit("bye", socket["nickname"]));
    })
    socket.on("message", (msg, roomName, done)=>{
        socket.to(roomName).emit("messageFromServer", msg, socket["nickname"]);
        done();
    })
    socket.on("nickname", (nickname)=>{
        socket["nickname"]=nickname;
    });
})



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

 

 

SocketIO Adapter 사용하기

socketIO는 rooms의 아이디를 따로 관리하게 되어 있다. 생성된 socketIO의 서버에 생성된 룸들은 Adapter가 관리하고 있으며, 우리는 룸의 정보를 Adapter를 이용해서 쉽게 가져올 수 있다.

 

socket.onAny((event)=>{
    console.log(socketIOServer.sockets.adapter);
})

 

프론트에서 새로운 Room을 만들어서 서버에서 Adapter를 출력해보면 Room에 대한 자세한 정보를 얻어올 수 있다. 3개의 소켓과 3개의 Room이 생성되어 있다. 

<ref *2> Adapter {
  _events: [Object: null prototype] {},
  _eventsCount: 0,
  _maxListeners: undefined,
  nsp: <ref *1> Namespace {
    _events: [Object: null prototype] { connection: [Function (anonymous)] },
    _eventsCount: 1,
    _maxListeners: undefined,
    sockets: Map(3) {
      'lgIl9CvBBGUjgCQJAAAB' => [Socket],
      'U7bTyBP8pUWVzcbCAAAD' => [Socket],
      'vjt5zA2gIlEbFV-SAAAF' => [Socket]
    },
    _fns: [],
    _ids: 0,
    server: Server {
      _events: [Object: null prototype] {},
      _eventsCount: 0,
      _maxListeners: undefined,
      _nsps: [Map],
      parentNsps: Map(0) {},
      _path: '/socket.io',
      clientPathRegex: /^\/socket\.io\/socket\.io(\.msgpack|\.esm)?(\.min)?\.js(\.map)?(?:\?|$)/,
      _connectTimeout: 45000,
      _serveClient: true,
      _parser: [Object],
      encoder: Encoder {},
      _adapter: [class Adapter extends EventEmitter],
      sockets: [Circular *1],
      opts: {},
      eio: [Server],
      httpServer: [Server],
      engine: [Server],
      [Symbol(kCapture)]: false
    },
    name: '/',
    adapter: [Circular *2],
    [Symbol(kCapture)]: false
  },
  rooms: Map(3) {
    'lgIl9CvBBGUjgCQJAAAB' => Set(1) { 'lgIl9CvBBGUjgCQJAAAB' },
    'U7bTyBP8pUWVzcbCAAAD' => Set(1) { 'U7bTyBP8pUWVzcbCAAAD' },
    'vjt5zA2gIlEbFV-SAAAF' => Set(1) { 'vjt5zA2gIlEbFV-SAAAF' }
  },
  sids: Map(3) {
    'lgIl9CvBBGUjgCQJAAAB' => Set(1) { 'lgIl9CvBBGUjgCQJAAAB' },
    'U7bTyBP8pUWVzcbCAAAD' => Set(1) { 'U7bTyBP8pUWVzcbCAAAD' },
    'vjt5zA2gIlEbFV-SAAAF' => Set(1) { 'vjt5zA2gIlEbFV-SAAAF' }
  },
  encoder: Encoder {},
  [Symbol(kCapture)]: false
}

 

socket과 Room에 대한 정보를 가져오기 위해서는 Maps를 사용해줘야 한다. socketIOServer.sockets.adapter.sids와 socketIOServer.sockets.adapter.rooms에 해당 정보가 들어가 있다. 룸과 소켓의 성보를 publicRooms배열을 생성해서 순회를 하면서 publicRooms를 찾아올 수 있다.

// app.listen(3000, handleListen);
const server = http.createServer(app);

// SocketIO Server create
const socketIOServer = SocketIO(server);

// public room
function publicRooms(){
    const {
        sockets : {
            adapter: {sids, rooms}
        }
    } = socketIOServer;
    const publicRooms = [];
    rooms.forEach((_, key)=>{
        if(sids.get(key) === undefined){
            publicRooms.push(key);
        }
    })
    return publicRooms;
}

 

브로드캐스팅 기능

서버에서 모든 socket에게 이벤트 전달이 가능하다. 서버에 연결된 모든 Room name을 브로드캐스팅 해주면 룸에 연결된 모든 룸을 모니터링 할 수 있다.

 

// SocketIO Server create
const socketIOServer = SocketIO(server);

// Broadcasting
socketIOServer.sockets.emit("room_change", publicRooms());

 

Room에 입장한 socket counting

sockets의 rooms에는 room에 입장한 소켓의 정보가 나온다. 소켓들을 카운팅해서 최초 Room에 접속할 경우 소켓을 카운팅해서 보여주고, 만약 Room을 나갈 경우에도 룸에 접속한 소켓의 총 갯수를 확인할 수 있다.

 

먼저 소켓의 갯수를 counting하는 function을 만들어준다.

// Count Rooms which is made by the user in the Application.
function countRoom(roomName){
    return socketIOServer.sockets.adapter.rooms.get(roomName)?.size;
}

 

Room에 입장하거나 퇴장할 때 소켓의 갯수를 세는 함수를 인자로 태워 보낸다.

socket.on("room", (roomName, done)=>{
    // 소켓의 갯수를 세어 Event를 보낸다.
    socket.to(roomName).emit("welcome", socket["nickname"], countRoom(roomName));
});
socket.on("disconnecting", ()=>{
	// Room을 떠나기 전 소켓의 갯수를 세어 Event를 보낸다.
    socket.rooms.forEach((room) => socket.to(room).emit("bye", socket["nickname"], countRoom(room)-1));
});

 

프론트엔드에서는 welcome, bye 이벤트를 받아서 넘겨받은 소켓의 갯수를 타이틀 우측에 표시해준다. 실제 브라우저에서 2개 이상 소켓을 한개의 Room에 연결하고 테스팅을 하면 입장/퇴장 시 Room에 접속한 소켓의 갯수를 모니터링할 수 있게 된다.

socket.on("welcome", (e, count)=>{
    const title = document.getElementById("title");
    title.innerText = `Room Title : ${roomName} (${count})`;
    appendMessage(`${e} Joined`);
})

socket.on("bye", (e, count)=>{
    const title = document.getElementById("title");
    title.innerText = `Room Title : ${roomName} (${count})`;
    appendMessage(`${e} close connection`);
})

 

 

Admin UI pannel 이용하기

Room과 Socket에 대한 정보를 GUI로 확인할 수 있는 컨트롤 패널을 Socket.IO에서 무료로 제공하고 있다. 

설치

$ npm i @socket.io/admin-ui

 

Server 재구성

서버를 재구성하고, cors issue를 제거한 후 instrument를 적용해주고, 아래 URL로 접속하면 웹소켓 서버를 모니터링할 수 있는 admin pannel를 돌릴 수 있다.

const { createServer } = require("http");
const { Server } = require("socket.io");
const { instrument } = require("@socket.io/admin-ui");

const httpServer = createServer();

const io = new Server(httpServer, {
  cors: {
    origin: ["https://admin.socket.io"],
    credentials: true
  }
});

instrument(io, {
  auth: false
});

httpServer.listen(3000);

 

 

 

Socket.IO Admin UI

 

admin.socket.io

 

 

 

 

더 많은 콘텐츠

 

JavaScript 기초 문법 #5 let, const 변수

let 변수 var 키워드의 단점을 보완하기 위해 ES6에서 적용된 키워드가 let, const 변수 키워드다. let 키워드로 선언한 변수는 중복 선언시 systax error를 반환한다. let 벼수는 모든 코드블록을 지역 스

incomeplus.tistory.com

 

반응형

댓글