브라우저 기반 통신 총정리, HTTP부터 WebRTC까지, 어디에 부하가 가는가
HTTP/1.1·2·3, Long Polling, Server-Sent Events, WebSocket, WebRTC, WebTransport를 Stateless/Stateful 관점에서 정리하고, 클라이언트·서버·네트워크 부하를 실측 수치로 비교한다.
시작, 왜 이렇게 많은 통신 방식이 있나
웹은 처음엔 단순했다. 브라우저가 페이지를 요청하면 서버가 HTML을 돌려주는 것. 요청 한 번, 응답 한 번. HTTP/1.0의 모든 것.
그런데 시간이 흐르며 요구사항이 늘어났다.
- “여러 자원을 동시에 받고 싶다” → HTTP/1.1 keep-alive, HTTP/2 multiplexing
- “서버가 먼저 알려주고 싶다” → Long Polling, Server-Sent Events
- “양쪽이 자유롭게 주고받고 싶다” → WebSocket
- “서버를 거치지 말고 둘이 직접 연결되고 싶다” → WebRTC
- “HTTP/3 위에서 더 가볍게 양방향 stream이 필요하다” → WebTransport
각 통신 방식은 직전 방식의 한계를 푸는 한 걸음씩이다. 이 글에서는 각 방식의 통신 주체·프로토콜·내용·리소스 사용량 을 차례로 보고, “부하는 어디에 가는가” 라는 질문에 답하는 한 장의 표와 실측 차트로 정리한다.
먼저 모든 선택의 첫 단추인 Stateless vs Stateful 부터.
한 단계 뒤로, Stateless vs Stateful 이라는 첫 단추
Stateless vs Stateful 의 선택은 통신 방식 선택보다 먼저 와야 한다. 이 결정이 수평 확장 전략·인프라 복잡도·운영 비용을 결정하기 때문이다.
| 측면 | Stateless | Stateful |
|---|---|---|
| 서버 메모리 | 요청 처리에만 필요 | 활성 세션마다 메모리 |
| 수평 확장 | 매우 쉬움, 어느 서버든 OK | Sticky Session 필요 |
| 장애 복원 | 서버 교체 무영향 | 세션 손실 (외부 store 시 완화) |
| 로드밸런서 | 단순 round-robin | client → server 일관 라우팅 |
| 캐싱 | URL 기반 캐시 효율적 | 캐시 효율 낮음 |
각 통신 방식의 상태성:
| 방식 | Stateless / Stateful |
|---|---|
| HTTP (1.1/2/3) | Stateless |
| HTTP + 쿠키/JWT | 상태를 클라이언트 에 위임한 Stateless |
| Long Polling | Stateless (각 요청 독립) |
| Server-Sent Events | Stateful (지속 연결, 단방향) |
| WebSocket | Stateful (지속 연결, 양방향) |
| WebRTC | Stateful (P2P 세션) |
IMPORTANT
“Stateful 한 통신을 쓰겠다”는 결정은 곧 “서버 메모리에 연결 상태가 N개 쌓일 것을 감수하고, Sticky Session 또는 외부 라우팅 인프라를 운영하겠다” 는 결정과 같다. 단순히 “기능적으로 필요하다”는 이유만으로 가볍게 선택하면 운영 비용이 따라온다.
이 관점을 가지고 HTTP부터 보자.
기본 HTTP, Stateless 의 정수
HTTP/1.1: TCP 한 연결 위에서 순차적 요청·응답
가장 익숙한 형태. 브라우저가 GET/POST 등의 요청을 보내면 서버가 응답한다.
GET /api/users HTTP/1.1
Host: example.com
Accept: application/json
---
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 42
{"users":[{"id":1,"name":"alice"}]}
Keep-Alive, 연결을 재사용하는 첫 번째 시도
HTTP/1.0 의 큰 문제는 요청마다 TCP 연결을 새로 맺는 것이었다. TCP 3-way handshake에 1 RTT, TLS handshake에 1-2 RTT, 모바일 네트워크에서 100~300 ms 가 매 요청마다 추가됐다.
HTTP/1.1 은 기본적으로 Connection: keep-alive 를 활성화해 한 TCP 연결을 여러 요청에 재사용한다.
GET /api/users HTTP/1.1
Host: example.com
Connection: keep-alive ← (HTTP/1.1 기본값이라 생략 가능)
GET /api/orders HTTP/1.1
Host: example.com
Connection: keep-alive ← 같은 TCP 연결로 다음 요청
서버는 응답 후 연결을 닫지 않고 일정 시간 대기 (Keep-Alive: timeout=5 같은 헤더로 표현). 이 시간 내에 같은 클라이언트가 또 요청하면 핸드셰이크 비용 없이 처리.
한계 1, Pipelining 의 실패: HTTP/1.1 의 pipelining (응답을 기다리지 않고 다음 요청 보내기)은 거의 모든 프록시에서 비활성화됨. 실무에선 같은 연결의 다음 요청은 이전 응답을 기다려야 한다.
한계 2, Head-of-Line Blocking: 한 연결 안에서는 응답이 순서대로 와야 한다. 첫 응답이 5초 걸리면 그 연결의 두 번째 응답은 5초 뒤에야 시작.
한계 3, 도메인당 6 연결: 브라우저는 같은 호스트에 최대 6~8 개의 TCP 연결만 동시에 유지한다. 자원이 많은 페이지(이미지 100장)는 6 줄 줄로 받아야 함.
이걸 푸는 게 HTTP/2.
HTTP/2: 한 연결에서 multiplexing
HTTP/2 는 단일 TCP 연결 안에서 여러 stream 을 동시에 주고받는다. 단순히 “효과”를 외우기보다 각 기능이 무엇을 하는지 부터 보자.
Multiplexing, frame 단위 인터리브
한 TCP 연결을 여러 stream 으로 나누고, 각 stream 의 데이터를 잘게 frame 으로 쪼개 인터리브해 전송한다. 각 frame 헤더에 stream ID 가 붙어 어느 stream 의 것인지 식별한다.
1 TCP 연결
├ Stream 1 (CSS): frame ─ frame ─ frame
├ Stream 3 (JS): frame ─ frame
└ Stream 5 (Image): frame ─ frame ─ frame ─ frame
서버는 빠른 응답(작은 CSS)을 먼저 보내고, 느린 응답(이미지)을 그 사이사이에 끼워 보낼 수 있다.
→ 효과: HTTP/1.1 의 도메인당 6 연결 한계 + 연결 내 HOL Blocking 모두 해소. 동시 요청이 많은 페이지에서 큰 이득.
HPACK 헤더 압축
HTTP/1.1 은 매 요청마다 같은 헤더(User-Agent, Cookie, Accept-Encoding 등)를 텍스트로 반복 전송. 한 페이지 로드에 헤더만 수십 KB.
HPACK 은 두 단계로 압축한다.
- Static Table:
:method GET,:path /같은 자주 쓰이는 헤더를 미리 정의된 인덱스(1~61) 로 치환 - Dynamic Table: 처음 본 헤더는 인덱스에 등록 → 다음부터는 인덱스만 송신
- Huffman 인코딩: 텍스트 부분도 Huffman 코드로 압축
→ 효과: 페이지 로드 시 헤더 크기 80~95% 절약. 모바일 (TCP slow start 단계) 에서 특히 큼.
Stream Priority
각 stream 에 우선순위와 의존 관계를 부여 (PRIORITY frame). 서버는 우선순위 높은 stream 의 frame 부터 송신.
브라우저:
- main.css (priority 256, blocking)
- app.js (priority 220, blocking)
- hero.png (priority 100, dependent on main.css)
→ 효과: 같은 대역폭에서 critical rendering path 가 먼저 도착 → First Contentful Paint 가 빨라진다.
Server Push, 왜 deprecated 되었나
서버가 클라이언트가 요청하지 않은 자원도 미리 push 하는 기능이었다.
GET /index.html
→ 서버: "어차피 main.css 도 곧 요청할 거잖아? 미리 보낼게"
→ PUSH_PROMISE frame + main.css 내용 함께 송신
이론상: HTML 파싱 → CSS 발견 → 요청 → 응답 의 RTT 한 번을 절약.
실제로 deprecated 된 이유:
- 캐시 미스를 모름: 서버는 클라이언트가 이미 캐시에 main.css 를 가지고 있는지 알 수 없다 → 불필요한 중복 전송이 빈번
- 우선순위 충돌: push 된 자원이 실제 critical 자원의 대역폭을 빼앗는 경우 발생
- 구현 복잡도: 서버가 “어떤 자원을 push 할지” 정확히 판단하기 어려움
- 103 Early Hints 가 더 나음: 같은 효과를 캐시 친화적으로 달성
Chrome 은 2022년 (M106) HTTP/2 Server Push 지원을 제거했다. 대신 103 Early Hints 를 권장한다, 서버가 최종 응답 전에 “곧 필요할 자원 URL 목록”을 미리 알려주면 브라우저가 자기 캐시 상태를 보고 직접 요청하는 방식.
남은 한계: TCP HOL Blocking
HTTP/2 가 HTTP 레벨 의 Head-of-Line Blocking 을 풀었지만, TCP 레벨 의 HOL Blocking 은 그대로다. 한 TCP 패킷이 손실되면, 그 패킷이 어느 stream 에 속하든 TCP 는 순서 유지를 위해 모든 stream 의 후속 패킷을 대기시킨다.
흥미로운 실측: 요청 수가 적을 때 HTTP/2 가 HTTP/1.1 보다 오히려 느릴 수 있다. 단일 대용량 요청은 HTTP/1.1 의 한 연결이 HTTP/2 의 multiplexing 오버헤드보다 가볍다. 100개 동시 요청 같은 시나리오에서 HTTP/2 의 진가가 발휘된다.
HTTP/3: QUIC 위에서
HTTP/3 는 TCP 가 아니라 UDP 기반의 QUIC 프로토콜을 쓴다.
- Stream 격리: 한 stream 의 패킷 손실이 다른 stream 에 영향 안 줌, TCP HOL Blocking 의 본질적 해결
- 0-RTT resumption: 재접속 시 첫 패킷에 데이터 동봉
- Connection migration: 와이파이 → 셀룰러 전환 시 IP 가 바뀌어도 같은 연결 유지
- TLS 1.3 통합: 핸드셰이크에 보안 내장 (1 RTT)
브라우저 코드는 그대로다.
const response = await fetch('https://example.com/api/users');
const data = await response.json();
어떤 프로토콜 버전이 사용되는지는 서버 설정과 네트워크 환경이 결정한다. 개발자 도구 Network 탭 Protocol 열에 h2, h3 으로 표시된다.
HTTP 통합 비교, 어디에 부하가 가나
HTTP 자체는 Stateless 다. 서버가 stateless 라는 건 수평 확장이 매우 단순하다는 뜻이다. 트래픽 증가 시 서버 인스턴스를 늘리고 로드밸런서가 round-robin 분배하면 끝.
| 버전 | 클라이언트 | 서버 | 네트워크 |
|---|---|---|---|
| HTTP/1.1 | 6 연결 관리, 핸드셰이크 다수 | Stateless, 매우 가벼움 | 헤더 반복 + HOL 영향 |
| HTTP/2 | 1 연결 + stream 관리 | Stateless, 가벼움 | 멀티플렉싱 효율적 |
| HTTP/3 | QUIC stack (CPU 약간 더) | Stateless, 가벼움 | UDP 차단 환경에서 폴백 |
REST API, 정적 자원 fetch, 일회성 폼 제출 등 거의 모든 비실시간 요청이 HTTP 로 충분하다.
문제는 “서버가 알려주고 싶을 때”다. HTTP의 요청-응답 모델은 클라이언트가 먼저 요청해야만 응답이 가능하다. 그래서 등장한 첫 번째 우회 방법이 polling.
Polling, HTTP 위에서 실시간 흉내내기
Short Polling: 매 N초마다 묻기
setInterval(async () => {
const res = await fetch('/api/notifications');
const events = await res.json();
events.forEach(showNotification);
}, 3000);
- 구현 간단, HTTP 그대로 (여전히 Stateless 서버)
- 단점: 대부분의 요청은 빈 응답. 매 요청마다 TCP 핸드셰이크(또는 keep-alive 점유) + 헤더 비용
- 부하: 클라이언트 1000명이 5초마다 polling → 서버는 초당 200 RPS 처리. 빈 응답이 대부분
Long Polling: 서버가 응답을 보류
async function longPoll() {
while (true) {
const res = await fetch('/api/notifications/wait');
const event = await res.json();
if (event) showNotification(event);
}
}
longPoll();
서버는 새 이벤트가 발생할 때까지 응답을 잡고 있다가, 발생하면 즉시 응답한다.
- 장점: 빈 응답 거의 없음
- 단점: 서버가 connection 을 길게 점유 → 연결 수 = 동시 클라이언트 수
- 상태: HTTP 의미상 Stateless 지만, 운영 입장에선 TCP 연결 점유가 Stateful 과 비슷한 부담
위 애니메이션이 Short Polling, Long Polling, 그리고 다음에 볼 SSE, WebSocket 의 메시지 흐름 차이를 한눈에 보여준다.
Polling 의 부하 특성
c5.xlarge EC2 (4 vCPU, 8GB RAM) 에서 10K 동시 연결 측정 (BirJob 2026):
- Long Polling 메모리: 1.8 GB (연결당 ~180 KB)
- Long Polling CPU: 45% (constant 재연결 처리)
이 수치를 다음 절들의 SSE/WebSocket 수치와 비교하면 차이가 명확해진다.
Server-Sent Events, 서버가 스트림으로 push
SSE 는 사실상 하나의 HTTP 응답이 영원히 끝나지 않는 방식이다. HTTP/1.1 위에서 동작하지만 Stateful 한 연결을 유지한다.
const es = new EventSource('/api/stream');
es.onmessage = (e) => {
console.log('event:', e.data);
};
es.addEventListener('user-joined', (e) => {
const user = JSON.parse(e.data);
showUser(user);
});
es.onerror = () => {
// 자동으로 재연결 시도됨
};
서버 측 응답 포맷:
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
event: user-joined
data: {"id": 1, "name": "alice"}
id: 42
data: hello
id: 43
retry: 5000
각 이벤트는 data: , event: , id: , retry: 같은 필드로 구성되고 빈 줄로 구분된다.
SSE 의 자동 재연결 + Last-Event-ID
가장 큰 장점이다. EventSource 는 연결이 끊어지면 자동으로 재시도하고, 마지막으로 받은 이벤트 ID 를 Last-Event-ID 헤더로 보낸다. 서버는 그 ID 이후의 이벤트만 다시 보내면 된다.
// Node.js + Express 예시
app.get('/api/stream', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const lastId = parseInt(req.header('Last-Event-ID') ?? '0', 10);
const events = getEventsSince(lastId);
events.forEach((evt) => {
res.write(`id: ${evt.id}\n`);
res.write(`event: ${evt.type}\n`);
res.write(`data: ${JSON.stringify(evt.payload)}\n\n`);
});
const onNewEvent = (evt) => {
res.write(`id: ${evt.id}\n`);
res.write(`data: ${JSON.stringify(evt.payload)}\n\n`);
};
eventBus.on('new', onNewEvent);
req.on('close', () => eventBus.off('new', onNewEvent));
});
이 자동 재연결 + 상태 복원이 WebSocket 에는 없어서 직접 구현해야 하는 부분이다.
SSE 의 부하 특성 (실측)
- 연결당 메모리: ~1.2 KB (Ark Protocol 2026) ~ ~60 KB (Wolf-tech 2026, Node.js net layer 포함)
- 10K 동시 연결 총 메모리: ~600 MB (BirJob 2026)
- CPU idle: 8% (BirJob 2026)
- 연결당 idle 대역폭: ~0.1 KB/keepalive
- 메시지 지연 p50/p99: 12ms / 45ms
WebSocket 보다 SSE 가 메모리 35-40% 적은 이유: SSE 는 HTTP 스택을 그대로 쓰므로 WebSocket 의 frame buffer + state machine + protocol framing metadata 같은 추가 자료구조가 필요 없다. 구조적으로 가벼움이지 최적화 결과가 아니다.
SSE 가 잘 맞는 시나리오
- 실시간 알림 (새 메시지, 댓글)
- 진행률 표시 (파일 처리, 빌드 로그)
- 주가/스코어 ticker
- LLM 응답 스트리밍 (ChatGPT, Claude, Gemini 모두 SSE)
도메인당 6개 연결 한계의 메커니즘
브라우저는 같은 호스트(도메인+포트 조합)에 대해 동시 TCP 연결을 6~8개로 제한한다. 이 숫자는 표준이 아니라 브라우저 구현 기본값 (Chrome/Edge 6, Firefox 6, Safari 6).
이 한계가 SSE 에 특히 치명적인 이유:
- SSE 한 stream = TCP 연결 1개를 영구 점유. 응답이 끝나지 않으므로 연결이 자유롭게 되지 않는다.
- 같은 호스트에 SSE 6개를 열면 7번째부터 다른 모든 요청 (이미지, API call, 다른 SSE) 도 큐에서 대기.
- 사용자가 같은 사이트를 여러 탭에 열면 각 탭이 자기 SSE 를 열려고 시도 → 7번째 탭부터 영구 대기 (탭이 안 보일 정도).
왜 6개 제한이 존재하나, 역사적 이유. HTTP/1.x 는 연결당 동시 하나의 요청만 처리 가능 (pipelining 실패). 만약 무제한이면 한 사용자가 서버 자원을 독점할 수 있어 RFC 2616 권장은 2개, 실무 브라우저는 6~8개로 절충.
해결책 3가지:
- HTTP/2 이상으로 운영, 단일 TCP 연결에서 multiplexing 으로 6개 한계 완전 우회. 가장 권장.
- 여러 서브도메인 분산,
sse1.example.com,sse2.example.com등 (구식 해결책, 인증서/CORS 부담) - SharedWorker, 모든 탭이 공유하는 단일 worker 가 SSE 연결 1개만 유지, 메시지를 각 탭에 분배
TIP
대부분의 모던 인프라(Cloudflare, AWS CloudFront, Vercel 등) 는 HTTP/2 또는 HTTP/3 를 기본 제공하므로 이 한계는 실무에서 거의 만나지 않는다. 단 사내 레거시 서버 / 일부 프록시 / Nginx 미튜닝 환경에서는 여전히 발생.
SSE 의 한계는 명확하다, 단방향이다. 양방향이 필요하면 다음 단계.
WebSocket, 한 번 열고 양방향
WebSocket 은 HTTP 핸드셰이크로 시작해서 프로토콜을 통째로 바꾸는 방식이다. 명확히 Stateful 통신.
핸드셰이크: HTTP Upgrade
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
---
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
101 Switching Protocols 이후로는 같은 TCP 연결을 WebSocket frame 형식으로 사용한다. 더 이상 HTTP 가 아니다.
브라우저 API
const ws = new WebSocket('wss://example.com/chat');
ws.onopen = () => {
ws.send('hello');
ws.send(new Uint8Array([1, 2, 3]));
};
ws.onmessage = (e) => {
if (typeof e.data === 'string') {
console.log('text:', e.data);
} else {
console.log('binary:', e.data);
}
};
ws.onclose = () => {
// 자동 재연결 없음, 직접 처리
setTimeout(connect, 1000);
};
Frame 구조
WebSocket 메시지는 frame 이라는 단위로 송수신된다. 한 frame 헤더는 2~14 bytes 로 매우 작다 (HTTP 헤더 수백 바이트와 대조). 작은 메시지를 자주 주고받기에 최적.
WebSocket 이 직접 처리해야 하는 것들
| 기능 | SSE | WebSocket |
|---|---|---|
| 자동 재연결 | ✓ | ✗ (직접 구현 또는 라이브러리) |
| 마지막 이벤트 복원 | ✓ (Last-Event-ID) | ✗ |
| Heartbeat / Keep-alive | 별도 ping 불필요 | 필요 (proxy timeout 방지) |
| 메시지 직렬화 | 자동 (text) | 직접 (JSON.stringify 등) |
이래서 실무에서는 Socket.IO 같은 라이브러리를 자주 쓴다. 재연결, 룸/네임스페이스, 폴백, 메시지 ACK 등을 제공.
서버 구현 예시
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', (ws) => {
ws.on('message', (data) => {
wss.clients.forEach((client) => {
if (client !== ws && client.readyState === ws.OPEN) {
client.send(data);
}
});
});
// Keep-alive ping (proxy idle timeout 방지)
const pingInterval = setInterval(() => {
if (ws.readyState === ws.OPEN) ws.ping();
}, 30000);
ws.on('close', () => clearInterval(pingInterval));
});
WebSocket 의 부하 특성 (실측)
| 측정 | 값 | 출처 |
|---|---|---|
| 연결당 메모리 (raw ws) | ~3.5 KB | Ark Protocol 2026 |
| 연결당 메모리 (Socket.IO) | ~60-90 KB | Wolf-tech 2026 |
| 10K 동시 연결 총 메모리 (raw) | ~400 MB | BirJob 2026 |
| 5K 동시 연결 (Socket.IO) | ~400-500 MB | Wolf-tech 2026 |
| 100K 연결 (raw) | ~342 MB | Ark Protocol c5.2xlarge |
| CPU idle (10K 연결) | 5% | BirJob 2026 |
| 메시지 지연 p50/p99 | 8ms / 22ms | BirJob 2026 |
Stateful 의 진짜 비용은 메모리가 아니라 운영 복잡도다. 수평 확장하려면:
- Sticky Session: 로드밸런서가 같은 클라이언트를 같은 서버로
- Pub/Sub Bus: 서버 간 메시지 라우팅 (Socket.IO + Redis adapter)
- Backpressure: 클라이언트가 느릴 때 서버 send buffer 가 차오름 → 명시적 관리
WebSocket 시나리오
- 채팅
- 협업 도구 (커서 위치, 실시간 편집)
- 게임 (실시간 입력)
- 거래 시스템
CAUTION
“양방향이니까 그냥 WebSocket 쓰자”는 흔한 함정이다. 단방향이면 SSE 가 훨씬 단순하고 부담이 적다. WebSocket 의 stateful 특성은 인프라 복잡도를 크게 늘린다.
WebRTC, 서버를 거치지 않는 P2P
WebRTC 는 두 브라우저가 서로 직접 연결되는 방식이다. 단, 만남 자체는 서버를 한 번 거쳐야 한다.
세 가지 서버 역할
- Signaling Server: 두 피어가 서로의 정보(SDP)를 주고받기 위한 통로
- STUN Server: 각 피어가 자기의 공인 IP/포트를 알아내는 데 사용
- TURN Server: 직접 연결 실패 시 데이터 중계
STUN 은 가볍지만 TURN 은 모든 데이터가 통과하므로 비용이 크다.
SDP Offer/Answer 교환
// Peer A (Caller)
const pc = new RTCPeerConnection({
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
],
});
const dataChannel = pc.createDataChannel('chat');
dataChannel.onopen = () => dataChannel.send('hello!');
dataChannel.onmessage = (e) => console.log('peer:', e.data);
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
signaling.send({ type: 'offer', sdp: offer });
signaling.on('answer', async ({ sdp }) => {
await pc.setRemoteDescription(sdp);
});
pc.onicecandidate = (e) => {
if (e.candidate) signaling.send({ type: 'ice', candidate: e.candidate });
};
signaling.on('ice', ({ candidate }) => pc.addIceCandidate(candidate));
DataChannel: SCTP over DTLS over UDP
RTCDataChannel 은 SCTP 를 사용한다. SCTP 는 다음을 설정할 수 있다:
ordered: 순서 보장 여부 (기본 true)maxRetransmits: 재전송 횟수 (UDP 스타일은 0)maxPacketLifeTime: 패킷 유효 시간
게임처럼 “약간 잃어도 좋으니 빠르게” 가 필요하면 unordered + maxRetransmits 0 (사실상 UDP)으로 설정. 파일 전송처럼 정확성이 중요하면 기본값.
모든 통신은 DTLS 로 자동 암호화된다, UDP 위에서 동작하는 TLS 변형.
WebRTC 부하 특성
- 시그널링 서버: 만남 단계만 부담 (수 kB SDP + 수 십개 ICE candidate 메시지)
- STUN 서버: 매우 가벼움 (단순 응답)
- TURN 서버: 통화 1쌍에 양방향 대역폭, 가장 비싼 인프라
- 클라이언트: ICE 후보 수집, 암호화/복호화, 미디어 인코딩
IMPORTANT
TURN 의존도가 높으면 서버 부담이 다시 커진다. 모바일 사용자가 많고 NAT 가 엄격한 환경(통신사망 뒤)에서는 50~80% 가 TURN 을 거친다는 보고도 있다. WebRTC 가 서버 비용을 0 으로 만들지는 않는다.
WebRTC 시나리오
- 영상/음성 통화
- 화면 공유
- 파일 직접 전송 (대용량을 서버 거치지 않고)
- P2P 게임
WebTransport, HTTP/3 위의 양방향 stream (미래)
WebTransport 는 HTTP/3 QUIC 위에서 양방향 stream + datagram 을 제공하는 새 API.
const wt = new WebTransport('https://example.com/wt');
await wt.ready;
const stream = await wt.createBidirectionalStream();
const writer = stream.writable.getWriter();
await writer.write(new TextEncoder().encode('hello'));
const datagramWriter = wt.datagrams.writable.getWriter();
await datagramWriter.write(new Uint8Array([1, 2, 3]));
WebSocket 대비:
- 다중 stream: 한 연결에서 여러 독립 stream
- Datagram: UDP 스타일 unreliable 전송도 가능
- 연결 마이그레이션: QUIC 의 성질 그대로
브라우저 지원이 아직 부분적이지만, WebSocket 의 후속으로 자리잡을 가능성이 크다.
벤치마크, 실측 차트
여기까지 본 부하 수치를 한눈에. 데이터는 BirJob 2026 c5.xlarge 10K 측정 + Ark Protocol 2026 c5.2xlarge 100K 측정 + Wolf-tech 2026 Next.js production 의 종합.
10,000 동시 연결에서 서버 메모리
연결당 메모리 오버헤드
CPU 사용량 (10K idle 연결)
메시지 지연 (p50 / p99)
차트의 한 줄 결론
- 메모리: Long Polling >> WebSocket(Socket.IO) > SSE > WebSocket(raw)
- CPU: Long Polling >> SSE > WebSocket
- 지연: Long Polling은 사실상 실시간 X. SSE/WebSocket 만 실시간
벤치마크 코드는 별도 프로젝트로 분리, /Users/[user]/0099-web-comm-bench (Node.js + ws + 모든 4 가지 엔드포인트 + 5초 메모리 스냅샷).
종합 비교, 어디에 부하가 가나
| 방식 | 통신 주체 | 프로토콜 | 상태 | 클라이언트 | 서버 | 네트워크 | 부하 위치 |
|---|---|---|---|---|---|---|---|
| HTTP/1.1/2/3 | Client ↔ Server | TCP/UDP | Stateless | 낮음 | 낮음 | 헤더 + RTT | 서버 분산 |
| Short Polling | Client → Server | HTTP | Stateless | 낮음 | 중간 | 낭비 큼 | 서버 분산 (빈 응답) |
| Long Polling | Client → Server | HTTP | Stateless* | 낮음 | 높음 | 효율적 | 서버 연결 풀 |
| SSE | Server → Client | HTTP | Stateful | 낮음 | 중간 | 효율적 | 서버 연결 + 6 연결 한계 |
| WebSocket | Client ↔ Server | TCP frame | Stateful | 중간 | 높음 | 매우 효율적 | 서버 메모리 + Sticky Session |
| WebRTC | Peer ↔ Peer | SCTP/DTLS/UDP | Stateful | 높음 | 시그널링만 | P2P 직접 | 클라이언트 + P2P, TURN 시 서버 |
| WebTransport | Client ↔ Server | QUIC | Stateful | 중간 | 중간 | HTTP/3 효율 | HTTP/3 인프라 |
*Long Polling 은 HTTP 의미상 Stateless 지만 운영상 연결 점유가 Stateful 과 비슷.
시나리오별 선택 가이드
| 시나리오 | 권장 |
|---|---|
| REST API · 정적 자원 | HTTP/2 또는 HTTP/3 |
| 알림 push (한쪽 방향) | SSE, 단순 + 자동 재연결 |
| LLM 응답 스트리밍 | SSE, 사실상 표준 |
| 채팅 (양방향) | WebSocket + 라이브러리 |
| 실시간 협업 (Figma, Notion) | WebSocket, CRDT/OT 메시지 |
| 게임 (실시간 입력) | WebSocket 또는 WebRTC DataChannel |
| 영상/음성 통화 | WebRTC |
| 대용량 파일 P2P | WebRTC DataChannel |
| 미래 지향 양방향 + datagram | WebTransport |
잘못된 선택이 비용이 되는 패턴
- “알림 받아야 하니까 WebSocket” → SSE 면 충분한데 인프라 복잡도 늘림
- “채팅이니까 polling” → 클라이언트 100명 × 1초 polling = 서버 6000 RPS 빈 응답
- “P2P니까 WebRTC” → 시그널링 + STUN/TURN 인프라가 의외로 큰 비용
- “HTTP/3이 좋다니까 무조건 HTTP/3” → 사내망/특정 환경에선 UDP 가 막혀 fallback
정리
브라우저 통신은 한 방향으로 발전해왔다.
“요청-응답”이라는 HTTP의 본질적 제약을 어떻게 우회하느냐, 그리고 “서버에 얼마나 의존하느냐”
| 방식 | 우회 방법 | 서버 의존도 |
|---|---|---|
| HTTP | 우회 안 함 | 모든 트래픽 |
| Polling | 자주 요청 | 모든 트래픽 (낭비 많음) |
| Long Polling | 응답 보류 | 모든 트래픽 (효율적) |
| SSE | 응답을 영원히 안 끝냄 | 모든 트래픽 (단방향) |
| WebSocket | HTTP 졸업, 양방향 프로토콜 | 모든 트래픽 |
| WebRTC | 만남만 서버, 데이터는 P2P | 시그널링만 |
| WebTransport | HTTP/3 위 다중 stream | 모든 트래픽 (효율적) |
선택은 결국 “부하를 어디에 두고 싶은가”의 문제다.
IMPORTANT
“최신이니까 좋다”는 함정이다. WebSocket 한 줄로 끝낼 알림을 WebRTC 로 만들면 운영 비용이 10 배로 늘 수 있다. 반대로 영상 통화를 WebSocket 으로 만들면 서버 대역폭이 폭발한다.
요구사항 → Stateless vs Stateful 결정 → 데이터 흐름 방향 → 부하 위치 선택 → 통신 방식 결정 순서로 생각하면 거의 틀리지 않는다.
ℹ️ 이 글은 MDN Web Docs, RFC (HTTP/2 RFC 7540, HTTP/3 RFC 9114, WebSocket RFC 6455, QUIC RFC 9000, HPACK RFC 7541), web.dev 의 WebRTC 문서, 그리고 2026년 실측 벤치마크 자료(BirJob, Ark Protocol, Wolf-tech)를 참조하여 AI(Claude)와 함께 작성 되었습니다.
참고 자료
- MDN, HTTP/3
- MDN, Server-Sent Events
- MDN, WebSocket API
- MDN, WebRTC Signaling and video calling
- web.dev, WebRTC DataChannels
- Salesforce, HTTP/2 와 Head-of-Line Blocking
- RxDB, WebSockets vs SSE vs Long-Polling vs WebRTC vs WebTransport
- Ably, WebSockets vs SSE
- BirJob, Real-Time Systems with WebSockets, SSE, Long Polling
- Ark Protocol, WebSockets vs SSE at 100K Connections
- Wolf-tech, Next.js 15 SSE vs WebSockets vs Polling Decision Matrix 2026
- RFC 6455, The WebSocket Protocol
- RFC 9114, HTTP/3
- RFC 9000, QUIC
이 글의 용어 (11개)
- [Network] HTTP/3: QUIC 위의 HTTP, 0-RTT, connection migrationnetwork
- 정의 HTTP/3 (2022, RFC 9114) 는 HTTP 의 전송 레이어를 TCP → QUIC 으로 교체한 버전. UDP 위에 QUIC, QUIC 위에 HTTP. 핵심 동기:…
- DTLSnetwork
- 정의 DTLS (Datagram Transport Layer Security)는 위에서 동작하는 의 변형. 데이터그램 (UDP 패킷) 기반 통신에 보안을 제공한다. 와 WebRT…
- Head-of-Line Blockingnetwork
- 정의 Head-of-Line Blocking (HOLB) 은 처리 순서가 정해진 큐에서 맨 앞 항목의 지연이 그 뒤 항목 전체의 지연으로 전파되는 현상이다. 네트워크 프로토콜에서…
- HPACKnetwork
- 정의 HPACK (RFC 7541)은 HTTP/2 에서 요청·응답 헤더를 압축하기 위해 설계된 방식. HTTP/1.1 의 헤더는 매 요청마다 텍스트로 반복 전송되었다 (한 페이지…
- Pub/Sub Busconcurrency
- 정의 Pub/Sub Bus (Publish-Subscribe bus)는 메시지 송신자(publisher)와 수신자(subscriber)가 서로를 모르고도 통신할 수 있게 해주는 …
- Statefulconcurrency
- 정의 Stateful (상태유지) 서버는 클라이언트와의 세션·연결 컨텍스트를 메모리에 유지한다. 후속 요청·이벤트는 이 컨텍스트를 활용해 처리된다. 같은 클라이언트의 요청은 같은…
- Statelessconcurrency
- 정의 Stateless (무상태) 서버는 클라이언트의 요청 간 상태를 기억하지 않는다. 각 요청은 완전히 독립적이며, 처리에 필요한 모든 정보는 요청 자체에 담겨 있어야 한다. …
- Sticky Sessionconcurrency
- 정의 Sticky Session (= Session Affinity)은 로드밸런서가 같은 클라이언트의 요청을 항상 같은 백엔드 서버로 라우팅하도록 하는 동작이다. 서버, 즉 세션…
- TCPnetwork
- 정의 TCP (Transmission Control Protocol)는 신뢰성 있는 연결 지향 전송 계층 프로토콜이다. RFC 9293 으로 정의되어 있다. HTTP/1.1·2,…
- TLS / SSLnetwork
- 정의 TLS (Transport Layer Security)는 (또는 위 ) 위에서 동작하는 암호화·인증 프로토콜이다. SSL 은 TLS 의 전신 (Netscape 1995, 더…
- UDPnetwork
- 정의 UDP (User Datagram Protocol)는 비연결·비신뢰 전송 계층 프로토콜이다. RFC 768 (1980) 로 정의되었다. 와 대비된다. "보내고 잊는다(fir…
💬 댓글