46.1 웹은 원래 한쪽 말만 오가는 거 아니었어요?

우리가 배운 웹은 대부분 요청하고 응답받으면 한 판이 끝이에요. 브라우저가 "이 페이지 줘" 하면 서버가 "여기" 하고 건네주고, 그 대화는 그걸로 닫혀요. 다음에 또 궁금한 게 생기면 처음부터 새로 물어봐야 하죠. 이걸 HTTP(하이퍼텍스트 전송 규약)의 요청-응답 구조라고 불러요.


이 구조엔 큰 특징이 하나 있어요. 항상 브라우저가 먼저 말을 걸어야 한다는 거예요. 서버는 누가 물어봐야만 답할 수 있고, 자기가 먼저 "야, 새 소식 있어"라고 부를 방법이 없어요. 그런데 채팅이나 주식 호가창처럼 서버 쪽에서 시시각각 사건이 벌어지는 화면에선 이게 참 답답해요. 저는 첫 실시간 프로젝트에서 3초마다 서버를 두드려 새 메시지를 확인했는데, 열에 아홉은 "없어"라는 답만 돌아와 서버만 지쳤어요. 이 문제를 풀려고 나온 게 WebSocket(웹소켓)이에요.

46.2 그래서 웹소켓은 뭐가 다르죠?

웹소켓의 핵심은 딱 하나예요. 한 번 연결해두면 그 통로가 계속 열려 있어요. HTTP처럼 답 한 번 주고 닫는 게 아니라, 전화처럼 선을 물려둔 채 양쪽이 아무 때나 말을 주고받아요. 브라우저가 보내고 싶으면 보내고, 서버가 알릴 게 생기면 즉시 밀어넣어요. 상대가 물어보길 기다릴 필요가 없죠.


이렇게 양쪽이 동시에 말할 수 있는 방식을 전이중(full-duplex, 양쪽이 동시에 통신)이라고 해요. 무전기는 한 사람이 말하는 동안 상대는 듣기만 해야 하는데, 웹소켓은 전화에 가까워요. 둘 다 동시에 말해도 되죠. 그리고 한 번 연결을 세워두면, 이후엔 인사말(헤더)을 매번 다시 붙일 필요가 없어요. HTTP는 요청마다 헤더로 수백 바이트를 얹는데, 웹소켓은 연결 후엔 알맹이만 오가서 가볍고 빨라요. 실시간이 실시간다워지는 이유가 여기 있어요.

46.3 연결은 어떻게 시작되나요?

재밌는 건 웹소켓이 HTTP로 시작한다는 점이에요. 처음부터 별개의 연결을 여는 게 아니라, 평범한 HTTP 요청을 보내면서 "이 연결을 웹소켓으로 승격시켜 줘"라고 부탁해요. 이 절차를 핸드셰이크(handshake, 악수, 연결을 성사시키는 첫 인사)라고 해요.


브라우저가 보내는 첫 요청은 이렇게 생겼어요.


GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13


여기서 Upgrade: websocket 이 줄이 핵심이에요. "지금은 HTTP로 말 걸지만 웹소켓으로 바꾸고 싶어"라는 신호죠. 서버가 이걸 받아들이면 보통의 200(성공)이 아니라 101(Switching Protocols, 프로토콜 전환)이라는 특별한 응답을 돌려줘요.


HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=


101이 오는 순간, 방금까지 HTTP였던 그 연결이 그대로 웹소켓 통로로 바뀌어요. 새 연결을 여는 게 아니라 쓰던 선을 용도 변경하는 거죠. 중간의 Key와 Accept는 서로 진짜 웹소켓 상대가 맞는지 확인하는 값인데, 우리가 계산할 일은 없고 브라우저와 서버가 알아서 주고받아요.

46.4 왜 http 대신 ws라고 쓰죠?

연결 주소를 보면 http가 아니라 ws로 시작해요. 웹소켓은 자기만의 주소 체계를 써요. 암호화가 없는 ws와, 암호화된 wss 두 가지가 있어요. http와 https의 관계랑 똑같아요.


const ws = new WebSocket("wss://example.com/chat");


실제 서비스에선 무조건 wss를 쓰세요. ws는 오가는 내용이 평문이라 중간에서 누가 훔쳐볼 수 있어요. 게다가 요즘 브라우저는 https 페이지 안에서 ws로 연결하려 들면 보안 문제라며 막아버려요. 저는 로컬에선 ws로 잘 되다가 실서버(https)에 올리자마자 연결이 막혀 당황한 적이 있어요. 원인이 딱 이거였죠. 처음부터 wss로 짜두면 이런 사고가 없어요.

46.5 핸드셰이크 다음엔 데이터가 어떻게 오가죠?

연결이 서고 나면 이제 데이터를 주고받아요. 그런데 웹소켓은 데이터를 통째로 흘려보내지 않고 프레임(frame, 데이터를 담는 작은 조각) 단위로 쪼개 보내요. 우리가 보낸 메시지 하나가 한 개, 혹은 여러 개의 프레임에 실려 가는 거예요.


프레임 하나엔 알맹이 데이터 앞에 아주 짧은 머리표가 붙어요. 이 머리표에 op코드(opcode, 이 프레임이 뭔지 알려주는 표시)가 들어 있어요. "이건 글자 메시지야", "이건 연결을 닫자는 신호야" 같은 종류를 몇 비트로 구분하는 거죠. 핵심은 이 머리표가 HTTP 헤더처럼 뚱뚱하지 않다는 거예요. 겨우 몇 바이트라, 짧은 메시지를 쉴 새 없이 주고받아도 낭비가 거의 없어요. 채팅처럼 작은 메시지가 폭포처럼 오가는 상황에 딱 맞는 구조죠.


다행히 이 프레임을 우리가 직접 조립할 일은 없어요. 브라우저가 send로 넘긴 글자를 알아서 프레임에 싸고, 받은 프레임을 풀어 event.data에 담아줘요. 그래도 "메시지가 프레임 단위로 오간다"는 그림을 알아두면 웹소켓이 왜 이렇게 가벼운지가 이해돼요.

46.6 자바스크립트에선 어떻게 다루죠?

원리를 알았으니 코드는 오히려 쉬워요. 연결은 한 줄이고, 거기에 네 가지 사건에 반응하는 처리를 붙여요. 열렸을 때(open), 메시지가 왔을 때(message), 닫혔을 때(close), 오류가 났을 때(error)예요.


const ws = new WebSocket("wss://example.com/chat");

ws.onopen = () => {
console.log("연결 성사");
};

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

ws.onclose = (event) => {
console.log("닫힘", event.code);
};

ws.onerror = () => {
console.log("오류");
};


이 네 사건이 아까 본 프로토콜과 그대로 이어져요. onopen은 핸드셰이크가 성공해 101을 받은 순간, onmessage는 데이터 프레임이 도착할 때마다 불리고 알맹이는 event.data에 담겨 와요. onclose는 연결이 닫힐 때, onerror는 문제가 생겼을 때 불리고요. 내가 보낼 땐 ws.send 한 줄이면 돼요. 이때 send는 연결이 열린 뒤에만 통해요. 아직 onopen이 안 왔는데 send부터 부르면 오류가 나니, 보낼 것들은 onopen 안이나 그 뒤에 두세요.

46.7 그럼 뭘 주고받아야 하죠?

웹소켓은 그냥 글자를 실어 나를 뿐, 그 글자를 어떻게 해석할지는 우리가 약속해요. 그래서 실무에선 보통 JSON(자바스크립트 객체 표기법)으로 봉투를 만들어 주고받고, 메시지마다 "이게 무슨 종류인지"를 적은 type 칸을 둬요.


// 보낼 때: 객체를 문자열로 바꿔서 send
ws.send(JSON.stringify({
type: "chat",
text: "안녕하세요"
}));

// 받을 때: 문자열을 객체로 풀어서 갈래 나누기
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === "chat") {
showMessage(msg.text);
}
};


보낼 땐 JSON.stringify로 객체를 문자열로 만들고, 받을 땐 JSON.parse로 다시 객체로 풀어요. send가 오직 문자열만 받기 때문이에요. 여기서 type 칸이 진짜 중요해요. 채팅 메시지도, 입장 알림도 전부 같은 통로로 섞여 들어오는데, type이 있어야 받는 쪽에서 "이건 채팅, 저건 알림" 하고 갈래를 나눌 수 있거든요. 저는 이 칸 없이 시작했다가 종류가 늘자 받는 코드가 엉망이 됐어요. 처음부터 봉투에 종류를 적으세요.

46.8 오늘 원리를 묶어볼게요

웹소켓의 원리를 한 장으로 접어볼게요.


핵심
1. 웹소켓은 한 번 연결하면 통로가 계속 열린 채, 양쪽이 동시에 주고받는 전이중 방식이에요.
2. 연결은 HTTP 요청에 Upgrade를 실어 부탁하고, 101 응답으로 승격돼요(핸드셰이크).
3. 주소는 ws/wss를 쓰고, 실서비스는 무조건 wss예요.
4. 데이터는 가벼운 프레임 단위로 오가고, 자바스크립트에선 네 사건과 send로 다뤄요.


원리를 알면 실시간이 마법이 아니라 구조로 보여요. HTTP로 악수하고, 101로 선을 바꿔 물리고, 그 위로 가벼운 프레임이 양방향으로 흐른다. 이 그림 하나만 쥐고 있으면, 어떤 실시간 기능을 만들든 지금 무슨 일이 벌어지는지가 머릿속에 그려져요. 그게 연결이 말썽을 부릴 때 당황하지 않는 힘이 돼요.