🛠️ 실시간 채팅 앱 만들기 with Socket.io – 완성까지 한번에!

개발 에러 해결 가이드 - FixLog 노트

실시간 채팅 앱 만들기 with Socket.io – 완성까지 한번에!

1. 프로젝트 소개 및 목표

실시간 채팅 앱 만들기 with Socket.io 프로젝트는 웹소켓 기술을 활용하여 실시간 양방향 통신이 가능한 채팅 애플리케이션을 구축하는 프로젝트입니다. 이 가이드를 통해 Socket.io 라이브러리를 사용하여 사용자 간 즉각적인 메시지 전송, 입장/퇴장 알림, 온라인 사용자 목록 표시 등의 핵심 기능을 구현할 수 있습니다. Node.js와 Express를 백엔드로, HTML/CSS/JavaScript를 프론트엔드로 사용하며, 실무에서 사용되는 실시간 통신 패턴을 직접 경험할 수 있습니다. 완성된 프로젝트는 포트폴리오로 활용하기에 적합하며, 실시간 협업 도구나 고객 지원 시스템 등으로 확장 가능합니다.

2. 필요한 기술 스택

이 프로젝트를 완성하기 위해 다음 기술들이 필요합니다:

  • Node.js: 서버 사이드 JavaScript 런타임 환경
  • Express.js: 웹 서버 프레임워크
  • Socket.io: 실시간 양방향 통신 라이브러리
  • HTML/CSS/JavaScript: 프론트엔드 개발
  • npm: 패키지 관리 도구

기본적인 JavaScript 지식과 Node.js에 대한 이해가 있다면 쉽게 따라올 수 있습니다.

3. 프로젝트 셋업

먼저 프로젝트 디렉토리를 생성하고 필요한 패키지를 설치합니다:

mkdir realtime-chat-app
cd realtime-chat-app
npm init -y
npm install express socket.io

프로젝트 구조는 다음과 같이 구성합니다:

realtime-chat-app/
├── server.js
├── package.json
└── public/
    ├── index.html
    ├── style.css
    └── client.js

이 구조는 서버 코드와 클라이언트 코드를 명확히 분리하여 유지보수를 쉽게 합니다. public 폴더는 정적 파일들을 제공하는 용도로 사용됩니다.

4. 단계별 구현 과정

4.1 서버 설정 (server.js)

Express 서버와 Socket.io를 설정합니다:

const express = require('express');
const app = express();
const http = require('http');
const server = http.createServer(app);
const { Server } = require('socket.io');
const io = new Server(server);

// 정적 파일 제공
app.use(express.static('public'));

// 연결된 사용자 관리
const users = {};

io.on('connection', (socket) => {
  console.log('새로운 사용자 연결:', socket.id);

  // 사용자 입장
  socket.on('join', (username) => {
    users[socket.id] = username;
    socket.broadcast.emit('user-connected', username);
    io.emit('update-users', Object.values(users));
  });

  // 메시지 전송
  socket.on('send-message', (data) => {
    io.emit('receive-message', {
      username: users[socket.id],
      message: data.message,
      timestamp: new Date().toLocaleTimeString('ko-KR')
    });
  });

  // 타이핑 중 상태
  socket.on('typing', () => {
    socket.broadcast.emit('user-typing', users[socket.id]);
  });

  socket.on('stop-typing', () => {
    socket.broadcast.emit('user-stop-typing', users[socket.id]);
  });

  // 사용자 퇴장
  socket.on('disconnect', () => {
    const username = users[socket.id];
    if (username) {
      delete users[socket.id];
      io.emit('user-disconnected', username);
      io.emit('update-users', Object.values(users));
    }
    console.log('사용자 연결 해제:', socket.id);
  });
});

const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
  console.log(`서버가 포트 ${PORT}에서 실행 중입니다`);
});

4.2 프론트엔드 HTML (public/index.html)

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>실시간 채팅 앱</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <div class="container">
    <div id="login-screen">
      <h1>실시간 채팅방</h1>
      <input type="text" id="username-input" placeholder="닉네임을 입력하세요" maxlength="20">
      <button id="join-btn">입장하기</button>
    </div>

    <div id="chat-screen" style="display: none;">
      <div class="header">
        <h2>채팅방</h2>
        <div class="online-users">
          <span>온라인: </span>
          <span id="user-count">0</span>
        </div>
      </div>

      <div class="sidebar">
        <h3>참여자 목록</h3>
        <ul id="users-list"></ul>
      </div>

      <div class="main-chat">
        <div id="messages"></div>
        <div id="typing-indicator"></div>
        <div class="input-area">
          <input type="text" id="message-input" placeholder="메시지를 입력하세요..." autocomplete="off">
          <button id="send-btn">전송</button>
        </div>
      </div>
    </div>
  </div>

  <script src="/socket.io/socket.io.js"></script>
  <script src="client.js"></script>
</body>
</html>

4.3 클라이언트 JavaScript (public/client.js)

const socket = io();
let currentUsername = '';
let typingTimer;

// DOM 요소
const loginScreen = document.getElementById('login-screen');
const chatScreen = document.getElementById('chat-screen');
const usernameInput = document.getElementById('username-input');
const joinBtn = document.getElementById('join-btn');
const messageInput = document.getElementById('message-input');
const sendBtn = document.getElementById('send-btn');
const messagesDiv = document.getElementById('messages');
const usersList = document.getElementById('users-list');
const userCount = document.getElementById('user-count');
const typingIndicator = document.getElementById('typing-indicator');

// 입장하기
joinBtn.addEventListener('click', () => {
  const username = usernameInput.value.trim();
  if (username) {
    currentUsername = username;
    socket.emit('join', username);
    loginScreen.style.display = 'none';
    chatScreen.style.display = 'flex';
    messageInput.focus();
  }
});

// 엔터키로 입장
usernameInput.addEventListener('keypress', (e) => {
  if (e.key === 'Enter') joinBtn.click();
});

// 메시지 전송
sendBtn.addEventListener('click', sendMessage);
messageInput.addEventListener('keypress', (e) => {
  if (e.key === 'Enter') sendMessage();
});

function sendMessage() {
  const message = messageInput.value.trim();
  if (message) {
    socket.emit('send-message', { message });
    messageInput.value = '';
    socket.emit('stop-typing');
  }
}

// 타이핑 감지
messageInput.addEventListener('input', () => {
  socket.emit('typing');
  clearTimeout(typingTimer);
  typingTimer = setTimeout(() => {
    socket.emit('stop-typing');
  }, 1000);
});

// 메시지 수신
socket.on('receive-message', (data) => {
  const messageEl = document.createElement('div');
  messageEl.classList.add('message');
  if (data.username === currentUsername) {
    messageEl.classList.add('own-message');
  }
  messageEl.innerHTML = `
    ${data.username}
    ${data.timestamp}
    

${escapeHtml(data.message)}

`; messagesDiv.appendChild(messageEl); messagesDiv.scrollTop = messagesDiv.scrollHeight; }); // 사용자 연결/해제 socket.on('user-connected', (username) => { addSystemMessage(`${username}님이 입장했습니다.`); }); socket.on('user-disconnected', (username) => { addSystemMessage(`${username}님이 퇴장했습니다.`); }); // 사용자 목록 업데이트 socket.on('update-users', (users) => { usersList.innerHTML = ''; users.forEach(user => { const li = document.createElement('li'); li.textContent = user; usersList.appendChild(li); }); userCount.textContent = users.length; }); // 타이핑 표시 socket.on('user-typing', (username) => { typingIndicator.textContent = `${username}님이 입력 중...`; }); socket.on('user-stop-typing', () => { typingIndicator.textContent = ''; }); // 시스템 메시지 function addSystemMessage(text) { const messageEl = document.createElement('div'); messageEl.classList.add('system-message'); messageEl.textContent = text; messagesDiv.appendChild(messageEl); messagesDiv.scrollTop = messagesDiv.scrollHeight; } // XSS 방지 function escapeHtml(text) { const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return text.replace(/[&<>"']/g, m => map[m]); }

4.4 스타일링 (public/style.css)

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
}

.container {
  width: 90%;
  max-width: 1200px;
  height: 90vh;
  background: white;
  border-radius: 10px;
  box-shadow: 0 10px 40px rgba(0,0,0,0.2);
  overflow: hidden;
}

#login-screen {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  height: 100%;
  padding: 20px;
}

#login-screen h1 {
  margin-bottom: 30px;
  color: #667eea;
}

#username-input {
  width: 300px;
  padding: 15px;
  font-size: 16px;
  border: 2px solid #ddd;
  border-radius: 5px;
  margin-bottom: 15px;
}

#join-btn {
  width: 300px;
  padding: 15px;
  font-size: 16px;
  background: #667eea;
  color: white;
  border: none;
  border-radius: 5px;
  cursor: pointer;
  transition: background 0.3s;
}

#join-btn:hover {
  background: #5568d3;
}

#chat-screen {
  display: flex;
  flex-direction: column;
  height: 100%;
}

.header {
  background: #667eea;
  color: white;
  padding: 15px 20px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.main-chat {
  flex: 1;
  display: flex;
  flex-direction: column;
  padding: 20px;
}

#messages {
  flex: 1;
  overflow-y: auto;
  margin-bottom: 10px;
  padding: 10px;
  background: #f9f9f9;
  border-radius: 5px;
}

.message {
  margin-bottom: 15px;
  padding: 10px;
  background: white;
  border-radius: 5px;
  box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}

.own-message {
  background: #e3f2fd;
}

.system-message {
  text-align: center;
  color: #888;
  font-style: italic;
  margin: 10px 0;
}

.timestamp {
  font-size: 12px;
  color: #999;
  margin-left: 10px;
}

.input-area {
  display: flex;
  gap: 10px;
}

#message-input {
  flex: 1;
  padding: 12px;
  border: 2px solid #ddd;
  border-radius: 5px;
  font-size: 14px;
}

#send-btn {
  padding: 12px 30px;
  background: #667eea;
  color: white;
  border: none;
  border-radius: 5px;
  cursor: pointer;
  transition: background 0.3s;
}

#send-btn:hover {
  background: #5568d3;
}

.sidebar {
  width: 200px;
  background: #f5f5f5;
  padding: 15px;
  border-right: 1px solid #ddd;
}

#users-list {
  list-style: none;
}

#users-list li {
  padding: 8px;
  margin: 5px 0;
  background: white;
  border-radius: 3px;
}

#typing-indicator {
  height: 20px;
  color: #667eea;
  font-style: italic;
  font-size: 14px;
  margin-bottom: 5px;
}

5. 테스트 및 배포

개발이 완료되면 로컬 환경에서 테스트를 진행합니다:

node server.js

브라우저에서 http://localhost:3000에 접속하여 여러 개의 탭을 열고 다른 닉네임으로 입장하여 실시간 메시지 전송, 사용자 목록 업데이트, 입장/퇴장 알림 등이 정상 작동하는지 확인합니다. 배포는 Heroku, Vercel, Railway 등의 플랫폼을 사용할 수 있습니다. Heroku 배포 시 Procfile을 생성합니다:

web: node server.js

환경 변수로 PORT를 설정하고, Socket.io의 CORS 설정을 프로덕션 환경에 맞게 조정해야 합니다. 배포 후에는 실제 인터넷 환경에서 여러 기기로 접속하여 WebSocket 연결이 정상적으로 유지되는지 확인합니다.

6. 마무리 및 확장 아이디어

실시간 채팅 앱 만들기 with Socket.io 프로젝트를 통해 WebSocket 기반 실시간 통신의 핵심 개념을 익혔습니다. 추가 기능으로 채팅방(Room) 분리, 파일 전송, 이모지 지원, 메시지 검색, 사용자 프로필 이미지, 읽음 표시, 데이터베이스 연동(MongoDB)을 통한 채팅 히스토리 저장 등을 구현할 수 있습니다. 또한 JWT 인증을 추가하여 보안을 강화하고, Redis를 활용한 세션 관리로 확장성을 높일 수 있습니다. 이 프로젝트는 실시간 협업 도구, 고객 지원 채팅, 게임 로비 등 다양한 실무 애플리케이션의 기반이 됩니다.

📚 함께 읽으면 좋은 글

1

실시간 채팅 앱 만들기 with Socket.io – 완성까지 한번에!

📂 프로젝트 아이디어
📅 2025. 10. 27.
🎯 실시간 채팅 앱 만들기 with Socket.io

2

실시간 채팅 앱 만들기 with Socket.io – 완성까지 한번에!

📂 프로젝트 아이디어
📅 2025. 10. 25.
🎯 실시간 채팅 앱 만들기 with Socket.io

3

실시간 채팅 앱 만들기 with Socket.io – 완성까지 한번에!

📂 프로젝트 아이디어
📅 2025. 10. 18.
🎯 실시간 채팅 앱 만들기 with Socket.io

4

실시간 채팅 앱 만들기 with Socket.io – 완성까지 한번에!

📂 프로젝트 아이디어
📅 2025. 10. 9.
🎯 실시간 채팅 앱 만들기 with Socket.io

5

30분만에 만드는 Todo App 완성 가이드 – 완성까지 한번에!

📂 프로젝트 아이디어
📅 2025. 10. 28.
🎯 30분만에 만드는 Todo App 완성 가이드

💡 위 글들을 통해 더 깊이 있는 정보를 얻어보세요!

📢 이 글이 도움되셨나요? 공유해주세요!

여러분의 공유 한 번이 더 많은 사람들에게 도움이 됩니다 ✨


📘 페이스북


🐦 트위터


✈️ 텔레그램

🔥 공유할 때마다 블로그 성장에 큰 힘이 됩니다! 감사합니다 🙏

💬 여러분의 소중한 의견을 들려주세요!

여러분은 실시간 채팅 앱 만들기 with Socket.io에 대해 어떻게 생각하시나요?

💡
유용한 정보 공유

궁금한 점 질문

🤝
경험담 나누기

👍
의견 표현하기

⭐ 모든 댓글은 24시간 내에 답변드리며, 여러분의 의견이 다른 독자들에게 큰 도움이 됩니다!
🎯 건설적인 의견과 경험 공유를 환영합니다 ✨

🔔 블로그 구독하고 최신 글을 받아보세요!

📚
다양한 주제
17개 카테고리

정기 업데이트
하루 3회 발행

🎯
실용적 정보
바로 적용 가능

💡
최신 트렌드
2025년 기준

🌟 프로젝트 아이디어부터 다양한 실생활 정보까지!
매일 새로운 유용한 콘텐츠를 만나보세요 ✨

📧 RSS 구독 | 🔖 북마크 추가 | 📱 모바일 앱 알림 설정
지금 구독하고 놓치는 정보 없이 업데이트 받아보세요!

📱 전체 버전 보기