웹 소켓의 실시간 양방향 통신 [feat. WS vs Socket.io]
목차
📌 과거 발표자료를 토대로 정리했습니다
🤔 학습 이유
과거 웹소켓 관련 컨퍼런스 발표까지 해보았지만 실제 프로젝트에서 써보지 않아 휘발되었다…ㅜㅜ
이번 기회에 실시간 & 양방향 통신을 위한 기술들을 조사해보고 어떤 방식으로 우리 서비스의 실시간 채팅 기능을 구현해 볼 지 고민해보고자 한다.
HTTP 통신은 클라이언트의 요청이 있을 때 서버가 응답하는 단방향 통신
HTTP 통신
그럼 실시간 채팅처럼 양방향이면서 실시간 통신이 필요한 기능 구현은 어떻게 할까? 우선 실시간 통신을 위한 몇가지 통신 방식을 알아보자
🚀 실시간 통신을 위한 노력
AJAX Polling
image.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
image.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
%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)
image.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
위 영상을 토대로 공부했다!
image.png
image.png
image.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가 우리 서비스에 더 적절할 것이라고 생각됨
Subscribe to Liboo.blog
Get the latest posts delivered right to your inbox