...
WS (Web Socket)
웹 소켓은 실시간 양방향 데이터 전송을 위한 기술로서, ws 프로토콜 사용하여 통신한다.
최신 브라우저는 대부분 웹 소켓을 지원하며, 노드는 ws나 Socket.IO같은 패키지를 통해 웹 소켓 사용 가능하다.
웹 소켓 이전에는 폴링이라는 방식을 사용했었다.
HTTP가 클라이언트에서 서버로만 요청이 가기 때문에 주기적으로 서버에 요청을 보내 업데이트가 있는지 확인했었다.
하지만 이렇게 할 경우 서버에 부담이 되기 때문에, Server Sent Event라는 기술이 나왔는데
따로 클라이언트에서 요청 없이, 서버가 주체적으로 클라이언트에게 데이터를 전송할 수 있게 하는 기술이다.
하지만 이 기술은, 마치 라디오 처럼 클라이언트는 데이터를 서버로 보낼수는 없고 받기만 할 수 있기 때문에 채팅방 같은 양방향 통신을 구현하기에는 한계가 있었다.
반면, 웹 소켓은 이벤트 방식으로 동작하여 연결도 한 번만 맺으면 되고, HTTP와 포트 공유도 가능하며, 성능도 매우 좋아 서버와 통신할 상황이 생긴다면 모두 이 기술을 사용하는 편이다.
웹소켓(WS) 프로젝트 구조
package.json
{
"name": "nodechat",
"version": "0.0.1",
"description": "웹소켓 채팅방",
"main": "app.js",
"scripts": {
"start": "nodemon app"
},
"author": "Inpa",
"license": "ISC",
"dependencies": {
"cookie-parser": "^1.4.4",
"dotenv": "^10.0.0",
"express": "^4.17.1",
"express-session": "^1.17.0",
"morgan": "^1.9.1",
"nunjucks": "^3.2.0",
"ws": "^8.2.3"
},
"devDependencies": {
"nodemon": "^2.0.2"
}
}
$ npm install
app.js
const express = require('express');
const path = require('path');
const morgan = require('morgan');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const nunjucks = require('nunjucks');
const dotenv = require('dotenv');
dotenv.config();
const webSocket = require('./socket');
const indexRouter = require('./routes');
const app = express();
app.set('port', process.env.PORT || 8005);
app.set('view engine', 'njk');
nunjucks.configure('views', {
express: app,
watch: true,
});
app.use(morgan('dev'));
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser("123123"));
app.use(
session({
resave: false,
saveUninitialized: false,
secret: "123123",
cookie: {
httpOnly: true,
secure: false,
},
}),
);
app.use('/', indexRouter);
app.use((req, res, next) => {
const error = new Error(`${req.method} ${req.url} 라우터가 없습니다.`);
error.status = 404;
next(error);
});
app.use((err, req, res, next) => {
res.locals.message = err.message;
res.locals.error = process.env.NODE_ENV !== 'production' ? err : {};
res.status(err.status || 500);
res.render('error');
});
const server = app.listen(app.get('port'), () => {
console.log(app.get('port'), '번 포트에서 대기중');
});
//? express 서버와 웹소켓 서버를 연결 시킨다.
webSocket(server);
socket.js
const WebSocket = require('ws');
module.exports = server => {
//? express 서버와 웹소켓 서버를 연결 시킨다.
// 변수이름은 wss(web socket server)
const wss = new WebSocket.Server({ server });
//* 프론트에서 new WebSocket("ws://localhost:8005") 보냈을때, 웹소켓 연결 실행
wss.on('connection', (ws, req) => {
//* ip 파악
// req.headers['x-forwarded-for'] : 프록시 서버를 경유할때 ip가 변조될수 있다. 이를 감지하고 본ip파악해줄 수 있다.
const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
console.log('새로운 클라이언트 접속 ip : ', ip);
//* 클라이언트로부터 온 메시지
ws.on('message', message => {
console.log(message.toString());
});
//* 에러 시
ws.on('error', error => {
console.error(error);
});
//* 연결 종료 시 (유저가 채팅방을 나간다거나)
ws.on('close', () => {
console.log('클라이언트 접속 해제', ip);
clearInterval(ws.interval); // 연결 끊기면 setInterval 중지
});
// 3초마다 클라이언트로 메시지 전송
ws.interval = setInterval(() => {
//! 웹소켓은 비동기이기 때문에 삑 날 수 있어, 웹소켓이 클라이언트랑 연결이 되었는지 검사하는 안전 장치
if (ws.readyState !== ws.OPEN) {
return;
}
ws.send('서버에서 클라이언트로 메시지를 보냅니다.');
}, 3000);
});
};
index.njk
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>노드 채팅방</title>
</head>
<body>
<div>F12를 눌러 console 탭과 network 탭을 확인하세요.</div>
<script>
// websocket으로 서버와 http 포트 연결. http포트와 공유해서 통신할 수 있음
// wesocket객체는 기본적으로 브라우저에서 지원
const webSocket = new WebSocket("ws://localhost:8005");
// 서버에서 wss.on('connection' 이 성공적으로 되면, 이벤트 실행
webSocket.onopen = function () {
console.log('서버와 웹소켓 연결 성공!');
};
// 사실상 .onmessage 와 .send 로 메세지 통신을 하게 되는 것이다
webSocket.onmessage = function (event) {
console.log(event.data);
webSocket.send('클라이언트에서 서버로 답장을 보냅니다'); // 서버로부터 메세지 받으면 바로 서버로 메세지 보냄
};
</script>
</body>
</html>
웹소켓(WS) 동작 원리
웹소켓 통신을 하기위한 초기 설정은 간단하다.
ws 모듈을 불러온 후 서버를 웹 소켓 서버와 연결하면 된다.
익스프레스 (HTTP) 와 웹 소켓 (WS) 은 같은 포트를 공유할 수 있으므로 별도의 작업이 필요없다.
사진의 소스 코드는 위의 웹소켓 프로젝트의 코드이다.
연결 후에는 웹 소켓 서버 (wss) 에 이벤트 리스너를 붙인다.
웹 소켓은 이벤트 기반으로 작동한다고 생각하면 된다. 실시간으로 데이터를 전달받기 때문에 항상 대기하고 있어야 한다.
connection 이벤트는 클라이언트가 서버와 웹 소켓 연결을 맺으면 발생한다.
req.headers['x-forwarded-for'] || req.connection.remoteAddress 는 클라이언트의 IP를 알아내는 유명한 방법 중 하나이므로 알아두는 것이 좋다.
익스프레스에서는 IP를 확인할 때, proxy-addr 패키지를 사용하므로 이 패키지를 사용해도 좋다.
로컬 호스트로 접속한 경우, 크롬에서는 IP가 ::1 으로 뜬다. 다른 브라우저에선 ::1 외의 다른 IP가 뜰 수 있다.
::1 은 ipv6 주소 체계의 127.0.0.1(localhost)를 뜻한다.
익스프레스 서버와 연결한 후, 웹 소켓 객체 (ws) 에 이벤트 리스너, message, error, close를 연결한다.
- message는 클라이언트로부터 메시지가 왔을 때,
- error는 웹 소켓 연결 중 문제가 생겼을 때,
- close는 클라이언트와 연결이 끊겼을 때 발생한다.
setInterval은 3초마다 연결된 모든 클라이언트에 메시지를 보낸다.
먼저 readyState가 OPEN 상태인지 확인한다.
웹 소켓에는 네 가지 상태가 있는데,
바로 CONNECTING (연결 중), OPEN (열림), CLOSING (닫는 중), CLOSED (닫힘) 이다.
OPEN일 때만 에러 없이 메시지를 보낼 수 있다.
확인 후 ws.send 메서드로 하나의 클라이언트에 메시지를 보낸다.
웹 소켓은 단순히 서버에서 설정한다고 동작하는 것이 아니다. 당연히 클라이언트에도 웹 소켓을 적용해줘야 한다.
WebSocket 생성자에 연결할 서버 주소를 넣고 webSocket 객체를 생성한다.
서버 주소의 프로토콜이 ws인 것에 주의하자.
클라이언트 역시 이벤트 기반으로 동작한다.
- 서버와 연결이 맺어지는 경우에는 onopen 이벤트 리스너가 호출되고,
- 서버로부터 메시지가 오는 경우엔 onmessage 이벤트 리스너가 호출된다.
현재 코드에선 서버에서 메시지가 오면 서버로 답장을 보내게 설정 하였다.
서버를 실행하는 순간, 서버는 클라이언트에 3초마다 메시지를 보내고, 클라이언트도 서버로부터 메시지가 오는 순간 바로 답장을 보낸다.
브라우저와 노드 콘솔에서 결과를 확인해보자.
[서버]
[클라이언트]
접속하는 순간부터 노드의 콘솔과 브라우저의 콘솔에 3초마다 메시지가 찍힐 것이다.
브라우저의 콘솔은 메시지가 왔기 때문에 event의 data를 출력하고,
노드의 콘솔은 다시 메시지를 전달받았기 때문에 메시지가 출력된다.
웹소켓은 사실상 .onmessage 와 .send 저 두개로 메세지 통신을 하게 되는 것이다.
이 글이 좋으셨다면 구독 & 좋아요
여러분의 구독과 좋아요는
저자에게 큰 힘이 됩니다.