본문 바로가기

Back-End

웹소켓 이해하기

반응형

웹소켓에 대해 공부하며 알게된 내용들을 정리해보았다.

1. 개요

웹소켓은 클라이언트와 서버 간의 양방향, 지속적인 통신을 가능하게 하는 프로토콜이다. 일반적인 HTTP 통신과는 다르게, 한번 연결이 성립되면 서버와 클라이언트가 끊기지 않고 계속해서 데이터를 주고받을 수 있다는 점이 가장 큰 특징이다. 이 때문에,

  • 실시간 채팅
  • 온라인 게임
  • 실시간 주식/환율 데이터 전송

등의 분야에서 자주 사용되곤 한다.

2. 웹소켓 vs HTTP

항목 HTTP Websocket
통신 방식 요청/응답 (Request/Response) 양방향 (Full-duplex)
연결 유지 요청마다 새로 연결 한 번 연결 후 계속 유지
실시간성 느림 빠름 (실시간 게임, 채팅 등에 적합)
사용 용도 웹페이지 로딩, API 통신 등 채팅, 주식 실시간 데이터, 게임 등

HTTP /1.1부터는 연결 유지되는거 아닌가?

맞다. HTTP/1은 요청마다 새로 연결되지만, 1.1부터는 Connection: Keep-Alive 가 기본이라 연결을 유지 가능하다.

그러면 왜 "HTTP는 요청마다 새로 연결된다"고 말할까?

컨셉 차이 때문이다.
HTTP는 요청/응답 기반 통신 방식이다. 즉, 클라이언트가 요청하지 않으면 서버는 데이터를 보낼 수 없다.
반면, WebSocket은 연결이 유지되면서 양방향 통신이 가능하다. 서버도 클라이언트에게 먼저 데이터를 보낼 수 있고, 실시간 push가 가능하다.

HTTP/2부터는 서버 푸시도 되는데?

HTTP/2의 서버 푸시와 WebSocket은 비슷해 보일 수 있지만 역할도 다르고, 동작 방식도 완전히 다르다. 

HTTP/2의 서버 푸시는 사용자가 어떤 웹사이트의 HTML을 요청했을 때, 서버가 “얘 이거 끝나고 CSS랑 JS도 필요하겠네” 하고 미리 보내주는 구조이다. 하지만 이건 서버가 예측해서 보내는 정적 자원에만 좋고 "채팅 메시지" 같은 실시간 데이터에는 안 어울린다. 웹소켓은 서버가 통신방을 열어두고, 필요할 때마다 자유롭게 얘기하는 구조이므로 채팅방, 게임, 실시간 알림 같은 데 최적화되어 있다.

항목 HTTP/2 Server Push WebSocket
통신 방식 요청/응답 기반 (클라이언트가 먼저 요청) 양방향 (클/서버 모두 먼저 전송 가능)
서버가 먼저 보낼 수 있는가? ✅ 가능하지만 제한적 ✅ 완전 자유
연결 유지
사용 목적 페이지 로딩 최적화 (ex. HTML 요청 시 CSS도 미리 전송) 실시간 채팅, 게임, 주식, 알림 등
데이터 흐름 서버가 예측해서 "추가 응답"을 보내는 구조 자유롭게 메시지 주고받는 구조
제한점 클라이언트가 원치 않으면 거절할 수 있음 연결 유지 시 지속 통신 가능
브라우저 지원 최신 브라우저 대부분 지원하지만, HTTP/3로 점점 대체됨 대부분 지원 (Chrome, Firefox, Safari 등)

3. 웹소켓 구현 방법

간단한 채팅앱을 만들며 구현 방법을 익혀보자.

1. 먼저 필요한 패키지를 설치한다.

npm init -y
npm install ws

2. 서버 코드를 작성한다.

const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', (ws) => {
  console.log('클라이언트 연결됨');

  ws.on('message', (message) => {
    console.log(`받은 메시지: ${message}`);
    ws.send(`서버 응답: ${message}`);
  });

  ws.on('close', () => {
    console.log('클라이언트 연결 종료');
  });
});

console.log('웹소켓 서버 실행 중 (ws://localhost:8080)');

3. 클라이언트 코드를 작성한다.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>WebSocket Chat</title>
</head>
<body>
  <h1>WebSocket 채팅</h1>
  <input id="messageInput" type="text" placeholder="메시지 입력" />
  <button onclick="sendMessage()">보내기</button>
  <div id="chatLog"></div>

  <script>
    const socket = new WebSocket('ws://localhost:8080');

    socket.onopen = () => {
      console.log('웹소켓 연결됨');
    };

    socket.onmessage = (event) => {
      const chatLog = document.getElementById('chatLog');
      chatLog.innerHTML += `<p>${event.data}</p>`;
    };

    function sendMessage() {
      const input = document.getElementById('messageInput');
      socket.send(input.value);
      input.value = '';
    }
  </script>
</body>
</html>

 

실행 화면

4. 구현시 주의사항

4. 1. 보안 (Security)

wss:// 사용 공개 서비스에서는 꼭 wss:// (SSL/TLS 기반)로 암호화해야 함. 그렇지 않으면 패킷 가로채기 가능.
인증 처리 필요 WebSocket 자체는 인증 기능이 없음. 초기 HTTP 요청 단계에서 토큰, 세션 쿠키 등으로 인증 처리 필요.
CORS가 아닌 origin 검증 CORS는 적용되지 않지만, 서버에서 Origin 헤더를 직접 확인해서 허용된 도메인인지 검증하는 게 좋음.
DoS 방어 무한 연결 시도, 큰 메시지 반복 등 악의적인 연결을 막기 위한 rate limiting 필요.

4. 2. 연결 관리 (Connection Handling)

타임아웃/핑 처리 연결이 끊어졌는지 감지하려면 ping/pong 메시지 주기적으로 보내야 함. (서버, 클라이언트 둘 다 가능)
끊김 감지 및 재연결 클라이언트 측에서 onclose, onerror 이벤트로 끊김을 감지하고 자동 재연결 로직 구현하는 게 좋음
연결 수 제한 서버는 동시에 유지할 수 있는 WebSocket 연결 수에 한계가 있음 (scale-out 구조 고려)

4.3. 에러 처리 및 디버깅

에러 이벤트 핸들링 onerror, onclose 등을 항상 처리해서 예외 상황에 대응
로그 남기기 누가 접속했는지, 어떤 메시지를 주고받았는지 로깅해서 문제 발생 시 디버깅 가능하게 하기
메시지 사이즈 제한 너무 큰 메시지는 처리 중 장애 유발 가능, 제한 두는 게 좋음 (ex. 1MB 이하 등)

4.4. 스케일링과 아키텍처

로드 밸런서 주의 웹소켓은 지속 연결이므로 일반적인 로드밸런싱이 안 통할 수 있음 → Sticky session 설정 필요
분산 처리 구조 고려 연결 수가 많아질 경우 Redis Pub/Sub, Kafka 같은 메시지 브로커로 분산 처리 고려

5. wss 적용법

wss를 사용하려면 HTTPS 서버 위에 웹소켓 서버를 올리면 된다.

HTTPS 서버 위에 웹소켓 서버를 올린다는 건 정확히는 SSL 인증이 적용된 https 서버를 먼저 만들고, 그 서버를 통해 암호화된 WebSocket 연결을 처리한다는 뜻이다.

 

아래는 구현 예시이다.

const https = require('https');
const fs = require('fs');
const WebSocket = require('ws');

// 인증서 파일 로드
const server = https.createServer({
  cert: fs.readFileSync('cert.pem'),
  key: fs.readFileSync('key.pem')
});

// WebSocket 서버는 HTTPS 서버 위에 올림
const wss = new WebSocket.Server({ server });

wss.on('connection', (ws) => {
  console.log('클라이언트 연결됨');

  ws.on('message', (msg) => {
    console.log('메시지:', msg);
    ws.send(`서버 응답: ${msg}`);
  });
});

server.listen(443, () => {
  console.log('✅ wss 서버 실행됨: wss://your-domain.com');
});

 

다음과 같이 http 요청과 웹소켓 연결을 모두 처리하는 것도 가능하다.

const fs = require('fs');
const https = require('https');
const express = require('express');
const WebSocket = require('ws');

const app = express();

// 🔐 HTTPS 인증서 로드
const server = https.createServer({
  cert: fs.readFileSync('cert.pem'),
  key: fs.readFileSync('key.pem')
}, app);

// ✅ 일반 HTTP 요청 처리 (API, 정적 파일 등)
app.get('/', (req, res) => {
  res.send('여긴 일반 HTTPS 요청입니다.');
});

// 💬 WebSocket 서버는 HTTPS 위에 생성
const wss = new WebSocket.Server({ server });

wss.on('connection', (ws) => {
  console.log('웹소켓 연결됨');
  ws.send('안녕하세요! WebSocket 연결 성공');

  ws.on('message', (msg) => {
    console.log('받은 메시지:', msg);
    ws.send(`서버가 받은 메시지: ${msg}`);
  });
});

// 🟢 서버 실행
server.listen(443, () => {
  console.log('HTTPS + WSS 서버 실행 중 (https://your-domain.com)');
});
반응형