노드 생태계에서는 웹 소켓이란 말을 들으면 Socket.IO를 먼저 떠올리는 경우가 많습니다. 하지만 Socket.IO는 웹 소켓을 활요하는 라이브러리이지 웹소켓 그 자체는 아닙니다. 나중에 Socket,IO를 사용하기 위해서는 기반 기술인 웹소켓을 먼저 알아야 합니다.
웹 소켓은 HTML5에 새로 추가된 스펙으로 실시간 양방향 데이터 전송을 위한 기술이며,HTTP 와 다르게 WS라는 프로토콜을 사용합니다. 따라서 브라우저와 서버가 WS프로토콜 지원하면 사용할 수 있습니다. 최신 브라우저는 대부분 웹소켓을 지원하고, 노드에서는 ws나 Socket.IO 같은 패키지를 통해 웹 소켓을 사용할 수 있습니다.
웹 소켓이 나오기 이전에는 HTTP 기술을 사용해 실시간 데이터 전송을 구현했습니다. 그중 한가지가 폴링(polling)이라는 방식입니다. HTTP가 클라이언트에서 서버로 향하는 단방향 통신이므로 주기적으로 서버에 새로운 업데이트가 있는지 확인하는 요청을 보내고, 있다면 새로운 내용을 가져오는 단순 무식한 방법이었습니다.
그러다가 HTML5가 나오면서 웹 브라우저와 웹 서버가 지속적으로 연결된 라인을 통해 실시간으로 데이터를 주고받을 수 있는 웹 소켓이 등장했습니다. 처음에 웹소켓 연결이 이뤄지고 나면, 그다음부터는 계속 연결된 상태로 있어 따로 업데이트가 있는지 요청을 보낼 필요가 없습니다. 업데이트할 내용이 생겼다면 서버에서 바로 클라이언트에 알립니다. HTTP프로토콜과 포트를 공유할 수 있으므로 다른 포트에 연결할 필요도 없습니다. 폴링 방식에 비해 성능도 매우 개선되었습니다.
참고로 서버센트 이벤트(Server Sent Events)(이하 SSE) 라는 기술도 등장했습니다. 이는 Event Source라는 객체를 사용하는데요, 처음에 한 번만 연결하면 서버가 클라이언트에 지속적으로 데이터를 보냅니다. 웹 소켓과 다른 점은 클라이언트에서 서버로는 데이터를 보낼 수 없다는 점입니다. 즉 서버에서 클라이언트로 데이터를 보내는 단방향 통신입니다. 웹 소켓만이 진정한 양방향 통신입니다. 양방향 통신이므로 SSE에서 할 수 있는 것은 웹 소켓으로도 모두 할 수 있습니다.
하지만 주식 차트업데이트나 SNS에서 새로운 게시물 가져오기 등 굳이 양방향 통신을 할 필요가 없는 경우도 많습니다. 서버에서 일방적으로 데이터를 내려주기만 하면 되기 때문입니다.
Socket.IO는 웹 소켓을 편리하게 사용할 수 있도록 도와주는 라이브러리입니다. Socket.IO는 웹 소켓을 지원하지 않는 IE9과 같은 브라우저에서는 알아서 웹소켓 대신 폴링 방식을 사용해 실시간 데이터 전송을 가능하게 합니다. 클라이언트 측에서 웹 소켓 연결이 끊겼다면 자동으로 재연결을 시도하고, 채팅방을 쉽게 구현할 수 있도록 메서드를 준비해뒀습니다.
Socket.IO를 사용하기 전에 ws 모듈로 웹 소켓이 무엇인지 알아보겠습니다.
ws 모듈로 웹 소켓 사용
패키지를 설치하고(package.json) .env 와 app.js , routes/index.js 파일을 작성합니다.
$ npm install -y
// .env
COOKIE_SECRET=gifchat
// 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", "html");
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(process.env.COOKIE_SECRET));
app.use(
session({
resave: false,
saveUninitialized: false,
secrest: process.env.COOKIE_SECRET,
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 !== "production" ? err : {};
res.status(err.status || 500);
res.render("error");
});
const server = app.listen(app.get("port"), () => {
console.log(app.get("port"), "번 포트 대기중");
});
webSocket(server); // 웹 소켓을 익스프레스 서버에 연결 후
// routes/index.js
const express = require("express");
const router = express.Router();
router.get("/", (req, res) => {
res.render("index");
});
module.exports = router;
이제 ws 모듈을 설치해 노드에 웹 소켓을 구현해봅시다.
$ npm install ws
웹 소켓은 익스프레스 서버에 연결합니다.
// socket.js
const WebSocket = require("ws");
module.exports = (server) => {
const wss = new WebSocket.Server({ server });
wss.on("connection", (ws, req) => {
const ip = req.headers["x-forwarded-for"] || req.socket.remoteAddress;
console.log("새로운 클라이언트 잡속", ip);
ws.on("message", (message) => {
console.log(message.toString());
});
ws.on("error", (error) => {
console.log(error);
});
ws.on("close", () => {
console.log("클라이언트 접속 해제", ip);
clearInterval(ws.interval);
});
ws.interval = setInterval(() => {
if (ws.readyState === ws.OPEN) {
ws.send("서버에서 클라이언트로 메시지를 보냅니다");
}
}, 3000);
});
};
ws 모듈을 불러온 후 익스프레스 서버를 웹 소켓 서버와 연결했습니다. 익스프레스(HTTP)와 웹 소켓(WS)은 같은 포트를 공유할 수 있으므로 별도의 작업이 필요하지 않습니다.
연결 후에는 웹 소켓 서버(wss)에 이벤트 리스너를 붙입니다. 웹 소켓은 이벤트 기반으로 작동한다고 생각하면 됩니다. 실시간으로 데이터를 전달받으므로 항상 대기하고 있어야 합니다. connection 이벤트는 클라이언트가 서버와 웹 소켓 연결을 맺을 때 발생합니다. req.headers['x-forwarded-for'] || req.socket.remoteAddress 는 클라이언트의 IP를 알아내는 유명한 방법중 하나이므로 알아두는게 좋습니다. 익스프레스에서는 IP 를 확인할 때 , proxy-addr 패키지를 사용하므로 이 패키지를 사용해도 괜찮습니다. 로컬 호스트로 접속한 경우, 크롬에서는 IP가 ::1로 뜹니다. 다른 브라우저에서는 ::1 외의 다른 IP가 뜰 수 있습니다.
익스프레스 서버와 연결한 후 ,웹 소켓 객체(ws)에 이벤트 리스너 세개, 즉 message,error,close 를 연결했습니다. message는 클라이언트로부터 메시지가 왔을 때 발생하고 ,error는 웹 소켓 연결 중 문제가 생겼을 때 발생합니다. close 이벤트는 클라이언트와 연결이 끊겼을 때 발생합니다.
setInterval은 3초 마다 연결된 모든 클라이언트에 메시지를 보내는 부분입니다. 먼저 readyState가 OPEN 상태인지 확인합니다. 웹 소켓에는 네 가지 상태가 있습니다. CONNECTING(연결중), OPEN(열림), CLOSING(닫는중), CLOSED(닫힘) 입니다. OPEN일 때만 에러 없이 메시지를 보낼 수 있습니다. 확인 후 ws.send 메서드로 하나의 클라이언트에 메시지를 보냅니다. close 이벤트에서 setInterval을 clearInterval로 정리하는 것도 꼭 기억해두길 바랍니다. 이부분이 없다면 메모리 누수가 발생합니다.
웹 소켓은 단순히 서버에서 설정한다고 해서 작동하는 것이 아닙니다. 클라이언트에서도 웹 소켓을 사용해야합니다. 양방향 통신이기 때문입니다. views 폴더를 만들고 index.html 파일을 작성해서 script 태그에 웹 소켓 코드를 넣겠습니다. views 폴더 안에 error.html 도 같이 작성합니다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GIF 채팅방</title>
</head>
<body>
<div>F12를 눌러 console 탭과 newtwork탭을 확인</div>
<script>
const webSocket = new WebSocket("ws://localhost:8005");
webSocket.onopen = () => {
console.log("서버와 웹 소켓 연결 성공!");
};
webSocket.onmessage = (event) => {
console.log(event.data);
webSocket.send("클라이언트에서 서버로 답장을 보냅니다.");
};
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>error</title>
</head>
<body>
<h1>{{message}}</h1>
<h2>{{error.status}}</h2>
<pre>{{error.stack}}</pre>
</body>
</html>
WebSocket 생성자에 연결할 서버 주소를 넣고 webSocket 객체를 생성합니다. 서버 주소의 프로토콜이 ws인 것에 주의해야합니다. 클라이언트에서도 역시 이벤트 기반으로 동작합니다. 서버와 연결이 맺어지는 경우에는 onopen 이벤트 리스너가 호출되고, 서버로부터 메시지가 오는 경우에는 onmessage 이벤트 리스너가 호출됩니다. 서버에서 메시지가 오면 서버로 답장을 보냅니다.
http://localhost:8005 에 접속후 콘솔 네트워크 확인하기
서버로부터 3초마다 메시지가 오지만, 보낸 네트워크 요청은 처음 http://localhost:8005 를 청한 것과 웹 소켓을 요청한 것 두번 뿐입니다. HTTP를 사용하는 폴링 방식이었다면 매번 요청을 보내서 응답을 받아와야 했을 것입니다.
Socket.IO
ws 패키지는 간단하게 웹 소켓을 사용하고자 할 때 좋습니다. 하지만 구현하려는 서비스가 좀 더 복잡해진다면 Socket.IO를 서용하는 것이 편합니다. Socket.IO가 할 수 있는 일을 ws패키지가 못한다는 뜻은 아닙니다. Socket.IO에 편의 기능이 많이 추가되어 있다는 뜻입니다.
먼저 Socket.IO를 설치합니다.
$ npm install socket.io
그리고 ws 패키지 대신 Socket.IO를 연결합니다.
// socket.js
const SocketIO = require("socket.io");
module.exports = (server) => {
const io = SocketIO(server, { path: "/socket.io" });
io.on("connection", (socket) => {
const req = socket.request;
const ip = req.headers["x-forwarded-for"] || req.socket.remoteAdress;
console.log("새로운 클라이언트 접속", ip, socket.id, req.id);
socket.on("disconnect", () => {
console.log("클라이언트 접속 해제", ip, socket.id);
clearInterval(socket.interval);
});
socket.on("error", (error) => {
console.error(error);
});
socket.on("reply", (data) => {
console.log(data);
});
socket.interval = setInterval(() => {
socket.emit("news", "Hello Socket.IO");
}, 3000);
});
};
아직까지는 ws 패키지와 크게 다른 점이 없습니다. 먼저 socket.io 패키지를 불러와서 익스프레스 서버와 연결합니다.
SocketIO 객체의 두 번째 인수로 옵션 객체를 넣어 서버으에 관한 여러가지 설정을 할 수 있습니다. 여기서는 클라이언트가 접속할 경로인 path 옵션만 사용했습니다. 클라이언트에서도 이 경로와 일치하는 path를 넣어야합니다.
연결 후에는 이벤트 리스너를 붙입니다. connection 이벤트는 클라이언트가 접속했을 때 발생하고, 볼백으로 소켓 객체를(socket)를 제공합니다. io 와 socket 객체가 Socket.IO 의 핵심입니다.
socket.request 속성으로 요청 객체에 접근할 수 있습니다. socket.request.res 로는 응답 객체에 접근할 수 있습니다.. 또한, socket.id로 소켓 고유의 아이디를 가져올 수 있습니다. 이 아이디로 소켓의 주인이 누군지 특정할 수 있습니다.
socket에도이벤트 리스너를 붙였습니다. disconnect는 클라이언트가 연결을 끊었을 때 발생하고, error는 통신 과정 중에 에러가 나왔을 때 발생합니다. reply는 사용자가 직접 만든 이벤트입니다. 클라이언트에서 reply라는 이벤트명으로 데이토를 보낼 때 서버에서 받는 부분입니다. 이렇게 이벤트명을 사용하는 것이 ws 모듈과는 다릅니다. 아래에 emit 메서드로 3초 마다 클라이언트 한 명에게 메시지를 보내는 부분이 있는데, 인수가 두개입니다. 첫 번째 인수는 이벤트 이름, 두 번째 인수는 데이터입니다. 즉, news라는 이벤트 이름으로 Hello Socket,IO 라는 데이터를 클라이언트에 보낸 것입니다. 클라이언트가 이 메시지를 받기 위해서는 news이벤트 리스너를 만들어둬야 합니다.
클라이언트 부분도 바꿔줍시다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Native WebSocket Example</title>
</head>
<body>
<p>hello world</p>
<script>
const ws = new WebSocket("w://localhost:6000");
ws.onopen = function (event) {
ws.send("Client message Hi");
};
ws.onmessage = function (event) {
console.log("Server message:", event.data);
};
ws.onerror = function (event) {
console.log("Server error:", event.data);
};
</script>
</body>
</html>
/socket.io/socket.io.js/는 Socket.IO 에서 클라이언트로 제공하는 스크립트이며, 실제 파일이 아닙니다. 익스프레스 서버에 GET / socket.io.js/ 라는 라우터가 생겼다고 생각하면 됩니다. 이 스크립트를 통해 서버와 유사한 API로 웹 소켓 통신이 가능합니다. 스크립트가 제공하는 io 객체에 서버 주소를 적어 연결합니다. ws 프로토콜이 아니라 http 프로토콜을 사용한다는 점이 ws 모듈과 다름니다. 옵션으로 path를 주었는데 , 이부분이 서버의 path 옵션과 일치해야 통신이 가능합니다.
서버에서 보내는 news이벤트를 받기 위해 news이벤트 리스너를 붙였뒀습니다. news 이벤트가 발생하면 emit 메서드로 다시 서버에 답장을 하며 서버의 reply 이벤트리스너로 답장이 갑니다.
'블록체인 sw개발자' 카테고리의 다른 글
react-query 활용 (0) | 2024.01.02 |
---|---|
<React> 지뢰 찾기 컴포넌트 (1) | 2023.11.28 |
클래스형 컴포넌트( comment 기능) (0) | 2023.10.16 |
crypto 모듈 단방향&양방향 암호화 (0) | 2023.09.23 |
[MySQL] DB, 서버통신 (0) | 2023.09.20 |