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

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

1. 프로젝트 소개 및 목표

실시간 채팅 앱 만들기 with Socket.io는 웹소켓 기반의 양방향 통신을 학습하고 실제 채팅 애플리케이션을 구현하는 프로젝트입니다. Socket.io는 실시간 이벤트 기반 통신을 간편하게 구현할 수 있는 라이브러리로, 채팅, 알림, 실시간 대시보드 등 다양한 분야에 활용됩니다.

이 프로젝트를 통해 클라이언트-서버 간 실시간 통신의 원리를 이해하고, 메시지 송수신, 사용자 관리, 방(Room) 개념 등 실무에서 필요한 핵심 기능들을 직접 구현해볼 수 있습니다. 완성된 채팅 앱은 포트폴리오로도 활용 가능하며, 실시간 웹 애플리케이션 개발의 기초를 탄탄히 다질 수 있습니다.

2. 필요한 기술 스택

이 프로젝트를 진행하기 위해 다음과 같은 기술 스택이 필요합니다:

  • 백엔드: Node.js, Express.js, Socket.io
  • 프론트엔드: HTML5, CSS3, JavaScript (Vanilla JS 또는 React)
  • 개발 도구: npm 또는 yarn, 코드 에디터(VS Code 권장)
  • 선택 사항: TypeScript, MongoDB(채팅 기록 저장), Redis(확장성)

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

3. 프로젝트 셋업

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

# 프로젝트 디렉토리 생성
mkdir realtime-chat-app
cd realtime-chat-app

# package.json 초기화
npm init -y

# 필요한 패키지 설치
npm install express socket.io
npm install --save-dev nodemon

package.json 파일에 개발 스크립트를 추가합니다:

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

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

realtime-chat-app/
├── server.js          # 서버 진입점
├── public/            # 정적 파일
│   ├── index.html
│   ├── style.css
│   └── client.js
└── package.json

4. 단계별 구현 과정

4.1 서버 구축 (server.js)

Socket.io를 사용한 실시간 채팅 앱 만들기 with Socket.io의 핵심은 서버 설정입니다. Express와 Socket.io를 통합하여 기본 서버를 구축합니다:

const express = require('express');
const app = express();
const http = require('http').createServer(app);
const io = require('socket.io')(http);
const path = require('path');

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

// 연결된 사용자 관리
const users = new Map();

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

  // 사용자 입장
  socket.on('join', (username) => {
    users.set(socket.id, username);
    socket.broadcast.emit('user-connected', username);
    
    // 현재 접속자 목록 전송
    io.emit('update-users', Array.from(users.values()));
  });

  // 메시지 수신 및 브로드캐스트
  socket.on('chat-message', (data) => {
    const message = {
      username: users.get(socket.id),
      message: data.message,
      timestamp: new Date().toLocaleTimeString('ko-KR')
    };
    io.emit('chat-message', message);
  });

  // 타이핑 중 표시
  socket.on('typing', () => {
    socket.broadcast.emit('typing', users.get(socket.id));
  });

  socket.on('stop-typing', () => {
    socket.broadcast.emit('stop-typing');
  });

  // 연결 해제
  socket.on('disconnect', () => {
    const username = users.get(socket.id);
    users.delete(socket.id);
    if (username) {
      io.emit('user-disconnected', username);
      io.emit('update-users', Array.from(users.values()));
    }
    console.log('사용자 접속 해제:', socket.id);
  });
});

const PORT = process.env.PORT || 3000;
http.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 class="login-screen" id="loginScreen">
      <h1>실시간 채팅 앱</h1>
      <input type="text" id="usernameInput" placeholder="사용자 이름을 입력하세요" maxlength="20">
      <button id="joinBtn">입장하기</button>
    </div>

    <div class="chat-screen" id="chatScreen" style="display: none;">
      <div class="sidebar">
        <h2>접속자 목록</h2>
        <ul id="usersList"></ul>
      </div>
      
      <div class="chat-container">
        <div class="chat-header">
          <h2>채팅방</h2>
          <span id="currentUser"></span>
        </div>
        
        <div class="messages" id="messages"></div>
        
        <div class="typing-indicator" id="typingIndicator"></div>
        
        <div class="message-input">
          <input type="text" id="messageInput" placeholder="메시지를 입력하세요..." autocomplete="off">
          <button id="sendBtn">전송</button>
        </div>
      </div>
    </div>
  </div>

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

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

const socket = io();

const loginScreen = document.getElementById('loginScreen');
const chatScreen = document.getElementById('chatScreen');
const usernameInput = document.getElementById('usernameInput');
const joinBtn = document.getElementById('joinBtn');
const messageInput = document.getElementById('messageInput');
const sendBtn = document.getElementById('sendBtn');
const messages = document.getElementById('messages');
const usersList = document.getElementById('usersList');
const currentUser = document.getElementById('currentUser');
const typingIndicator = document.getElementById('typingIndicator');

let username = '';
let typingTimer;

// 로그인 처리
joinBtn.addEventListener('click', () => {
  username = usernameInput.value.trim();
  if (username) {
    socket.emit('join', username);
    loginScreen.style.display = 'none';
    chatScreen.style.display = 'flex';
    currentUser.textContent = `환영합니다, ${username}님!`;
    messageInput.focus();
  }
});

usernameInput.addEventListener('keypress', (e) => {
  if (e.key === 'Enter') joinBtn.click();
});

// 메시지 전송
function sendMessage() {
  const message = messageInput.value.trim();
  if (message) {
    socket.emit('chat-message', { message });
    messageInput.value = '';
    socket.emit('stop-typing');
  }
}

sendBtn.addEventListener('click', sendMessage);
messageInput.addEventListener('keypress', (e) => {
  if (e.key === 'Enter') sendMessage();
});

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

// 메시지 수신
socket.on('chat-message', (data) => {
  const messageDiv = document.createElement('div');
  messageDiv.classList.add('message');
  if (data.username === username) {
    messageDiv.classList.add('own-message');
  }
  
  messageDiv.innerHTML = `
    
${data.username} ${data.timestamp}
${escapeHtml(data.message)}
`; messages.appendChild(messageDiv); messages.scrollTop = messages.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; if (user === username) li.classList.add('current-user'); usersList.appendChild(li); }); }); // 타이핑 표시 socket.on('typing', (username) => { typingIndicator.textContent = `${username}님이 입력 중...`; }); socket.on('stop-typing', () => { typingIndicator.textContent = ''; }); // 시스템 메시지 추가 function addSystemMessage(message) { const messageDiv = document.createElement('div'); messageDiv.classList.add('message', 'system-message'); messageDiv.textContent = message; messages.appendChild(messageDiv); messages.scrollTop = messages.scrollHeight; } // XSS 방지 function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; }

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: 100%;
  height: 100%;
  max-width: 1200px;
  max-height: 800px;
  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%;
  gap: 20px;
  padding: 40px;
}

.login-screen h1 {
  color: #667eea;
  font-size: 2.5em;
}

.login-screen input {
  width: 300px;
  padding: 15px;
  border: 2px solid #ddd;
  border-radius: 5px;
  font-size: 16px;
}

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

.login-screen button:hover {
  background: #5568d3;
}

.chat-screen {
  display: flex;
  height: 100%;
}

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

.sidebar h2 {
  font-size: 1.2em;
  margin-bottom: 15px;
  color: #333;
}

.sidebar ul {
  list-style: none;
}

.sidebar li {
  padding: 10px;
  margin: 5px 0;
  background: white;
  border-radius: 5px;
  font-size: 14px;
}

.sidebar li.current-user {
  background: #667eea;
  color: white;
  font-weight: bold;
}

.chat-container {
  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: 10px 0;
  padding: 12px;
  background: white;
  border-radius: 8px;
  box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}

.message.own-message {
  background: #e3f2fd;
  margin-left: auto;
  max-width: 70%;
}

.message.system-message {
  background: #fff3cd;
  text-align: center;
  font-style: italic;
  color: #856404;
}

.message-header {
  display: flex;
  justify-content: space-between;
  margin-bottom: 5px;
  font-size: 0.9em;
}

.timestamp {
  color: #999;
  font-size: 0.85em;
}

.message-body {
  word-wrap: break-word;
}

.typing-indicator {
  padding: 10px 20px;
  color: #667eea;
  font-style: italic;
  min-height: 30px;
}

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

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

.message-input button {
  margin-left: 10px;
  padding: 12px 30px;
  background: #667eea;
  color: white;
  border: none;
  border-radius: 5px;
  cursor: pointer;
  transition: background 0.3s;
}

.message-input button:hover {
  background: #5568d3;
}

5. 테스트 및 배포

5.1 로컬 테스트

개발 서버를 실행하여 로컬에서 테스트합니다:

npm run dev

브라우저에서 http://localhost:3000에 접속하여 여러 탭을 열어 실시간 채팅을 테스트합니다. 메시지 송수신, 사용자 입장/퇴장 알림, 타이핑 표시 등 모든 기능이 정상 작동하는지 확인합니다.

5.2 배포 (Heroku 예시)

# Heroku CLI 설치 후
heroku create my-chat-app
git init
git add .
git commit -m "Initial commit"
git push heroku main

또는 Vercel, Railway, Render 등 다양한 플랫폼에 배포할 수 있습니다. 배포 시 환경 변수 설정과 웹소켓 연결을 지원하는 플랫폼인지 확인해야 합니다.

5.3 추가 테스트 항목

  • 다중 사용자 동시 접속 테스트
  • 네트워크 끊김 시 재연결 동작 확인
  • 긴 메시지, 특수문자, 이모지 처리 확인
  • 모바일 브라우저 호환성 테스트

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

실시간 채팅 앱 만들기 with Socket.io 프로젝트를 통해 웹소켓 통신의 핵심 개념과 실시간 애플리케이션 개발 방법을 학습했습니다. 이 기본 구조를 바탕으로 다음과 같은 기능들을 추가하여 더욱 완성도 높은 애플리케이션으로 발전시킬 수 있습니다:

  • 다중 채팅방: Room 기능을 활용한 여러 채팅방 생성
  • 파일 전송: 이미지, 문서 등 파일 공유 기능
  • 메시지 저장: MongoDB를 사용한 채팅 기록 영구 저장
  • 사용자 인증: JWT를 활용한 로그인 시스템
  • 이모지 및 멘션: 이모지 피커, @멘션 기능
  • 읽음 표시: 메시지 읽음/안읽음 상태 표시
  • 음성/영상 통화: WebRTC를 활용한 화상 채팅
  • 알림 시스템: 브라우저 푸시 알림

이 프로젝트는 포트폴리오로 활용하기에 적합하며, 실시간 통신이 필요한 다양한 애플리케이션의 기반 기술로 응용할 수 있습니다. 꾸준히 기능을 추가하고 개선하며 실력을 향상시켜 보세요!

📚 함께 읽으면 좋은 글

1

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

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

2

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

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

3

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

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

4

JWT 인증 시스템 구현하기 – 완성까지 한번에!

📂 프로젝트 아이디어
📅 2025. 10. 24.
🎯 JWT 인증 시스템 구현하기

5

JWT 인증 시스템 구현하기 – 완성까지 한번에!

📂 프로젝트 아이디어
📅 2025. 10. 23.
🎯 JWT 인증 시스템 구현하기

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

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

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

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

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

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

💡
유용한 정보 공유

궁금한 점 질문

🤝
경험담 나누기

👍
의견 표현하기

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

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

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

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

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

💡
최신 트렌드
2025년 기준

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

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

답글 남기기