회사에서 하드웨어와 실시간 통신하는 웹을 개발하며 Socket.IO를 사용했습니다.
웹소켓에 대해 두루뭉술하게 알고 있었기 때문에 이참에 웹소켓에 대한 개념을 정리해 보고 Socket.IO를 사용해 Next.js에서 양방향 실시간 통신을 구현하는 방법까지 알아보도록 하겠습니다.
출처 : 피그마 홈페이지
피그마로 협업을 하다 보면 다른 분들이 작업하고 계시는 모습이 보이거나, 실시간으로 커서 챗을 나눌 수도 있죠. 이런 실시간, 양방향 통신은 웹소켓을 통해 구현할 수 있습니다.
이 외에도 Slack이나 Discord 같은 채팅 서비스, 문서 협업 툴인 Google Docs, 실시간 주식 거래 플랫폼이나 온라인 게임에서도 웹소켓이 사용되고 있습니다.
즉, 웹소켓은 클라이언트와 서버 간의 실시간 양방향 통신을 가능하게 하는 프로토콜(통신 규약) 입니다. 기존의 HTTP 통신은 요청-응답 방식으로 작동하여 클라이언트가 요청을 보내야만 서버가 응답을 보낼 수 있는 반면, 웹소켓은 한 번 연결이 설정되면 클라이언트와 서버가 서로 자유롭게 데이터를 주고받을 수 있습니다.
[그림] 웹소켓 작동과정
웹소켓은 초기 연결 시 HTTP 업그레이드 요청을 통해 시작되며 양방향, 실시간 통신을 제공합니다. 웹소켓의 작동 과정에 대해 좀 더 자세하게 알아보도록 하겠습니다.
1️⃣ Opening Handshake
웹소켓은 처음에 HTTP 프로토콜을 사용해서 연결을 시작합니다.
클라이언트가 서버에 연결 요청을 보낼 때 "나는 웹소켓을 사용하고 싶어!" 하며 요청을 보내며 이 요청은 일반적인 HTTP 요청과 비슷하지만 업그레이드 헤더(Upgrade: websocket) 등의 정보를 포함합니다.
서버가 이 요청을 보고 "좋아, 웹소켓 연결을 수락할게!"라고 응답하면 HTTP가 아니라 WS(웹소켓 프로토콜)로 통신이 전환됩니다.
HandShake(핸드셰이크) : 클라이언트와 서버 간의 연결을 설정하는 과정을 의미합니다.
2️⃣ Data Transfer
웹소켓 연결이 성공적으로 이루어지면, 클라이언트와 서버는 양방향 통신을 진행할 수 있습니다. 데이터는 프레임(frame)이라는 단위로 전송됩니다. 프레임은 데이터 전송 과정에서 가장 작은 단위의 데이터로, 작은 헤더와 payload로 구성되어 있습니다.
3️⃣ Closing Handshake
데이터 전송이 끝나면 클라이언트나 서버 중 한쪽이 연결 종료 요청을 보내고, 이를 승인하면 응답으로 Close 프레임이 전송되며 연결이 종료됩니다.
Socket.IO는 웹소켓 기술을 활용하는 라이브러리로, 웹소켓을 지원하지 않는 브라우저에서는 롱폴링 방식으로 대체하여 통신을 도와줍니다.
롱폴링은 웹소켓 이전에 사용되던 HTTP 기반의 실시간 통신 방식 중 하나로, 이 외에 폴링, 서버 센트 이벤트가 있습니다.
폴링은 클라이언트가 주기적으로 서버에 요청을 보내는 방식입니다. 일정 시간마다 새로운 데이터가 있는지 요청을 보내므로 서버는 새로운 데이터가 없어도 응답을 보냅니다. 따라서 불필요한 요청으로 인해 서버의 부담이 증가합니다.
롱폴링은 폴링 보다 조금 더 개선된 방식입니다.
클라이언트가 서버에 요청을 보낸 이후 서버는 새로운 데이터가 없다면 응답을 하지 않고, 새로운 데이터가 있을 때까지 기다립니다. 일정 시간 동안 새로운 데이터가 없다면 Time Out이 발생하고 Time Out에 대한 응답을 보냅니다.
폴링 방식보다는 서버의 부담이 줄겠지만 불필요한 요청을 하는 것은 여전하며, 요청과 응답 사이의 대기시간이 길어질 수 있습니다.
서버 센트 이벤트는 HTTP 통신을 종료하지 않고 연결을 유지하는 방식으로 클라이언트는 최초로 한 번 서버에 연결을 요청합니다. 서버는 요청을 받고 새로운 데이터가 생길 때마다 클라이언트에게 응답을 보냅니다.
이 방식은 클라이언트의 최초 요청 이후 서버만 응답을 하는 단방향 통신을 진행합니다.
위와 같은 방법들로 실시간 통신을 구현할 수 있지만 웹소켓은 연결이 유지되는 동안 필요한 데이터만 실시간으로 주고받을 수 있기 때문에 폴링이나 롱폴링처럼 추가적인 HTTP 요청을 반복적으로 보내는 비효율적인 방식이 필요 없으며, 서버 센트 이벤트와는 다르게 단방향이 아닌, 양방향 통신을 지원합니다.
하지만, 이렇게 양방향 실시간 통신에서 강점을 보이는 웹소켓에도 한계점은 존재합니다. 하지만 Socket.IO를 통해 웹소켓을 더 효율적으로 사용할 수 있습니다.
1️⃣ HTTP long-polling as fallback : 웹 소켓은 HTML5 표준 기술로 HTML5를 지원하지 않는 환경에서는 사용할 수 없어, Socket.IO는 웹소켓을 지원하지 않는 브라우저에서는 롱폴링 방식으로 대체하여 통신을 도와줍니다.
2️⃣ Automatic Reconnection : 네트워크가 불안정하거나 에러로 인해 연결이 끊어졌을 때 웹소켓은 자동으로 재연결을 시도하지 않아 로직을 직접 구현해야 하지만, Socket.IO는 자동으로 재연결을 시도합니다.
3️⃣ 그 외에도 Emitting events, Packet Buffering, Broadcasting 등 다양한 기능을 제공 합니다.
공식문서에 나와있는 방법을 정리하여 소개해 보겠습니다.
1. Socket.IO install
npm install socket.io socket.io-client
2. 서버 설정 : 프로젝트의 root 위치에 server.js 파일 생성
import { createServer } from "node:http";
import next from "next";
import { Server } from "socket.io";
const dev = process.env.NODE_ENV !== "production";
const hostname = "localhost";
const port = 3000;
// when using middleware `hostname` and `port` must be provided below
const app = next({ dev, hostname, port });
const handler = app.getRequestHandler();
app.prepare().then(() => {
const httpServer = createServer(handler);
const io = new Server(httpServer);
io.on("connection", (socket) => {
// ...
});
httpServer
.once("error", (err) => {
console.error(err);
process.exit(1);
})
.listen(port, () => {
console.log(`> Ready on http://${hostname}:${port}`);
});
});
3. package.json에 서버 실행 적용
{
"scripts": {
"dev": "node server.js",
"start": "NODE_ENV=production node server.js",
}
}
4. 클라이언트에서 사용할 Socket.IO 설정
"use client";
import { io } from "socket.io-client";
export const socket = io('엔드포인트');
5. 통신 구현
"use client";
import { useEffect, useState } from "react";
import { socket } from "../socket";
export default function Home() {
const [isConnected, setIsConnected] = useState(false);
const [transport, setTransport] = useState("N/A");
useEffect(() => {
if (socket.connected) {
onConnect();
}
function onConnect() {
setIsConnected(true);
setTransport(socket.io.engine.transport.name);
socket.io.engine.on("upgrade", (transport) => {
setTransport(transport.name);
});
}
function onDisconnect() {
setIsConnected(false);
setTransport("N/A");
}
socket.on("connect", onConnect);
socket.on("disconnect", onDisconnect);
return () => {
socket.off("connect", onConnect);
socket.off("disconnect", onDisconnect);
};
}, []);
return (
<div>
<p>Status: { isConnected ? "connected" : "disconnected" }</p>
<p>Transport: { transport }</p>
</div>
);
}
설정이 완료되면, Next.js 환경에서 실시간 양방향 통신을 진행하는 웹을 개발할 수 있습니다 😁
웹소켓에 대해 공부하며 파고들수록 OSI 계층, TCP, 소켓, HTTP keep alive 등... 연계되는 네트워크 지식들이 많았습니다. 모두 정리하기엔 글이 너무 길어지고 저의 역량이 부족하기도 해서 웹소켓에 대한 것만 집중해서 작성했는데, 가능하다면 관련된 시리즈물을 작성해 보고 싶습니다.