데이터베이스

[Firebase] 5장 실시간 채팅 기능 구현

파아란기쁨1 2025. 8. 23. 09:45
반응형

 

🎠 Firebase 실시간 채팅 웹앱 만들기

안녕하세요! 오늘은 놀이공원 테마의 실시간 채팅 앱을 만들어볼 거예요. 위의 예제를 보면서 함께 배워보아요!

📚 오늘 배울 내용

1. 채팅방 목록 기능

  • 여러 개의 채팅방을 만들고 선택할 수 있어요
  • 롤러코스터, 관람차, 회전목마, 범퍼카 방이 있어요!

2. 메시지 입력 및 전송

  • 텍스트를 입력하고 전송 버튼을 눌러 메시지를 보내요
  • 엔터키로도 전송할 수 있어요

3. 실시간 메시지 표시

  • 다른 사람이 보낸 메시지가 즉시 나타나요
  • 내가 보낸 메시지는 오른쪽에, 다른 사람 메시지는 왼쪽에 표시돼요

4. 스크롤 자동 내리기

  • 새 메시지가 올 때마다 자동으로 맨 아래로 스크롤되어요

5. 사용자 정보 표시

  • 각 메시지마다 누가 보냈는지, 언제 보냈는지 표시돼요

🔧 주요 기능 설명

1️⃣ 채팅방 선택하기

 
 
javascript
// 채팅방을 클릭하면 이 함수가 실행돼요
function setupRoomListeners() {
    roomList.addEventListener('click', (e) => {
        // 클릭한 방을 활성화하고
        // 그 방의 메시지들을 보여줘요
    });
}

2️⃣ 메시지 보내기

 
 
javascript
// 메시지를 전송하는 함수예요
function sendMessage() {
    const messageText = messageInput.value.trim();
    if (!messageText) return; // 빈 메시지는 안 보내요
    
    // 새 메시지 객체를 만들고
    const newMessage = {
        user: currentUser,
        message: messageText,
        timestamp: new Date()
    };
    
    // 메시지 목록에 추가해요
    messages[currentRoom].push(newMessage);
}

3️⃣ 실시간 업데이트

 
 
javascript
// Firebase에서는 이런 식으로 실시간 업데이트를 받을 수 있어요
// onSnapshot(collection(db, 'messages'), (snapshot) => {
//     // 새 메시지가 오면 자동으로 화면에 표시!
// });

4️⃣ 자동 스크롤

 
 
javascript
// 새 메시지가 올 때마다 맨 아래로 스크롤해요
function scrollToBottom() {
    messagesContainer.scrollTop = messagesContainer.scrollHeight;
}

🚀 Firebase 연결하기 (다음 단계)

실제로 Firebase를 연결하려면 이런 설정이 필요해요:

  1. Firebase 프로젝트 설정
  2. Firestore 규칙 설정
  3. 실시간 리스너 추가
  4. 메시지 저장 및 불러오기

💡 초등학생을 위한 쉬운 설명

채팅 앱이 어떻게 작동하나요?

  1. 편지함 같은 개념: 각 채팅방은 큰 편지함이에요
  2. 실시간 업데이트: 누군가 편지를 넣으면 모든 사람이 바로 볼 수 있어요
  3. 자동 정리: 새 편지가 오면 자동으로 맨 위로 올라와요
  4. 사용자 구분: 각 편지마다 누가 썼는지 이름표가 붙어있어요

 

전체 소스코드

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>놀이공원 실시간 채팅</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        
        body {
            font-family: 'Arial', sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            height: 100vh;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        
        .container {
            width: 90%;
            max-width: 800px;
            height: 90vh;
            background: white;
            border-radius: 20px;
            box-shadow: 0 20px 40px rgba(0,0,0,0.2);
            display: flex;
            overflow: hidden;
        }
        
        /* 채팅방 목록 영역 */
        .chat-rooms {
            width: 300px;
            background: #f8f9ff;
            border-right: 2px solid #e2e8f0;
            display: flex;
            flex-direction: column;
        }
        
        .rooms-header {
            background: #4a5568;
            color: white;
            padding: 20px;
            text-align: center;
            font-size: 18px;
            font-weight: bold;
        }
        
        .room-list {
            flex: 1;
            overflow-y: auto;
            padding: 10px;
        }
        
        .room-item {
            background: white;
            margin-bottom: 10px;
            padding: 15px;
            border-radius: 10px;
            cursor: pointer;
            border: 2px solid transparent;
            transition: all 0.3s;
        }
        
        .room-item:hover {
            background: #e2e8f0;
            border-color: #4299e1;
        }
        
        .room-item.active {
            background: #4299e1;
            color: white;
            border-color: #3182ce;
        }
        
        .room-name {
            font-weight: bold;
            margin-bottom: 5px;
        }
        
        .room-description {
            font-size: 12px;
            color: #666;
        }
        
        .room-item.active .room-description {
            color: #e2e8f0;
        }
        
        /* 채팅 영역 */
        .chat-area {
            flex: 1;
            display: flex;
            flex-direction: column;
        }
        
        .chat-header {
            background: #4299e1;
            color: white;
            padding: 20px;
            display: flex;
            align-items: center;
            justify-content: space-between;
        }
        
        .current-room {
            font-size: 18px;
            font-weight: bold;
        }
        
        .user-info {
            display: flex;
            align-items: center;
            gap: 10px;
        }
        
        .user-avatar {
            width: 35px;
            height: 35px;
            border-radius: 50%;
            background: #fff;
            display: flex;
            align-items: center;
            justify-content: center;
            font-weight: bold;
            color: #4299e1;
        }
        
        /* 메시지 표시 영역 */
        .messages-container {
            flex: 1;
            overflow-y: auto;
            padding: 20px;
            background: #f7fafc;
        }
        
        .message {
            margin-bottom: 15px;
            display: flex;
            align-items: flex-start;
            gap: 10px;
        }
        
        .message.my-message {
            flex-direction: row-reverse;
        }
        
        .message-avatar {
            width: 30px;
            height: 30px;
            border-radius: 50%;
            background: #4299e1;
            display: flex;
            align-items: center;
            justify-content: center;
            color: white;
            font-size: 12px;
            font-weight: bold;
            flex-shrink: 0;
        }
        
        .message-content {
            max-width: 70%;
        }
        
        .message-info {
            font-size: 10px;
            color: #666;
            margin-bottom: 3px;
        }
        
        .my-message .message-info {
            text-align: right;
        }
        
        .message-bubble {
            background: white;
            padding: 10px 15px;
            border-radius: 18px;
            box-shadow: 0 2px 5px rgba(0,0,0,0.1);
        }
        
        .my-message .message-bubble {
            background: #4299e1;
            color: white;
        }
        
        /* 메시지 입력 영역 */
        .message-input-area {
            padding: 20px;
            background: white;
            border-top: 2px solid #e2e8f0;
            display: flex;
            gap: 10px;
            align-items: center;
        }
        
        .message-input {
            flex: 1;
            padding: 12px 15px;
            border: 2px solid #e2e8f0;
            border-radius: 25px;
            outline: none;
            font-size: 14px;
        }
        
        .message-input:focus {
            border-color: #4299e1;
        }
        
        .send-button {
            width: 45px;
            height: 45px;
            background: #4299e1;
            color: white;
            border: none;
            border-radius: 50%;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 18px;
            transition: all 0.3s;
        }
        
        .send-button:hover {
            background: #3182ce;
            transform: scale(1.05);
        }
        
        .send-button:disabled {
            background: #a0aec0;
            cursor: not-allowed;
            transform: none;
        }
        
        /* 스크롤바 스타일링 */
        .messages-container::-webkit-scrollbar,
        .room-list::-webkit-scrollbar {
            width: 8px;
        }
        
        .messages-container::-webkit-scrollbar-track,
        .room-list::-webkit-scrollbar-track {
            background: #f1f1f1;
            border-radius: 10px;
        }
        
        .messages-container::-webkit-scrollbar-thumb,
        .room-list::-webkit-scrollbar-thumb {
            background: #cbd5e0;
            border-radius: 10px;
        }
        
        .messages-container::-webkit-scrollbar-thumb:hover,
        .room-list::-webkit-scrollbar-thumb:hover {
            background: #a0aec0;
        }
        
        /* 반응형 디자인 */
        @media (max-width: 768px) {
            .container {
                width: 100%;
                height: 100vh;
                border-radius: 0;
            }
            
            .chat-rooms {
                width: 250px;
            }
            
            .message-content {
                max-width: 85%;
            }
        }
    </style>
</head>
<body>
    <div class="container">
        <!-- 채팅방 목록 -->
        <div class="chat-rooms">
            <div class="rooms-header">
                🎠 놀이공원 채팅방
            </div>
            <div class="room-list" id="roomList">
                <div class="room-item active" data-room="roller-coaster">
                    <div class="room-name">🎢 롤러코스터</div>
                    <div class="room-description">스릴 넘치는 이야기를 나누어요!</div>
                </div>
                <div class="room-item" data-room="ferris-wheel">
                    <div class="room-name">🎡 관람차</div>
                    <div class="room-description">높은 곳에서 보는 경치 이야기</div>
                </div>
                <div class="room-item" data-room="carousel">
                    <div class="room-name">🎠 회전목마</div>
                    <div class="room-description">따뜻하고 평화로운 대화</div>
                </div>
                <div class="room-item" data-room="bumper-car">
                    <div class="room-name">🚗 범퍼카</div>
                    <div class="room-description">재미있는 게임과 놀이 이야기</div>
                </div>
            </div>
        </div>
        
        <!-- 채팅 영역 -->
        <div class="chat-area">
            <!-- 채팅 헤더 -->
            <div class="chat-header">
                <div class="current-room" id="currentRoomName">🎢 롤러코스터</div>
                <div class="user-info">
                    <div class="user-avatar" id="userAvatar">🧑</div>
                    <span id="userName">놀이공원 친구</span>
                </div>
            </div>
            
            <!-- 메시지 표시 영역 -->
            <div class="messages-container" id="messagesContainer">
                <div class="message">
                    <div class="message-avatar">🎪</div>
                    <div class="message-content">
                        <div class="message-info">놀이공원 가이드 • 방금전</div>
                        <div class="message-bubble">안녕하세요! 롤러코스터 채팅방에 오신 걸 환영해요! 🎉</div>
                    </div>
                </div>
            </div>
            
            <!-- 메시지 입력 영역 -->
            <div class="message-input-area">
                <input type="text" class="message-input" id="messageInput" 
                       placeholder="메시지를 입력하세요..." maxlength="500">
                <button class="send-button" id="sendButton">📤</button>
            </div>
        </div>
    </div>

    <script>
        // 현재 사용자 정보
        const currentUser = {
            id: 'user-' + Math.random().toString(36).substr(2, 9),
            name: '놀이공원 친구',
            avatar: '🧑'
        };
        
        // 현재 선택된 채팅방
        let currentRoom = 'roller-coaster';
        
        // 채팅방 정보
        const chatRooms = {
            'roller-coaster': { name: '🎢 롤러코스터', emoji: '🎢' },
            'ferris-wheel': { name: '🎡 관람차', emoji: '🎡' },
            'carousel': { name: '🎠 회전목마', emoji: '🎠' },
            'bumper-car': { name: '🚗 범퍼카', emoji: '🚗' }
        };
        
        // 메시지 데이터 (실제로는 Firebase에서 가져올 데이터)
        const messages = {
            'roller-coaster': [
                {
                    id: 1,
                    user: { id: 'guide', name: '놀이공원 가이드', avatar: '🎪' },
                    message: '안녕하세요! 롤러코스터 채팅방에 오신 걸 환영해요! 🎉',
                    timestamp: new Date(Date.now() - 60000)
                }
            ],
            'ferris-wheel': [
                {
                    id: 1,
                    user: { id: 'guide', name: '놀이공원 가이드', avatar: '🎪' },
                    message: '관람차에서 보는 야경이 정말 아름다워요! ✨',
                    timestamp: new Date(Date.now() - 120000)
                }
            ],
            'carousel': [],
            'bumper-car': []
        };
        
        // DOM 요소들
        const roomList = document.getElementById('roomList');
        const messagesContainer = document.getElementById('messagesContainer');
        const messageInput = document.getElementById('messageInput');
        const sendButton = document.getElementById('sendButton');
        const currentRoomName = document.getElementById('currentRoomName');
        const userName = document.getElementById('userName');
        const userAvatar = document.getElementById('userAvatar');
        
        // 초기화
        function init() {
            // 사용자 정보 표시
            userName.textContent = currentUser.name;
            userAvatar.textContent = currentUser.avatar;
            
            // 채팅방 목록 이벤트 설정
            setupRoomListeners();
            
            // 메시지 입력 이벤트 설정
            setupMessageInput();
            
            // 현재 채팅방 메시지 표시
            displayMessages();
        }
        
        // 채팅방 목록 이벤트 설정
        function setupRoomListeners() {
            roomList.addEventListener('click', (e) => {
                const roomItem = e.target.closest('.room-item');
                if (roomItem) {
                    // 이전 활성화된 방 비활성화
                    document.querySelector('.room-item.active').classList.remove('active');
                    
                    // 새 방 활성화
                    roomItem.classList.add('active');
                    
                    // 현재 방 변경
                    currentRoom = roomItem.dataset.room;
                    currentRoomName.textContent = chatRooms[currentRoom].name;
                    
                    // 메시지 표시
                    displayMessages();
                    
                    console.log('채팅방 변경:', currentRoom);
                }
            });
        }
        
        // 메시지 입력 이벤트 설정
        function setupMessageInput() {
            // 전송 버튼 클릭
            sendButton.addEventListener('click', sendMessage);
            
            // 엔터키로 전송
            messageInput.addEventListener('keypress', (e) => {
                if (e.key === 'Enter' && !e.shiftKey) {
                    e.preventDefault();
                    sendMessage();
                }
            });
            
            // 입력 내용에 따른 버튼 상태 변경
            messageInput.addEventListener('input', () => {
                const hasContent = messageInput.value.trim().length > 0;
                sendButton.disabled = !hasContent;
                sendButton.style.opacity = hasContent ? '1' : '0.5';
            });
        }
        
        // 메시지 전송
        function sendMessage() {
            const messageText = messageInput.value.trim();
            if (!messageText) return;
            
            // 새 메시지 객체 생성
            const newMessage = {
                id: Date.now(),
                user: currentUser,
                message: messageText,
                timestamp: new Date()
            };
            
            // 메시지 배열에 추가
            if (!messages[currentRoom]) {
                messages[currentRoom] = [];
            }
            messages[currentRoom].push(newMessage);
            
            // 입력창 초기화
            messageInput.value = '';
            sendButton.disabled = true;
            sendButton.style.opacity = '0.5';
            
            // 메시지 표시 업데이트
            displayMessages();
            
            console.log('메시지 전송:', newMessage);
            
            // 실제 Firebase에서는 여기서 데이터를 저장합니다
            // addDoc(collection(db, 'messages'), newMessage);
        }
        
        // 메시지 표시
        function displayMessages() {
            const roomMessages = messages[currentRoom] || [];
            messagesContainer.innerHTML = '';
            
            if (roomMessages.length === 0) {
                messagesContainer.innerHTML = `
                    <div style="text-align: center; color: #a0aec0; margin-top: 50px;">
                        <div style="font-size: 48px; margin-bottom: 10px;">${chatRooms[currentRoom].emoji}</div>
                        <div>첫 번째 메시지를 보내보세요!</div>
                    </div>
                `;
                return;
            }
            
            roomMessages.forEach(msg => {
                const messageElement = createMessageElement(msg);
                messagesContainer.appendChild(messageElement);
            });
            
            // 스크롤을 맨 아래로 이동 (자동 스크롤)
            scrollToBottom();
        }
        
        // 메시지 요소 생성
        function createMessageElement(msg) {
            const messageDiv = document.createElement('div');
            messageDiv.className = 'message';
            
            // 내 메시지인지 확인
            const isMyMessage = msg.user.id === currentUser.id;
            if (isMyMessage) {
                messageDiv.classList.add('my-message');
            }
            
            // 시간 포맷팅
            const timeString = formatTime(msg.timestamp);
            
            messageDiv.innerHTML = `
                <div class="message-avatar">${msg.user.avatar}</div>
                <div class="message-content">
                    <div class="message-info">${msg.user.name} • ${timeString}</div>
                    <div class="message-bubble">${escapeHtml(msg.message)}</div>
                </div>
            `;
            
            return messageDiv;
        }
        
        // 시간 포맷팅
        function formatTime(timestamp) {
            const now = new Date();
            const diff = now - timestamp;
            
            if (diff < 60000) { // 1분 미만
                return '방금전';
            } else if (diff < 3600000) { // 1시간 미만
                return Math.floor(diff / 60000) + '분 전';
            } else if (diff < 86400000) { // 24시간 미만
                return Math.floor(diff / 3600000) + '시간 전';
            } else {
                return timestamp.toLocaleDateString();
            }
        }
        
        // HTML 이스케이프
        function escapeHtml(text) {
            const div = document.createElement('div');
            div.textContent = text;
            return div.innerHTML;
        }
        
        // 스크롤을 맨 아래로 이동
        function scrollToBottom() {
            setTimeout(() => {
                messagesContainer.scrollTop = messagesContainer.scrollHeight;
            }, 100);
        }
        
        // 실시간 업데이트 시뮬레이션 (실제로는 Firebase 리스너 사용)
        function simulateRealTimeUpdates() {
            setInterval(() => {
                // 랜덤하게 다른 사용자의 메시지 추가 (데모용)
                if (Math.random() > 0.98) { // 2% 확률
                    const randomMessages = [
                        '와! 정말 재미있어요! 😄',
                        '다음에 또 타고 싶어요!',
                        '무서웠지만 스릴 넘쳤어요! 🎢',
                        '친구들과 함께 와서 더 재미있었어요!',
                        '사진 찍기 좋은 곳이에요! 📸'
                    ];
                    
                    const randomUsers = [
                        { id: 'user1', name: '재미있는 친구', avatar: '😊' },
                        { id: 'user2', name: '모험가', avatar: '🤠' },
                        { id: 'user3', name: '카메라맨', avatar: '📷' }
                    ];
                    
                    const randomMessage = {
                        id: Date.now() + Math.random(),
                        user: randomUsers[Math.floor(Math.random() * randomUsers.length)],
                        message: randomMessages[Math.floor(Math.random() * randomMessages.length)],
                        timestamp: new Date()
                    };
                    
                    if (!messages[currentRoom]) {
                        messages[currentRoom] = [];
                    }
                    messages[currentRoom].push(randomMessage);
                    
                    displayMessages();
                    console.log('실시간 메시지 수신:', randomMessage);
                }
            }, 1000);
        }
        
        // 앱 시작
        init();
        simulateRealTimeUpdates();
        
        console.log('놀이공원 채팅 앱이 시작되었습니다! 🎠');
        console.log('Firebase를 연결하면 실제 실시간 채팅이 가능해요!');
    </script>
</body>
</html>

미리 보기

반응형