실시간 알림, 채팅, 통신 구현을 위해 WebSocket 사용하기
실시간 통신 서버
세미와 파이널 프로젝트 때 사용자 경험 향상을 위해 실시간 알림 기능을 추후 추가할 기능으로 설계했다.
그리고 "실시간" 통신 기능을 위해 http server가 아닌, WebSocket server 를 이용해서 구현해야 한다고 알게 되었다.
WebSocket VS HTTP

HTTP 서버 작동 방식 (좌측)
그림과 같이 Client 측에서 1개의 request 를 보내면 Server 측에서 1개의 response 를 보낼 수 있다. 그리고 Server는 계속해서 request 가 오길 기다린다. request 없이는, response를 보낼 수 없기 때문이다.
WebSocket 서버 작동 방식 (우측)
Client 측에서 request 를 보내는 것과 관련 없이, 연결된 Client에게서 Server 가 여러 번의 response를 보낼 수 있다.
EX ) HR 인사관리 프로그램에서 회사 구성원이 출퇴근 시간 오류에 대한 정정 요청을 보내면, 서버에서는 인사팀과 같은 회사 관리자에게 정정 요청 알림 response를 바로 보낼 수 있다.
이는 양방향 통신이 가능하기 때문이다.
WebSocket VS Socket IO
그런데 WebSocket을 공부하다보면 WebSocket과 Socket IO 두 가지로 구현하는데, 차이는 아래와 같다.
WebSocket
- HTML5 웹 표준 기술
- 지원하지 않는 브라우저에서는 사용 불가능하다.
# front-end
import express from "express";
const app = express(); # express 를 이용해서 서버 만들기
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });
const sockets = [];
wss.on("connection", (socket) => { # websocket 에 연결되었을 때
sockets.push(socket); # sockets 배열에는 socket에 연결되는 브라우저들을 담음
socket.on("message", (msg) => {
const message = JSON.parse(msg); # JavaScript object 형식에서 string 형식으로 변환
switch (message.type) { # type에 따라서 message와 nickname을 구분
case "new_message":
sockets.forEach((aSocket) => # 연결된 브라우저들에게 보내는 메세지
aSocket.send(`${socket.nickname}: ${message.payload}`)
);
case "nickname":
socket["nickname"] = message.payload;
}
});
});
WebSocket 에서는 front 에서 server 로 보내기 위해 정해진 message event 를 사용해야 한다.
그리고 각각의 브라우저=Client=front 에서 하나의 message event 를 이용해 입력 받은 사용자 닉네임과 각각 입력한 채팅 메세지들도 보내야 하는데, 이를 구별하기 아래와 같이 type 을 붙여 JavaScript object 형식으로 만든다.
{
type: "new_message",
payload: "hello everyone!"
}
{
type: "nickname",
payload: "duccku"
}
but, 기껏만든 JavaScript object 형식이 아닌, string 으로 바꿔서 보내야 하는데..
❗JavaScript object 형식을 server로 보낼 때 string 으로 바꿔주는 이유
JSON.parse() 함수를 이용해서 JavaScript object ➡️ string 변경해줌
front 와 연결하고 싶은 back 서버가 JavaScript 가 아니라 Java 등 다양한 언어일 가능성이 있다.
websocket 은 브라우저에 있는 api 이고, back 에서 어떤 언어를 사용하든 api가 어떠한 판단을 하면 안되기 때문에 back에서 어떤 언어를 사용하던지 받아서 사용할 수 있는 string 형식으로 보내는 것이다.
❗반대로 back 에서 받은 string 을 다시 front 로 보내주기 위해서
JSON.stringify() 함수를 이용해서 string ➡️ JavaScript object 변경해줌
✅emit : event 를 전달하는 함수
✅on : event 를 받는 함수
Socket IO
- WebSocket 을 지원하는 프레임 워크
- WebSocket 지원 여부를 체크하고 지원된다면, Websocket 을 사용, 지원되지 않는다면 다른 방식 (Long Polling 등)을 이용해서 실시간 통신을 지원한다.
- WebSocket 에서는 개발자가 일일이 구현해줘야하는 기능들도 Socket IO에서는 이미 함수로 내장되어 있기 때문에 실시간 알림, 채팅 등의 기능을 비교적 쉽게 구현할 수 있다.
# front-end
const socket = io();
function backendDone() { # 백엔드에서 마지막에 작동할 함수
console.log("backend done");
};
function addMessage(msg) { # 입장한 룸의 채팅화면에서 추가되는 메세지
const ul = room.querySelector("ul");
const li = document.createElement("li");
li.innerText = msg;
ul.appendChild(li);
};
function handleMessageSubmit(event) { # 내가 보내는 메세지
event.preventDefault();
const input = room.querySelector("input");
const value = input.value;
socket.emit("new_message", input.value, roomName, () => {
addMessage(`YOU: ${value}`);
});
input.value = "";
};
function showRoom() { # 방에 들어갔을 때 함수
const h3 = room.querySelector("h3");
h3.innerText = `Room ${roomName}`;
const form = room.querySelector("form");
form.addEventListener("submit", handleMessageSubmit);
};
socket.emit("enter_room", input.value, backendDone); # emit 시 마지막으로 작동할 함수는 마지막 인자로 보내줌
socket.on("welcome", () => { # 나를 제외한 방에 참가한 나머지 socket (사람)에게 message 보냄
addMessage("Someone joined!");
});
socket.on("bye", () => { # socket (사람) 한명이 접속 중단함
addMessage("someone left");
});
socket.on("new_message", addMessage); # 상대방이 보낸 메세지 받음
Socket IO 에서는 어떤 이름이든 내가 원하는 이름으로 event 를 emit 할 수 있다.
위와 같이 방을 만들어서 실시간 채팅을 구현할 때 front 에서 server 로 send 가 아닌, enter_room 이라는 내가 만들어준 event 를 emit 한다.
그리고 뒤에 javaScript object 형식으로 입력된 값을 함께 보낼 수 있다. (string 형식으로 변경 필요 x)
- 첫번째 인자 : event 이름
- 두번째 인자 : 보내고 싶은 payload (어떤 형식이든 상관 없음)
- n번째 인자 : 서버에서 호출 function (이 function 은 front-end 에 있음)
=> 끝날 때 실행되는 function 을 보내고 싶으면 마지막 인자에 넣어서 보낸다.
# back-end
const httpServer = http.createServer(app);
const wsServer = SocketIO(httpServer);
wsServer.on("connection", (socket) => {
socket.on("enter_room", (roomName, done) => {
# 매개변수는 front-end 에서 보낸 인자 roomName = input.value / done = function backendDone
socket.join(roomName); # 실시간 대화를 할 방에 참가함
done(); # done function은 보안 문제로 back-end 에서 실행하지 않고 front-end 에서 실행시킴
socket.to(roomName).emit("welcome"); # 방에 입장하면 모든 사용자에게 welcome 메세지를 보냄
});
socket.on("disconnecting", () => { # 내장 event "disconnecting"
socket.rooms.forEach((room) => socket.to(room).emit("bye"));
});
socket.on("new_message", (msg, room, done) => { # 어떤 메세지를, 어떤 룸으로 보낼지 매개변수로 받음
socket.to(room).emit("new_message", ${msg});
done();
});
});
방에 참가
socket.join(roomName); 에서 roomName이 이미 존재할 경우 해당 room으로 이동한다.
그러나 roomName 이 없을 경우 새로 만들어서 해당 room으로 이동한다.
그래서 방에 입장하려는 브라우저들은 참가하려는 roomName을 알고 있어야 한다.
접속 중단 (방을 떠나지는 않음)
socket.on("disconnecting") 에서 내장 event "disconnecting"을 사용하여 사용자가 접속은 중단하지만 방을 떠나지는 않는 상태를 만들 수 있다.