최근에 웹 소켓, Socket.io 를 사용해볼 일이 생겼습니다. 음... 그 동안은 간단한 개념만 알고 있었고 실제 사용해볼일은 없었습니다. 막상 해보니까 그렇게 어려운 건 아닌거 같더라고요. 사용하기 쉽고... 재밌습니다.
이번시간에는 간단하게 웹 소켓 개념과 Socket.io 사용법, 방 만들기 등을 해보면서 기본 사용법에 대해 알아보겠습니다.
참고 : https://ko.wikipedia.org/wiki/웹소켓
웹소켓은 하나의 TCP 접속에 전이중 통신 채널을 제공하는 컴퓨터 통신 프로토콜 입니다. 2011년 표준화되었다고 하네요. 따라서 대부분의 브라우저가 이 프로토콜을 지원하고 있습니다.
웹 소켓은 사용자의 브라우저와 서버 사이의 인터액티브 통신 세션을 설정할 수 있게 하는 고급 기술입니다. 개발자는 웹 소켓 API를 통해 서버로 메시지를 보내고 서버의 응답을 위해 서버를 폴링하지 않고도 이벤트 중심 응답을 받는 것이 가능합니다.
예를 들어, 채팅, 실시간 알림 등을 사용할 때 웹 소켓을 사용할 수 있겠죠?
참고 : https://socket.io/docs/v4/tutorial/introduction
Socket.io는 WebSocket을 쉽게 사용할 수 있도록 만들어진 라이브러리입니다. socket.io는 두 부분으로 구성됩니다.
import express from 'express';
import { createServer } from 'node:http';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { Server } from 'socket.io';
const app = express();
const server = createServer(app);
const io = new Server(server);
const __dirname = dirname(fileURLToPath(import.meta.url));
app.get('/', (req, res) => {
res.sendFile(join(__dirname, 'index.html'));
});
io.on('connection', (socket) => {
console.log('a user connected');
});
server.listen(3000, () => {
console.log('server running at http://localhost:3000');
});
클라이언트 측에서는 이런식으로 사용하면 됩니다.
<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io();
</script>
이렇게 하고 브라우저에서 웹페이지를 새로 고치면 서버에서 사용자가 연결되었다는 콘솔이 출력되는 것을 확인할 수 있을 겁니다.
요청과 응답 플로우를 살펴보면 재밌는 점을 확인해볼 수 있습니다. 먼저 요청 메시지를 보겠습니다.
GET ws://localhost:3001/socket.io/?EIO=4&transport=websocket HTTP/1.1
Host: localhost:3001
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36
Upgrade: websocket
Origin: http://localhost:3000
Sec-WebSocket-Version: 13
Accept-Encoding: gzip, deflate, br
Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7
Sec-WebSocket-Key: lyFiq8x1UpYPkbd96Cb0+g==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Connection: Upgrade
의미 : 이미 생성된 커넥션을 다른 프로토콜로 업그레이드/변경 하겠다는 의미입니다. 즉, webscoket으로 변경하겠다는 것입니다.
응답을 살펴보면 다음과 같습니다. Switching Protocols
이라고 나옵니다. 웹 소켓으로 프로토콜을 제대로 변경했다는 거겠죠?
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: Q4jPf8sCiOPncyzEBGfVqN7+wJo=
브라우저 개발자 도구를 확인하면 웹소캣에서 어떤 데이터를 주고 받는지 볼 수 있습니다. 처음에는 뭔가 설정값 같은걸 보내는거 같고. 이후에는 주기적으로 (25초마다) 통신하고 있는 것을 알 수 있습니다. pingInterval이 25000로 되어있네요. 이러한 이유는 연결 끊기지 말라고 하는거겠죠?
초록색 화살표는 브라우저에서 서버로 전송, 빨강색 화살표는 서버에서 브라우저로 보내는 데이터를 나타냅니다. 처음에 보면 좀 헷갈릴 수 있겠네요.
브라우저와 서버간 통신할 때를 살펴보면 이벤트-이벤트 리스너 개념이랑 동일하다고 보면 됩니다. 이벤트를 발생시키고, 이벤트를 전달받는 그런 개념이죠.
Socket.IO의 기본 아이디어는 원하는 데이터와 함께 원하는 이벤트를 보내고 받을 수 있다는 것입니다. JSON으로 인코딩할 수 있는 모든 개체가 가능하며 이진 데이터 도 지원됩니다.
사용자가 메시지를 입력하면 서버가 이를 이벤트로 받도록 만들어 보겠습니다.
먼저 클라이언트 측입니다.
// socket.emit('이벤트 명', 넘겨줄 값)
socket.emit('hello', 'world');
다음은 서버 측 코드입니다.
io.on('connection', (socket) => {
// socket.on('이벤트 명', 넘겨진 값)
socket.on('hello', (arg) => {
console.log(arg); // 'world'
});
});
emit으로 내보내고, on으로 이벤트를 전달받는 구조. 알아보기 쉽죠?
간단한 실습을 위해 노드로 소켓 테스트 서버를 띄우겠습니다. 먼저 필요한 라이브러리를 설치해줍니다.
{
"name": "socket-server",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "nodemon",
"build": "rimraf build && tsc",
"start": "pnpm run build && node build/server.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"cors": "^2.8.5",
"express": "^4.18.2",
"socket.io": "^4.7.3",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/node": "^20.10.8",
"nodemon": "^3.0.2",
"rimraf": "^5.0.5",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
}
}
개발 편의를 위해 nodemon 설정을 해줍니다. nodemon.json 파일을 아래와 같이 만들어줍니다.
{
"watch": ["src"],
"ext": ".ts",
"exec": "ts-node src/server.ts"
}
기본적인 서버를 띄우도록 해봅니다. 아직 socket.io를 사용하지 않은 상태입니다.
import express from "express";
import cors from "cors";
import http from "http";
const app = express();
app.use(cors());
const server = http.createServer(app);
const PORT = process.env.PORT || 3400;
server.listen(PORT, () =>
console.log(`Server is running on port ${PORT} now!`)
);