WebSocket

임철종·2022년 8월 11일
1
post-thumbnail

실시간으로 작동하는 채팅 어플리케이션을 구현해보자.

Setting up 🛠

1. Server setup

백엔드는 Express.js 를 사용하여 Node.js 서버로 만들 것입니다.

Express는 Node.js의 웹 프레임워크입니다.

템플릿 엔진은 Pug.js를 사용할 것입니다.

템플릿 엔진이란 SSR(Server Side Rendering)에서 HTML을 Javascript와 함께 작성하여 렌더링 할 수 있도록 돕는 프로그램이다. (pug는 express의 뷰 엔진)

1-1 서버 만들기

import express from 'express'
const app = express()
app.listen(3000)

express를 사용하여 서버를 만들어줍니다.
localhost:3000으로 이동시 Not found가 아닌 Cannot GET / 페이지가 나오면 성공!

2. Frontend setup

2-1 뷰 엔진 설정하기

const app = express()
app.set('view engine','pug')
app.set('views', __dirname+'/views')
app.get('/',(_,res)=>res.render('home'))

만들어진 express 앱에 view engine과 경로 설정을 해줍니다.
이제 localhost:3000으로 이동시 home.pug가 렌더링 될 것입니다.

2-2 static 파일 설정하기

app.use('/public', express.static(__dirname + '/public'))

static(정적) 파일이란 HTML에서 사용되는 js, css, image 파일 등을 가리킵니다.
서버에서 정적 파일을 다루기 위해서는 express의 static() 메소드를 이용합니다.

express.static() 메소드를 사용하여 /public/app.js 파일을 렌더링 될 home.pug에 연결합니다.

2-3 Pug HTML 작성 (styling with MVP.css)

...
        title ChitChat
        link(rel="stylesheet", href="https://unpkg.com/mvp.css")
    body
        header
            h1 ChitChat
        main
            h2 Welcome to ChitChat
            
            form#message
                input(type='text' placeholder='Write a message' required)
                button Send
        script(src="/public/js/app.js")

MVP.css로 미니멀한 스타일링 적용


실시간 서비스는 어떻게 구현 할 수 있을까? 🤔

자바스크립트만을 사용하여 P2P(Peer to Peer) 통신, WebSocket을 활용하여 채팅을 구현할 수 있습니다.

HTTP? WebSocket?

HTTP

HTTP(HyperText Transfer Protocol)는 하이퍼텍스트를 빠르게 교환하기 위한 프로토콜의 일종으로, 서버와 클라이언트의 사이에서 어떻게 메시지를 교환할지를 정해 놓은 규칙이다.
요청(Request)응답(Response)으로 구성되어 있으며, 일반적으로 80번 포트를 사용한다.
예를 들면 '클라이언트가 웹 페이지에서 링크가 걸려있는 텍스트를 클릭(요청)하면 링크를 타고 새로운 페이지로 넘어간다(응답)'. 따라서 우리가 사용하는 웹 브라우저에서 인터넷 주소 맨 앞에 들어가는 http://가 바로 이 프로토콜을 사용해서 정보를 교환하겠다는 표시인 것이다.


WebSocket

웹소켓은 TCP통신 방식으로 서버와 클라이언트 사이에 데이터를 주고 받을 수 있는 기술이다.
웹소켓은 서버와 클라이언트 사이에서 데이터를 주고 받을 수 있는데, 이는 다시말해 서로 메신저를 이용해 1:1 채팅을 한다고 볼수 있다.
HTTP REST 메서드인 POST 보다 빠르다.
이런 장점 때문에 여러 API 또는 여러 게임 멀티플레이에도 사용된다.

TCP(Transmission Control Protocol)
컴퓨터가 다른 컴퓨터와 데이터 통신을 하기 위한 규약(프로토콜)의 일종이다.


중요 포인트💡

  • HTTP는 Stateless하므로 클라이언트는 요청만, 서버는 응답만 할 수 있습니다.
  • WebSocket은 한번 연결이 성립되면, 양방향 연결이 되어 서로 데이터를 주고 받을 수 있습니다.

WebSockets in NodeJS

Node.js로 WebSocket 서버를 만들기 위해 ws라는 패키지를 사용할 것입니다.

ws는 사용하기 쉽고, 빠르고, 철저하게 테스트된 WebSocket 클라이언트 및 서버 구현입니다.

HTTP 표준이 되는 규약을 따라서 만든, WebSocket protocol을 실행하는 패키지인 것이죠.

채팅방은 WebSocket protocol에 포함되어 있지 않은 추가적인 기능이므로 ws자체에 포함되어 있지 않습니다.

npm i ws로 ws를 설치하고 만들어 둔 express 서버에 ws 기능을 추가하겠습니다.

server.js

HTTP와 WebSocket(서로 다른 프로토콜)을 같은 서버에서 사용하기

view engine과 static files, redirection등을 사용하기 위해 http 를 사용할 수 있는 서버가 필요하고
실시간 채팅을 위해 WebSocket을 사용할 수 있는 서버가 필요하므로 하나의 서버에서 둘 다를 이해할 수 있도록 만드는 것입니다.

  • 기존에 express 서버를 시작하기 위해 사용했던 부분을 지웁니다.
const app = express();
app.listen(3000)
  • 대신 node.js에 내장되어있는 http 패키지를 사용하여 server 객체를 생성합니다.
const server = http.createServer(app);
  • 이제 서버에 접근할 수 있으니 WebSocket 서버를 만들어 주겠습니다.
    WebSocketServer에 생성한 http 서버 객체를 전달하여 http 서버 위에 WebSocket 서버를 생성합니다.
import {WebSocketServer} from "ws";
const wss = new WebSocketServer({ server })
  • 이제 app.listen(3000)을 했듯이 server 객체의 listen() 메소드를 사용합니다.
server.listen(3000, () => console.log('✅ Listening on http://localhost:3000'))
  • 🙋‍♂️ 이제 ws 연결이 준비되었습니다!

WebSocket Events

WebSocket은 frontend의 event와 작동 방식이 비슷합니다.
버튼을 만들고 addEventListener로 이벤트와 콜백을 등록하듯이 이벤트와 콜백으로 이루어집니다.

wss.on("event", callback) 과 같은 방식으로 사용합니다.
on()메소드에서는 event가 발생하는 것을 기다리고, 이벤트가 발생되면 callback을 실행합니다. 그리고 backend에 연결된 사람의 정보를 socket 객체로 제공해줍니다.

My first Connection!

자바스크립트 API중 frontend와 backend를 연결해주는 WebSocket 생성자가 있습니다.

  • app.js
    const socket = new WebSocket(`wss://${window.location.host}`);
    WebSocket을 사용하기 때문에 http가 아닌 wss로 시작합니다.
    생성자에 현재 host를 전달하여 WebSocket을 생성해주면 backend와 연결 완료!

  • server.js
    wss.on("connection", (socket) => console.log(socket))
    확인을 위해 connection이 일어나면, 연결된 사람(브라우저)의 정보(socket)를 console에 보여주는 코드를 작성합니다.

그리고 브라우저를 새로고침하면...


WebSocket이 터미널에 표시됩니다! 연결이 잘 되었다는 것이죠.

🙋‍♂️ ws 연결이 되었습니다!

중요 포인트💡

  • backend(server.js)에서 socket은 연결된 브라우저입니다.
  • frontend(app.js)에서 socket은 서버로의 연결입니다.

My first Message!

메세지 보내기

  • server.js
    wss.on("connection", (socket) => {
        socket.send("hello:)")
    }
    backend socketsend(message) 메소드를 이용하여 메세지를 보냅니다.

메세지 받기

  • app.js
    socket.addEventListener("message", (message) => {
    	console.log(message)
    })
    frontend socketmessage 이벤트리스너를 등록하여 message 객체를 받습니다.

message 객체


message로 보낸 내용은 data에 있다는 것을 알 수 있습니다.

Let's Chat ✉

  1. home.pug에 메세지가 표시될 부분인 ul을 만들어줍니다.
  2. app.js message form에서 메세지를 submit하면 socket(서버로의 연결)으로 메세지를 보냅니다.
  3. server.js message 이벤트가 발생하면 모든 socket(연결된 브라우저)에 메세지를 보냅니다.
  4. app.js message 이벤트가 발생하면 메세지를 ul에 표시합니다.

app.js

function handleMessageSubmit(event) {
  event.preventDefault();
  const input = messageForm.querySelector('input');
  socket.send(input.value);
  input.value = '';
}
messageForm.addEventListener('submit', handleMessageSubmit);

server.js

let sockets = []; // 연결된 socket들의 배열
wss.on('connection', (socket) => {
  console.log('✅ Connected to browser');
  sockets.push(socket); // 연결되면 socket을 배열에 추가
  socket.on('message', (message) => {
    sockets.forEach((aSocket) =>
    	aSocket.send(message.toString()) // 각 소켓에 디코딩한 메세지 보내주기
    );
...     

app.js

socket.addEventListener('message', (message) => {
  const li = document.createElement('li');
  li.innerText = message.data;
  messageList.appendChild(li); // 받은 메세지 생성
});


이제 두개의 다른 브라우저에서 실시간으로 각자의 메세지를 볼 수 있습니다! 🙆‍♂️

Nickname🤸‍♂️

  1. home.pug에 닉네임이 표시될 부분과 닉네임을 입력할 부분을 만들어줍니다.

  2. app.js에서 보내는 메세지의 타입이 두가지로 늘어났으므로 메세지를 { type, payload } 형태의 오브젝트로 만들어 보내줍니다. 이때, string타입의 메세지만 보내고 받을 수 있으므로 JSON.stringify()를 사용하여 string타입으로 바꿔줍니다.

  3. server.js에서 message 이벤트가 발생하면 switch를 사용하여 텍스트 메세지인 경우와 닉네임 변경인 경우를 나누어 처리합니다.

  4. socket은 기본적으로 객체이므로 nickname이라는 키에 메세지로 받은 닉네임을 저장합니다.

  5. 메세지를 보낸 사람의 닉네임과 메세지 텍스트가 보일 수 있도록 변경하여 보내줍니다.
    app.js

    function makeMessage(type, payload) {
      const message = { type, payload }; // 오브젝트로 만들어준다.
      return JSON.stringify(message); // string형태로 만들어준다.
    }
    
    function handleNicknameSubmit(event) {
      event.preventDefault();
      const input = nicknameForm.querySelector('input');
      socket.send(makeMessage('nickname', input.value)); // message 객체를 만든다.
      nickname.innerText = `Your Nickname: ${input.value}`; // 자신의 닉네임을 표시
      input.value = '';
    }
    nicknameForm.addEventListener('submit', handleNicknameSubmit);
    

    server.js

    wss.on('connection', (socket) => {
    console.log('✅ Connected to browser');
    socket.on('close', () => console.log('❌ Disconnected from the browser'));
    sockets.push(socket);
    socket['nickname'] = 'Anonymous'; // socket에 nickname 키를 생성하고 기본 닉네임을 저장합니다.
    socket.on('message', (message) => {
      const parsedMessage = JSON.parse(message); // 다시 오브젝트로 만들어줍니다.
      switch (parsedMessage.type) {
        case 'new_message': // 텍스트 메세지인 경우
          sockets.forEach((aSocket) =>
            aSocket.send(`${socket.nickname}: ${parsedMessage.payload}`) // 보낸 사람 닉네임 표시
          );
          break;
        case 'nickname':
          socket['nickname'] = parsedMessage.payload; // 닉네임 변경한 경우
          break;
        default:
          break;
        }
      });
    ...

완성! 🤹‍♂️

profile
🌑🌘🌗🌖🌕

0개의 댓글