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

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

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

1. 프로젝트 소개 및 목표

실시간 채팅 앱 만들기 with Socket.io는 웹소켓 기술을 활용하여 실시간으로 메시지를 주고받을 수 있는 채팅 애플리케이션을 구축하는 프로젝트입니다. 이 프로젝트를 통해 클라이언트와 서버 간의 양방향 통신을 이해하고, 실시간 데이터 전송의 원리를 학습할 수 있습니다. Socket.io는 웹소켓을 기반으로 하며, 연결이 끊어졌을 때 자동으로 재연결하는 등의 편리한 기능을 제공합니다. 최종적으로 사용자가 방에 입장하고, 메시지를 전송하며, 다른 사용자의 입장 및 퇴장을 실시간으로 확인할 수 있는 완성도 높은 채팅 앱을 만들어봅니다.

2. 필요한 기술 스택

이 프로젝트를 구현하기 위해 필요한 기술 스택은 다음과 같습니다:

  • Node.js: 서버 사이드 JavaScript 런타임
  • Express.js: Node.js 웹 애플리케이션 프레임워크
  • Socket.io: 실시간 양방향 통신 라이브러리
  • HTML/CSS: 프론트엔드 구조 및 스타일링
  • JavaScript: 클라이언트 사이드 로직

Node.js와 npm이 설치되어 있어야 하며, 기본적인 JavaScript 문법과 비동기 프로그래밍에 대한 이해가 있으면 더욱 수월하게 진행할 수 있습니다.

3. 프로젝트 셋업

먼저 프로젝트를 위한 디렉토리를 생성하고 npm을 초기화합니다:

mkdir realtime-chat-app
cd realtime-chat-app
npm init -y

필요한 패키지들을 설치합니다:

npm install express socket.io
npm install --save-dev nodemon

프로젝트 구조를 다음과 같이 설정합니다:

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

package.json의 scripts 부분에 개발 서버 실행 스크립트를 추가합니다:

"scripts": {
  "start": "node server.js",
  "dev": "nodemon server.js"
}

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 = {};

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

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

  // 메시지 수신 및 브로드캐스트
  socket.on('send-message', (message) => {
    const username = users[socket.id];
    io.emit('receive-message', {
      username: username,
      message: 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('user-list', 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-container">
      <h1>실시간 채팅방</h1>
      <input type="text" id="username-input" placeholder="사용자 이름을 입력하세요" maxlength="20">
      <button id="join-btn">입장하기</button>
    </div>

    <div id="chat-container" style="display: none;">
      <div class="sidebar">
        <h3>접속 중인 사용자</h3>
        <ul id="user-list"></ul>
      </div>
      
      <div class="chat-section">
        <div class="chat-header">
          <h2>채팅방</h2>
          <span id="current-user"></span>
        </div>
        
        <div id="messages"></div>
        <div id="typing-indicator"></div>
        
        <div class="input-container">
          <input type="text" id="message-input" placeholder="메시지를 입력하세요...">
          <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 스타일링 (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;
}

#login-container {
  background: white;
  padding: 40px;
  border-radius: 10px;
  text-align: center;
  box-shadow: 0 10px 40px rgba(0,0,0,0.2);
}

#login-container h1 {
  margin-bottom: 20px;
  color: #333;
}

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

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

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

#chat-container {
  display: flex;
  background: white;
  border-radius: 10px;
  overflow: hidden;
  height: 100%;
  box-shadow: 0 10px 40px rgba(0,0,0,0.2);
}

.sidebar {
  width: 250px;
  background: #f7f7f7;
  padding: 20px;
  border-right: 1px solid #ddd;
}

.sidebar h3 {
  margin-bottom: 15px;
  color: #333;
}

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

#user-list li {
  padding: 8px;
  margin-bottom: 5px;
  background: white;
  border-radius: 5px;
  border-left: 3px solid #667eea;
}

.chat-section {
  flex: 1;
  display: flex;
  flex-direction: column;
}

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

#messages {
  flex: 1;
  overflow-y: auto;
  padding: 20px;
  background: #fafafa;
}

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

.message-header {
  display: flex;
  justify-content: space-between;
  margin-bottom: 5px;
  font-size: 12px;
  color: #666;
}

.message-username {
  font-weight: bold;
  color: #667eea;
}

.message-text {
  color: #333;
}

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

#typing-indicator {
  padding: 0 20px;
  min-height: 20px;
  color: #666;
  font-size: 14px;
  font-style: italic;
}

.input-container {
  display: flex;
  padding: 20px;
  background: white;
  border-top: 1px solid #ddd;
}

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

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

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

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

const socket = io();

const loginContainer = document.getElementById('login-container');
const chatContainer = document.getElementById('chat-container');
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 userListUl = document.getElementById('user-list');
const currentUserSpan = document.getElementById('current-user');
const typingIndicator = document.getElementById('typing-indicator');

let currentUsername = '';
let typingTimeout;

// 입장 버튼 클릭
joinBtn.addEventListener('click', () => {
  const username = usernameInput.value.trim();
  if (username) {
    currentUsername = username;
    socket.emit('join', username);
    loginContainer.style.display = 'none';
    chatContainer.style.display = 'flex';
    currentUserSpan.textContent = `접속: ${username}`;
    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(typingTimeout);
  typingTimeout = setTimeout(() => {
    socket.emit('stop-typing');
  }, 1000);
});

// 메시지 수신
socket.on('receive-message', (data) => {
  const messageDiv = document.createElement('div');
  messageDiv.classList.add('message');
  messageDiv.innerHTML = `
    
${data.username} ${data.timestamp}
${escapeHtml(data.message)}
`; messagesDiv.appendChild(messageDiv); messagesDiv.scrollTop = messagesDiv.scrollHeight; }); // 사용자 연결/해제 socket.on('user-connected', (username) => { addSystemMessage(`${username}님이 입장했습니다.`); }); socket.on('user-disconnected', (username) => { addSystemMessage(`${username}님이 퇴장했습니다.`); }); // 사용자 목록 업데이트 socket.on('user-list', (users) => { userListUl.innerHTML = ''; users.forEach(user => { const li = document.createElement('li'); li.textContent = user; userListUl.appendChild(li); }); }); // 타이핑 표시 let typingUsers = new Set(); socket.on('user-typing', (username) => { typingUsers.add(username); updateTypingIndicator(); }); socket.on('user-stop-typing', (username) => { typingUsers.delete(username); updateTypingIndicator(); }); function updateTypingIndicator() { if (typingUsers.size > 0) { const users = Array.from(typingUsers).join(', '); typingIndicator.textContent = `${users}님이 입력 중...`; } else { typingIndicator.textContent = ''; } } function addSystemMessage(text) { const messageDiv = document.createElement('div'); messageDiv.classList.add('system-message'); messageDiv.textContent = text; messagesDiv.appendChild(messageDiv); messagesDiv.scrollTop = messagesDiv.scrollHeight; } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; }

5. 테스트 및 배포

5.1 로컬 테스트

개발 서버를 실행합니다:

npm run dev

브라우저에서 http://localhost:3000에 접속하여 여러 탭을 열어 실시간 채팅이 정상적으로 동작하는지 확인합니다. 다른 사용자 이름으로 입장하여 메시지 전송, 타이핑 표시, 사용자 목록 업데이트 등을 테스트해봅니다.

5.2 배포

Heroku, Render, Railway 등의 플랫폼을 사용하여 배포할 수 있습니다. 예를 들어 Render에 배포하는 경우:

  1. GitHub 저장소에 코드를 푸시합니다.
  2. Render 대시보드에서 New Web Service를 선택합니다.
  3. 저장소를 연결하고 빌드 명령을 npm install, 시작 명령을 npm start로 설정합니다.
  4. 환경 변수로 NODE_ENV=production을 설정합니다.
  5. 배포 후 제공된 URL로 접속하여 실시간 채팅을 테스트합니다.

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

실시간 채팅 앱 만들기 with Socket.io 프로젝트를 완성했습니다! 이제 기본적인 실시간 채팅 기능이 동작하는 애플리케이션을 갖추게 되었습니다. 추가로 확장할 수 있는 아이디어로는 다음과 같은 것들이 있습니다:

  • 방(Room) 기능 추가: 여러 채팅방을 만들어 사용자가 선택할 수 있도록 구현
  • 메시지 저장: MongoDB나 PostgreSQL을 연결하여 채팅 기록을 저장
  • 파일 공유: 이미지나 파일을 업로드하고 공유하는 기능
  • 이모지 및 리액션: 메시지에 이모지 반응을 추가
  • 사용자 인증: JWT를 사용한 로그인 시스템 구축
  • 프라이빗 메시지: 1:1 다이렉트 메시징 기능

이 프로젝트는 포트폴리오에 추가하기 좋은 실용적인 애플리케이션이며, 실시간 통신에 대한 이해를 높이는 데 큰 도움이 됩니다!

📚 함께 읽으면 좋은 글

1

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

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

2

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

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

3

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

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

4

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

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

5

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

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

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

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

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


📘 페이스북


🐦 트위터


✈️ 텔레그램

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

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

이 글을 읽고 새롭게 알게 된 정보가 있다면 공유해주세요!

💡
유용한 정보 공유

궁금한 점 질문

🤝
경험담 나누기

👍
의견 표현하기

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

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

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

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

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

💡
최신 트렌드
2025년 기준

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

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

📱 전체 버전 보기