한양과학기술고등학교

에듀테크 프로젝트 대시보드

웹소켓 (WebSocket)

웹소켓 (WebSocket)

HTTP는 매번 문을 두드려야 답을 준다 — 웹소켓은 문을 한 번만 열어두고 양쪽 다 아무 때나 말을 건다

채팅, 실시간 알림, 주식 시세, 온라인 게임처럼 서버가 먼저 클라이언트에게 데이터를 보내야 하는 서비스가 있다. 하지만 기본 HTTP는 클라이언트가 요청해야만 서버가 응답하는 구조라, 서버 쪽에서 스스로 먼저 말을 걸 수 없다.

가장 단순한 해결책은 폴링(Polling) — "새 소식 있어요?"를 몇 초마다 계속 물어보는 것이다. 대부분은 "없어요"라는 빈 대답만 돌아온다. 웹소켓은 연결을 한 번 맺어두고, 그 안에서 서버든 클라이언트든 할 말이 생기면 즉시 상대에게 밀어넣는(push) 방식이다.

① 폴링 vs 웹소켓 — 직접 돌려보기

양쪽 다 같은 속도로 "새 메시지"가 서버에 무작위로 생긴다고 가정한다. 왼쪽은 1초마다 클라이언트가 직접 물어보고, 오른쪽은 연결을 한 번 맺어두고 서버가 생기는 즉시 밀어준다. 10초 실행을 눌러 요청 수와 낭비를 비교해보자.

⏱️ HTTP 폴링 (1초마다)
🧑‍💻클라이언트
🖥️서버
0요청 수
0실제 새 소식
0헛수고(빈 응답)
🔌 웹소켓 (지속 연결)
🧑‍💻클라이언트
🖥️서버
1연결 수(핸드셰이크)
0실제 새 소식
0헛수고(빈 응답)
▶ 버튼을 눌러 10초 동안 두 방식을 동시에 시뮬레이션해보세요.

② 연결은 어떻게 시작될까 — HTTP Upgrade 핸드셰이크

웹소켓도 처음엔 평범한 HTTP 요청으로 시작한다. 다만 "이 연결을 웹소켓으로 업그레이드해달라"는 헤더를 붙인다. 서버가 동의하면 101 Switching Protocols 로 응답하고, 그 순간부터 같은 TCP 연결이 HTTP가 아닌 웹소켓 프레임을 주고받는 통로로 바뀐다.

# 클라이언트 → 서버 (여전히 HTTP 요청처럼 보임)
GET /ws/chat HTTP/1.1
Host: offset0xf.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

# 서버 → 클라이언트 (동의하면 101로 응답, 이후 계속 열려 있음)
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Accept 값은 어떻게 만들어질까? 서버가 클라이언트의 Sec-WebSocket-Key에 고정 문자열 258EAFA5-E914-47DA-95CA-C5AB0DC85B11을 붙여 SHA-1 해시한 뒤 Base64로 인코딩한다. 아무나 만든 값이 아니라는 걸 증명해, HTTP 프록시가 실수로 업그레이드를 흉내내는 걸 막기 위한 장치다.

③ 프레임 안에는 무엇이 들었나

Opcode의미비고
0x1텍스트 프레임보통 JSON 문자열
0x2바이너리 프레임이미지·파일 조각 등
0x8연결 종료(Close)양쪽 다 보낼 수 있음
0x9 / 0xAPing / Pong연결이 살아있는지 주기적으로 확인

클라이언트가 보내는 프레임은 마스킹(XOR)이 의무다. 캐시 프록시가 "이건 그냥 이미지 데이터야"라고 오해해 내용을 캐싱하거나 변형하는 걸 막기 위한 보안 장치다.

④ 실시간 채팅 미니 데모

아래 두 창은 같은 웹소켓 연결을 공유하는 두 사용자라고 가정한다. 한쪽에서 메시지를 보내면 폴링과 달리 새로고침이나 재요청 없이 즉시 반대편에 나타난다.

ws://offset0xf.com/ws/chat — 연결됨
🧑 학생 A
🧑‍🎓 학생 B

⑤ 서버 쪽 코드는 어떤 모습일까 (FastAPI)

from fastapi import FastAPI, WebSocket

app = FastAPI()
connections: list[WebSocket] = []

@app.websocket("/ws/chat")
async def chat(ws: WebSocket):
    await ws.accept()             # 101 응답 + 연결 유지 시작
    connections.append(ws)
    try:
        while True:
            msg = await ws.receive_text()   # 도착할 때까지 대기(블로킹 아님)
            for conn in connections:
                await conn.send_text(msg)   # 접속자 전원에게 즉시 push
    except Exception:
        connections.remove(ws)         # 연결 끊기면 목록에서 제거

⑥ 언제 무엇을 쓸까

상황추천이유
일반 API (목록 조회 등)일반 HTTP요청-응답 1회성이면 굳이 연결을 유지할 필요 없음
몇 분에 한 번 갱신되면 충분폴링 / 롱폴링구현이 단순하고 기존 인프라(캐시·LB) 그대로 활용
채팅, 실시간 알림, 게임, 시세웹소켓지연 없이 즉시 push, 서버→클라이언트 방향도 가능

한 줄 정리

HTTP는 "물어봐야 답한다", 웹소켓은 "연결을 열어두고 아무 때나 밀어넣는다". 한 번의 Upgrade 핸드셰이크로 같은 TCP 연결을 계속 재사용해, 폴링이 반복하던 헛수고(빈 응답, 매번 새 요청)를 없애는 것이 핵심이다.