본문 바로가기

WebSocket과 실시간 통신

폴링·SSE·WebSocket의 통신 방향과 비용을 비교하고 핸드셰이크·프레임·연결 관리로 실시간 기능을 구현합니다.

HTTP는 기본적으로 클라이언트가 요청을 열고 서버가 응답을 돌려주는 모델입니다.

서버가 브라우저 쪽에 임의의 순간 새 연결을 열어 데이터를 밀어 넣는 구조가 아니기 때문에, 채팅, 실시간 알림, 주식 시세처럼 지연이 짧은 양방향 통신에서는 단순한 요청-응답만으로 한계가 드러납니다.


HTTP로 실시간을 흉내 내는 방법들

짧은 주기로 계속 물어보는 polling은 구현이 쉽지만 빈 응답과 HTTP 헤더 비용이 큽니다.

Long polling은 서버가 새 이벤트가 생길 때까지 응답을 늦춰 낭비를 줄이지만, 응답이 끝날 때마다 다시 요청을 열어야 합니다.

SSE는 서버에서 클라이언트로 흐르는 이벤트 스트림에 잘 맞고, WebSocket은 한 번 연결한 뒤 같은 연결에서 양쪽이 독립적으로 메시지를 보낼 수 있습니다.


WebSocket 핸드셰이크

브라우저의 WebSocket 연결은 보통 HTTP/1.1 Upgrade 메커니즘으로 시작합니다.

wss://를 쓰면 이 핸드셰이크도 TLS 연결 위에서 이뤄집니다.

HTTP/1.1 Upgrade 뒤 같은 연결의 WebSocket 메시지

같은 HTTP/1.1 연결에서 Upgrade를 요청합니다. 서버도 독립적으로 메시지를 보낼 수 있습니다. 각 단계의 값과 전환 이유를 비교합니다.

WebSocket · HTTP/1.1 경로

101 전환 뒤에는, 양쪽이 독립적으로 메시지를 보냅니다

HTTP/1.1 Upgrade에 성공하고 클라이언트가 핸드셰이크 응답을 검증한 예입니다. 이후 두 메시지는 요청·응답 한 쌍일 필요가 없습니다. HTTP/2·3의 Extended CONNECT는 이 그림에 포함하지 않습니다.

HTTP/1.1 Upgrade 뒤 같은 연결의 WebSocket 메시지HTTP/1.1 WebSocket 핸드셰이크가 성공한 뒤 클라이언트와 서버가 각각 텍스트 메시지를 보내는 예시입니다. HTTP/2와 HTTP/3의 Extended CONNECT 경로는 제외합니다. 애플리케이션 메시지 두 개가 반드시 요청과 응답 한 쌍이라는 뜻은 아닙니다.GET /chat HTTP/1.1Upgrade: websocket101 Switching Protocols클라이언트가 Accept 검증텍스트 메시지클라이언트 → 서버 · 마스킹텍스트 메시지서버 → 클라이언트클라이언트서버
HTTP/1.1 Upgrade 뒤 같은 연결의 WebSocket 메시지클라이언트가HTTP/1.1 GET과Upgrade를보내고 서버의101응답을Accept값으로검증한뒤, 클라이언트→서버마스킹텍스트와서버→클라이언트텍스트를보내는예입니다. 아래두메시지는의무적인요청·응답쌍이아닙니다. 시간은위에서아래로읽고 HTTP/2·3의ExtendedCONNECT와실제시간간격은제외합니다.클라이언트서버GET /chatHTTP/1.1Upgrade: websocket101Switching ProtocolsAccept 검증텍스트 메시지클라이언트 → 서버마스킹텍스트 메시지서버 → 클라이언트

위에서 아래로 시간 순서이며 간격은 실제 시간이 아닙니다. 청록색은 101 전환 응답입니다. 서버→클라이언트 메시지의 점선은 방향 구분이며 응답 의무를 뜻하지 않습니다. Accept와 클라이언트 마스킹은 사용자 인증·암호화가 아니며 wss는 TLS를 사용합니다.

이 예의 조건

  • 본문과 전체 도표의 예를 따라 Upgrade 요청부터 서버 메시지까지 진행합니다.
관계도의 보충 설명
전체 도표의 표기·관계·조건
구분전체 도표의 내용
관계 설명클라이언트가 HTTP/1.1 GET과 Upgrade를 보내고 서버의 101 응답을 Accept 값으로 검증한 뒤, 클라이언트→서버의 마스킹 텍스트와 서버→클라이언트의 텍스트를 보내는 예입니다. 아래 두 메시지는 의무적인 요청·응답 쌍이 아닙니다. 시간은 위에서 아래로 읽고 HTTP/2·3의 Extended CONNECT와 실제 시간 간격은 제외합니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
Upgrade 요청

같은 HTTP/1.1 연결에서 Upgrade를 요청합니다.

  • 연결·메시지 1: GET /chat
  • 연결·메시지 2: Upgrade websocket
  • 클라이언트가 /chat 요청과 WebSocket 전환 헤더를 송신
101 확인

101 응답과 예상 Accept 값을 검증합니다.

  • 연결·메시지 1: 101
  • 연결·메시지 2: 예상 Accept 일치
  • 검증한 성공 응답만 전환으로 수락
클라이언트 메시지

클라이언트가 마스킹한 메시지를 보냅니다.

  • 연결·메시지 1: client→server masking
  • WebSocket 메시지 전달; HTTP 요청 형태는 끝남
서버 메시지

서버도 독립적으로 메시지를 보낼 수 있습니다.

  • 연결·메시지 1: server→client text
  • 의무적인 요청·응답 쌍이 아님; wss의 TLS 조건은 별도

HTTP/1.1 Upgrade 뒤 같은 연결의 WebSocket 메시지

같은 HTTP/1.1 연결에서 Upgrade를 요청합니다. 서버도 독립적으로 메시지를 보낼 수 있습니다. 각 단계의 값과 전환 이유를 비교합니다.

WebSocket · HTTP/1.1 경로

101 전환 뒤에는, 양쪽이 독립적으로 메시지를 보냅니다

HTTP/1.1 Upgrade에 성공하고 클라이언트가 핸드셰이크 응답을 검증한 예입니다. 이후 두 메시지는 요청·응답 한 쌍일 필요가 없습니다. HTTP/2·3의 Extended CONNECT는 이 그림에 포함하지 않습니다.

HTTP/1.1 Upgrade 뒤 같은 연결의 WebSocket 메시지HTTP/1.1 WebSocket 핸드셰이크가 성공한 뒤 클라이언트와 서버가 각각 텍스트 메시지를 보내는 예시입니다. HTTP/2와 HTTP/3의 Extended CONNECT 경로는 제외합니다. 애플리케이션 메시지 두 개가 반드시 요청과 응답 한 쌍이라는 뜻은 아닙니다.GET /chat HTTP/1.1Upgrade: websocket101 Switching Protocols클라이언트가 Accept 검증텍스트 메시지클라이언트 → 서버 · 마스킹텍스트 메시지서버 → 클라이언트클라이언트서버
HTTP/1.1 Upgrade 뒤 같은 연결의 WebSocket 메시지클라이언트가HTTP/1.1 GET과Upgrade를보내고 서버의101응답을Accept값으로검증한뒤, 클라이언트→서버마스킹텍스트와서버→클라이언트텍스트를보내는예입니다. 아래두메시지는의무적인요청·응답쌍이아닙니다. 시간은위에서아래로읽고 HTTP/2·3의ExtendedCONNECT와실제시간간격은제외합니다.클라이언트서버GET /chatHTTP/1.1Upgrade: websocket101Switching ProtocolsAccept 검증텍스트 메시지클라이언트 → 서버마스킹텍스트 메시지서버 → 클라이언트

위에서 아래로 시간 순서이며 간격은 실제 시간이 아닙니다. 청록색은 101 전환 응답입니다. 서버→클라이언트 메시지의 점선은 방향 구분이며 응답 의무를 뜻하지 않습니다. Accept와 클라이언트 마스킹은 사용자 인증·암호화가 아니며 wss는 TLS를 사용합니다.

이 예의 조건

  • 본문과 전체 도표의 예를 따라 Upgrade 요청부터 서버 메시지까지 진행합니다.

관계도의 보충 설명

전체 도표의 표기·관계·조건
구분전체 도표의 내용
관계 설명클라이언트가 HTTP/1.1 GET과 Upgrade를 보내고 서버의 101 응답을 Accept 값으로 검증한 뒤, 클라이언트→서버의 마스킹 텍스트와 서버→클라이언트의 텍스트를 보내는 예입니다. 아래 두 메시지는 의무적인 요청·응답 쌍이 아닙니다. 시간은 위에서 아래로 읽고 HTTP/2·3의 Extended CONNECT와 실제 시간 간격은 제외합니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
Upgrade 요청

같은 HTTP/1.1 연결에서 Upgrade를 요청합니다.

  • 연결·메시지 1: GET /chat
  • 연결·메시지 2: Upgrade websocket
  • 클라이언트가 /chat 요청과 WebSocket 전환 헤더를 송신
101 확인

101 응답과 예상 Accept 값을 검증합니다.

  • 연결·메시지 1: 101
  • 연결·메시지 2: 예상 Accept 일치
  • 검증한 성공 응답만 전환으로 수락
클라이언트 메시지

클라이언트가 마스킹한 메시지를 보냅니다.

  • 연결·메시지 1: client→server masking
  • WebSocket 메시지 전달; HTTP 요청 형태는 끝남
서버 메시지

서버도 독립적으로 메시지를 보낼 수 있습니다.

  • 연결·메시지 1: server→client text
  • 의무적인 요청·응답 쌍이 아님; wss의 TLS 조건은 별도

Sec-WebSocket-Accept는 클라이언트가 보낸 키에 매직 문자열 258EAFA5-E914-47DA-95CA-C5AB0DC85B11을 결합하고 SHA-1 해시를 취한 뒤 Base64로 인코딩한 값입니다.

이 값은 서버가 WebSocket 핸드셰이크를 이해했는지 확인하기 위한 장치이지, 사용자 인증이나 암호화 기능은 아닙니다.

예시: Accept는 사용자 비밀번호가 아닙니다 RFC 6455의 예시 키 dGhlIHNhbXBsZSBub25jZQ== 뒤에 위 매직 문자열을 그대로 붙여 SHA-1·Base64를 적용하면 s3pPLMBiTxaQ9kYGzzhZRbK+xOo=가 됩니다. 키를 먼저 Base64 디코딩한 바이트에 매직 문자열을 붙이는 계산과는 다릅니다. HTTP 상태가 101이어도 Accept가 예상 값과 다르면 브라우저는 연결을 받아들이지 않습니다. 공개된 계산을 누구나 할 수 있으므로 이 일치는 사용자가 로그인했다거나 전송이 암호화됐다는 증거가 아닙니다.

HTTP/2에서는 일반적인 Upgrade 헤더 대신 RFC 8441의 Extended CONNECT 방식으로 WebSocket을 시작할 수 있습니다.

다만 클라이언트, 서버, 프록시, 로드 밸런서가 모두 지원해야 하므로 실무에서는 인프라 지원 여부를 확인해야 합니다.


WebSocket 프레임 구조

WebSocket은 메시지를 하나 이상의 프레임으로 나눠 보낼 수 있습니다.

클라이언트가 서버로 보내는 프레임은 마스킹이 필수이며, 이는 중간 장비 오동작을 줄이기 위한 프로토콜 장치이지 암호화가 아닙니다.

Opcode의미
0x0Continuation: 앞서 시작한 메시지의 연속 프레임
0x1Text: UTF-8 텍스트 메시지
0x2Binary: 바이너리 메시지
0x8Close: 연결 종료
0x9Ping: 연결 확인 등의 제어 메시지
0xAPong: Ping 응답 또는 비요청 알림

제어 프레임은 분할할 수 없고 페이로드는 최대 125바이트입니다.

프레임 길이를 독립적으로 계산하기 확장을 사용하지 않고 UTF-8 변환 뒤 125 B인 텍스트를 클라이언트가 한 프레임으로 보냅니다. 기본 헤더 2 B와 마스킹 키 4 B를 더하면 WebSocket 프레임은 몇 B인가요? 본문이 126 B로 늘면 무엇이 더 필요할까요? 같은 126 B를 Ping의 페이로드로 보낼 수 있을까요?

본문 길이와 제어 프레임 제한을 구분한 풀이

125 B 텍스트는 2+4+125=131 B입니다. 126 B 텍스트는 길이 표지가 126이고 뒤에 2 B 확장 길이를 추가하므로 2+2+4+126=134 B입니다. 마스킹 키는 본문 길이에 포함하지 않습니다. TCP·TLS·IP와 확장 데이터는 이 계산에서 제외했습니다. 프레임 길이 형식과 달리 Ping은 제어 프레임의 최대 125 B 조건에 걸리므로 126 B를 보낼 수 없습니다. Ping을 나누어 제한을 피하는 것도 허용되지 않습니다.


WebSocket 구현

websocket_server.py
import asyncio
import websockets
from websockets.exceptions import ConnectionClosed

connected = set()

async def handler(websocket):
    connected.add(websocket)
    try:
        async for message in websocket:
            # 보낸 사람을 제외한 클라이언트에게 순차 전송
            for ws in connected.copy():
                if ws != websocket:
                    try:
                        await ws.send(message)
                    except ConnectionClosed:
                        pass
    finally:
        connected.discard(websocket)

async def main():
    async with websockets.serve(handler, "0.0.0.0", 8080):
        print("WebSocket server on ws://0.0.0.0:8080")
        await asyncio.Future()  # 서버 유지

asyncio.run(main())

접속 집합을 복사해 await 사이의 접속·종료가 순회를 깨뜨리지 않게 하고, 복사 후 종료된 수신자는 건너뜁니다. 공식 broadcast 문서에서 설명하듯 순차 send는 느린 수신자의 버퍼가 비워질 때까지 다른 전송도 기다립니다. 이 학습 예제에는 인증·Origin 검사·재연결·메시지 재생이나 수신자별 대기열 정책이 구현되어 있지 않습니다.

websocket_client.js
const ws = new WebSocket("ws://localhost:8080");

ws.onopen = () => {
  console.log("Connected");
  ws.send("Hello!");
};

ws.onmessage = (event) => {
  console.log("Received:", event.data);
};

ws.onclose = () => {
  console.log("Disconnected");
};

ws.onerror = (error) => {
  console.error("Error:", error);
};

원래 코드의 수신자를 먼저 예측하기 서버에 A만 연결돼 있고 A가 Hello!를 보낸다면 수신자는 없습니다. handler는 보낸 사람을 제외하므로 에코 서버처럼 A에게 그대로 돌려주지 않습니다. A가 계속 연결된 상태에서 B도 접속해 서버의 connected 집합에 들어간 뒤 B의 onopen이 Hello!를 보내면 A는 그 메시지를 받지만 B 자신은 받지 않습니다. B 접속 전의 A 메시지는 보관해 뒀다가 B에게 보내지 않습니다. 이 순서와 성공한 send를 가정한 코드 추론이며 새 서버·브라우저 실행 로그가 아닙니다.

받은 순서로 독립 추론하기 A·B·C가 모두 집합에 있고 종료나 전송 오류가 없을 때 A가 메시지를 보냅니다. 어느 클라이언트가 받을까요? B와 C 중 누가 먼저 받는지도 이 코드만으로 확정할 수 있나요?

수신 대상과 순회 순서를 나누는 풀이

B와 C가 대상이고 A는 제외됩니다. 집합의 복사본 순회에는 B·C의 고정 순서 계약이 없으므로 먼저 받는 클라이언트를 확정하지 않습니다. 먼저 순회한 수신자의 send가 지연되면 다음 send도 시작을 기다린다는 문제는 순서가 무엇이든 남습니다. 재접속한 수신자에게 누락 메시지를 재생하는 저장소도 원래 코드에 없습니다.


SSE (Server-Sent Events)

모든 실시간 통신에 WebSocket이 필요한 것은 아닙니다.

SSE(Server-Sent Events)는 HTTP 응답을 text/event-stream으로 유지하면서 서버에서 클라이언트로 이벤트를 흘려보냅니다.

SSE는 브라우저의 EventSource가 재연결과 Last-Event-ID 전달을 도와주므로 알림·피드·진행률에 적합합니다. 누락 이벤트 복구에는 서버의 ID 부여·보관·재생 처리가 별도로 필요합니다.

반대로 클라이언트도 높은 빈도로 메시지를 보내야 하거나 바이너리 데이터를 주고받아야 한다면 WebSocket이 더 자연스럽습니다.

방식방향·연결·복구와 사용처
WebSocketws://·wss:// 연결에서 양방향 텍스트·바이너리를 주고받습니다. 채팅·게임·협업에 적합하며 재연결은 애플리케이션이 구현합니다. HTTP/2·3 전송에는 인프라 지원이 필요합니다.
SSE일반 HTTP 응답의 UTF-8 이벤트 스트림입니다. 브라우저가 재연결을 돕고 알림·피드에 적합하지만, 누락 복구는 서버 재생 정책에 달려 있습니다.
Long pollingHTTP 응답이 끝나면 새 요청을 엽니다. 응답 형식에 따라 바이너리도 가능하며 이전 환경과의 호환에 활용합니다.

세 방식 모두 프록시의 버퍼링·유휴 시간 제한·연결 종료 정책을 확인해야 합니다. SSE라는 이유만으로 즉시 전달이 보장되지는 않습니다.

Socket.IO의 역할

Socket.IO는 단순한 WebSocket wrapper라기보다 자체 클라이언트와 서버 프로토콜을 가진 실시간 통신 라이브러리입니다.

Socket.IO의 기본 연결 순서는 HTTP long polling으로 연결한 뒤 WebSocket 업그레이드를 시도하는 방식입니다. 현재 지원 전송에는 WebTransport도 있으며, 순서와 대체 시도는 클라이언트 옵션·서버 지원에 따라 달라집니다.

Socket.IO는 재연결, heartbeat, 방(room), namespace, acknowledgement 같은 기능을 제공하지만, 순수 WebSocket 클라이언트와 그대로 호환되는 프로토콜은 아닙니다.

양쪽 모두 Socket.IO를 쓰는 애플리케이션인지, 아니면 표준 WebSocket API만 필요한지 먼저 구분해야 합니다.

운영에서는 heartbeat와 유휴 제한, 재접속 간격, 인증 만료 시 연결 종료, 메시지 ID에 의한 중복 처리 여부를 함께 정합니다. TCP 연결 유지나 재연결 성공은 메시지 전달 완료와 별개입니다.

다음 장에서는 코드를 넘어 실무 네트워크 인프라 — CDN, 로드 밸런서, 프록시, 클라우드 네트워크 — 를 다루겠습니다.