Socket.IO를 활용해 간단한 채팅 시스템을 만들어봤습니다. 처음 접속했을 때 기존 메시지를 불러오고, 이후에는 새 메시지를 실시간으로 주고받도록 구현했습니다. React 클라이언트와 Node.js 서버가 어떻게 연결되는지 정리해 보려 합니다.
전체 구성
[React 클라이언트]
│
│ REST (초기 메시지 히스토리)
▼
[Express 서버] ──── Socket.IO ──── [다른 클라이언트들]
▲ │
└────── Socket.IO (실시간 송수신) ────┘
처음 접속하면 REST API로 기존 메시지를 불러옵니다. 이후 채팅 중에 주고받는 메시지는 Socket.IO로 전달합니다.
서버에서는 Express와 Socket.IO를 같은 포트에서 실행했습니다.
클라이언트 구조
App.tsx: 채팅 화면과 메시지 처리
기존 메시지는 REST로 불러오고, 실시간 메시지는 socket.emit()으로 보내고 socket.on()으로 받습니다.
useEffect로 페이지 진입 시 /api/messages에서 초기 메시지 히스토리를 불러옵니다.
전송 버튼 클릭 시 sendMessage를 통해 서버에 메시지를 전송하고, 동시에 self: true로 UI에 즉시 반영합니다.
서버에서 실시간으로 전달되는 메시지는 handleMessage 콜백을 통해 수신 후 메시지 목록에 추가됩니다.
메시지를 불러오고 주고받는 코드는 다음과 같습니다.
// 메시지 전송
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!input.trim()) return;
sendMessage(input); // 서버로 emit
setMessages(prev => [...prev, { self: true, text: input }]);
setInput('');
};
// 메시지 수신
const handleMessage = useCallback((text: string) => {
setMessages(prev => [...prev, { self: false, text }]);
}, []);
useSocket.ts: Socket.IO 커스텀 훅
// 핵심 요약
// - useRef로 소켓과 최신 콜백 관리
// - useEffect로 연결/해제 생명주기 관리
// - emit으로 전송, on으로 수신
export function useSocket(url: string, onMessage: (msg: string) => void) {
const socketRef = useRef<Socket | null>(null);
const onMessageRef = useRef(onMessage);
// onMessage 최신화
useEffect(() => {
onMessageRef.current = onMessage;
}, [onMessage]);
// 소켓 연결 및 수신 이벤트 등록
useEffect(() => {
const socket = io(url);
socketRef.current = socket;
socket.on('chat message', (msg) => {
onMessageRef.current(msg);
});
return () => socket.disconnect();
}, [url]);
// 전송 함수
const sendMessage = useCallback((msg: string) => {
socketRef.current?.emit('chat message', msg);
}, []);
return { sendMessage };
}
소켓 연결 처리를 훅으로 옮겨서, App.tsx에서는 sendMessage와 handleMessage로 메시지를 보내고 받도록 했습니다.
서버 구조
Express와 Socket.IO 서버 설정
const app = express();
const httpServer = http.createServer(app);
const io = new IOServer(httpServer, { cors: { origin: '*' } });
Express로 REST API를 제공하고, Socket.IO 서버를 같은 포트에서 구동합니다.
CORS 설정을 통해 개발 중 프론트엔드에서 자유롭게 접속할 수 있게 열어둡니다.
REST API: 기존 메시지 조회
const messages: { id: number; text: string; timestamp: string }[] = [];
app.get('/api/messages', (_req, res) => {
res.json(messages);
});
서버 메모리에 간단히 메시지를 저장하고, 클라이언트가 처음 접속할 때 이 API로 기존 기록을 가져갑니다.
Socket.IO: 실시간 메시지 송수신
io.on('connection', socket => {
console.log(`🔌 클라이언트 연결: ${socket.id}`);
socket.on('chat message', (text: string) => {
const msg = { id: messages.length, text, timestamp: new Date().toISOString() };
messages.push(msg);
socket.broadcast.emit('chat message', text);
});
socket.on('disconnect', () => {
console.log(`❌ 연결 해제: ${socket.id}`);
});
});
클라이언트가 보낸 메시지를 저장하고, 메시지를 보낸 클라이언트를 제외한 나머지 클라이언트에 전달합니다.
메시지를 보낸 클라이언트는 이미 화면에 해당 메시지를 추가했습니다. 같은 메시지를 다시 받지 않도록 broadcast.emit을 사용해 나머지 클라이언트에만 전달했습니다.