WebSocket React 실시간 통신 구현 가이드 - Next.js TypeScript 활용
React와 Next.js에서 WebSocket을 활용한 실시간 통신 구현 방법을 TypeScript 코드 예제와 함께 상세히 알아보세요. 채팅, 알림 등 다양한 활용 사례를 다룹니다.
WebSocket React 실시간 통신 구현 가이드 - Next.js TypeScript 활용
현대 웹 애플리케이션에서 실시간 통신은 필수 기능이 되었습니다. 채팅, 라이브 알림, 실시간 데이터 업데이트 등 사용자 경험을 크게 향상시키는 WebSocket 기술을 React와 Next.js 환경에서 구현하는 방법을 살펴보겠습니다.
WebSocket의 기본 개념과 장점
WebSocket은 클라이언트와 서버 간의 지속적인 양방향 통신을 가능하게 하는 프로토콜입니다. 기존 HTTP 요청-응답 방식과 달리 연결이 유지되어 실시간 데이터 교환이 가능합니다.
주요 장점은 다음과 같습니다:
- 낮은 지연 시간으로 즉각적인 데이터 전송
- 서버에서 클라이언트로 능동적인 데이터 푸시 가능
- HTTP 폴링 대비 네트워크 오버헤드 감소
- 실시간 상호작용이 필요한 애플리케이션에 최적화
React에서 WebSocket Hook 구현
React에서 WebSocket을 효율적으로 관리하기 위해 커스텀 Hook을 만들어보겠습니다:
import { useEffect, useRef, useState, useCallback } from 'react';
interface UseWebSocketOptions {
onOpen?: (event: Event) => void;
onMessage?: (event: MessageEvent) => void;
onError?: (event: Event) => void;
onClose?: (event: CloseEvent) => void;
shouldReconnect?: boolean;
reconnectInterval?: number;
}
export const useWebSocket = (url: string, options: UseWebSocketOptions = {}) => {
const [connectionStatus, setConnectionStatus] = useState<'Connecting' | 'Open' | 'Closing' | 'Closed'>('Closed');
const [lastMessage, setLastMessage] = useState<MessageEvent | null>(null);
const ws = useRef<WebSocket | null>(null);
const reconnectTimeoutId = useRef<NodeJS.Timeout | null>(null);
const {
onOpen,
onMessage,
onError,
onClose,
shouldReconnect = true,
reconnectInterval = 3000
} = options;
const connect = useCallback(() => {
if (ws.current?.readyState !== WebSocket.OPEN) {
ws.current = new WebSocket(url);
setConnectionStatus('Connecting');
ws.current.onopen = (event) => {
setConnectionStatus('Open');
onOpen?.(event);
};
ws.current.onmessage = (event) => {
setLastMessage(event);
onMessage?.(event);
};
ws.current.onerror = (event) => {
onError?.(event);
};
ws.current.onclose = (event) => {
setConnectionStatus('Closed');
onClose?.(event);
if (shouldReconnect && !event.wasClean) {
reconnectTimeoutId.current = setTimeout(() => {
connect();
}, reconnectInterval);
}
};
}
}, [url, onOpen, onMessage, onError, onClose, shouldReconnect, reconnectInterval]);
const sendMessage = useCallback((message: string | object) => {
if (ws.current?.readyState === WebSocket.OPEN) {
const messageToSend = typeof message === 'string' ? message : JSON.stringify(message);
ws.current.send(messageToSend);
}
}, []);
const disconnect = useCallback(() => {
if (reconnectTimeoutId.current) {
clearTimeout(reconnectTimeoutId.current);
}
ws.current?.close();
}, []);
useEffect(() => {
connect();
return () => {
disconnect();
};
}, [connect, disconnect]);
return {
connectionStatus,
lastMessage,
sendMessage,
connect,
disconnect
};
};
실시간 채팅 컴포넌트 구현
WebSocket Hook을 활용하여 실시간 채팅 기능을 구현해보겠습니다:
import React, { useState, useEffect, useRef } from 'react';
import { useWebSocket } from '../hooks/useWebSocket';
interface Message {
id: string;
user: string;
content: string;
timestamp: Date;
}
interface ChatComponentProps {
roomId: string;
userId: string;
}
export const ChatComponent: React.FC<ChatComponentProps> = ({ roomId, userId }) => {
const [messages, setMessages] = useState<Message[]>([]);
const [inputValue, setInputValue] = useState('');
const messagesEndRef = useRef<HTMLDivElement>(null);
const { connectionStatus, sendMessage } = useWebSocket(
`ws://localhost:8080/chat/${roomId}`,
{
onMessage: (event) => {
try {
const message: Message = JSON.parse(event.data);
setMessages(prev => [...prev, message]);
} catch (error) {
console.error('메시지 파싱 오류:', error);
}
},
onOpen: () => {
console.log('채팅방에 연결되었습니다.');
},
onError: (error) => {
console.error('WebSocket 오류:', error);
}
}
);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [messages]);
const handleSendMessage = (e: React.FormEvent) => {
e.preventDefault();
if (inputValue.trim() && connectionStatus === 'Open') {
const message = {
id: Date.now().toString(),
user: userId,
content: inputValue.trim(),
timestamp: new Date()
};
sendMessage(message);
setInputValue('');
}
};
return (
<div className="chat-container">
<div className="connection-status">
상태: {connectionStatus === 'Open' ? '연결됨' : '연결 중...'}
</div>
<div className="messages-container">
{messages.map((message) => (
<div
key={message.id}
className={`message ${message.user === userId ? 'own-message' : 'other-message'}`}
>
<div className="message-user">{message.user}</div>
<div className="message-content">{message.content}</div>
<div className="message-timestamp">
{new Date(message.timestamp).toLocaleTimeString()}
</div>
</div>
))}
<div ref={messagesEndRef} />
</div>
<form onSubmit={handleSendMessage} className="message-input-form">
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="메시지를 입력하세요..."
disabled={connectionStatus !== 'Open'}
className="message-input"
/>
<button
type="submit"
disabled={connectionStatus !== 'Open' || !inputValue.trim()}
className="send-button"
>
전송
</button>
</form>
</div>
);
};
Next.js API Routes로 WebSocket 서버 구현
Next.js 13+ App Router에서 WebSocket 서버를 구현하는 방법입니다:
// app/api/websocket/route.ts
import { NextRequest } from 'next/server';
import { WebSocketServer } from 'ws';
import { createServer } from 'http';
const wss = new WebSocketServer({ port: 8080 });
interface Client {
ws: any;
userId: string;
roomId: string;
}
const clients = new Map<string, Client>();
wss.on('connection', (ws, req) => {
const url = new URL(req.url!, `http://${req.headers.host}`);
const roomId = url.searchParams.get('roomId');
const userId = url.searchParams.get('userId');
if (!roomId || !userId) {
ws.close(1008, 'roomId와 userId가 필요합니다');
return;
}
const clientId = `${userId}-${Date.now()}`;
clients.set(clientId, { ws, userId, roomId });
console.log(`클라이언트 연결: ${userId} (방: ${roomId})`);
ws.on('message', (data: Buffer) => {
try {
const message = JSON.parse(data.toString());
// 같은 방의 모든 클라이언트에게 메시지 전송
clients.forEach((client) => {
if (client.roomId === roomId && client.ws.readyState === 1) {
client.ws.send(JSON.stringify({
...message,
timestamp: new Date().toISOString()
}));
}
});
} catch (error) {
console.error('메시지 처리 오류:', error);
}
});
ws.on('close', () => {
clients.delete(clientId);
console.log(`클라이언트 연결 해제: ${userId}`);
});
ws.on('error', (error: Error) => {
console.error('WebSocket 오류:', error);
clients.delete(clientId);
});
});
export async function GET(request: NextRequest) {
return new Response('WebSocket 서버가 포트 8080에서 실행 중입니다.', {
status: 200
});
}
상태 관리와 Context 활용
여러 컴포넌트에서 WebSocket 연결을 공유하기 위해 Context를 활용할 수 있습니다:
import React, { createContext, useContext, ReactNode } from 'react';
import { useWebSocket } from '../hooks/useWebSocket';
interface WebSocketContextType {
connectionStatus: string;
sendMessage: (message: string | object) => void;
lastMessage: MessageEvent | null;
}
const WebSocketContext = createContext<WebSocketContextType | undefined>(undefined);
interface WebSocketProviderProps {
children: ReactNode;
url: string;
}
export const WebSocketProvider: React.FC<WebSocketProviderProps> = ({ children, url }) => {
const webSocket = useWebSocket(url, {
shouldReconnect: true,
reconnectInterval: 5000
});
return (
<WebSocketContext.Provider value={webSocket}>
{children}
</WebSocketContext.Provider>
);
};
export const useWebSocketContext = () => {
const context = useContext(WebSocketContext);
if (context === undefined) {
throw new Error('useWebSocketContext는 WebSocketProvider 내에서 사용되어야 합니다');
}
return context;
};
CSS 스타일링과 사용자 경험 개선
WebSocket 상태에 따른 시각적 피드백을 제공하는 CSS를 추가합니다:
.chat-container {
display: flex;
flex-direction: column;
height: 500px;
border: 1px solid #e1e5e9;
border-radius: 8px;
overflow: hidden;
}
.connection-status {
padding: 8px 16px;
background: #f8f9fa;
border-bottom: 1px solid #e1e5e9;
font-size: 14px;
color: #6c757d;
}
.messages-container {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.message {
max-width: 70%;
padding: 12px;
border-radius: 12px;
position: relative;
}
.own-message {
align-self: flex-end;
background: #007bff;
color: white;
}
.other-message {
align-self: flex-start;
background: #f1f3f4;
color: #333;
}
.message-user {
font-size: 12px;
font-weight: 600;
margin-bottom: 4px;
opacity: 0.8;
}
.message-content {
margin-bottom: 4px;
line-height: 1.4;
}
.message-timestamp {
font-size: 11px;
opacity: 0.7;
}
.message-input-form {
display: flex;
padding: 16px;
border-top: 1px solid #e1e5e9;
background: white;
}
.message-input {
flex: 1;
padding: 12px;
border: 1px solid #ddd;
border-radius: 20px;
outline: none;
margin-right: 8px;
}
.message-input:focus {
border-color: #007bff;
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}
.send-button {
padding: 12px 20px;
background: #007bff;
color: white;
border: none;
border-radius: 20px;
cursor: pointer;
transition: background-color 0.2s;
}
.send-button:hover:not(:disabled) {
background: #0056b3;
}
.send-button:disabled {
background: #6c757d;
cursor: not-allowed;
}
에러 처리와 재연결 로직
안정적인 WebSocket 연결을 위한 고급 에러 처리 방법입니다:
export const useRobustWebSocket = (url: string) => {
const [reconnectAttempts, setReconnectAttempts] = useState(0);
const maxReconnectAttempts = 5;
const baseReconnectDelay = 1000;
const calculateReconnectDelay = (attempts: number) => {
return Math.min(baseReconnectDelay * Math.pow(2, attempts), 30000);
};
const { connectionStatus, sendMessage, connect } = useWebSocket(url, {
shouldReconnect: false, // 수동으로 재연결 관리
onClose: (event) => {
if (!event.wasClean && reconnectAttempts < maxReconnectAttempts) {
const delay = calculateReconnectDelay(reconnectAttempts);
setTimeout(() => {
setReconnectAttempts(prev => prev + 1);
connect();
}, delay);
}
},
onOpen: () => {
setReconnectAttempts(0); // 연결 성공 시 재시도 횟수 초기화
}
});
return {
connectionStatus,
sendMessage,
reconnectAttempts,
isReconnecting: reconnectAttempts > 0 && connectionStatus === 'Closed'
};
};
마무리
WebSocket을 활용한 실시간 통신은 현대 웹 애플리케이션의 핵심 기능입니다. React와 Next.js 환경에서 TypeScript를 활용하여 타입 안전성을 보장하면서 구현할 수 있으며, 적절한 에러 처리와 재연결 로직을 통해 안정적인 서비스를 제공할 수 있습니다. 이러한 기술들을 조합하여 채팅, 실시간 알림, 협업 도구 등 다양한 실시간 기능을 구현해보시기 바랍니다.
관련 게시글
React Server Components (RSC) 심층 가이드: Next.js App Router 활용
React Server Components(RSC)의 개념, 동작 원리, 장점, 그리고 Next.js App Router에서 RSC를 활용하는 방법을 심층적으로 탐구합니다. 프론트엔드 개발의 새로운 패러다임을 이해하고 실전 코드 예제를 통해 RSC를 마스터하세요.
Progressive Web App (PWA) 구축 실전: Next.js와 React를 활용한 PWA 개발 가이드
Next.js와 React 기반 PWA 구축 실전 가이드. Service Worker, Web App Manifest 설정 및 next-pwa 활용법을 통해 사용자 경험을 향상시키세요.
CSS Container Queries: 반응형 웹 디자인의 새로운 지평
CSS Container Queries를 활용하여 컴포넌트 기반 반응형 웹 디자인을 구현하는 방법을 심층적으로 알아봅니다. React, Next.js 환경에서 실전 예제를 통해 강력한 기능을 경험하세요.