본문으로 건너뛰기

🔌 웹소켓이란?

📖 정의

WebSocket은 클라이언트와 서버 간의 양방향 실시간 통신을 가능하게 하는 프로토콜입니다. HTTP와 달리 한 번 연결되면 지속적으로 연결을 유지하며, 서버에서 클라이언트로 먼저 데이터를 보낼 수 있습니다(push). 채팅, 실시간 알림, 게임, 협업 도구 등에 사용됩니다.

🎯 비유로 이해하기

전화 vs 우편

HTTP (우편)
├─ 편지 보내기 (요청)
├─ 답장 받기 (응답)
├─ 다시 편지 쓰기 (요청)
└─ 매번 새로운 편지 필요 (오버헤드)

WebSocket (전화)
├─ 한 번 전화 연결
├─ 계속 대화 가능
├─ 양쪽 모두 먼저 말할 수 있음
└─ 끊기 전까지 연결 유지

택배 vs 직통 파이프

HTTP
┌─────────┐ 요청 ┌─────────┐
│클라이언트│ ──────────→ │ 서버 │
└─────────┘ └─────────┘
┌─────────┐ 응답 ┌─────────┐
│클라이언트│ ←────────── │ 서버 │
└─────────┘ └─────────┘
매번 새 연결!

WebSocket
┌─────────┐ ┌─────────┐
│클라이언트│ ←─────────→ │ 서버 │
└─────────┘ 지속 연결 └─────────┘
↕ 양방향

⚙️ 작동 원리

1. WebSocket 연결 과정

1. HTTP로 핸드셰이크 요청
GET /chat HTTP/1.1
Upgrade: websocket
Connection: Upgrade

2. 서버 승인
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade

3. WebSocket 연결 수립
↕ 양방향 통신 시작

4. 데이터 전송
메시지 ←→ 메시지 ←→ 메시지

5. 연결 종료
close() 호출

2. HTTP vs WebSocket

HTTP (단방향, 요청-응답)
클라이언트 → 서버: 요청
클라이언트 ← 서버: 응답
[연결 종료]

WebSocket (양방향, 지속)
클라이언트 ↔ 서버
- 서버가 먼저 메시지 전송 가능
- 연결 유지
- 실시간 통신

💡 실제 예시

기본 WebSocket (클라이언트)

// 웹 브라우저에서
const ws = new WebSocket('ws://localhost:8080');

// 연결 성공
ws.onopen = () => {
console.log('연결됨!');
ws.send('안녕하세요!'); // 메시지 전송
};

// 메시지 수신
ws.onmessage = (event) => {
console.log('받은 메시지:', event.data);
};

// 에러 처리
ws.onerror = (error) => {
console.error('에러:', error);
};

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

// 메시지 전송
ws.send('Hello Server!');
ws.send(JSON.stringify({ type: 'chat', message: 'Hi' }));

// 연결 종료
ws.close();

기본 WebSocket 서버 (Node.js)

const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 8080 });

console.log('WebSocket 서버 실행: ws://localhost:8080');

// 클라이언트 연결 시
wss.on('connection', (ws) => {
console.log('클라이언트 연결!');

// 환영 메시지
ws.send('환영합니다!');

// 메시지 수신
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('에러:', error);
});
});

Socket.io (실전 채팅 앱)

// ============ 서버 (server.js) ============
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);

app.use(express.static('public'));

const users = new Map(); // 접속 중인 사용자

io.on('connection', (socket) => {
console.log('사용자 연결:', socket.id);

// 사용자 입장
socket.on('join', (username) => {
users.set(socket.id, username);

// 입장 알림 (모두에게)
io.emit('user-joined', {
username,
userCount: users.size
});

console.log(`${username} 입장 (총 ${users.size}명)`);
});

// 채팅 메시지
socket.on('chat-message', (message) => {
const username = users.get(socket.id);

// 모두에게 브로드캐스트
io.emit('chat-message', {
username,
message,
timestamp: new Date()
});
});

// 타이핑 중
socket.on('typing', () => {
const username = users.get(socket.id);
socket.broadcast.emit('user-typing', username);
});

// 연결 종료
socket.on('disconnect', () => {
const username = users.get(socket.id);
users.delete(socket.id);

// 퇴장 알림
io.emit('user-left', {
username,
userCount: users.size
});

console.log(`${username} 퇴장 (남은 인원: ${users.size}명)`);
});
});

server.listen(3000, () => {
console.log('서버 실행: http://localhost:3000');
});
<!-- ============ 클라이언트 (public/index.html) ============ -->
<!DOCTYPE html>
<html>
<head>
<title>실시간 채팅</title>
<style>
#messages {
height: 300px;
overflow-y: auto;
border: 1px solid #ccc;
padding: 10px;
margin-bottom: 10px;
}
.message {
margin-bottom: 10px;
}
.system {
color: #999;
font-style: italic;
}
</style>
</head>
<body>
<h1>실시간 채팅</h1>
<div id="messages"></div>
<div id="typing"></div>
<input id="message-input" type="text" placeholder="메시지 입력...">
<button id="send-btn">전송</button>

<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io();
const messagesDiv = document.getElementById('messages');
const messageInput = document.getElementById('message-input');
const sendBtn = document.getElementById('send-btn');
const typingDiv = document.getElementById('typing');

// 사용자명 입력
const username = prompt('사용자명을 입력하세요:') || 'Anonymous';
socket.emit('join', username);

// 사용자 입장 알림
socket.on('user-joined', (data) => {
addMessage(`${data.username}님이 입장했습니다. (${data.userCount}명 접속)`, 'system');
});

// 사용자 퇴장 알림
socket.on('user-left', (data) => {
addMessage(`${data.username}님이 퇴장했습니다. (${data.userCount}명 접속)`, 'system');
});

// 채팅 메시지 수신
socket.on('chat-message', (data) => {
const time = new Date(data.timestamp).toLocaleTimeString();
addMessage(`[${time}] ${data.username}: ${data.message}`);
});

// 타이핑 중 표시
socket.on('user-typing', (username) => {
typingDiv.textContent = `${username}님이 입력 중...`;
setTimeout(() => {
typingDiv.textContent = '';
}, 1000);
});

// 메시지 전송
function sendMessage() {
const message = messageInput.value.trim();
if (message) {
socket.emit('chat-message', message);
messageInput.value = '';
}
}

sendBtn.addEventListener('click', sendMessage);
messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
sendMessage();
}
});

// 타이핑 이벤트
let typingTimeout;
messageInput.addEventListener('input', () => {
clearTimeout(typingTimeout);
socket.emit('typing');
typingTimeout = setTimeout(() => {
// 타이핑 멈춤
}, 500);
});

// 메시지 추가
function addMessage(text, className = '') {
const messageEl = document.createElement('div');
messageEl.className = `message ${className}`;
messageEl.textContent = text;
messagesDiv.appendChild(messageEl);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
</script>
</body>
</html>

Room 기능 (그룹 채팅)

// 서버
io.on('connection', (socket) => {
// 방 입장
socket.on('join-room', (roomId) => {
socket.join(roomId);
console.log(`${socket.id}${roomId} 방에 입장`);

// 같은 방에 있는 사람들에게만 전송
socket.to(roomId).emit('user-joined-room', {
userId: socket.id,
roomId
});
});

// 특정 방에 메시지 전송
socket.on('room-message', ({ roomId, message }) => {
// 해당 방에만 전송
io.to(roomId).emit('room-message', {
userId: socket.id,
message
});
});

// 방 퇴장
socket.on('leave-room', (roomId) => {
socket.leave(roomId);
socket.to(roomId).emit('user-left-room', socket.id);
});
});

실시간 알림 시스템

// 서버
const notifications = io.of('/notifications');

notifications.on('connection', (socket) => {
console.log('알림 연결:', socket.id);

// 사용자 인증
const userId = socket.handshake.query.userId;

// 개인 방에 입장
socket.join(`user-${userId}`);

// 특정 사용자에게 알림 전송
socket.on('send-notification', ({ targetUserId, notification }) => {
notifications.to(`user-${targetUserId}`).emit('notification', notification);
});
});

// 어디서든 알림 전송
function sendNotification(userId, notification) {
notifications.to(`user-${userId}`).emit('notification', {
type: notification.type,
message: notification.message,
timestamp: new Date()
});
}

// 예: 새 주문 알림
app.post('/api/orders', async (req, res) => {
const order = await createOrder(req.body);

// 판매자에게 실시간 알림
sendNotification(order.sellerId, {
type: 'new-order',
message: `새로운 주문이 접수되었습니다!`,
orderId: order.id
});

res.json(order);
});

실시간 협업 (공동 편집)

// 서버
io.on('connection', (socket) => {
socket.on('join-document', (docId) => {
socket.join(docId);

// 현재 문서 내용 전송
const document = getDocument(docId);
socket.emit('document-loaded', document);
});

// 문서 수정
socket.on('document-change', ({ docId, changes }) => {
// 문서 업데이트
updateDocument(docId, changes);

// 다른 사용자들에게 변경사항 전송
socket.to(docId).emit('document-updated', {
userId: socket.id,
changes
});
});

// 커서 위치 공유
socket.on('cursor-move', ({ docId, position }) => {
socket.to(docId).emit('cursor-update', {
userId: socket.id,
position
});
});
});

🤔 자주 묻는 질문

Q1. WebSocket vs HTTP Polling?

A:

// HTTP Polling (비효율적)
// 클라이언트가 주기적으로 서버에 요청

setInterval(() => {
fetch('/api/messages')
.then(res => res.json())
.then(messages => {
// 새 메시지 확인
});
}, 1000); // 1초마다 요청

// 문제점:
// - 불필요한 요청 (새 메시지 없어도 요청)
// - 지연 시간 (최대 1초)
// - 서버 부하

// WebSocket (효율적)
// 실시간 양방향 통신

const socket = io();

socket.on('new-message', (message) => {
// 새 메시지 즉시 수신!
});

// 장점:
// - 즉시 수신 (지연 없음)
// - 서버가 push
// - 효율적

Q2. WebSocket은 항상 좋은가요?

A:

// ✅ WebSocket이 좋은 경우
1. 실시간 채팅
2. 실시간 알림
3. 협업 도구 (공동 편집)
4. 실시간 게임
5. 주식 시세 (실시간 데이터)
6. IoT 디바이스 제어

// ❌ WebSocket이 불필요한 경우
1. 일반 웹사이트 (블로그, 뉴스)
2. REST API (CRUD)
3. 정적 콘텐츠
4. 실시간 업데이트 불필요

// HTTP가 더 나은 경우:
// - 간단한 요청-응답
// - 캐싱 필요
// - RESTful 설계

Q3. Socket.io vs 순수 WebSocket?

A:

// 순수 WebSocket
const ws = new WebSocket('ws://localhost:8080');
ws.send('Hello');

// 장점: 가볍고 빠름
// 단점: 추가 기능 없음, 폴백 없음

// Socket.io
const socket = io();
socket.emit('message', 'Hello');

// 장점:
// 1. 자동 재연결
// 2. Room/Namespace 지원
// 3. 폴백 (WebSocket 안 되면 Polling)
// 4. 이벤트 기반
// 5. 바이너리 지원

// 단점:
// 1. 무거움 (라이브러리 크기)
// 2. 순수 WebSocket과 호환 안 됨

// 선택 기준:
// 간단한 프로젝트 → 순수 WebSocket
// 복잡한 프로젝트 → Socket.io

Q4. WebSocket 보안은?

A:

// 1. wss:// 사용 (WebSocket Secure)
// HTTP → HTTPS처럼
// ws:// → wss://

const socket = new WebSocket('wss://example.com'); // ✅ 암호화
const socket = new WebSocket('ws://example.com'); // ❌ 평문

// 2. 인증
// Socket.io
io.use((socket, next) => {
const token = socket.handshake.auth.token;

if (isValidToken(token)) {
next();
} else {
next(new Error('인증 실패'));
}
});

// 클라이언트
const socket = io({
auth: {
token: 'user-token'
}
});

// 3. 권한 확인
socket.on('delete-message', (messageId) => {
const userId = socket.userId;

if (canDeleteMessage(userId, messageId)) {
deleteMessage(messageId);
} else {
socket.emit('error', '권한이 없습니다');
}
});

// 4. Rate Limiting
const rateLimit = require('socket.io-rate-limit');

io.use(rateLimit({
max: 10, // 최대 10개
interval: 1000 // 1초당
}));

// 5. 입력 검증
socket.on('message', (message) => {
// XSS 방지
const sanitized = sanitizeHtml(message);

if (sanitized.length > 1000) {
return socket.emit('error', '메시지가 너무 깁니다');
}

broadcast(sanitized);
});

Q5. WebSocket 연결 관리는?

A:

// 1. 자동 재연결 (Socket.io)
const socket = io({
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 1000
});

socket.on('disconnect', () => {
console.log('연결 끊김');
});

socket.on('reconnect', () => {
console.log('재연결 성공');
});

// 2. 하트비트 (연결 확인)
// 서버
setInterval(() => {
wss.clients.forEach((ws) => {
if (ws.isAlive === false) {
return ws.terminate(); // 응답 없으면 종료
}

ws.isAlive = false;
ws.ping(); // 핑 전송
});
}, 30000);

ws.on('pong', () => {
ws.isAlive = true;
});

// 3. 메모리 관리
const MAX_CONNECTIONS = 1000;

wss.on('connection', (ws) => {
if (wss.clients.size > MAX_CONNECTIONS) {
ws.close(1008, '서버가 가득 찼습니다');
return;
}

// 연결 처리...
});

// 4. Graceful Shutdown
process.on('SIGTERM', () => {
console.log('서버 종료 중...');

// 모든 연결에 종료 알림
wss.clients.forEach((ws) => {
ws.send(JSON.stringify({ type: 'server-shutdown' }));
ws.close();
});

wss.close(() => {
console.log('WebSocket 서버 종료');
process.exit(0);
});
});

🎓 다음 단계

WebSocket을 이해했다면, 다음을 학습해보세요:

  1. Node.js란? - WebSocket 서버 구축
  2. React란? - 실시간 UI 구현
  3. Docker란? (문서 작성 예정) - WebSocket 서버 배포

실습해보기

# Socket.io 채팅 앱 만들기

# 1. 프로젝트 초기화
mkdir chat-app
cd chat-app
npm init -y

# 2. 패키지 설치
npm install express socket.io

# 3. 서버 만들기 (위 예시 참고)
# server.js

# 4. 실행
node server.js

# 5. http://localhost:3000 접속

🎬 마무리

WebSocket은 실시간 웹의 핵심 기술입니다:

  • 양방향 통신: 서버 ↔ 클라이언트
  • 지속 연결: 한 번 연결, 계속 사용
  • 실시간: 즉각적인 데이터 전송
  • 효율적: Polling보다 훨씬 효율적

실시간 기능이 필요하다면 WebSocket을 사용하세요! 🔌✨