현대 웹 애플리케이션에서는 실시간으로 데이터가 오가는 경험이 점점 더 중요해지고 있습니다.
예를 들어, 채팅 앱에서 상대방이 메시지를 보내자마자 바로 화면에 뜨거나, 주식 시세가 실시간으로 업데이트되는 상황을 생각해보세요.
이러한 실시간 데이터 통신을 가능하게 해주는 핵심 기술 중 하나가 바로 웹소켓(WebSocket) 입니다.
기존의 HTTP 통신 방식은 클라이언트가 요청(Request)을 보내야 서버가 응답(Response)을 주는 구조입니다.
이 구조에서는 서버가 클라이언트에게 먼저 무언가를 보낼 수 없고, 클라이언트가 정기적으로 서버에 "새로운 게 있어?" 하고 물어보는 방식(폴링, Long Polling 등)을 사용해야 했습니다.
이는 서버 자원 낭비가 크고 실시간성과도 거리가 멉니다.
웹소켓은 이런 한계를 해결하기 위해 등장했습니다.
한 번의 연결만으로 클라이언트와 서버 간에 양방향 통신을 실시간으로 할 수 있게 해주는 프로토콜입니다.
작동원리
- 초기 연결: 클라이언트는 HTTP 프로토콜을 사용해 웹소켓으로 업그레이드 요청을 보냅니다. (HTTP Handshake)
- 연결 성립: 서버가 이를 수락하면, 연결은 WebSocket 프로토콜로 전환되고 연결이 유지됩니다.
- 양방향 통신: 연결이 유지된 상태에서 클라이언트와 서버는 자유롭게 데이터를 주고받을 수 있습니다.
- 연결 종료: 클라이언트 또는 서버가 명시적으로 연결을 닫을 때까지 연결은 살아있습니다.
활용 예시
웹소켓은 다양한 실시간 서비스에 활용됩니다.
- 채팅 애플리케이션: 카카오톡, WhatsApp과 같은 실시간 채팅 앱
- 실시간 알림 시스템: 새로운 댓글, 좋아요 알림 등
- 온라인 게임: 사용자 간 실시간 상호작용
- 주식/코인 시세 표시: 초 단위 가격 변동을 실시간 반영
- 콜라보레이션 툴: Google Docs와 같은 실시간 공동 작업 도구
웹소켓의 장단점
장점
- 실시간성: 데이터를 기다리지 않고 즉시 받을 수 있어 반응성이 뛰어남
- 효율성: HTTP 대비 오버헤드가 적고, 연결을 재사용할 수 있어 리소스 낭비가 적음
- 서버 푸시 가능: 서버에서도 클라이언트로 직접 메시지를 보낼 수 있음
단점
- 리소스 사용량 증가: 연결을 지속적으로 유지하기 때문에, 동시 접속자가 많을 경우 서버 리소스를 많이 사용함
- 보안 고려: ws:// 대신 wss:// 사용 필요, 인증 및 권한 처리가 별도로 필요함
- 환경 제약: 일부 방화벽이나 프록시에서는 웹소켓이 차단되기도 함
예시
- 재연결 로직 구현: 일시적 연결 끊김에 대비해 자동 재연결 로직 작성
- 연결 상태 관리: 상태 관리 라이브러리(Redux, Zustand 등)와 연동하면 유지 관리가 쉬움
- 메시지 큐 처리: 수신 메시지를 큐에 넣어 UI 렌더링과 분리하여 성능 개선
- 백엔드 연동: NestJS, Express, Fastify 등에서 웹소켓 서버 구현 가능. socket.io, ws 등의 라이브러리 사용
웹소켓 예제 (주식 시세 실시간 표시)
- 사용자는 드롭다운/체크박스 등으로 구독할 종목을 선택함
- 선택한 종목이 웹소켓을 통해 서버로 전송됨
- 서버는 선택된 종목에 대한 시세 데이터를 주기적으로 보내줌
- 클라이언트는 해당 데이터를 수신해서 실시간으로 렌더링함
백엔드 (WebSocket 서버 코드)
// server.ts
import WebSocket, { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080, path: '/stocks' });
type Client = {
socket: WebSocket;
subscribedSymbols: string[];
};
function generatePrice(): number {
return +(100 + Math.random() * 50).toFixed(2);
}
function broadcastTicker(client: Client) {
client.subscribedSymbols.forEach((symbol) => {
const ticker = {
symbol,
price: generatePrice(),
timestamp: Date.now(),
};
client.socket.send(JSON.stringify(ticker));
});
}
wss.on('connection', (ws) => {
const client: Client = {
socket: ws,
subscribedSymbols: [],
};
ws.on('message', (message) => {
try {
const data = JSON.parse(message.toString());
if (data.action === 'subscribe' && Array.isArray(data.symbols)) {
client.subscribedSymbols = data.symbols;
console.log('✅ 구독 종목:', data.symbols);
}
} catch (err) {
console.error('❌ 파싱 오류:', err);
}
});
const interval = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
broadcastTicker(client);
}
}, 1000);
ws.on('close', () => clearInterval(interval));
ws.on('error', () => clearInterval(interval));
});
프론트엔드 (React + TypeScript)
// StockClient.tsx
import React, { useEffect, useRef, useState } from 'react';
type Ticker = {
symbol: string;
price: number;
timestamp: number;
};
const availableSymbols = ['AAPL', 'TSLA', 'GOOGL', 'AMZN', 'MSFT'];
const StockClient: React.FC = () => {
const [selectedSymbols, setSelectedSymbols] = useState<string[]>([]);
const [tickers, setTickers] = useState<Record<string, Ticker>>({});
const socketRef = useRef<WebSocket | null>(null);
const toggleSymbol = (symbol: string) => {
setSelectedSymbols((prev) =>
prev.includes(symbol)
? prev.filter((s) => s !== symbol)
: [...prev, symbol]
);
};
useEffect(() => {
socketRef.current = new WebSocket('ws://localhost:8080/stocks');
socketRef.current.onopen = () => {
console.log('📡 연결됨');
};
socketRef.current.onmessage = (event) => {
const data: Ticker = JSON.parse(event.data);
setTickers((prev) => ({ ...prev, [data.symbol]: data }));
};
return () => {
socketRef.current?.close();
};
}, []);
useEffect(() => {
if (
socketRef.current &&
socketRef.current.readyState === WebSocket.OPEN
) {
socketRef.current.send(
JSON.stringify({ action: 'subscribe', symbols: selectedSymbols })
);
}
}, [selectedSymbols]);
return (
<div>
<h2>📈 실시간 주식 시세</h2>
<div>
{availableSymbols.map((symbol) => (
<label key={symbol} style={{ marginRight: '1rem' }}>
<input
type="checkbox"
checked={selectedSymbols.includes(symbol)}
onChange={() => toggleSymbol(symbol)}
/>
{symbol}
</label>
))}
</div>
<table>
<thead>
<tr>
<th>종목</th>
<th>가격</th>
<th>시간</th>
</tr>
</thead>
<tbody>
{selectedSymbols.map((symbol) =>
tickers[symbol] ? (
<tr key={symbol}>
<td>{symbol}</td>
<td>{tickers[symbol].price.toFixed(2)}</td>
<td>{new Date(tickers[symbol].timestamp).toLocaleTimeString()}</td>
</tr>
) : null
)}
</tbody>
</table>
</div>
);
};
export default StockClient;'개발 공부' 카테고리의 다른 글
| 콜백 지옥 (Callback Hell) (0) | 2025.04.29 |
|---|---|
| 정규화(Normalization) (0) | 2025.04.27 |
| REST API (0) | 2025.03.27 |
| Zod 검증 (0) | 2025.03.12 |
| Request Waterfall 방식과 Parallel 방식 (0) | 2025.02.22 |