A photo by Sherwin Ker from Unsplash
클라우드플레어로 공짜로 웹소켓 구조를 구현해 본다. Stock dashboard 앞으로 채팅, 실시간 협업이나 게임에서 활용할 수 있을 것이다.
또 흥미로운 개발 주제가 무엇이 있을 까 하니 예전에 WebRTC 클라이언트를 만들어 영상 정보를 화면에 띄우고 녹화를 위해 서버에 스트리밍 했던 것이 기억난다. 예전에 한 번 시도했지만 성공하지 못했던 웹소켓 구조를 한 번 다시 만들어보려고 했다. 간단히 주식 정보를 실시간 대쉬보드 앱을 통해서 구현해 보기로 한다.
따라서 Nextjs에는 앱을 올리게 된다면 웹소켓 자체는 다른 공간에 존재해야 할 필요가 있다.
[Wokrer docs에 따르면]
WebSockets utilize an event-based system for receiving and sending messages, much like the Workers runtime model of responding to events.워커의 런타임은 웹소켓과 같은 이벤트 기반 시스템을 사용하기 때문에 호환해서 사용할 수 있다.
async function handleRequest(request) {
const webSocketPair = new WebSocketPair();
const [client, server] = Object.values(webSocketPair);
server.accept();
server.addEventListener('message', event => {
console.log(event.data);
});
return new Response(null, {
status: 101,
webSocket: client,
});
}
위 작업으로 얻은 wss 엔드포인트는 하나 뿐이다. 한 명 이상이 웹 앱에 접속해서 다른 소켓 요청을 날린다면 어떻게 될까? 유저를 구별할 수 있는 세션과 id가 필요하지 않을까?
이를 해결하기 위해서는 Durable Object 라고 해서 워커에서 지원하는 스테이트가 필요했다. Docs에서는 '유니크한 세션ID 하나 당 싱글턴 인스턴스를 하나씩 유지'해 준다고 한다.
또한 지금 주식 대쉬보드의 유즈 케이스에서는 필요없지만, 앞으로 무조건 필요할 듯한 것까지 구현하였다. 웹소켓과 같은 실시간 기능을 구현하는 경우 가장 쉽게 생각할 수 있는 유저 스토리는 바로 이럴 것이다:
위 경우에 대해 확인해볼 가치는 충분하다고 생각한다. 이 경우 리소스적인 부하는 어떨까? 클라우드에서 서비스하는 앱의 경우 연결을 유지하는 것의 가격은 얼마정도일까?
import { DurableObject } from "cloudflare:workers";
// Durable Object
export class WebSocketHibernationServer extends DurableObject {
async fetch(request) {
// Creates two ends of a WebSocket connection.
const webSocketPair = new WebSocketPair();
const [client, server] = Object.values(webSocketPair);
this.ctx.acceptWebSocket(server);
return new Response(null, {
status: 101,
webSocket: client,
});
}
async webSocketMessage(ws, message) {
// Upon receiving a message from the client, reply with the same message,
// but will prefix the message with "[Durable Object]: " and return the
// total number of connections.
ws.send(
`[Durable Object] message: ${message}, connections: ${this.ctx.getWebSockets().length}`,
);
}
async webSocketClose(ws, code, reason, wasClean) {
// If the client closes the connection, the runtime will invoke the webSocketClose() handler.
ws.close(code, "Durable Object is closing WebSocket");
}
}