← 글 목록

개발 기록

React와 Socket.IO로 실시간 채팅 구현하기

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을 사용해 나머지 클라이언트에만 전달했습니다.