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를 활용하여 타입 안전성을 보장하면서 구현할 수 있으며, 적절한 에러 처리와 재연결 로직을 통해 안정적인 서비스를 제공할 수 있습니다. 이러한 기술들을 조합하여 채팅, 실시간 알림, 협업 도구 등 다양한 실시간 기능을 구현해보시기 바랍니다.
관련 게시글
Vite Build Tool: Fast Frontend Development Guide
Vite는 현대적인 프론트엔드 개발을 위한 빠르고 효율적인 빌드 도구입니다. 이 가이드에서는 Vite의 핵심 기능, React 및 TypeScript 프로젝트 설정, 플러그인 활용법, 그리고 빌드 최적화 전략까지 완벽하게 다룹니다.
React Server Components (RSC) 심층 가이드: Next.js와 함께하는 Full-stack React
React Server Components (RSC)의 개념, 등장 배경, 동작 원리, 그리고 Next.js 13+ App Router에서의 활용법을 심층적으로 다룹니다. 클라이언트/서버 컴포넌트 분리 전략과 실전 코드 예제를 통해 RSC의 강력한 이점을 이해하고 웹 애플리케이션 성능을 최적화하는 방법을 알아봅니다.
Next.js Middleware: 강력한 요청 처리 활용법
Next.js Middleware를 활용하여 사용자 인증, 국제화, A/B 테스트 등 다양한 요청 처리 로직을 효율적으로 구현하는 방법을 심층적으로 알아봅니다. 실전 코드 예제를 통해 Next.js 애플리케이션의 프론트엔드 기능을 강화하세요.