웹 소켓의 실시간 양방향 통신 [feat. WS vs Socket.io]

목차

📌 과거 발표자료를 토대로 정리했습니다

🤔 학습 이유

과거 웹소켓 관련 컨퍼런스 발표까지 해보았지만 실제 프로젝트에서 써보지 않아 휘발되었다…ㅜㅜ

이번 기회에 실시간 & 양방향 통신을 위한 기술들을 조사해보고 어떤 방식으로 우리 서비스의 실시간 채팅 기능을 구현해 볼 지 고민해보고자 한다.


HTTP 통신은 클라이언트의 요청이 있을 때 서버가 응답하는 단방향 통신

0HTTP 통신

그럼 실시간 채팅처럼 양방향이면서 실시간 통신이 필요한 기능 구현은 어떻게 할까? 우선 실시간 통신을 위한 몇가지 통신 방식을 알아보자

🚀 실시간 통신을 위한 노력

AJAX Polling

1image.png

  • 클라이언트가 일정한 주기로 서버에 새로운 업데이트가 없는지 확인하는 HTTP 요청을 보내는 방법
  • 장점
    • 구현이 간단하며 대부분의 브라우저에서 지원
    • 기존 HTTP 인프라를 그대로 사용 가능
  • 단점
    • 불필요한 요청과 커넥션을 생성하여 서버의 부담이 커짐
    • Real-time 통신이라고 부르기 애매할 정도의 실시간성
    • 데이터 업데이트 지연 발생
  • 사용 사례
    • 응답을 실시간으로 받지 않아도 되는 경우
    • 다수의 사용자가 동시에 사용하는 경우에 적합
    • ex. 뉴스 피드, 공지사항 업데이트
javascript
setInterval(() => {
  fetch('/api/data')
    .then(response => response.json())
    .then(data => {
      console.log('받은 데이터:', data);
    })
    .catch(error => console.error('오류:', error));
}, 5000); 

Long Polling

2image.png

  • 클라이언트가 서버에 요청을 보내고 서버에서 변경이 일어날 때까지 응답을 지연시키는 방식 ⇒ 데이터가 준비되면 서버가 응답을 보내고, 클라이언트는 즉시 새로운 요청을 보내는 방식으로 지속적인 데이터 수신 구현
  • 장점
    • 불필요한 요청 감소, 지속적으로 요청을 보내는 폴링보다 부담이 덜 한 방식
    • 실시간성 향상, 클라이언트가 새로운 데이터 즉시 받음
  • 단점
    • 커넥션의 유지시간을 짧게 갖는다면 폴링과 차이점 x
    • 지속적으로 연결되어 있기 때문에 다수의 클라이언트에게 동시에 이벤트가 발생하면 순간적 부담이 급증
  • 사용 사례
    • 실시간 전달이 중요한데 상태가 빈번하게 갱신되진 않을 때 적합
    • 채팅, 실시간 알림시스템, 실시간 업데이트가 필요한 협업 도구
javascript
// 클라이언트 측 Long Polling 예시
function longPoll() {
  fetch('/api/long-poll')
    .then(response => response.json())
    .then(data => {
      console.log('받은 데이터:', data);
      // 데이터 처리 로직
      longPoll(); // 재귀 호출로 지속적인 폴링
    })
    .catch(error => {
      console.error('오류:', error);
      setTimeout(longPoll, 5000); // 오류 발생 시 재시도
    });
}

longPoll(); // 초기 호출

javascript
//클라이언트 요청 저장
let clients = [];
let messages = [];

app.get('/api/long-poll', (req, res) => {
  // 클라이언트가 요청을 보낼 때 콜백을 저장
  clients.push(res);

  // 클라이언트 연결이 끊어지면 콜백 제거
  req.on('close', () => {
    clients = clients.filter(client => client !== res);
  });
});

// 새로운 메시지가 도착했을 때 모든 클라이언트에게 전송
function broadcastMessage(message) {
  messages.push(message); // 메시지를 저장할 수도 있음.
  clients.forEach(client => client.json(message));
  clients = []; // 모든 클라이언트 응답을 보낸 후 클라이언트 리스트 초기화
}

HTTP Streaming

3%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-11-11%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_9.37.58.png_

  • 이벤트가 발생했을 때 응답을 내려주되, 응답을 완료시키지 않고 계속 연결을 유지하는 방식
  • 장점
    • 롱폴링에 비해 응답마다 다시 요청을 하지 않아도 되므로 효율적
  • 단점
    • 연결시간이 길어질수록 연결 유효성 관리 부담 증가
    • 클라이언트에서 서버로의 데이터 송신이 어려움

Server-Sent Events (SSE)

4image.png

  • 서버와 클라이언트가 첫 연결에 성공하면, 서버는 이벤트 발생 주기별로 클라이언트에게 필요한 데이터를 자동전송
  • 클라이언트에서 서버로 데이터를 보내는 것은 불가능 (단방향)
  • 장점
    • 구현이 비교적 간단하고 효율적임
    • 자동 재연결 기능 제공
    • 텍스트 기반 데이터 전송에 최적화됨
  • 단점
    • 단방향 통신만 가능함
    • 과거 브라우저에서 지원되지 않을 수 있음
  • 사용 사례
    • 클라이언트 요청 필요 없이 서버가 독자적으로 응답을 날리는 연결 형태
    • 실시간 뉴스 피드, 실시간 주식 시세 업데이트, 실시간 스포츠 경기 점수 업데이트

SSE 처리 흐름 (루카스 참고)

  • 클라이언트에서 서버로 연결 요청
    • 클라이언트는 EventSource 객체를 생성해 /sse 경로로 GET 요청을 보냄.
    • 이 요청을 통해 데이터를 지속적으로 수신할 준비를 함.
  • 클라이언트는 EventSource 객체를 생성해 /sse 경로로 GET 요청을 보냄.
  • 이 요청을 통해 데이터를 지속적으로 수신할 준비를 함.
  • 서버에서 연결을 유지하고 스트리밍 응답 시작
    • 서버는 클라이언트의 요청에 대해 Content-Type: text/event-stream 헤더를 설정하고 응답을 유지함.
    • 이 상태에서 서버는 데이터를 계속해서 클라이언트로 푸시할 수 있음.
  • 서버는 클라이언트의 요청에 대해 Content-Type: text/event-stream 헤더를 설정하고 응답을 유지함.
  • 이 상태에서 서버는 데이터를 계속해서 클라이언트로 푸시할 수 있음.
  • 서버가 주기적으로 데이터를 전송
    • 서버는 특정 이벤트나 일정 시간 간격에 따라 데이터를 res.write() 메서드를 사용해 클라이언트로 전송함.
    • 각 메시지는 data:로 시작하고 두 개의 개행 문자(\n\n)로 끝내야 하며, 이를 통해 클라이언트는 메시지의 끝을 구분할 수 있음.
  • 서버는 특정 이벤트나 일정 시간 간격에 따라 데이터를 res.write() 메서드를 사용해 클라이언트로 전송함.
  • 각 메시지는 data:로 시작하고 두 개의 개행 문자(\n\n)로 끝내야 하며, 이를 통해 클라이언트는 메시지의 끝을 구분할 수 있음.
  • 클라이언트에서 실시간으로 데이터 수신
    • 클라이언트는 onmessage 이벤트 핸들러를 통해 서버에서 전송된 데이터를 실시간으로 수신하고 처리할 수 있음.
  • 클라이언트는 onmessage 이벤트 핸들러를 통해 서버에서 전송된 데이터를 실시간으로 수신하고 처리할 수 있음.
javascript
// 클라이언트 측 SSE 연결 시작
const eventSource = new EventSource('/sse');

eventSource.onmessage = (event) => {
  console.log('받은 데이터:', event.data); //서버에서 전송된 데이터 수신 및 처리
};

eventSource.onerror = (error) => {
  console.error('SSE 오류:', error); // 오류 발생 시 처리
};

javascript
const express = require('express');
const app = express();

app.get('/sse', (req, res) => {
  // SSE 응답 헤더 설정
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');

  // 클라에게 응답
  const sendMessage = () => {
    const data = `서버 시간: ${new Date().toLocaleTimeString()}`;
    res.write(`data: ${data}\n\n`); // 클라이언트로 데이터 전송
  };

  // 5초마다 메시지 전송
  const intervalId = setInterval(sendMessage, 5000);

  // 클라이언트가 연결을 닫으면 interval 중단
  req.on('close', () => {
    clearInterval(intervalId);
  });
});


⇒ 결과적으로 위 모든 방법이 HTTP를 통해 통신하기 때문에 Request, Response 둘 다 Header가 불필요하게 큼

🌐 WebSocket 웹 소켓

  • W3C와 IETF에 의해 자리잡은 표준 프로토콜 중 하나
  • HTML5 표준 기술로, 사용자의 브라우저와 서버 사이의 동적인 양방향 연결 채널을 구성
  • Websocket API를 통해 서버로 메세지를 보내고, 요청 없이 응답을 받아오는 것이 가능!
    • TCP프로토콜을 기반으로 작동하며, 데이터가 순서대로 전송되도록 하고 오류가 발생할 경우 자동으로 복구.
    • HTTP 핸드셰이크로 연결을 시작한 후 TCP 소켓을 통해 양방향 데이터 전송을 지속함.
  • 양방향 통신 & 실시간 네트워킹

웹 소켓의 동작 과정

Request와 Response의 구체적인 내용에 대해서는 첨부하지 않았다..

https://www.youtube.com/watch?v=MPQHvwPxDUw

위 영상을 토대로 공부했다!

5image.png

6image.png

7image.png

javascript
// 클라이언트 측 WebSocket 예시
const socket = new WebSocket('ws://localhost:8080');

socket.onopen = () => {
  console.log('WebSocket 연결 성공');
  socket.send('서버야 이거 받아볼래?');
};

socket.onmessage = (event) => {
  console.log('서버로부터 메시지:', event.data);
  // 메시지 처리 로직
};

socket.onerror = (error) => {
  console.error('WebSocket 오류:', error);
};

socket.onclose = () => {
  console.log('WebSocket 연결 종료');
};

👀 WS 모듈 vs Socket.io

https://www.peterkimzz.com/websocket-vs-socket-io

ws 모듈과 Socket.io는 둘 다 실시간 양방향 통신을 가능하게 하는 Node.js 기반의 라이브러리이지만, 기능과 사용 편의성 측면에서 차이가 있음

WS 모듈

  • 특징
    • 웹소켓 표준 구현: ws는 기본적인 웹소켓 프로토콜에 대한 가벼운 구현입니다.
    • 최소한의 종속성: 다른 라이브러리에 비해 종속성이 적고, 성능에 초점을 맞춘 경량 라이브러리입니다.
    • 자유도: 개발자가 더 세부적인 부분을 제어할 수 있습니다.
  • 장점
    • 성능: 단순하고 가벼운 구조로 인해 성능이 좋습니다.
    • 유연성: 개발자가 직접 다양한 기능을 구현할 수 있어 커스터마이징에 유리합니다.
    • 표준 준수: 웹소켓 표준 프로토콜을 직접 구현하므로, 다른 웹소켓 클라이언트와 호환이 잘 됩니다.
  • 단점
    • 저수준 인터페이스: 사용이 비교적 복잡하며, 연결 관리, 재시도, 브로드캐스팅 같은 기능을 직접 구현해야 합니다.
    • 부가 기능 부족: 자동 재연결, 룸 기능, 이벤트 기반 구조 같은 고급 기능이 없습니다.

Socket.io

  • 특징
    • 웹소켓 + 폴백: 기본적으로 웹소켓을 사용하지만, 웹소켓을 지원하지 않는 환경에서는 폴링(polling)으로 폴백(fallback)합니다.
    • 추가 기능 제공: 이벤트 기반 통신, 자동 재연결, 브로드캐스트, 네임스페이스 및 룸 기능을 제공합니다.
    • 클라이언트 라이브러리 필요: 클라이언트에서도 Socket.io 라이브러리가 필요합니다.
  • 장점
    • 편리성: 복잡한 설정 없이 간단하게 사용할 수 있습니다.
    • 풍부한 기능: 자동 재연결, 브로드캐스트, 룸 기능 등 실시간 애플리케이션 구축에 필요한 다양한 기능을 내장하고 있습니다.
    • 다양한 폴백 지원: 웹소켓을 사용할 수 없는 환경에서도 폴백을 통해 통신이 가능합니다.
  • 단점
    • 추가적인 오버헤드: 여러 기능이 내장되어 있어 ws 모듈보다 무겁습니다.
    • 표준과 다름: 표준 웹소켓과 100% 호환되지 않을 수 있어 특정 상황에서는 제한이 있을 수 있습니다.
    • 클라이언트 라이브러리 의존성: 서버와 클라이언트가 모두 Socket.io를 사용해야 하므로, 웹소켓 클라이언트와의 호환성 문제가 생길 수 있습니다.

주요 차이점 정리

  • 단순함 vs. 편리함: ws는 경량으로 단순한 웹소켓 구현에 유리하고, Socket.io는 다양한 기능과 편리성을 제공합니다.
  • 표준 준수 vs. 기능 확장성: ws는 웹소켓 표준을 따르고, Socket.io는 추가 기능을 위해 표준을 약간 벗어날 수 있습니다.
  • 커스터마이징: ws는 모든 것을 개발자가 직접 구성할 수 있으며, Socket.io는 다양한 기능이 이미 구현되어 있어 개발 시간을 단축할 수 있습니다.

추천 시나리오

  • ws 사용: 최대 성능이 필요하거나, 단순한 실시간 통신 기능만 필요한 경우.
  • Socket.io 사용: 빠르게 개발해야 하거나, 자동 재연결 및 룸 기능 같은 고급 기능이 필요한 경우.

이러한 차이를 이해하면 프로젝트 요구 사항에 맞는 적절한 라이브러리를 선택할 수 있습니다.

⇒ 생산성을 고려해서 브로드캐스트, 네임스페이스, 자동 재연결이 구현되어있는 socket.io가 우리 서비스에 더 적절할 것이라고 생각됨