- Published on
WebSocket은 HTTP인가? — 열림 핸드셰이크
- Authors

- Name
- Nostrss
- Github
- Github

WebSocket 프로토콜 학습 시리즈의 첫 번째 레슨. 매일 쓰는
new WebSocket(url)한 줄 뒤에서 정확히 무슨 일이 일어나는지, RFC 6455 원문 수준에서 본다.
핵심 답부터
WebSocket은 HTTP로 시작하지만, HTTP가 아니다. 연결의 첫 왕복만 HTTP/1.1 요청/응답 형식을 빌리고(이를 열림 핸드셰이크, opening handshake 라 한다), 서버가 101 Switching Protocols 로 응답하는 순간부터 그 TCP 연결 위에는 HTTP가 아닌 **WebSocket 프레임**이 흐른다. 같은 TCP 연결을 재활용하되, 프로토콜을 갈아끼우는 것이다.
The WebSocket Protocol enables two-way communication... over a single TCP connection.
HTTP 폴링은 요청마다 헤더 오버헤드를 지불하고 여러 TCP 연결을 유지해야 했다는 문제의식에서 출발했다.
핸드셰이크 실물
브라우저가 new WebSocket("wss://example.com/chat") 를 실행하면 이 요청이 나간다 (RFC 6455 §4.1):
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: https://example.com
서버는 이렇게 응답한다 (§4.2):
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
이 줄 이후로 이 연결에는 더 이상 HTTP가 흐르지 않는다.
| 헤더 | 역할 |
|---|---|
Upgrade: websocket | HTTP/1.1의 프로토콜 전환 메커니즘. "이 연결을 websocket으로 바꿔달라" |
Sec-WebSocket-Key | 무작위 16바이트를 base64 인코딩한 값. 매 연결마다 새로 생성 |
Sec-WebSocket-Accept | 서버가 Key로부터 계산해 돌려주는 증명값 (아래 참조) |
Sec-WebSocket-Version: 13 | RFC 6455 최종판의 버전 번호. 지금도 13이다 |
요청 쪽 규칙을 좀 더 자세히 보면 (§4.1):
- 반드시 GET 메서드, HTTP/1.1 이상이어야 한다.
Host,Upgrade,Connection,Sec-WebSocket-Key,Sec-WebSocket-Version은 필수 헤더이고, 헤더 값의 대소문자는 구분하지 않는다. Origin은 브라우저 클라이언트라면 필수다. 서버가 허용하지 않는 출처의 연결을 거부하는 근거가 된다.- 선택 헤더도 둘 있다:
Sec-WebSocket-Protocol(서브프로토콜 협상 — 클라이언트가 후보 목록을 보내면 서버가 하나를 골라 응답),Sec-WebSocket-Extensions(확장 협상 — 대표적으로 압축 확장permessage-deflate).
응답 쪽 규칙: 클라이언트는 101 이 아닌 모든 응답을 핸드셰이크 실패로 처리한다. 예외적으로 426 Upgrade Required 는 버전 불일치 시 서버가 지원 버전을 알려주는 용도다 (§4.2.2). 참고로 URI 스킴 ws:// 와 wss:// 의 기본 포트는 HTTP와 같은 80, 443이다.
Sec-WebSocket-Key는 인증이 아니다
가장 흔한 오해. 이 키의 목적은 보안 인증이 아니라 "상대가 진짜 WebSocket을 이해하는 서버인지" 확인하는 것이다. 서버는 받은 Key 뒤에 스펙에 박혀 있는 고정 GUID 문자열을 이어붙이고, SHA-1 해시 후 base64 인코딩해 돌려줘야 한다:
Accept = base64( SHA1( Key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" ) )
이 계산을 올바르게 해내려면 서버가 RFC 6455를 알고 의도적으로 구현했어야 한다. 즉 WebSocket을 모르는 HTTP 서버나 캐싱 프록시가 우발적으로(혹은 캐시된 응답으로) 연결을 수락하는 사고를 막는 장치다 (§1.3). 값이 틀리면 브라우저는 연결을 즉시 실패시킨다 — 래퍼에서 보던 "연결이 열리기도 전에 죽는" 케이스 중 하나가 이것이다.
RFC 6455 §1.3의 예시 키 dGhlIHNhbXBsZSBub25jZQ== 로 직접 계산해보면 스펙이 명시한 정답 s3pPLMBiTxaQ9kYGzzhZRbK+xOo= 가 나온다. 브라우저 콘솔에서 확인할 수 있다:
const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
const key = 'dGhlIHNhbXBsZSBub25jZQ==';
const data = new TextEncoder().encode(key + GUID);
const hash = await crypto.subtle.digest('SHA-1', data);
const accept = btoa(String.fromCharCode(...new Uint8Array(hash)));
// → 's3pPLMBiTxaQ9kYGzzhZRbK+xOo='
브라우저가 실제로 하는 일(무작위 16바이트 → base64)도 한 줄이다:
btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(16))));
그래서, 웹표준인가?
그렇다 — 정확히는 두 개의 표준이다. 이 구분이 "웹표준인지"라는 질문의 정답이다:
| 무엇 | 표준화 주체 | 문서 | |
|---|---|---|---|
| 프로토콜 | 와이어 위의 바이트: 핸드셰이크, 프레임 | IETF (인터넷 표준) | RFC 6455 (2011) |
| 브라우저 API | new WebSocket(), 이벤트, readyState | WHATWG (웹 표준) | WebSockets Standard |
매일 호출하는 것은 WHATWG 표준이고, 그 아래에서 흐르는 것은 IETF 표준이다. HTML/DOM과 같은 이중 구조다(HTTP는 IETF, fetch() 는 WHATWG인 것과 정확히 같은 관계). 참고로 HTTP/2와 HTTP/3에는 Upgrade 헤더가 없어서 별도의 부트스트랩 방식(RFC 8441/9220)이 존재하는데, 이는 나중 레슨에서 다룬다.
확인 퀴즈
Q1. 핸드셰이크 성공 시 서버가 반환하는 HTTP 상태 코드는? (101 / 200 / 301 / 426)
정답 보기
101 Switching Protocols. 426 Upgrade Required는 버전 불일치 시 서버가 지원 버전을 알려줄 때 쓴다 (§4.2.2).
Q2. Sec-WebSocket-Key 헤더의 목적은? (클라이언트 신원 인증 / 비WebSocket 서버의 우발적 수락 방지 / 메시지 암호화 초기 키 / 프록시 캐시 갱신 토큰)
정답 보기
비WebSocket 서버의 우발적 수락 방지. 인증도 암호화도 아니다. 상대가 RFC 6455를 의도적으로 구현한 서버임을 확인하는 장치다 (§1.3).
Q3. WebSocket의 표준화 주체를 올바르게 짝지은 것은? (프로토콜 IETF·API WHATWG / 프로토콜 W3C·API IETF / 프로토콜 WHATWG·API W3C / 프로토콜 ISO·API ECMA)
정답 보기
프로토콜은 IETF, API는 WHATWG. 와이어 프로토콜은 IETF RFC 6455, 브라우저 API는 WHATWG WebSockets Standard. fetch() 와 HTTP의 관계와 같다.
수업 중 나온 질문
Q. Accept 계산은 백엔드 개발자가 직접 구현하는 건가? 아니면 브라우저처럼 서버도 자동으로 처리되나?
직접 구현하진 않지만, "브라우저처럼 자동"도 아니다. 브라우저 쪽 WebSocket 은 플랫폼(브라우저 엔진)에 내장돼 있지만, 순수한 HTTP 서버(Node http 모듈, Flask, 순정 nginx)는 WebSocket을 전혀 모른다. 백엔드 개발자가 ws(Node), gorilla/websocket(Go), spring-websocket(Java) 같은 RFC 6455 구현 라이브러리를 명시적으로 붙여야 동작하며, Accept 계산은 그 라이브러리 내부에 들어 있다. Node ws 의 실제 내부 코드는 위의 계산기와 동일하다:
const digest = createHash('sha1')
.update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
.digest('base64');
덤: 리버스 프록시는 계산하지 않는다. nginx가 중간에 있어도 Accept 계산은 최종 애플리케이션 서버의 몫이다. nginx는
proxy_set_header Upgrade $http_upgrade로 헤더를 통과시켜줄 뿐이며, 이 설정 누락이 배포 시 핸드셰이크 실패의 단골 원인이다.
Q. WHATWG 표준과 IETF 표준의 관계를 모르겠다. 웹표준이 두 개인 건가?
웹표준이 두 개인 게 아니라, 계층마다 담당 표준화 기구가 다르다. 인터넷 프로토콜(TCP, DNS, TLS, HTTP, WebSocket 프로토콜 — 기계끼리의 바이트)은 IETF가 RFC로 표준화하고, 브라우저 플랫폼(HTML, DOM, fetch(), WebSocket API — JS 개발자에게 노출되는 창구)은 WHATWG가 Living Standard로 표준화한다. JS 언어 자체(ECMAScript)는 또 다른 기구인 Ecma TC39 소관이다.
둘은 경쟁이 아니라 참조 관계다: WHATWG WebSockets Standard는 연결 수립 시 "RFC 6455에 따르라"고 IETF 문서를 직접 참조한다. 프로토콜이 브라우저 밖(Node 서버, Python 클라이언트, IoT)에서도 쓰이기 때문에 인터넷 전체를 관할하는 IETF가 맡는 것이다.
W3C는? 원래 HTML/DOM을 표준화했으나 2019년 그 권한을 WHATWG에 공식 이양했다. 현재 W3C는 CSS, 접근성(WCAG), WebRTC 등을 담당한다. "웹표준"이라 하면 실질적으로 WHATWG Living Standard + W3C 스펙 생태계를 통칭한다.
다음 단계
1차 자료 읽기. 오늘의 원문: RFC 6455 §1.3 (Opening Handshake 개요) — 2쪽 분량이며, 이 레슨을 읽었다면 이제 원문이 그대로 읽힐 것이다. 여유가 되면 High Performance Browser Networking Ch.17도 추천.
실무 확인: DevTools Network 탭에서 아무 WebSocket 연결이나 열고 101 응답과 Sec-WebSocket-Accept 헤더를 직접 찾아보라. 오늘 배운 것이 그대로 보인다.
시리즈 참고: WebSocket 시리즈 용어집
