본문으로 건너뛰기

🔴 웹소켓 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>
);
}

Q4. 보안은 어떻게 하나요?

A:

// ========== 1. HTTPS/WSS 사용 ==========

// ❌ HTTP/WS (평문 전송)
const ws = new WebSocket('ws://api.example.com');

// ✅ HTTPS/WSS (암호화)
const ws = new WebSocket('wss://api.example.com');

// ========== 2. 인증 ==========

// 방법 1: JWT in URL (간단하지만 권장 안 함)
const token = getJWTToken();
const ws = new WebSocket(`wss://api.example.com?token=${token}`);
// ⚠️ URL에 토큰 노출 위험

// 방법 2: 연결 후 인증 (권장)
const ws = new WebSocket('wss://api.example.com');

ws.onopen = () => {
// 인증 메시지 전송
ws.send(JSON.stringify({
type: 'auth',
token: getJWTToken()
}));
};

// 서버
io.use((socket, next) => {
const token = socket.handshake.auth.token;

try {
const decoded = jwt.verify(token, SECRET_KEY);
socket.userId = decoded.userId;
next();
} catch (error) {
next(new Error('인증 실패'));
}
});

// 방법 3: 쿠키 (HttpOnly)
// 클라이언트
const ws = new WebSocket('wss://api.example.com');
// 쿠키는 자동으로 전송됨

// 서버
io.use((socket, next) => {
const cookies = parseCookies(socket.request.headers.cookie);
const sessionId = cookies.sessionId;

if (isValidSession(sessionId)) {
next();
} else {
next(new Error('인증 실패'));
}
});

// ========== 3. CORS 설정 ==========

const io = socketIo(server, {
cors: {
origin: 'https://myapp.com', // 특정 도메인만
credentials: true
}
});

// 여러 도메인
const allowedOrigins = ['https://myapp.com', 'https://admin.myapp.com'];

const io = socketIo(server, {
cors: {
origin: (origin, callback) => {
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('CORS 거부'));
}
},
credentials: true
}
});

// ========== 4. Rate Limiting ==========

const rateLimitMap = new Map();

io.on('connection', (socket) => {
socket.on('message', (data) => {
const userId = socket.userId;
const now = Date.now();

// 사용자별 메시지 수 추적
if (!rateLimitMap.has(userId)) {
rateLimitMap.set(userId, []);
}

const userMessages = rateLimitMap.get(userId);

// 최근 1분간 메시지 필터링
const recentMessages = userMessages.filter(
timestamp => now - timestamp < 60000
);

// 분당 10개 제한
if (recentMessages.length >= 10) {
socket.emit('error', '메시지 전송 제한 초과');
return;
}

recentMessages.push(now);
rateLimitMap.set(userId, recentMessages);

// 메시지 처리
handleMessage(data);
});
});

// ========== 5. 입력 검증 ==========

socket.on('message', (data) => {
// 타입 검증
if (typeof data !== 'object') {
return socket.emit('error', '잘못된 데이터 형식');
}

// 필수 필드 검증
if (!data.type || !data.content) {
return socket.emit('error', '필수 필드 누락');
}

// 길이 검증
if (data.content.length > 1000) {
return socket.emit('error', '메시지가 너무 깁니다');
}

// XSS 방지
const sanitizedContent = sanitizeHtml(data.content);

// 처리
broadcastMessage(sanitizedContent);
});

// ========== 6. 네임스페이스 및 Room ==========

// 네임스페이스로 격리
const chatNamespace = io.of('/chat');
const adminNamespace = io.of('/admin');

chatNamespace.on('connection', (socket) => {
// 일반 채팅
});

adminNamespace.use(authenticateAdmin);
adminNamespace.on('connection', (socket) => {
// 관리자 전용
});

// Room으로 권한 제어
socket.on('join-room', (roomId) => {
if (canAccessRoom(socket.userId, roomId)) {
socket.join(roomId);
} else {
socket.emit('error', '방 접근 권한 없음');
}
});

// ========== 7. DDoS 방지 ==========

// 연결 수 제한
const MAX_CONNECTIONS_PER_IP = 5;
const connectionsByIP = new Map();

io.on('connection', (socket) => {
const ip = socket.handshake.address;

const count = connectionsByIP.get(ip) || 0;

if (count >= MAX_CONNECTIONS_PER_IP) {
socket.disconnect();
return;
}

connectionsByIP.set(ip, count + 1);

socket.on('disconnect', () => {
const newCount = connectionsByIP.get(ip) - 1;
if (newCount <= 0) {
connectionsByIP.delete(ip);
} else {
connectionsByIP.set(ip, newCount);
}
});
});

Q5. 에러 처리는?

A:

// ========== 클라이언트 에러 처리 ==========

class WebSocketClient {
constructor(url) {
this.url = url;
this.ws = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 10;
this.reconnectDelay = 1000;
}

connect() {
try {
this.ws = new WebSocket(this.url);

this.ws.onopen = () => {
console.log('연결 성공');
this.reconnectAttempts = 0;
this.onConnected();
};

this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this.onMessage(data);
} catch (error) {
console.error('메시지 파싱 에러:', error);
}
};

this.ws.onerror = (error) => {
console.error('WebSocket 에러:', error);
this.onError(error);
};

this.ws.onclose = (event) => {
console.log('연결 끊김:', event.code, event.reason);

// 정상 종료 (1000)
if (event.code === 1000) {
console.log('정상 종료');
return;
}

// 재연결 시도
this.reconnect();
};
} catch (error) {
console.error('연결 실패:', error);
this.reconnect();
}
}

reconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('재연결 포기');
this.onReconnectFailed();
return;
}

this.reconnectAttempts++;
const delay = Math.min(
this.reconnectDelay * Math.pow(2, this.reconnectAttempts),
30000
);

console.log(`${delay}ms 후 재연결 시도 (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);

setTimeout(() => {
this.connect();
}, delay);
}

send(data) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
try {
this.ws.send(JSON.stringify(data));
} catch (error) {
console.error('전송 에러:', error);
}
} else {
console.error('WebSocket이 열려있지 않음');
// 메시지 큐에 저장
this.queueMessage(data);
}
}

close() {
if (this.ws) {
this.ws.close(1000, '클라이언트 종료');
}
}

// 이벤트 핸들러 (오버라이드)
onConnected() {}
onMessage(data) {}
onError(error) {}
onReconnectFailed() {}

queueMessage(data) {
// 오프라인 메시지 큐 구현
}
}

// 사용
const client = new WebSocketClient('wss://api.example.com');

client.onConnected = () => {
console.log('연결됨!');
};

client.onMessage = (data) => {
console.log('메시지:', data);
};

client.onError = (error) => {
console.error('에러 발생:', error);
// UI에 에러 표시
showErrorNotification('연결 에러');
};

client.onReconnectFailed = () => {
// 재연결 실패 처리
showErrorModal('서버에 연결할 수 없습니다');
};

client.connect();

// ========== 서버 에러 처리 ==========

io.on('connection', (socket) => {
// 에러 핸들러
socket.on('error', (error) => {
console.error('소켓 에러:', error);
});

// 메시지 처리 에러
socket.on('message', async (data) => {
try {
// 입력 검증
if (!isValidMessage(data)) {
throw new Error('유효하지 않은 메시지');
}

// 처리
await processMessage(data);
} catch (error) {
console.error('메시지 처리 에러:', error);

// 클라이언트에 에러 전송
socket.emit('error', {
code: 'MESSAGE_PROCESSING_ERROR',
message: error.message
});
}
});

// 연결 끊김 처리
socket.on('disconnect', (reason) => {
console.log('연결 끊김:', reason);

// 사용자 정리
cleanupUser(socket.userId);

// 다른 사용자에게 알림
socket.broadcast.emit('user-left', socket.userId);
});
});

// 전역 에러 핸들러
io.engine.on('connection_error', (error) => {
console.error('연결 에러:', error);
});

// ========== 타임아웃 처리 ==========

socket.on('message', (data, callback) => {
// 타임아웃 설정 (5초)
const timeout = setTimeout(() => {
callback({
error: 'TIMEOUT',
message: '응답 시간 초과'
});
}, 5000);

// 처리
processMessage(data)
.then(result => {
clearTimeout(timeout);
callback({ success: true, data: result });
})
.catch(error => {
clearTimeout(timeout);
callback({ error: error.message });
});
});

🎓 다음 단계

실시간 통신을 이해했다면, 다음을 학습해보세요:

  1. HTTP 기본 (문서 작성 예정) - HTTP 프로토콜 이해
  2. API란? (문서 작성 예정) - API 기본 개념
  3. REST API vs GraphQL - API 설계

실습해보기

# ========== 1. Socket.io 채팅 앱 ==========

mkdir chat-app
cd chat-app
npm init -y
npm install express socket.io

# server.js 작성 후
node server.js

# ========== 2. SSE 실시간 알림 ==========

mkdir sse-demo
cd sse-demo
npm install express

# server.js 작성
# index.html 작성

node server.js
# http://localhost:3000 접속

# ========== 3. WebSocket 게임 ==========

mkdir websocket-game
cd websocket-game
npm install ws express

# 멀티플레이어 게임 구현
node server.js

🎬 마무리

실시간 통신은 현대 웹의 핵심 기술입니다:

  • 롱 폴링: 레거시 지원, HTTP 기반
  • SSE: 서버 푸시, 간단한 구현
  • WebSocket: 양방향 실시간, 최고 성능
  • 선택 기준: 요구사항, 환경, 복잡도

프로젝트에 맞는 실시간 통신 방식을 선택하여 훌륭한 사용자 경험을 만드세요! 🔴