Cloudflare worker - Websocket 구현

정지현·2025년 5월 19일
0

Websocket으로 무언가를 만들어 보고 싶다.


A photo by Sherwin Ker from Unsplash

클라우드플레어로 공짜로 웹소켓 구조를 구현해 본다. Stock dashboard 앞으로 채팅, 실시간 협업이나 게임에서 활용할 수 있을 것이다.

또 흥미로운 개발 주제가 무엇이 있을 까 하니 예전에 WebRTC 클라이언트를 만들어 영상 정보를 화면에 띄우고 녹화를 위해 서버에 스트리밍 했던 것이 기억난다. 예전에 한 번 시도했지만 성공하지 못했던 웹소켓 구조를 한 번 다시 만들어보려고 했다. 간단히 주식 정보를 실시간 대쉬보드 앱을 통해서 구현해 보기로 한다.

  1. 서빙하는 메인서버와 웹소켓 서버는 따로 존재한다.
  2. Vercel의 디플로이 공간인 Vercel functions에서는 스테이트가 필요한 웹소켓을 지원하지 않는다. 추가적인 데이터 스토리지와의 연동 구현이 필요하다.

따라서 Nextjs에는 앱을 올리게 된다면 웹소켓 자체는 다른 공간에 존재해야 할 필요가 있다.

Cloudflare에서는 가능하다고 하던데..

  • Cloudflare의 서버리스 플랫폼인 Worker는 Nextjs의 Function과는 다르게 웹소켓을 자체적으로 지원한다고 해서 한번 확인해 보기로 한다.

[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,

});

}
  • 공식 docs의 예제에서 확인할 수 있는 것은, WebSocketPair라는 워커의 자체적인 인스턴스를 사용한다는 것이다.
  • 해당 인스턴스는 클라이언트와 서버를 동시에 리턴하고 서버는 그대로 워커에서 사용, 클라이언트는 요청으로 주며 클라이언트에서는 해당 요청을 기반으로 이벤트리스너를 붙이는 방향으로 진행할 수 있었다.

세션 & Hibernation

위 작업으로 얻은 wss 엔드포인트는 하나 뿐이다. 한 명 이상이 웹 앱에 접속해서 다른 소켓 요청을 날린다면 어떻게 될까? 유저를 구별할 수 있는 세션과 id가 필요하지 않을까?

이를 해결하기 위해서는 Durable Object 라고 해서 워커에서 지원하는 스테이트가 필요했다. Docs에서는 '유니크한 세션ID 하나 당 싱글턴 인스턴스를 하나씩 유지'해 준다고 한다.

또한 지금 주식 대쉬보드의 유즈 케이스에서는 필요없지만, 앞으로 무조건 필요할 듯한 것까지 구현하였다. 웹소켓과 같은 실시간 기능을 구현하는 경우 가장 쉽게 생각할 수 있는 유저 스토리는 바로 이럴 것이다:

  1. 유저가 웹소켓 연결을 시작한다.
  2. 유저는 오랜 기간 동안 아무 행동을 하지 않는다.

위 경우에 대해 확인해볼 가치는 충분하다고 생각한다. 이 경우 리소스적인 부하는 어떨까? 클라우드에서 서비스하는 앱의 경우 연결을 유지하는 것의 가격은 얼마정도일까?

  • Cloudflare에서는 이러한 케이스를 해결하기 위해서 Hibernation api 라는 것을 제공한다.
  • 해당 api는 위의 Durable object를 사용한다.

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");

}

}
  • 특별히 디버깅에 시간을 들이지 않고 Docs의 내용만으로 개발할 수 있었다.
  • 인스턴스 세션 및 하이버네이션의 경우 클라우드플레어로 아주 간단하게 해결할 수 있었다. 직접 만들 경우 꽤 오래 걸리지 않을까?
  • 이외에도 생각해야 할 것들이 꽤 있을 것으로 보인다. 보안과 관련된 경우는 어떨까? 엔드포인트가 노출될 수 밖에 없으므로 그 부분을 생각해야 할 것 같다.

References

profile
Can an old dog learn new tricks?

0개의 댓글