사용자가 F5(새로고침) 키를 누르지 않아도, 새로운 데이터가 화면에 저절로 나타나는 경험을 해본 적이 있나요? 주식 시세가 실시간으로 바뀌고, 채팅 앱에 새로운 메시지가 바로 뜨는 것처럼 말이죠. 이런 마법 같은 기능을 ‘실시간 업데이트’라고 부릅니다.
과거에는 이런 기능을 구현하는 것이 매우 복잡했지만, 현대 웹 기술 덕분에 이제는 우리도 충분히 도전해 볼 수 있습니다. 이번 글에서는 실시간 통신 기술의 대표 주자인 WebSocket을 활용하여, 데이터가 변경될 때마다 화면이 즉시 반응하는 ‘살아있는’ 데이터 그리드를 만드는 방법을 기초부터 차근차근 알아보겠습니다.
📚 주요 개념 설명
실시간 업데이트의 세계로 들어가기 전, 핵심 개념들을 먼저 이해해 봅시다.
- Server-Sent Events (SSE): ‘서버의 일방적인 라디오 방송’에 비유할 수 있습니다. 서버가 클라이언트에게 “새 소식 있어!”라고 일방적으로 데이터를 계속 보내주는 단방향 통신입니다. 실시간 알림이나 뉴스 피드처럼 서버가 보내주는 정보만 받으면 될 때 유용합니다.
- WebSocket: ‘실시간 양방향 고속도로’와 같습니다. 서버와 클라이언트가 서로에게 언제든지 데이터를 자유롭게 주고받을 수 있는 통신 채널을 계속 열어둡니다. 채팅 앱처럼 양쪽 모두가 활발하게 소통해야 할 때 필수적인 기술입니다. 이 글에서는 WebSocket을 사용합니다.
- 실시간 UI (Real-time UI): 서버로부터 실시간 데이터를 받았을 때, 이 변화를 사용자에게 즉각적으로, 그리고 시각적으로 명확하게 보여주는 모든 화면 구성을 의미합니다. 단순히 숫자를 바꾸는 것을 넘어, 색상을 바꾸거나 애니메이션 효과를 주는 것까지 포함됩니다.
- 성능 최적화 (Performance Optimization): 실시간으로 데이터가 쏟아져 들어올 때, 앱이 버벅거리거나 느려지지 않도록 관리하는 기술입니다. 불필요한 화면 깜빡임을 줄이고, 연결이 끊겼을 때를 대비하는 등의 처리가 포함됩니다.
💡 실제 사용 예제: 실시간 주식 시세 그리드
이제 WebSocket을 사용하여 실시간으로 변하는 주식 시세를 보여주는 데이터 그리드를 만들어 보겠습니다.
1단계: 모든 복잡함을 숨겨주는 마법의 망토, useWebSocket
훅 만들기
WebSocket 연결은 생각보다 신경 쓸 것이 많습니다. 연결이 끊겼을 때 재연결을 시도해야 하고, 컴포넌트가 사라질 때 연결을 깔끔하게 정리해야 하죠. 이런 복잡한 로직을 useWebSocket
이라는 커스텀 훅(Hook) 안에 모두 담아두면, 우리는 이 훅을 가져다 쓰기만 하면 됩니다.
// hooks/useWebSocket.ts
'use client';
import { useState, useEffect, useCallback, useRef } from 'react';
// ... (WebSocketOptions 인터페이스 정의는 동일)
// WebSocket 연결을 책임지는 커스텀 훅
export function useWebSocket({
url,
onMessage,
reconnectDelay = 3000,
maxRetries = 5
}: WebSocketOptions) {
const [connected, setConnected] = useState(false);
const retryCount = useRef(0); // 재연결 시도 횟수는 ref로 관리
const connect = useCallback(() => {
// 최대 재시도 횟수를 초과하면 더 이상 시도하지 않음
if (retryCount.current >= maxRetries) {
console.log('최대 재연결 횟수를 초과했습니다.');
return;
}
const ws = new WebSocket(url);
// 이벤트 핸들러 정의
ws.onopen = () => {
console.log('WebSocket 연결됨');
setConnected(true);
retryCount.current = 0; // 연결 성공 시 재시도 횟수 초기화
};
ws.onclose = () => {
console.log('WebSocket 연결 끊김');
setConnected(false);
// 재연결 로직
setTimeout(() => {
retryCount.current++;
console.log(`${retryCount.current}번째 재연결 시도...`);
connect();
}, reconnectDelay);
};
ws.onerror = (error) => console.error('WebSocket 에러:', error);
ws.onmessage = (event) => onMessage?.(JSON.parse(event.data));
// 컴포넌트가 사라질 때를 대비한 정리 함수
return () => {
ws.onclose = null; // 메모리 누수 방지를 위해 핸들러 정리
ws.close();
};
}, [url, onMessage, reconnectDelay, maxRetries]);
useEffect(() => {
const cleanup = connect();
return cleanup;
}, [connect]);
// 외부에서 사용할 수 있도록 연결 상태와 메시지 전송 함수 반환
return { connected };
}
2단계: 살아 움직이는 데이터 그리드 만들기 (RealTimeDataGrid.tsx
)
이제 위에서 만든 useWebSocket
훅을 사용하여 실제 UI 컴포넌트를 만들어 보겠습니다. 서버로부터 주식 데이터를 받아서 ReactTabulator
그리드에 실시간으로 뿌려주는 역할을 합니다.
// components/DataGrid/RealTimeDataGrid.tsx
'use client';
import { useState } from 'react';
import { useWebSocket } from '@/hooks/useWebSocket';
import { ReactTabulator } from 'react-tabulator';
import { Paper, Box, Typography, Alert, CircularProgress } from '@mui/material';
// ... (StockData 인터페이스 정의는 동일)
export function RealTimeDataGrid() {
const [stocks, setStocks] = useState<StockData[]>([]);
// 1. useWebSocket 훅 호출! URL과 메시지 처리 로직만 전달하면 끝.
const { connected } = useWebSocket({
url: 'wss://api.example.com/stocks', // 실제 WebSocket 서버 주소
onMessage: (newStockData: StockData) => {
setStocks(prevStocks => {
const stockIndex = prevStocks.findIndex(s => s.symbol === newStockData.symbol);
// 새로운 종목이면 목록에 추가
if (stockIndex === -1) {
return [...prevStocks, newStockData];
}
// 기존 종목이면 정보 업데이트
const updatedStocks = [...prevStocks];
updatedStocks[stockIndex] = newStockData;
return updatedStocks;
});
}
});
// 2. 테이블 컬럼 정의 (시각적 피드백 추가)
const columns = [
// ... (종목, 가격, 거래량 컬럼은 동일)
{
title: "변동", field: "change", width: 120,
formatter: function(cell: any) { // 값에 따라 색상 변경
const value = cell.getValue();
const color = value >= 0 ? 'green' : 'red';
const sign = value >= 0 ? '+' : '';
return `<span style="color: ${color}">${sign}${value.toFixed(2)}%</span>`;
}
},
// ... (시간 컬럼은 동일)
];
// 3. UI 렌더링
return (
<Paper sx={{ p: 3 }}>
<Box sx={{ mb: 3, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="h6">실시간 주식 시세</Typography>
{/* 연결 상태를 명확하게 보여줌 */}
<Alert severity={connected ? 'success' : 'warning'} sx={{ py: 0 }}>
{connected ? '실시간 연결됨' : '연결 중...'}
</Alert>
</Box>
{stocks.length === 0 && !connected ? (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
<CircularProgress />
<Typography sx={{ ml: 2 }}>데이터를 불러오는 중입니다...</Typography>
</Box>
) : (
<ReactTabulator
data={stocks}
columns={columns}
options={{
layout: "fitColumns",
reactiveData: true, // 데이터 변경 시 테이블이 자동으로 다시 그려지도록 설정
}}
/>
)}
</Paper>
);
}
🎓 주니어 개발자를 위한 핵심 정리
이 예제를 통해 우리는 무엇을 배울 수 있을까요?
- 복잡성은 훅(Hook) 안에 숨기세요:
useWebSocket
훅은 WebSocket의 지저분한 연결, 재연결, 해제 로직을 모두 캡슐화합니다. 덕분에 UI 컴포넌트는 오직 “무슨 데이터를 받아서 어떻게 보여줄지”에만 집중할 수 있습니다. 이것이 바로 ‘관심사의 분리’라는 중요한 프로그래밍 원칙입니다. - 실시간 데이터 처리는 함수형으로:
onMessage
콜백에서setStocks
를 사용할 때, 이전 상태(prevStocks
)를 받아 새로운 배열을 반환하는 함수형 업데이트 방식을 사용했습니다. 이는 React에서 상태를 안전하고 예측 가능하게 관리하는 표준적인 방법입니다. - 사용자 경험(UX)이 핵심입니다:
- 단순히 데이터만 업데이트하는 것이 아니라, 연결 상태를
Alert
컴포넌트로 명확히 보여주었습니다. - 주가 변동률을 빨간색과 초록색으로 구분하여 사용자가 변화를 즉시 인지할 수 있도록 했습니다.
- (예제 코드에 포함된) CSS 애니메이션 효과는 데이터가 업데이트되었다는 사실을 시각적으로 강조하여 사용자 경험을 한층 더 끌어올립니다.
- 단순히 데이터만 업데이트하는 것이 아니라, 연결 상태를
- 정리(Cleanup)의 중요성:
useEffect
의return
문에서 연결을 해제하는 것은 매우 중요합니다. 이를 ‘클린업 함수’라고 부르며, 컴포넌트가 화면에서 사라졌을 때 불필요한 연결이 남아 메모리를 낭비하거나 에러를 일으키는 것을 방지합니다.