연결은 이렇게 열린다: 핸드셰이크 들여다보기
RFC 6455에 따르면 클라이언트는 Upgrade: websocket, Connection: Upgrade, 그리고 임의의 값인 Sec-WebSocket-Key 헤더가 담긴 HTTP GET 요청을 보냅니다. 서버가 받아들이면 상태 코드 101(Switching Protocols)과 함께 Sec-WebSocket-Accept 헤더를 돌려줍니다. 이 값은 클라이언트가 보낸 키 뒤에 RFC가 정한 고정 문자열(GUID) 258EAFA5-E914-47DA-95CA-C5AB0DC85B11을 이어 붙여 SHA-1 해시를 낸 뒤 base64로 인코딩한 것입니다. 상태 코드가 101이 아니거나 Accept 값이 맞지 않으면 연결은 성립하지 않습니다. 이 값은 '이 서버가 웹소켓 요청임을 알고 응답했다'는 확인용이며 암호화나 로그인 수단이 아닙니다.
직접 계산해 본 예시입니다. 키가 dGhlIHNhbXBsZSBub25jZQ==이면 위 규칙대로 만든 Accept 값은 s3pPLMBiTxaQ9kYGzzhZRbK+xOo=입니다(Node.js로 검산).
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=연결 뒤에는 무엇이 오갈까: 프레임 구조
업그레이드 이후 데이터는 '프레임'으로 오갑니다. RFC 6455의 기본 프레임에는 메시지의 마지막 조각을 알리는 FIN 비트, 확장용 RSV1~3 비트(확장이 정의하지 않으면 0), 4비트 opcode, MASK 비트, 페이로드 길이가 들어 있습니다. opcode 0x1은 텍스트, 0x2는 바이너리이고, 제어용으로 0x8(닫기), 0x9(Ping), 0xA(Pong)가 있습니다. 길이는 7비트, 또는 7+16비트, 또는 7+64비트로 표현되고 가능한 한 짧은 형태를 써야 합니다.
눈여겨볼 규칙이 둘 있습니다. 첫째, 클라이언트가 서버로 보내는 모든 프레임은 마스킹해야 하고 서버가 보내는 프레임은 마스킹하지 않습니다. 마스킹되지 않은 클라이언트 프레임을 받은 서버는 연결을 닫아야 합니다. 둘째, 제어 프레임(닫기·Ping·Pong)의 페이로드는 125바이트 이하이며 쪼갤 수 없습니다. Ping을 받은 쪽은 Pong으로 답해야 하므로, 연결이 살아 있는지 확인하는 용도로 쓰입니다.
HTTP 폴링과 무엇이 다른가
| 방식 | 누가 먼저 보내나 | 요청마다 헤더 | 적합한 경우 |
|---|---|---|---|
| 짧은 폴링 | 클라이언트만 (주기적으로 질문) | 매번 전체 HTTP 헤더 | 갱신이 드물고 단순한 화면 |
| 롱 폴링 | 클라이언트가 요청, 서버가 이벤트 때까지 응답 지연 | 응답마다 새 요청과 헤더 | 웹소켓을 쓸 수 없는 환경의 우회 |
| SSE | 서버 → 클라이언트 한 방향 | 연결 시 한 번 | 알림·시세처럼 일방향 스트림 |
| 웹소켓 | 양쪽 모두 | 핸드셰이크 뒤에는 짧은 프레임 헤더 | 채팅·게임·공동 편집 같은 양방향 상호작용 |
SSE(Server-Sent Events)는 text/event-stream 형식으로 서버가 계속 데이터를 흘려보내는 기술로, 브라우저에서는 EventSource API로 받습니다. Ian Hickson이 2004년 WHATWG 문서에서 처음 명세했고 W3C는 2015년 2월 3일 권고안으로 발표했습니다(위키백과 기준). 한 방향이면 충분한 서비스에서는 웹소켓보다 구성이 단순합니다.
사례로 보기
사례 1. 표준이 만들어지기까지: 프록시와 부딪힌 초안
웹소켓은 처음부터 지금의 형태가 아니었습니다. 위키백과에 따르면 2010년 중반의 hixie-76 초안은 헤더 뒤에 8바이트를 Content-Length로 알리지 않고 덧붙이는 방식이어서 일부 역방향 프록시가 이를 처리하지 못했습니다. 이후 초안은 그 키 정보를 Sec-WebSocket-Key 헤더로 옮겨 문제를 피했습니다. 지금도 일부 프록시는 웹소켓 트래픽을 막거나 망가뜨릴 수 있고, 암호화된 wss 트래픽이 평문 ws보다 더 안정적으로 통과하는 경우가 많다고 같은 문서는 적습니다. 프로토콜이 인터넷 중간 장비와 함께 동작해야 한다는 점이 설계에 반영된 사례이며, 서비스에서 가능하면 wss를 쓰는 이유를 보여 줍니다.
사례 2. 출처 확인을 빠뜨리면: Cable Haunt (2020)
웹소켓 요청은 브라우저의 동일 출처 정책 제한을 받지 않습니다. 그래서 서버가 요청의 Origin 헤더를 확인하지 않으면, 다른 사이트의 스크립트가 사용자의 브라우저를 통해 웹소켓 서버에 접속하는 '교차 사이트 웹소켓 하이재킹'이 가능해집니다. 위키백과는 실제 사례로 2020년 공개된 Cable Haunt를 들며, 서버가 Origin을 검증하고 민감한 데이터에는 토큰을 쓰라고 권합니다. 배울 점은 단순합니다. 웹소켓 서버를 만든다면 연결을 허용할 출처를 명시적으로 제한하고, 연결이 열렸다는 사실만으로 로그인 상태를 믿지 말아야 합니다.
사례 3. 가상 예시: 폴링이 만드는 트래픽
아래는 실제 서비스가 아니라 가상 예시입니다. 접속자 1,000명이 5초마다 서버에 새 메시지가 있는지 묻고, 요청과 응답의 HTTP 헤더가 합쳐 800바이트라고 가정하겠습니다(가정치). 그러면 초당 1,000 ÷ 5 = 200번의 질문이 들어오고, 본문이 비어 있어도 헤더만으로 초당 160,000바이트, 한 시간이면 약 576MB가 오갑니다(Node.js로 검산). 웹소켓에서는 연결을 맺은 뒤에는 새 메시지가 있을 때만 프레임이 오가고, 서버→클라이언트 프레임의 기본 헤더는 페이로드가 125바이트 이하일 때 2바이트, 클라이언트→서버는 마스킹 키 4바이트를 더해 6바이트입니다. 실제 차이는 메시지 빈도, 암호화, 프록시 등에 따라 달라지므로 이 계산은 규모감을 보기 위한 것일 뿐입니다.
직접 해 볼 점검 목록
- 브라우저 개발자 도구의 Network 탭에서 WS로 필터링하면 101 응답과 오가는 프레임을 볼 수 있습니다.
- HTTPS 페이지에서는
wss://를 사용하세요. 암호화되지 않은ws://연결은 혼합 콘텐츠로 차단되는 것이 일반적입니다. - 서버 코드에서 Origin 허용 목록과 인증 절차(연결 시 토큰 확인 등)가 있는지 확인하세요.
- 일정 시간 무응답이면 연결이 끊긴 것으로 보고 재연결하는 로직과, 연결 확인용 Ping/Pong 또는 앱 수준 하트비트를 넣을지 정하세요.
흔한 오해와 정정
- 오해: 웹소켓은 HTTP를 완전히 대체한다. 실제: 연결 시작이 HTTP 업그레이드이고, 일반 페이지·API 요청에는 여전히 HTTP를 씁니다.
- 오해: 웹소켓 포트는 별도로 열어야 한다. 실제: RFC 6455의 기본 포트는 ws가 80, wss가 443으로 HTTP·HTTPS와 같습니다.
- 오해: 핸드셰이크의 Sec-WebSocket-Accept가 보안을 제공한다. 실제: 웹소켓 요청임을 서로 확인하는 용도이고, 암호화는 wss(TLS)가, 접근 통제는 서버의 인증·Origin 검증이 맡습니다.
- 오해: 연결이 열려 있으면 항상 상대가 살아 있다. 실제: 네트워크 전환이나 중간 장비 때문에 끊긴 사실을 한쪽이 늦게 알 수 있어 확인 신호가 필요합니다.
한계와 더 알아볼 점
연결을 유지하는 만큼 서버는 동시 연결 수만큼 자원을 쓰므로 대규모 서비스에서는 연결 분산과 재연결 폭주 대비가 설계 과제가 됩니다. 또 웹소켓 자체는 메시지의 의미를 정해 주지 않아, 메시지 형식과 재전송·순서 보장 같은 규칙은 응용이 정해야 합니다. 핸드셰이크의 Sec-WebSocket-Protocol 헤더로 클라이언트가 선호하는 하위 프로토콜을 제안하고 서버가 그중 하나를 고르는 방식이 있습니다. DEFLATE 기반 압축 확장은 RFC 7692에 정의되어 있고, RFC 6455는 이후 RFC 7936·8307·8441로 갱신되었습니다(RFC 에디터 페이지 표시 기준). 세부 내용은 각 RFC 원문을 확인하세요.
HTTP 상태 코드 101이 궁금하다면
웹소켓 핸드셰이크에 쓰이는 상태 코드 101(Switching Protocols)을 포함해 다른 HTTP 상태 코드도 궁금하다면HTTP 상태 코드 완전정복을 확인해보세요.
HTTP 프로토콜 자체의 발전 과정도 함께 보면 좋아요
웹소켓이 등장한 배경이 되는 HTTP/1.1의 한계와 이후 발전 과정이 궁금하다면HTTP/1.1·HTTP/2·HTTP/3 차이 완전정복도 참고해보세요.
자주 묻는 질문
웹소켓 연결도 방화벽에 막힐 수 있나요?
네. 일부 사내망이나 공용 와이파이의 보수적인 방화벽 설정은 표준 HTTP·HTTPS 포트(80·443) 외의 연결이나 프로토콜 업그레이드 요청을 차단하기도 합니다. 다만 대부분의 웹소켓 서비스는 443번 포트를 그대로 활용해 이런 문제를 최소화합니다.
웹소켓은 모바일 환경에서도 안정적으로 동작하나요?
모바일 네트워크 전환(와이파이↔셀룰러)이나 화면이 꺼져 백그라운드로 전환될 때 연결이 끊길 수 있어, 실무에서는 연결이 끊기면 자동으로 재연결을 시도하는 로직을 함께 구현하는 경우가 많습니다.
웹소켓은 항상 폴링보다 좋은가요?
아닙니다. 갱신이 드물거나 서버에서 클라이언트로 한 방향이면 폴링이나 SSE가 더 단순하고 운영하기 쉽습니다. 양방향이고 지연이 중요한 상호작용에서 웹소켓의 이점이 커집니다.
웹소켓 메시지는 자동으로 암호화되나요?
ws:// 연결은 암호화되지 않습니다. TLS로 보호되는 wss:// 를 써야 하며, 그래도 로그인 확인과 Origin 검증은 서버가 따로 해야 합니다.
왜 클라이언트 프레임만 마스킹하나요?
RFC 6455는 클라이언트가 서버로 보내는 모든 프레임은 마스킹하고, 서버가 보내는 프레임은 마스킹하지 않도록 규정합니다. 마스킹 키는 프레임에 함께 실립니다. 이 규칙은 암호화가 아니므로 기밀성은 wss에 맡겨야 합니다.
더 읽어 보기
TLS 핸드셰이크 과정에서 wss의 암호화 협상을, 방화벽 기초에서 포트와 연결 차단 원리를 이어서 볼 수 있습니다.