본문으로 건너뛰기
김신건의 로그

브라우저 기반 통신 총정리, 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 의 선택은 통신 방식 선택보다 먼저 와야 한다. 이 결정이 수평 확장 전략·인프라 복잡도·운영 비용을 결정하기 때문이다.

측면StatelessStateful
서버 메모리요청 처리에만 필요활성 세션마다 메모리
수평 확장매우 쉬움, 어느 서버든 OKSticky Session 필요
장애 복원서버 교체 무영향세션 손실 (외부 store 시 완화)
로드밸런서단순 round-robinclient → server 일관 라우팅
캐싱URL 기반 캐시 효율적캐시 효율 낮음

각 통신 방식의 상태성:

방식Stateless / Stateful
HTTP (1.1/2/3)Stateless
HTTP + 쿠키/JWT상태를 클라이언트 에 위임한 Stateless
Long PollingStateless (각 요청 독립)
Server-Sent EventsStateful (지속 연결, 단방향)
WebSocketStateful (지속 연결, 양방향)
WebRTCStateful (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 은 두 단계로 압축한다.

  1. Static Table: :method GET, :path / 같은 자주 쓰이는 헤더를 미리 정의된 인덱스(1~61) 로 치환
  2. Dynamic Table: 처음 본 헤더는 인덱스에 등록 → 다음부터는 인덱스만 송신
  3. 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 된 이유:

  1. 캐시 미스를 모름: 서버는 클라이언트가 이미 캐시에 main.css 를 가지고 있는지 알 수 없다 → 불필요한 중복 전송이 빈번
  2. 우선순위 충돌: push 된 자원이 실제 critical 자원의 대역폭을 빼앗는 경우 발생
  3. 구현 복잡도: 서버가 “어떤 자원을 push 할지” 정확히 판단하기 어려움
  4. 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.16 연결 관리, 핸드셰이크 다수Stateless, 매우 가벼움헤더 반복 + HOL 영향
HTTP/21 연결 + stream 관리Stateless, 가벼움멀티플렉싱 효율적
HTTP/3QUIC 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 에 특히 치명적인 이유:

  1. SSE 한 stream = TCP 연결 1개를 영구 점유. 응답이 끝나지 않으므로 연결이 자유롭게 되지 않는다.
  2. 같은 호스트에 SSE 6개를 열면 7번째부터 다른 모든 요청 (이미지, API call, 다른 SSE) 도 큐에서 대기.
  3. 사용자가 같은 사이트를 여러 탭에 열면 각 탭이 자기 SSE 를 열려고 시도 → 7번째 탭부터 영구 대기 (탭이 안 보일 정도).

왜 6개 제한이 존재하나, 역사적 이유. HTTP/1.x 는 연결당 동시 하나의 요청만 처리 가능 (pipelining 실패). 만약 무제한이면 한 사용자가 서버 자원을 독점할 수 있어 RFC 2616 권장은 2개, 실무 브라우저는 6~8개로 절충.

해결책 3가지:

  1. HTTP/2 이상으로 운영, 단일 TCP 연결에서 multiplexing 으로 6개 한계 완전 우회. 가장 권장.
  2. 여러 서브도메인 분산, sse1.example.com, sse2.example.com 등 (구식 해결책, 인증서/CORS 부담)
  3. 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 이 직접 처리해야 하는 것들

기능SSEWebSocket
자동 재연결✗ (직접 구현 또는 라이브러리)
마지막 이벤트 복원✓ (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 KBArk Protocol 2026
연결당 메모리 (Socket.IO)~60-90 KBWolf-tech 2026
10K 동시 연결 총 메모리 (raw)~400 MBBirJob 2026
5K 동시 연결 (Socket.IO)~400-500 MBWolf-tech 2026
100K 연결 (raw)~342 MBArk Protocol c5.2xlarge
CPU idle (10K 연결)5%BirJob 2026
메시지 지연 p50/p998ms / 22msBirJob 2026

Stateful 의 진짜 비용은 메모리가 아니라 운영 복잡도다. 수평 확장하려면:

  1. Sticky Session: 로드밸런서가 같은 클라이언트를 같은 서버로
  2. Pub/Sub Bus: 서버 간 메시지 라우팅 (Socket.IO + Redis adapter)
  3. Backpressure: 클라이언트가 느릴 때 서버 send buffer 가 차오름 → 명시적 관리

WebSocket 시나리오

  • 채팅
  • 협업 도구 (커서 위치, 실시간 편집)
  • 게임 (실시간 입력)
  • 거래 시스템

CAUTION

“양방향이니까 그냥 WebSocket 쓰자”는 흔한 함정이다. 단방향이면 SSE 가 훨씬 단순하고 부담이 적다. WebSocket 의 stateful 특성은 인프라 복잡도를 크게 늘린다.


WebRTC, 서버를 거치지 않는 P2P

WebRTC 는 두 브라우저가 서로 직접 연결되는 방식이다. 단, 만남 자체는 서버를 한 번 거쳐야 한다.

세 가지 서버 역할

  1. Signaling Server: 두 피어가 서로의 정보(SDP)를 주고받기 위한 통로
  2. STUN Server: 각 피어가 자기의 공인 IP/포트를 알아내는 데 사용
  3. 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 동시 연결에서 서버 메모리

10K 동시 연결 시 서버 메모리 (MB)
Long Polling이 SSE 대비 3배, WebSocket 대비 4.5배 메모리 사용 (c5.xlarge / 4 vCPU 8GB)

연결당 메모리 오버헤드

단일 연결당 평균 메모리 (KB)
Long Polling은 대기 중 connection의 buffer 점유로 가장 무겁다. SSE가 구조적으로 가장 가볍다.

CPU 사용량 (10K idle 연결)

10K 유휴 연결 시 CPU 사용률 (%)
Long Polling은 끊임없는 재연결 처리로 9배 CPU 사용. WebSocket이 가장 효율적.

메시지 지연 (p50 / p99)

메시지 전송 지연 (ms, log scale)
Long Polling은 polling 주기에 의존, 사실상 실시간이 아님. SSE/WebSocket은 ms 단위.

차트의 한 줄 결론

  • 메모리: 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/3Client ↔ ServerTCP/UDPStateless낮음낮음헤더 + RTT서버 분산
Short PollingClient → ServerHTTPStateless낮음중간낭비 큼서버 분산 (빈 응답)
Long PollingClient → ServerHTTPStateless*낮음높음효율적서버 연결 풀
SSEServer → ClientHTTPStateful낮음중간효율적서버 연결 + 6 연결 한계
WebSocketClient ↔ ServerTCP frameStateful중간높음매우 효율적서버 메모리 + Sticky Session
WebRTCPeer ↔ PeerSCTP/DTLS/UDPStateful높음시그널링만P2P 직접클라이언트 + P2P, TURN 시 서버
WebTransportClient ↔ ServerQUICStateful중간중간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
대용량 파일 P2PWebRTC DataChannel
미래 지향 양방향 + datagramWebTransport

잘못된 선택이 비용이 되는 패턴

  • 알림 받아야 하니까 WebSocket” → SSE 면 충분한데 인프라 복잡도 늘림
  • 채팅이니까 polling” → 클라이언트 100명 × 1초 polling = 서버 6000 RPS 빈 응답
  • P2P니까 WebRTC” → 시그널링 + STUN/TURN 인프라가 의외로 큰 비용
  • HTTP/3이 좋다니까 무조건 HTTP/3” → 사내망/특정 환경에선 UDP 가 막혀 fallback

정리

브라우저 통신은 한 방향으로 발전해왔다.

“요청-응답”이라는 HTTP의 본질적 제약을 어떻게 우회하느냐, 그리고 “서버에 얼마나 의존하느냐”

방식우회 방법서버 의존도
HTTP우회 안 함모든 트래픽
Polling자주 요청모든 트래픽 (낭비 많음)
Long Polling응답 보류모든 트래픽 (효율적)
SSE응답을 영원히 안 끝냄모든 트래픽 (단방향)
WebSocketHTTP 졸업, 양방향 프로토콜모든 트래픽
WebRTC만남만 서버, 데이터는 P2P시그널링만
WebTransportHTTP/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)와 함께 작성 되었습니다.


참고 자료

이 글의 용어 (11개)
DTLSnetwork
정의 DTLS (Datagram Transport Layer Security)는 UDP 위에서 동작하는 TLS의 변형이다. 데이터그램 기반 통신에 TLS와 동등한 보안 보장(서버…
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)가 서로를 모르고도 통신할 수 있게 해주는 …
QUICnetwork
정의 QUIC (Quick UDP Internet Connections)은 Google이 2012년 처음 제안하고 2021년 RFC 9000으로 표준화된 UDP 기반 전송 프로토…
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) 로 정의되었다. TCP 와 대비된다. "보내고 잊는다…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기