Backend Development • 2026. 01. 18

Socket.io를 이용한 실시간 채팅 서버 구축 가이드

오늘날 현대적인 웹 애플리케이션에서 '실시간성(Real-time)'은 사용자 경험을 결정짓는 핵심 지표가 되었습니다. 사용자가 메시지를 입력하고 전송 버튼을 눌렀을 때, 상대방의 화면에 즉각적으로 말풍선이 떠오르는 기능은 이제 당연한 상식이 되었습니다. eleslog.work에서 제공하는 실시간 채팅 서버 역시 이러한 고도의 인터랙션을 구현하기 위해 Socket.io라는 강력한 라이브러리를 채택하였습니다. 이번 포스팅에서는 Socket.io의 탄생 배경부터 내부 프로토콜 동작 원리, 그리고 실제 상용 환경에서 고려해야 할 서버 아키텍처까지 심층적인 가이드로 다루어 보겠습니다.

1. 웹 실시간 통신의 역사와 Socket.io의 등장 배경

Socket.io를 깊이 이해하기 위해서는 먼저 과거의 웹이 어떻게 실시간 통신을 흉내 냈는지 알 필요가 있습니다. 초기의 웹은 HTTP 프로토콜의 특성상 클라이언트가 요청(Request)을 보내야만 서버가 응답(Response)을 줄 수 있는 단방향 구조였습니다. 이를 극복하기 위해 개발자들은 다음과 같은 편법을 사용했습니다.

이후 2011년, HTML5 표준과 함께 WebSocket 프로토콜이 등장했습니다. 웹소켓은 한 번의 핸드쉐이크만 성공하면 TCP 연결을 그대로 유지하며 양방향 통신을 수행할 수 있게 해주었습니다. 하지만 모든 브라우저가 이를 지원하지 않았고, 기업의 방화벽 설정에 따라 연결이 차단되는 이슈가 잦았습니다. 바로 이때, 어떠한 환경에서도 실시간 통신을 보장하기 위해 탄생한 엔진이 Socket.io입니다.

2. Socket.io의 핵심 메커니즘: Engine.io의 마법

많은 분이 Socket.io가 단순히 웹소켓을 감싼 것이라고 생각하지만, 실제로는 그보다 훨씬 복잡한 Engine.io라는 레이어 위에서 동작합니다. Socket.io는 처음부터 웹소켓 연결을 시도하지 않습니다.

  1. HTTP 롱 폴링으로 시작: 먼저 가장 안전한 HTTP 롱 폴링 방식으로 연결을 수립합니다.
  2. 업그레이드 테스트: 연결이 안정적이라고 판단되면 백그라운드에서 웹소켓 사용이 가능한지 테스트합니다.
  3. 프로토콜 업그레이드: 웹소켓 연결이 가능하면 즉시 기존 HTTP 연결을 끊고 웹소켓으로 갈아탑니다. 이를 'Upgrade'라고 부릅니다.

이러한 점진적 향상(Progressive Enhancement) 전략 덕분에 eleslog.work 채팅 서비스는 구형 브라우저나 불안정한 공공 와이파이 환경에서도 끊김 없는 서비스를 제공할 수 있는 것입니다.

3. 실제 서버 구축: 단계별 코드 분석

이제 eleslog.work의 백엔드를 지탱하는 핵심 코드를 살펴보며, 각 라인이 가지는 의미를 기술적으로 해석해 보겠습니다.


const express = require("express");
const http = require("http");
const { Server } = require("socket.io");

const app = express();
const server = http.createServer(app);

// Socket.io 인스턴스 생성 및 옵션 설정
const io = new Server(server, {
  cors: {
    origin: ["https://eleslog.work", "http://localhost:3000"],
    methods: ["GET", "POST"],
    credentials: true
  },
  pingTimeout: 60000, // 연결 유지를 위한 타임아웃 설정
  pingInterval: 25000 // 서버-클라이언트 간 생존 확인 주기
});

io.on("connection", (socket) => {
  const userIp = socket.handshake.address;
  console.log(`[LOG] 사용자 접속: ${socket.id} (IP: ${userIp})`);

  // 특정 채팅방 입장 (Room 기능)
  socket.on("joinRoom", (roomName) => {
    socket.join(roomName);
    console.log(`${socket.id} 사용자가 ${roomName} 방에 입장했습니다.`);
  });

  // 메시지 수신 및 전달 로직
  socket.on("chatMessage", (msgData) => {
    const { room, user, text } = msgData;
    
    // 데이터 유효성 검사 (XSS 방지 및 빈 메시지 차단)
    if (!text || text.length > 500) return;

    const refinedData = {
      user: user.trim(),
      message: text.replace(/</g, "&lt;"), // 보안을 위한 단순 치환
      time: new Date().toLocaleTimeString()
    };

    // 해당 방에 있는 모든 사용자에게 메시지 전송
    io.to(room).emit("receiveMessage", refinedData);
  });

  socket.on("disconnect", (reason) => {
    console.log(`[LOG] 접속 종료: ${socket.id}, 사유: ${reason}`);
  });
});

server.listen(4000, () => {
  console.log("실시간 채팅 서버가 4000번 포트에서 가동 중입니다.");
});
    

코드 상세 해설

위 코드에서 주목할 점은 CORS 설정입니다. 브라우저 보안 정책상 다른 도메인에서의 소켓 접근은 기본적으로 차단됩니다. eleslog.work는 명시적인 오리진 허용을 통해 보안을 강화했습니다. 또한 pingTimeout 설정을 통해 모바일 기기에서 앱이 백그라운드로 전환될 때 발생하는 연결 유실을 효율적으로 관리합니다.

4. 성능 최적화와 확장성(Scalability) 고려

채팅 서비스가 유명해져서 동시 접속자가 1만 명, 10만 명으로 늘어난다면 서버 한 대로 감당할 수 있을까요? 결코 그렇지 않습니다. 이때부터는 **분산 서버 아키텍처**를 고려해야 합니다.

서버를 여러 대 두게 되면 A 서버에 접속한 사용자와 B 서버에 접속한 사용자가 서로 메시지를 주고받지 못하는 문제가 발생합니다. 소켓 정보가 각 서버의 메모리에만 존재하기 때문입니다. 이를 해결하기 위해 우리는 Redis Pub/Sub 모델을 도입해야 합니다. Redis를 어댑터로 사용하면 메시지가 발생할 때마다 Redis 채널을 통해 모든 서버에 공유되므로, 어떤 서버에 접속해 있든 실시간으로 동기화된 채팅을 경험할 수 있습니다.

5. 보안: 실시간 서비스의 아킬레스건

단순한 소켓 연결은 공격자에게 노출될 경우 스팸 메시지 투하(Flood Attack)나 가짜 데이터 주입의 타겟이 되기 쉽습니다. eleslog.work의 안정성을 위해 반드시 적용해야 할 보안 수칙은 다음과 같습니다.

6. 마치며: eleslog.work가 나아갈 방향

지금까지 Socket.io를 활용한 실시간 채팅 서버 구축의 전 과정을 살펴보았습니다. 단순한 코드 복사보다는 프로토콜의 작동 원리와 네트워크의 물리적 한계를 이해하는 것이 진정한 풀스택 개발자로 거듭나는 길입니다. 앞으로 eleslog.work는 채팅 기능을 넘어 실시간 동시 문서 편집, 멀티플레이어 보드게임 등 다양한 인터랙티브 웹 툴로 확장해 나갈 예정입니다. 다음 포스팅에서는 대용량 트래픽 처리를 위한 **'Redis와 Socket.io의 연동 실전편'**을 준비해 오겠습니다.