🔴 웹소켓 vs SSE vs 롱폴링
📖 정의
실시간 통신은 서버와 클라이언트 간에 즉각적인 데이터 교환을 가능하게 하는 기술입니다. WebSocket은 양방향 실시간 통신을 제공하고, SSE(Server-Sent Events)는 서버에서 클라이언트로만 데이터를 푸시하며, 롱 폴링(Long Polling)은 HTTP를 이용한 실시간 통신 방식입니다. 각각 장단점이 있어 사용 시나리오에 따라 선택해야 합니다.
🎯 비유로 이해하기
전화 vs 라디오 vs 메신저
일반 HTTP = 우편
당신: "안녕하세요?" (편지 보냄)
↓ (며칠 후)
친구: "안녕!" (답장)
↓ (며칠 후)
당신: "잘 지내?" (또 편지)
- 느림
- 매번 새로 연결
- 실시간 불가능
롱 폴링 = 전화 (통화 대기)
당신: "뭔가 있으면 알려줘!" (전화 걸고 대기)
↓ (한참 기다림...)
친구: "지금 말할게 있어!" (응답)
↓ (통화 끊김)
당신: "또 뭔가 있으면 알려줘!" (다시 전화)
- HTTP 기반
- 연결 유지 → 응답 → 재연결
- 비효율적
SSE = 라디오 방송
친구: "여러분 안녕하세요!" (방송 시작)
"오늘 날씨는..." (계속 송출)
"다음 소식은..." (계속 송출)
당신: (듣기만 함)
- 서버 → 클라이언트 (단방향)
- 연결 유지
- 간단함
WebSocket = 화상 통화
당신: "안녕!" (즉시 전송)
친구: "반가워!" (즉시 응답)
당신: "뭐해?" (즉시 전송)
친구: "코딩 중!" (즉시 응답)
- 양방향 실시간
- 연결 유지
- 빠르고 효율적
식당 주문
일반 HTTP = 셀프 서비스
1. 카운터 가서 음식 주문
2. 기다림
3. 음식 받고 자리로
4. 다시 카운터 가서 음료 주문
5. 또 기다림
롱 폴링 = 벨 누르고 대기
1. 벨 누르고 직원 기다림 (연결 유지)
2. 직원 옴 → "주문하세요"
3. 주문하고 벨 다시 누름
4. 또 기다림...
SSE = 주방 디스플레이
주방: "1번 손님, 음식 나왔어요!"
주방: "2번 손님, 준비 중이에요!"
주방: "3번 손님, 곧 나와요!"
당신: (듣기만 함)
WebSocket = 테이블 서비스
당신: "물 주세요"
직원: "네, 가져다 드릴게요"
당신: "김치도요"
직원: "바로 가져올게요"
직원: "음식 나왔습니다"
당신: "감사합니다"
- 자유롭게 대화
- 빠른 응답
⚙️ 작동 원리
1. 일반 HTTP vs 실시간 통신
========== 일반 HTTP (요청-응답) ==========
클라이언트 서버
│ │
│ 1. 연결 (Request) │
│────────────────────────>│
│ │
│ │ 처리 중...
│ │
│ 2. 응답 (Response) │
│<────────────────────────│
│ │
│ 연결 종료 │
╳ ╳
│ 3. 다시 연결 │
│────────────────────────>│
│ │
특징:
- 클라이언트가 요청해야만 응답
- 매번 새로 연결
- 서버가 먼저 보낼 수 없음
- 실시간 불가능
========== 실시간 통신 ==========
클라이언트 서버
│ │
│ 연결 │
│<───────────────────────>│
│ │
│ 양방향 통신 유지 │
│<───────────────────────>│
│ │
│ 데이터 교환 │
│<──────── ───────────────>│
│ │
특징:
- 연결 유지
- 서버가 먼저 보낼 수 있음
- 실시간 가능
2. 롱 폴링 (Long Polling)
클라이언트 서버
│ │
│ 1. 요청 (새 데이터 있나요?) │
│──────────────────────────────────>│
│ │
│ 연결 유지 (대기 중...) │
│ │
│ │ 데이터 없음...
│ │ 계속 대기...
│ │
│ │ 새 데이터 발생!
│ │
│ 2. 응답 (여기 데이터 있어요) │
│<──────────────────────────────────│
│ │
│ 연결 종료 │
╳ ╳
│ │
│ 3. 즉시 재연결 │
│──────────────────────────────────>│
│ │
│ 다시 대기... │
과정:
1. 클라이언트 → 서버: "새 데이터 있나요?"
2. 서버: 데이터 생길 때까지 대기
3. 데이터 생김 → 응답
4. 연결 종료
5. 즉시 재연결 (반복)
장점:
✅ HTTP 기반 (기존 인프라 활용)
✅ 방화벽 문제 없음
✅ 구현 간단
단점:
❌ 비효율적 (계속 재연결)
❌ 서버 부담 큼
❌ 헤더 오버헤드
3. Server-Sent Events (SSE)
클라이언트 서버
│ │
│ 1. 연결 요청 │
│──────────────────────────────────>│
│ │
│ 2. 연결 유지 (스트림 시작) │
│<══════════════════════════════════│
│ │
│ 3. 데이터 푸시 │
│<──────────────────────────────────│
│ │
│ 4. 또 데이터 푸시 │
│<──────────────────────────────────│
│ │
│ 계속 연결 유지... │
│<══════════════════════════════════│
특징:
- 서버 → 클라이언트 (단방향)
- 연결 유지
- HTTP 기반
- 자동 재연결
장점:
✅ 간단함 (브라우저 내장)
✅ 자동 재연결
✅ 이벤트 ID로 재개 가능
✅ HTTP/2에서 효율적
단점:
❌ 단방향 (서버 → 클라이언트)
❌ 바이너리 데이터 불가
❌ IE 미지원
4. WebSocket
클라이언트 서버
│ │
│ 1. HTTP Upgrade 요청 │
│──────────────────────────────────>│
│ │
│ 2. Upgrade 승인 │
│<──────────────────────────────────│
│ │
│ WebSocket 프로토콜로 전환 │
│<══════════════════════════════════>│
│ │
│ 3. 데이터 전송 │
│──────────────────────────────────>│
│ │
│ 4. 데이터 수신 │
│<──────────────────────────────────│
│ │
│ 5. 데이터 전송 │
│──────────────────────────────────>│
│ │
│ 양방향 통신 계속... │
│<══════════════════════════════════>│
특징:
- 양방향 실시간 통신
- 별도 프로토콜 (ws://, wss://)
- 연결 유지
- 낮은 지연 시간
장점:
✅ 진정한 실시간
✅ 양방향 통신
✅ 낮은 오버헤드
✅ 바이너리 지원
단점:
❌ 복잡함
❌ 프록시/방화벽 문제 가능
❌ 서버 부담 (연결 유지)
💡 실제 예시
롱 폴링 예시
// ========== 서버 (Express.js) ==========
const express = require('express');
const app = express();
let messages = [];
let waitingClients = [];
// 메시지 추가 (다른 곳에서 호출됨)
function addMessage(message) {
messages.push(message);
// 대기 중인 클라이언트들에게 즉시 응답
waitingClients.forEach(client => {
client.json({ messages });
});
waitingClients = [];
}
// 롱 폴링 엔드포인트
app.get('/messages', (req, res) => {
const lastId = parseInt(req.query.lastId) || 0;
// 새 메시지가 있으면 즉시 응답
if (messages.length > lastId) {
return res.json({ messages: messages.slice(lastId) });
}
// 없으면 대기 목록에 추가 (최대 30초)
waitingClients.push(res);
// 30초 타임아웃
req.setTimeout(30000, () => {
const index = waitingClients.indexOf(res);
if (index > -1) {
waitingClients.splice(index, 1);
res.json({ messages: [] }); // 빈 응답
}
});
});
app.listen(3000);
// ========== 클라이언트 ==========
let lastMessageId = 0;
async function longPolling() {
while (true) {
try {
const response = await fetch(`/messages?lastId=${lastMessageId}`);
const data = await response.json();
// 새 메시지 처리
if (data.messages.length > 0) {
data.messages.forEach(msg => {
console.log('새 메시지:', msg);
displayMessage(msg);
});
lastMessageId += data.messages.length;
}
// 즉시 재연결
await longPolling();
} catch (error) {
console.error('에러:', error);
// 3초 후 재시도
await new Promise(resolve => setTimeout(resolve, 3000));
}
}
}
// 시작
longPolling();
// ========== 문제점 ==========
/*
1. 계속 재연결 (비효율적)
2. 네트워크 사용량 많음
3. 서버 부담 큼
4. 배터리 소모 (모바일)
*/
Server-Sent Events (SSE) 예시
// ========== 서버 (Express.js) ==========
const express = require('express');
const app = express();
app.use(express.static('public'));
// SSE 엔드포인트
app.get('/events', (req, res) => {
// SSE 헤더 설정
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// CORS (필요한 경우)
res.setHeader('Access-Control-Allow-Origin', '*');
// 연결 확인 메시지
res.write('data: Connected\n\n');
// 클라이언트 ID
const clientId = Date.now();
console.log(`클라이언트 ${clientId} 연결됨`);
// 5초마다 시간 전송
const intervalId = setInterval(() => {
const data = {
time: new Date().toLocaleTimeString(),
message: '안녕하세요!'
};
// SSE 형식으로 전송
res.write(`data: ${JSON.stringify(data)}\n\n`);
}, 5000);
// 클라이언트 연결 끊김 처리
req.on('close', () => {
console.log(`클라이언트 ${clientId} 연결 끊김`);
clearInterval(intervalId);
res.end();
});
});
// 이벤트 발행 API
const clients = [];
app.get('/events/stream', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// 클라이언트 저장
clients.push(res);
req.on('close', () => {
const index = clients.indexOf(res);
clients.splice(index, 1);
});
});
// 모든 클라이언트에게 메시지 전송
app.post('/broadcast', express.json(), (req, res) => {
const { message } = req.body;
clients.forEach(client => {
client.write(`data: ${JSON.stringify({ message })}\n\n`);
});
res.json({ success: true });
});
app.listen(3000);
// ========== 클라이언트 (HTML) ==========
/*
<!DOCTYPE html>
<html>
<body>
<div id="messages"></div>
<script>
// SSE 연결
const eventSource = new EventSource('/events');
// 메시지 수신
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('받은 데이터:', data);
const div = document.getElementById('messages');
div.innerHTML += `<p>${data.time}: ${data.message}</p>`;
};
// 연결 열림
eventSource.onopen = () => {
console.log('SSE 연결됨');
};
// 에러 처리
eventSource.onerror = (error) => {
console.error('SSE 에러:', error);
if (eventSource.readyState === EventSource.CLOSED) {
console.log('SSE 연결 끊김');
}
};
// 연결 종료 (페이지 벗어날 때)
window.addEventListener('beforeunload', () => {
eventSource.close();
});
</script>
</body>
</html>
*/
// ========== 고급 기능 ==========
// 1. 이벤트 타입 지정
app.get('/events/typed', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
// 여러 종류의 이벤트
setInterval(() => {
// 일반 메시지
res.write(`event: message\ndata: Hello\n\n`);
// 알림
res.write(`event: notification\ndata: New notification!\n\n`);
// 업데이트
res.write(`event: update\ndata: {"count": 10}\n\n`);
}, 5000);
});
// 클라이언트에서 이벤트 타입별 처리
/*
eventSource.addEventListener('message', (e) => {
console.log('메시지:', e.data);
});
eventSource.addEventListener('notification', (e) => {
console.log('알림:', e.data);
});
eventSource.addEventListener('update', (e) => {
const data = JSON.parse(e.data);
console.log('업데이트:', data);
});
*/
// 2. 이벤트 ID (재연결 시 이어받기)
let eventId = 0;
app.get('/events/resumable', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
const lastEventId = parseInt(req.headers['last-event-id']) || 0;
console.log('마지막 이벤트 ID:', lastEventId);
// lastEventId 이후의 이벤트만 전송
setInterval(() => {
eventId++;
res.write(`id: ${eventId}\ndata: Event ${eventId}\n\n`);
}, 1000);
});
// 3. 재시도 시간 설정
app.get('/events/retry', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
// 5초 후 재연결
res.write('retry: 5000\n');
res.write('data: Connected\n\n');
});
WebSocket 예시 (Socket.io)
// ========== 서버 (Socket.io) ==========
const express = require('express');
const http = require('http');
const socketIo = require('socket.io');
const app = express();
const server = http.createServer(app);
const io = socketIo(server, {
cors: {
origin: '*'
}
});
app.use(express.static('public'));
// 연결된 사용자 관리
const users = new Map();
// WebSocket 연결
io.on('connection', (socket) => {
console.log('새 사용자 연결:', socket.id);
// 사용자 정보 저장
socket.on('register', (username) => {
users.set(socket.id, { username, socket });
console.log(`${username} 등록됨`);
// 모든 사용자에게 알림
io.emit('user-joined', {
username,
totalUsers: users.size
});
});
// 메시지 수신
socket.on('chat-message', (message) => {
const user = users.get(socket.id);
console.log(`${user.username}: ${message}`);
// 모든 사용자에게 브로드캐스트
io.emit('chat-message', {
username: user.username,
message,
timestamp: new Date().toISOString()
});
});
// 타이핑 중 표시
socket.on('typing', () => {
const user = users.get(socket.id);
socket.broadcast.emit('user-typing', user.username);
});
socket.on('stop-typing', () => {
const user = users.get(socket.id);
socket.broadcast.emit('user-stop-typing', user.username);
});
// 개인 메시지
socket.on('private-message', ({ to, message }) => {
const targetSocket = Array.from(users.values())
.find(u => u.username === to)?.socket;
if (targetSocket) {
const sender = users.get(socket.id);
targetSocket.emit('private-message', {
from: sender.username,
message
});
}
});
// 연결 끊김
socket.on('disconnect', () => {
const user = users.get(socket.id);
if (user) {
console.log(`${user.username} 연결 끊김`);
users.delete(socket.id);
// 모든 사용자에게 알림
io.emit('user-left', {
username: user.username,
totalUsers: users.size
});
}
});
});
server.listen(3000, () => {
console.log('서버 실행: http://localhost:3000');
});
// ========== 클라이언트 (HTML + Socket.io) ==========
/*
<!DOCTYPE html>
<html>
<head>
<script src="/socket.io/socket.io.js"></script>
</head>
<body>
<div id="chat">
<div id="messages"></div>
<input id="username" placeholder="이름" />
<input id="message" placeholder="메시지" />
<button onclick="sendMessage()">전송</button>
</div>
<script>
// Socket.io 연결
const socket = io('http://localhost:3000');
// 연결 성공
socket.on('connect', () => {
console.log('연결됨:', socket.id);
});
// 사용자 등록
function register() {
const username = document.getElementById('username').value;
socket.emit('register', username);
}
// 메시지 전송
function sendMessage() {
const message = document.getElementById('message').value;
socket.emit('chat-message', message);
document.getElementById('message').value = '';
}
// 메시지 수신
socket.on('chat-message', (data) => {
const div = document.getElementById('messages');
div.innerHTML += `
<p><strong>${data.username}:</strong> ${data.message}</p>
`;
});
// 사용자 입장
socket.on('user-joined', (data) => {
console.log(`${data.username} 입장 (총 ${data.totalUsers}명)`);
});
// 사용자 퇴장
socket.on('user-left', (data) => {
console.log(`${data.username} 퇴장 (총 ${data.totalUsers}명)`);
});
// 타이핑 중
let typingTimeout;
document.getElementById('message').addEventListener('input', () => {
socket.emit('typing');
clearTimeout(typingTimeout);
typingTimeout = setTimeout(() => {
socket.emit('stop-typing');
}, 1000);
});
socket.on('user-typing', (username) => {
console.log(`${username}이(가) 타이핑 중...`);
});
// 연결 끊김
socket.on('disconnect', () => {
console.log('연결 끊김');
});
// 재연결
socket.on('reconnect', () => {
console.log('재연결됨');
});
</script>
</body>
</html>
*/
네이티브 WebSocket API
// ========== 서버 (ws 라이브러리) ==========
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws) => {
console.log('클라이언트 연결됨');
// 메시지 수신
ws.on('message', (message) => {
console.log('받은 메시지:', message.toString());
// 모든 클라이언트에게 브로드캐스트
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(message.toString());
}
});
});
// 연결 끊김
ws.on('close', () => {
console.log('클라이언트 연결 끊김');
});
// 에러
ws.on('error', (error) => {
console.error('WebSocket 에러:', error);
});
// 환영 메시지
ws.send('서버에 연결되었습니다!');
});
console.log('WebSocket 서버 실행: ws://localhost:8080');
// ========== 클라이언트 (브라우저) ==========
// WebSocket 연결
const ws = new WebSocket('ws://localhost:8080');
// 연결 열림
ws.addEventListener('open', (event) => {
console.log('WebSocket 연결됨');
ws.send('안녕하세요!');
});
// 메시지 수신
ws.addEventListener('message', (event) => {
console.log('서버로부터:', event.data);
});
// 에러
ws.addEventListener('error', (error) => {
console.error('WebSocket 에러:', error);
});
// 연결 끊김
ws.addEventListener('close', (event) => {
console.log('WebSocket 연결 끊김');
if (event.code === 1000) {
console.log('정상 종료');
} else {
console.log('비정상 종료:', event.code);
}
});
// 메시지 전송
function sendMessage(message) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(message);
} else {
console.error('WebSocket이 열려있지 않음');
}
}
// 연결 종료
function closeConnection() {
ws.close(1000, '정상 종료');
}
// ========== 바이너리 데이터 전송 ==========
// 파일 전송
async function sendFile(file) {
const arrayBuffer = await file.arrayBuffer();
ws.send(arrayBuffer);
}
// 서버에서 바이너리 수신
ws.addEventListener('message', (event) => {
if (event.data instanceof ArrayBuffer) {
console.log('바이너리 데이터 받음:', event.data.byteLength, 'bytes');
} else {
console.log('텍스트 데이터:', event.data);
}
});
// ========== Ping/Pong (연결 유지) ==========
// 서버
wss.on('connection', (ws) => {
ws.isAlive = true;
ws.on('pong', () => {
ws.isAlive = true;
});
});
// 30초마다 Ping
setInterval(() => {
wss.clients.forEach((ws) => {
if (ws.isAlive === false) {
return ws.terminate(); // 응답 없으면 연결 종료
}
ws.isAlive = false;
ws.ping();
});
}, 30000);
// ========== 자동 재연결 ==========
let ws;
let reconnectAttempts = 0;
const maxReconnectAttempts = 10;
function connect() {
ws = new WebSocket('ws://localhost:8080');
ws.addEventListener('open', () => {
console.log('연결됨');
reconnectAttempts = 0;
});
ws.addEventListener('close', (event) => {
console.log('연결 끊김');
// 자동 재연결
if (reconnectAttempts < maxReconnectAttempts) {
reconnectAttempts++;
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
console.log(`${delay}ms 후 재연결 시도...`);
setTimeout(connect, delay);
} else {
console.error('재연결 실패');
}
});
ws.addEventListener('error', (error) => {
console.error('에러:', error);
ws.close();
});
}
connect();
🤔 자주 묻는 질문
Q1. 어떤 방식을 선택해야 하나요?
A:
✅ 롱 폴링을 선택하는 경우:
├─ WebSocket/SSE 지원 안 되는 환경
├─ 레거시 시스템
├─ 간단한 알림
└─ 예: 오래된 브라우저 지원, 단순 폴링
사용 예시:
- 주식 시세 (업데이트 자주 안 함)
- 이메일 확인
- 간단한 알림
✅ SSE를 선택하는 경우:
├─ 서버 → 클라이언트 단방향
├─ 실시간 업데이트
├─ 간단 한 구현 원함
├─ 자동 재연결 필요
└─ 예: 뉴스 피드, 알림, 대시보드
사용 예시:
- 뉴스 피드 (실시간 업데이트)
- 주식 차트 (서버에서 푸시)
- 진행 상황 표시
- 로그 스트리밍
- 서버 모니터링
✅ WebSocket을 선택하는 경우:
├─ 양방향 실시간 통신
├─ 낮은 지연 시간 중요
├─ 빈번한 메시지 교환
├─ 바이너리 데이터
└─ 예: 채팅, 게임, 협업 도구
사용 예시:
- 채팅 애플리케이션
- 멀티플레이어 게임
- 협업 문서 편집 (Google Docs)
- 화상 회의
- IoT 실시간 제어
📊 비교표:
특성 | 롱 폴링 | SSE | WebSocket
-----------|---------|----------|----------
방향성 | 양방향 | 단방향 | 양방향
프로토콜 | HTTP | HTTP | WebSocket
지연시간 | 높음 | 낮음 | 매우 낮음
오버헤드 | 높음 | 보통 | 낮음
브라우저 | 전체 | 대부분 | 전체
복잡도 | 낮음 | 낮음 | 높음
재연결 | 수동 | 자동 | 수동
바이너리 | 가능 | 불가 | 가능
추천:
- 간단한 실시간: SSE
- 채팅/게임: WebSocket
- 레거시 지원: 롱 폴링
Q2. 성능 차이는?
A:
// ========== 벤치마크 예시 ==========
// 1. 롱 폴링
// 요청당:
// - HTTP 헤더: ~800 bytes
// - 재연결 시간: ~50ms
// - 서버 리소스: 연결당 1 스레드
// 100명 동시 접속 시:
// - 초당 요청: 100회
// - 데이터 전송: 80KB/s (헤더만)
// - 서버 부담: 높음
// 2. SSE
// 연결당:
// - HTTP 헤더: 초기 1회만 (~800 bytes)
// - 메시지 오버헤드: ~10 bytes
// - 서버 리소스: 연결 유지 (Keep-Alive)
// 100명 동시 접속 시:
// - 초기 연결: 80KB
// - 메시지당: 1KB (헤더 없음)
// - 서버 부담: 보통
// 3. WebSocket
// 연결당:
// - Upgrade 헤더: 1회만 (~500 bytes)
// - 프레임 오버헤드: 2~6 bytes
// - 서버 리소스: 연결 유지
// 100명 동시 접속 시:
// - 초기 연결: 50KB
// - 메시지당: ~0.002KB (프레임만)
// - 서버 부담: 낮음
// ========== 실제 측정 ==========
// 테스트: 100명이 초당 1개 메시지 전송
// 롱 폴링
const longPollingBenchmark = {
초당요청: 100,
평균지연시간: '200ms',
대역폭: '8MB/분',
서버CPU: '70%',
메모리: '500MB'
};
// SSE
const sseBenchmark = {
초당요청: 0, // 연결 유지
평균지연시간: '10ms',
대역폭: '600KB/분',
서버CPU: '30%',
메모리: '200MB'
};
// WebSocket
const webSocketBenchmark = {
초당요청: 0, // 연결 유지
평균지연시간: '2ms',
대역폭: '100KB/분',
서버CPU: '15%',
메모리: '100MB'
};
// ========== 실전 최적화 ==========
// 1. 연결 수 제한
const MAX_CONNECTIONS = 1000;
io.on('connection', (socket) => {
if (io.engine.clientsCount > MAX_CONNECTIONS) {
socket.disconnect();
return;
}
});
// 2. 메시지 압축
const io = socketIo(server, {
perMessageDeflate: {
threshold: 1024 // 1KB 이상만 압축
}
});
// 3. 배치 처리
const messageQueue = [];
setInterval(() => {
if (messageQueue.length > 0) {
io.emit('batch', messageQueue);
messageQueue.length = 0;
}
}, 100); // 100ms마다 배치 전송
// 4. 방(Room) 사용
socket.join('room1');
io.to('room1').emit('message', data); // room1만
// 5. 바이너리 전송
// WebSocket은 바이너리 효율적
const buffer = Buffer.from('Hello');
socket.send(buffer); // 텍스트보다 빠름
// 6. Heartbeat 최적화
const HEARTBEAT_INTERVAL = 25000; // 25초
const HEARTBEAT_TIMEOUT = 30000; // 30초
setInterval(() => {
wss.clients.forEach((ws) => {
if (ws.isAlive === false) {
return ws.terminate();
}
ws.isAlive = false;
ws.ping();
});
}, HEARTBEAT_INTERVAL);
Q3. 모바일 앱에서는?
A:
// ========== 모바일 고려사항 ==========
// 1. 배터리 소모
// 롱 폴링: ❌ 높음 (계속 재연결)
// SSE: ⚠️ 보통
// WebSocket: ✅ 낮음 (연결 유지)
// 2. 네트워크 전환
// WiFi <-> 모바일 데이터 전환 시
// 자동 재연결
let ws;
function connect() {
ws = new WebSocket('wss://api.example.com');
ws.onclose = () => {
// 지수 백오프
const delay = Math.min(1000 * Math.pow(2, attempts), 30000);
setTimeout(connect, delay);
};
}
// 네트워크 상태 감지
window.addEventListener('online', () => {
console.log('온라인 됨 - 재연결');
connect();
});
window.addEventListener('offline', () => {
console.log('오프라인');
ws.close();
});
// 3. 백그라운드 처리
// 앱이 백그라운드로 가면?
// React Native 예시
import { AppState } from 'react-native';
AppState.addEventListener('change', (nextAppState) => {
if (nextAppState === 'background') {
// 백그라운드 → 연결 유지 또는 종료
ws.close();
} else if (nextAppState === 'active') {
// 포그라운드 → 재연결
connect();
}
});
// 4. 데이터 절약 모드
// 사용자 설정에 따라
const isDataSaverMode = await getDataSaverSetting();
if (isDataSaverMode) {
// 롱 폴링 (덜 자주)
setInterval(poll, 60000); // 1분마다
} else {
// WebSocket (실시간)
connect();
}
// 5. 푸시 알림 연동
// WebSocket + FCM/APNs 조합
// 앱 실행 중: WebSocket
if (appIsActive) {
useWebSocket();
}
// 백그라운드: 푸시 알림
if (appIsBackground) {
usePushNotification();
}
// ========== React Native 예시 ==========
import React, { useEffect, useState } from 'react';
import { View, Text } from 'react-native';
function ChatScreen() {
const [ws, setWs] = useState(null);
const [messages, setMessages] = useState([]);
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
const websocket = new WebSocket('wss://api.example.com');
websocket.onopen = () => {
console.log('Connected');
setIsConnected(true);
};
websocket.onmessage = (event) => {
const message = JSON.parse(event.data);
setMessages(prev => [...prev, message]);
};
websocket.onerror = (error) => {
console.error('WebSocket error:', error);
};
websocket.onclose = () => {
console.log('Disconnected');
setIsConnected(false);
// 3초 후 재연결
setTimeout(() => {
// 재연결 로직
}, 3000);
};
setWs(websocket);
// Cleanup
return () => {
websocket.close();
};
}, []);
const sendMessage = (text) => {
if (ws && isConnected) {
ws.send(JSON.stringify({ text }));
}
};
return (
<View>
<Text>Connected: {isConnected ? 'Yes' : 'No'}</Text>
{messages.map((msg, i) => (
<Text key={i}>{msg.text}</Text>
))}
</View>
);
}