웹소켓 알아보기

웹소켓

프로토콜 조차 다르다

우리가 흔히 사용하는 HTTP 프로토콜을 사용하는 것이 아닌 웹소켓 프로토콜 (ws, wss)을 사용함

  • http:// 가 아닌 ws://wss://의 형태를 갖음

프로토콜 비교

0image.png

HTTP로는 실시간 통신을 사용할 수 없는 이유 (단방향 통신)

  • 클라이언트가 서버에게 데이터를 보내는 것은 가능하지만, 상대방이 내 데이터를 받는 것은 상대방이 특별한 요청이 없는 한 이루어질 수가 없음
  • HTTP의 Polling 기법으로 해결가능
    • 클라이언트가 주기적으로 서버에 요청을 보내서 상대가 새로운 챗을 보냈는지 확인
      • 요청주기에 따라서 실시간으로 받을 수가 없음
      • 불필요한 요청
    • Long Polling : 서버가 클라이언트 요청에 바로 응답하지 않고 업데이트가 발생할 때까지 기다림
      • 상대방이 챗을 보내거나 타임아웃에 걸리면 응답을 보내고 클라이언트는 다시 요청을 보내서 다음 응답을 기다림
      • 데이터 업데이트에 반응하는 속도는 빨라지지만 서버의 부담이 커짐 (여러명의 톡방)
  • 물론 HTTP/2 이상에서는 양방향 통신이 가능하지만, 장시간의 양방향 통신을 위한 설계는 아님

웹소켓의 과정

핸드쉐이크

  • 클라이언트는 서버에게 WebSocket을 연결하자는 요청을 HTTP를 통해 보냄
    • UpgradeConnection: Upgrade 헤더가 포함되어 웹소켓 전환을 요청
  • 서버는 그것이 가능한 경우 이를 수락하는 응답을 HTTP로 보냄
    • 101 Switching Protocols 응답을 보냄
  • 핸드쉐이크 과정이 끝나게 되면 클라이언트와 서버는 WebSocket 프로토콜을 사용하여 소통

데이터 전송

  • 핸드셰이크 이후 웹소켓은 프레임이라는 단위로 데이터를 주고받음
  • 프레임은 데이터의 작은 묶음으로, 텍스트나 바이너리 데이터로 구성

연결 종료

  • 웹소켓 연결은 양쪽에서 종료 신호를 보내면 종료됨
  • 클라이언트가 브라우저를 닫거나 명시적으로 연결을 끊을 때 연결이 종료되고, 서버에서도 특정 조건에 따라 클라이언트와의 연결을 종료할 수 있음

웹소켓 방식

네트워크 OSI 7계층에서 7계층에서 작동하며 TCP socket을 기반으로 작동

1image.png

웹소켓의 장점

실시간 양방향 통신 (Full-Duplex)

연결 유지

- 일반적인 HTTP에서는 요청과 응답이 끝나면 연결이 끊어지지만, 웹소켓은 한 번 연결이 성립되면 클라이언트와 서버 간의 연결이 계속 유지됨
- **오버헤드 감소**
- HTTP의 경우 매 요청마다 헤더와 같은 메타데이터를 전송해야 하지만, 웹소켓은 처음 핸드쉐이크 이후에는 이러한 추가 데이터를 보내지 않음

낮은 지연 시간 (Low Latency)

- 웹소켓은 연결이 열린 후 데이터를 빠르게 전송할 수 있기 때문에 **지연 시간이 매우 짧음**
- 웹소켓은 서버와 클라이언트가 직접 연결되어 있으므로, HTTP 기반의 요청/응답 주기보다 훨씬 빠른 응답을 제공

효율적인 대역폭 사용

- 웹소켓은 초기 핸드쉐이크 이후에 **헤더 크기가 작고** 필요한 데이터만을 전송하므로, 대역폭을 효율적으로 사용
- 반복적인 HTTP 요청/응답의 경우 상대적으로 많은 데이터가 오가는데, 웹소켓은 지속적인 연결로 인해 이러한 오버헤드가 줄어듦

서버 푸시 (Server Push)

- 서버는 클라이언트가 요청하지 않아도 실시간으로 데이터를 전송할 수 있음
- 이를 통해 실시간 알림, 이벤트 업데이트 등 클라이언트 측에서 주기적으로 서버를 조회할 필요 없이 서버에서 자동으로 데이터를 푸시할 수 있음

웹소켓 통신의 단점

초기 연결 오버헤드

- 웹소켓은 **초기 핸드쉐이크**가 필요함
- 클라이언트가 서버에 웹소켓 연결을 요청하고, 서버가 이를 승인하는 과정에서 HTTP를 사용하여 **헤더 크기가 큼**
- 한 번 연결된 후에는 오버헤드가 적지만, 초기 연결 설정에 시간이 걸릴 수 있음

상태 유지 문제 (Stateful)

- 웹소켓 연결은 **상태기반**, HTTP 연결은 무상태(**Stateless**)
- 연결이 끊어지면 해당 상태가 모두 사라지므로, 서버는 **클라이언트와의 연결을 유지하고 관리**해야 함
- **스케일링**이 필요한 애플리케이션에서는 여러 서버가 웹소켓 연결을 처리해야 하므로, 클라이언트 상태를 동기화하거나 클러스터 간에 연결을 관리하는 것이 추가적인 부담

브라우저 및 네트워크 제한

- 일부 **네트워크 환경**에서는 웹소켓 연결을 차단하거나 제한할 수 있음
	- 예를 들어, 방화벽이나 프록시 서버
- 브라우저에서도 웹소켓을 지원하지 않거나, 보안상의 이유로 일부 제한을 두는 경우가 있음

서버 리소스 소모

- 웹소켓 연결은 **서버 리소스를 많이 소모**
- 클라이언트와 연결이 지속적으로 유지되기 때문에, 많은 수의 클라이언트가 연결될 경우 서버는 이를 효율적으로 관리하기 위해 상당한 리소스를 소비할 수 있음
- 대규모의 웹소켓 서버를 운영하려면 **스케일링**과 **로드 밸런싱**을 잘 고려해야 함

보안 문제

- 기본적으로는 `ws://`(웹소켓)와 `wss://`(웹소켓 보안) 프로토콜을 사용하며, `wss://`는 HTTPS처럼 SSL/TLS로 암호화된 연결을 사용하지만 여전히 웹소켓 프로토콜을 악용하려는 공격이 있을 수 있으며, 이를 방어하기 위한 추가적인 보안 조치가 필요

브라우저의 제한된 연결 수

- 대부분의 **브라우저**는 각 도메인에서 동시에 열 수 있는 웹소켓 연결 수에 제한을 두고 있음
	- 예를 들어, 특정 브라우저에서는 한 도메인에 대해 최대 6개의 웹소켓 연결만을 허용

React에서 웹소켓 구현

  • 특별한 라이브러리 필요 없이 자바스크립트에서 제공하는 websocket 이용
typescript

import React, { useState, useEffect } from 'react';

const ChatApp = () => {
  const [ws, setWs] = useState<WebSocket | null>(null);
  const [messages, setMessages] = useState<string[]>([]);
  const [userId, setUserId] = useState<string>('');
  const [inputMessage, setInputMessage] = useState<string>('');

  useEffect(() => {
    // WebSocket 서버 연결
    const socket = new WebSocket('ws://localhost:8080');
    socket.onopen = () => {
      console.log('WebSocket connected');
    };

    // 서버로부터 메시지 수신
    socket.onmessage = (event) => {
      const data = JSON.parse(event.data);
      if (data.type === 'userId') {
        setUserId(data.userId);  // 서버로부터 사용자 ID 받기
      } else {
        setMessages((prevMessages) => [...prevMessages, data.message]);
      }
    };

    setWs(socket);

    // 컴포넌트 언마운트 시 WebSocket 연결 종료
    return () => {
      socket.close();
    };
  }, []);

  // 메시지 보내기
  const sendMessage = () => {
    if (ws && inputMessage) {
      const message = JSON.stringify({
        userId,
        message: inputMessage,
      });
      ws.send(message);
      setInputMessage('');
    }
  };

  return (
    <div>
      <h1>채팅방</h1>
      <div>
        {messages.map((message, index) => (
          <div key={index}>{message}</div>
        ))}
      </div>
      <input
        type="text"
        value={inputMessage}
        onChange={(e) => setInputMessage(e.target.value)}
        placeholder="메시지를 입력하세요"
      />
      <button onClick={sendMessage}>보내기</button>
    </div>
  );
};

export default ChatApp;