실시간으로 작동하는 채팅 어플리케이션을 구현해보자.
백엔드는 Express.js 를 사용하여 Node.js 서버로 만들 것입니다.
Express는 Node.js의 웹 프레임워크입니다.
템플릿 엔진은 Pug.js를 사용할 것입니다.
템플릿 엔진이란 SSR(Server Side Rendering)에서 HTML을 Javascript와 함께 작성하여 렌더링 할 수 있도록 돕는 프로그램이다. (pug는 express의 뷰 엔진)
import express from 'express'
const app = express()
app.listen(3000)
express를 사용하여 서버를 만들어줍니다.
localhost:3000으로 이동시 Not found가 아닌 Cannot GET / 페이지가 나오면 성공!
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가 렌더링 될 것입니다.
app.use('/public', express.static(__dirname + '/public'))
static(정적) 파일이란 HTML에서 사용되는 js, css, image 파일 등을 가리킵니다.
서버에서 정적 파일을 다루기 위해서는 express의 static() 메소드를 이용합니다.
express.static() 메소드를 사용하여 /public/app.js 파일을 렌더링 될 home.pug에 연결합니다.
...
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(HyperText Transfer Protocol)는 하이퍼텍스트를 빠르게 교환하기 위한 프로토콜의 일종으로, 서버와 클라이언트의 사이에서 어떻게 메시지를 교환할지를 정해 놓은 규칙이다.
요청(Request)과 응답(Response)으로 구성되어 있으며, 일반적으로 80번 포트를 사용한다.
예를 들면 '클라이언트가 웹 페이지에서 링크가 걸려있는 텍스트를 클릭(요청)하면 링크를 타고 새로운 페이지로 넘어간다(응답)'. 따라서 우리가 사용하는 웹 브라우저에서 인터넷 주소 맨 앞에 들어가는 http://가 바로 이 프로토콜을 사용해서 정보를 교환하겠다는 표시인 것이다.
웹소켓은 TCP통신 방식으로 서버와 클라이언트 사이에 데이터를 주고 받을 수 있는 기술이다.
웹소켓은 서버와 클라이언트 사이에서 데이터를 주고 받을 수 있는데, 이는 다시말해 서로 메신저를 이용해 1:1 채팅을 한다고 볼수 있다.
HTTP REST 메서드인 POST 보다 빠르다.
이런 장점 때문에 여러 API 또는 여러 게임 멀티플레이에도 사용된다.
TCP(Transmission Control Protocol)
컴퓨터가 다른 컴퓨터와 데이터 통신을 하기 위한 규약(프로토콜)의 일종이다.
Node.js로 WebSocket 서버를 만들기 위해 ws라는 패키지를 사용할 것입니다.
ws는 사용하기 쉽고, 빠르고, 철저하게 테스트된 WebSocket 클라이언트 및 서버 구현입니다.
HTTP 표준이 되는 규약을 따라서 만든, WebSocket protocol을 실행하는 패키지인 것이죠.
채팅방은 WebSocket protocol에 포함되어 있지 않은 추가적인 기능이므로 ws자체에 포함되어 있지 않습니다.
npm i ws
로 ws를 설치하고 만들어 둔 express 서버에 ws 기능을 추가하겠습니다.
view engine과 static files, redirection등을 사용하기 위해 http 를 사용할 수 있는 서버가 필요하고
실시간 채팅을 위해 WebSocket을 사용할 수 있는 서버가 필요하므로 하나의 서버에서 둘 다를 이해할 수 있도록 만드는 것입니다.
const app = express();
app.listen(3000)
const server = http.createServer(app);
import {WebSocketServer} from "ws";
const wss = new WebSocketServer({ server })
app.listen(3000)
을 했듯이 server 객체의 listen() 메소드를 사용합니다.server.listen(3000, () => console.log('✅ Listening on http://localhost:3000'))
WebSocket은 frontend의 event와 작동 방식이 비슷합니다.
버튼을 만들고 addEventListener로 이벤트와 콜백을 등록하듯이 이벤트와 콜백으로 이루어집니다.
wss.on("event", callback)
과 같은 방식으로 사용합니다.
on()
메소드에서는 event가 발생하는 것을 기다리고, 이벤트가 발생되면 callback을 실행합니다. 그리고 backend에 연결된 사람의 정보를 socket
객체로 제공해줍니다.
자바스크립트 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 연결이 되었습니다!
server.js
wss.on("connection", (socket) => {
socket.send("hello:)")
}
backend socket
의 send(message)
메소드를 이용하여 메세지를 보냅니다.app.js
socket.addEventListener("message", (message) => {
console.log(message)
})
frontend socket
에 message
이벤트리스너를 등록하여 message
객체를 받습니다.
message로 보낸 내용은 data
에 있다는 것을 알 수 있습니다.
app.js
message form에서 메세지를 submit하면 socket(서버로의 연결)으로 메세지를 보냅니다.server.js
message 이벤트가 발생하면 모든 socket(연결된 브라우저)에 메세지를 보냅니다.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); // 받은 메세지 생성
});
이제 두개의 다른 브라우저에서 실시간으로 각자의 메세지를 볼 수 있습니다! 🙆♂️
home.pug에 닉네임이 표시될 부분과 닉네임을 입력할 부분을 만들어줍니다.
app.js
에서 보내는 메세지의 타입이 두가지로 늘어났으므로 메세지를 { type, payload }
형태의 오브젝트로 만들어 보내줍니다. 이때, string
타입의 메세지만 보내고 받을 수 있으므로 JSON.stringify()
를 사용하여 string
타입으로 바꿔줍니다.
server.js
에서 message 이벤트가 발생하면 switch
를 사용하여 텍스트 메세지인 경우와 닉네임 변경인 경우를 나누어 처리합니다.
socket
은 기본적으로 객체이므로 nickname
이라는 키에 메세지로 받은 닉네임을 저장합니다.
메세지를 보낸 사람의 닉네임과 메세지 텍스트가 보일 수 있도록 변경하여 보내줍니다.
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;
}
});
...