10만 연결에서 배운 교훈: SSE가 WebSocket보다 메모리를 40% 덜 쓴다는 것
모든 게 당연해 보였던 선택
실시간 대시보드를 만들 때 WebSocket을 고르는 건 거의 공식처럼 굳어져 있습니다. 실시간 가격, 재고량, 알림을 화면에 띄워야 한다면 답은 WebSocket. 튜토리얼도 그렇게 쓰여 있고, 포럼의 시니어들도 입을 모아 같은 얘기를 하니까요.
저희 팀도 그 '당연함'을 한 번도 의심하지 않았습니다. 1만 연결까지는 멀쩡했고, 3만 연결에서도 그래프는 평온했거든요. 문제는 10만 연결 부하 테스트에서 터졌습니다. 메모리 사용량이 어느 순간 수직으로 치솟더니 시스템이 그대로 멈춰버렸어요.
그 뒤로 4개월. 삽질과 약 3,200달러의 불필요한 컴퓨팅 비용을 치르고 나서야 깨달았습니다. 문제는 튜닝이 아니라, 처음에 던졌어야 할 질문을 안 던진 것이었다는 걸요.

WebSocket vs SSE: 구조적 차이가 비용을 만든다
둘의 가장 큰 차이는 HTTP를 그대로 쓰느냐 벗어나느냐입니다. 이 하나가 운영 비용의 갈림길을 만듭니다.
WebSocket (전이중)
클라이언트 <──────────────> 서버
송수신
[ 상태 유지, 사용자 지정 프로토콜 ]
SSE (반이중)
클라이언트 <────────────── 서버
수신 전용
[ 일반 HTTP, 브라우저 자동 재연결 ]
WebSocket은 HTTP 업그레이드 핸드셰이크를 거친 뒤 자체 프로토콜로 갈아탑니다. 그 순간부터 서버는 모든 소켓 상태를 직접 들고 있어야 하고, 핑/퐁 하트비트로 연결이 살아있는지 계속 확인해야 하죠. 연결 하나하나가 서버 입장에서는 관리해야 할 '살아있는 객체'가 됩니다.
SSE는 결이 다릅니다. 그냥 HTTP 응답을 닫지 않고 열어둔 채 텍스트 이벤트를 흘려보내는 방식이에요. 사용자 지정 프레임도, 복잡한 협상도 없습니다. 연결이 끊기면 브라우저가 알아서 다시 붙습니다. 기존 HTTP 스택 위에 그대로 얹히는 구조라, 새로 짊어질 상태가 적다는 게 핵심이고요.
숫자로 보는 현실
같은 환경에서 10만 연결 테스트를 돌린 결과입니다. EC2 c5.2xlarge 인스턴스, 클라이언트당 초당 1회 업데이트를 보내는 조건이었어요.
| 메트릭 | WebSocket | SSE | 차이 |
|---|---|---|---|
| 연결당 메모리 | ~3.5KB | ~1.2KB | -65% |
| 10만 연결 시 총 RAM | ~342MB | ~205MB | -40% |
| CPU 유휴 부하 | 18% | 11% | -39% |
| 재연결 로직 | 수동 구현 | 브라우저 내장 | 개발 편의성 ↑ |
메모리 40% 절약은 영리한 튜닝의 결과가 아닙니다. 구조에서 나온 차이예요. WebSocket은 연결마다 버퍼, 상태 머신, 프로토콜 메타데이터를 들고 있어야 하지만, SSE는 서버가 이미 갖고 있는 HTTP 처리 경로를 재활용하니까요. 연결 하나가 비싼 게 아니라, 연결 하나에 붙는 부속물이 비싼 겁니다.
가장 등골이 서늘했던 건 따로 있었습니다. WebSocket 10만 연결 상태에서 트래픽이 한 번 튀면 시스템이 완전히 멈춘다는 점이었어요. 느려지는 게 아니라 아예 다운입니다. 예상 복구 시간은 22분. 실시간 데이터가 서비스의 본질인 곳에서 22분의 공백은, 사실상 사고죠.
원인과 결과를 떼어놓고 보면 이렇습니다. 메모리 압박은 원인이고, 시스템 정지는 결과입니다. 그리고 그 압박의 뿌리는 "연결마다 상태를 들고 있어야 한다"는 WebSocket의 구조였고요.
실제 코드로 보는 차이
운영에서 실제로 쓴 코드를 비교하면 복잡성 차이가 눈에 들어옵니다.
SSE 서버 (더 간단함):
const http = require('http');
http.createServer((req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
});
const interval = setInterval(() => {
res.write(`data: ${JSON.stringify({price: getPrice()})}\n\n`);
}, 1000);
req.on('close', () => clearInterval(interval));
}).listen(8080);
SSE 클라이언트 (재연결 자동):
const src = new EventSource('/events');
src.onmessage = (e) => {
const { price } = JSON.parse(e.data);
updateUI(price);
};
외부 라이브러리도 없고, 재연결 로직을 따로 짤 일도 없습니다. 브라우저 스펙이 알아서 처리하거든요. 전환하면서 재시도 처리 코드 200줄 정도를 통째로 지웠는데, 팀 누구도 아쉬워하지 않았어요. 오히려 "이게 우리가 그동안 붙들고 있던 코드냐"는 반응이었죠. 운영 PM 입장에서 보면, 줄어든 코드는 줄어든 장애 지점이기도 합니다.

선택 기준: 결국 질문 하나
이 경험에서 건진 판단 기준은 단순합니다.
클라이언트가 데이터를 받기만 하고 서버로 보낼 게 없다면 SSE. 실시간 피드, 대시보드, 알림, 배포 진행률, 로그 스트리밍이 전부 여기 들어갑니다.
클라이언트가 서버로 데이터를 밀어 올려야 한다면 WebSocket. 채팅, 공동 편집, 멀티플레이어 게임처럼 양방향이 본질인 경우죠.
저희의 실수는 WebSocket을 고른 게 아니었습니다. 애초에 양방향 통신이 정말 필요한지 묻지 않은 것이었어요. 대시보드는 데이터가 한 방향으로만 흐르는데, 우리는 양방향 채널을 깔아놓고 안 쓰는 절반의 비용까지 부담하고 있었던 셈입니다.
돌아보면 기술 선택에서 가장 비싼 건 잘못된 도구가 아니라, 던지지 않은 질문이더라고요.
놓치기 쉬운 세부사항
SSE 얘기를 꺼내면 거의 반사적으로 따라붙는 반론이 "HTTP/1.1에서 도메인당 6개 연결 제한" 이슈입니다. 맞는 지적이긴 한데, 전제가 빠져 있어요. HTTP/2 환경에서는 이 제한이 사라집니다. 전송 계층에서 연결이 다중화되기 때문이죠.
2026년 현재 프로덕션 환경 대부분이 이미 HTTP/2 위에서 돌아갑니다. 그러니 이 제한은 실무에서 발목을 잡는 일이 거의 없어요. 서버 설정이 HTTP/2를 켜고 있는지만 한 번 확인하면 됩니다. 오래된 반론이 여전히 정론처럼 돌아다니는 건, 기술이 바뀌었는데 통념이 그대로 남아서고요.

마무리
4개월의 삽질과 3,200달러. 비싸게 산 교훈이지만, 지나고 보니 꽤 값진 수업료였습니다.
얻은 건 SSE라는 도구가 아니라 습관이었어요. '당연한' 선택 앞에서 한 번 멈춰 서서, 이 구조가 정말 우리 문제에 맞는지를 먼저 묻는 습관. 도구는 늘 바뀝니다. 바뀌지 않는 건, 표면의 인기가 아니라 구조를 보고 고르는 태도라고 생각합니다.