마지막 메시지 미리보기
채팅방의 마지막 메시지를 채팅방 리스트에서 보여 줍니다.
채팅방 목록에 마지막 메시지를 미리보기 형태로 보여주는 것은
사용자 경험(UX)을 향상시키는 데 매우 중요한 역할을 합니다.
사용자가 채팅방을 열지 않고도 대화의 흐름이나 최근 상황을 파악할 수 있고
어떤 채팅방에 먼저 들어가야 할지 판단할 수 있게 도와줍니다.

🔧 1. Firestore 데이터 구조
chatRooms (컬렉션)
└─ roomId (문서)
├─ lastMessage: {
│ text: "내일 보자!",
│ senderId: "user123",
│ createdAt: 2025-05-20T12:00:00Z
│ }
└─ ... (기타 필드)
messages (컬렉션)
└─ roomId (서브컬렉션)
└─ messageId (문서)
├─ text: "내일 보자!"
├─ senderId: "user123"
├─ createdAt: ...
✅ 2. 미리보기용 필드: lastMessage
성능상 이유로 채팅방 문서에 lastMessage 필드를 저장해 두는 것이 일반적입니다.
이 필드는 새 메시지가 전송될 때마다 업데이트됩니다.
chatStore
const sendMessage = async (chatRoomId, senderId, receiverId, text) => {
const batch = writeBatch(db);
const messagesRef = collection(db, 'chatRooms', chatRoomId, 'messages');
const newMessageRef = doc(messagesRef); // 수동으로 새 메시지 문서 생성
const timestamp = serverTimestamp();
// 메시지 객체
const messageData = {
text,
senderId,
readBy: [senderId], // 보낸 사람은 읽은 것으로 처리
createdAt: timestamp,
readAt: null
};
// 참조
const chatRoomRef = doc(db, 'chatRooms', chatRoomId);
// 1. 메시지 추가
batch.set(newMessageRef, messageData);
batch.update(chatRoomRef, {
// 2. 수신자의 안 읽은 메시지 수 증가
[`unreadCountByUser.${receiverId}`]: increment(1),
// 3. 마지막 메시지 정보 갱신
lastMessage: {
text,
senderId,
createdAt: timestamp
}
});
// 4. 배치 커밋
await batch.commit();
}
🔍 3. Vue에서 채팅방 목록 보여줄 때
composables - useChatRooms.js
// src/composables/useChatRooms.js
import { collection, query, where, onSnapshot } from 'firebase/firestore';
import { useAuthStore } from '@/stores/authStore'; // ✅ authStore import
import { db } from '@/firebase';
import { ref } from 'vue';
let unsubscribe = null;
export function useChatRooms() {
const auth = useAuthStore(); // ✅ authStore 인스턴스
const chatRooms = ref([]);
const fetchChatRooms = (currentUserId) => {
if (!currentUserId)
return;
const q = query(
collection(db, 'chatRooms'),
where('userIds', 'array-contains', currentUserId)
);
unsubscribe = onSnapshot(q, async (snapshot) => {
const rooms = [];
for (const docSnap of snapshot.docs) {
const roomData = docSnap.data();
const otherUserId = roomData.userIds.find(uid => uid !== currentUserId);
const otherProfile = auth.profiles.find(p => p.id === otherUserId);
if(otherProfile) {
const otherUserName = otherProfile.name;
// 1:1 채팅방에서 서로 상대의 이름을 가져간다.
// 채팅방 리스트에 나타낸다.
rooms.push({
id: docSnap.id,
...roomData,
otherUserId,
otherUserName,
unreadCount: roomData.unreadCountByUser?.[currentUserId] || 0,
lastMessage: roomData.lastMessage || null,
});
}
}
chatRooms.value = rooms;
});
};
const stopListening = () => {
if (unsubscribe) {
unsubscribe();
unsubscribe = null;
}
};
return {
chatRooms,
fetchChatRooms,
stopListening
};
}
🖼️ 4. UI에 보여줄 때
ChatList.vue의 template
<div class="d-flex align-center">
<v-list-item-title>
<b>{{ room.otherUserName }}님</b>
</v-list-item-title>
<!-- 안읽은 메시지 수 -->
<v-badge
v-if="room.unreadCount > 0"
:content="room.unreadCount"
color="red"
overlap
class="ms-4"
content-class="mini-badge"
/>
</div>
<!--마지막 메시지 보기 -->
<v-list-item-subtitle v-if="room.lastMessage && room.lastMessage.text">
{{ room.lastMessage?.text || '메시지가 없습니다' }}
({{ formatTime(room.lastMessage.createdAt.toDate()) }})
</v-list-item-subtitle>
ChatList.vue
<!-- src/views/ChatList.vue -->
<template>
<v-container>
<h2>채팅룸</h2>
<v-list v-if="chatRooms && chatRooms.length">
<v-list-item
v-for="room in chatRooms"
:key="room.id"
@click="goToChat(room.id, room.otherUserId, room.otherUserName)"
>
<div class="d-flex align-center">
<v-list-item-title>
<b>{{ room.otherUserName }}님</b>
</v-list-item-title>
<!-- 안읽은 메시지 수 -->
<v-badge
v-if="room.unreadCount > 0"
:content="room.unreadCount"
color="red"
overlap
class="ms-4"
content-class="mini-badge"
/>
</div>
<!--마지막 메시지 보기 -->
<v-list-item-subtitle v-if="room.lastMessage && room.lastMessage.text">
{{ room.lastMessage?.text || '메시지가 없습니다' }}
({{ formatTime(room.lastMessage.createdAt.toDate()) }})
</v-list-item-subtitle>
</v-list-item>
</v-list>
<!-- 없을 때 안내 메시지 -->
<v-list v-else>
<v-list-item>
<v-list-item-title>채팅방이 없습니다.</v-list-item-title>
</v-list-item>
</v-list>
</v-container>
</template>
<script setup>
import { useAuthStore } from '@/stores/authStore';
import { useChatRooms } from '@/composables/useChatRooms';
import { watch } from 'vue';
import { useRouter } from 'vue-router';
import { format } from 'date-fns';
const router = useRouter();
const auth = useAuthStore();
const { chatRooms, fetchChatRooms } = useChatRooms();
const goToChat = (chatRoomId, otherUserId, otherUserName) => {
if(!auth.user)
return
// 채팅방으로 이동
router.push({ path: '/chat', query: {
otherUserId,
otherUserName,
roomId: chatRoomId
}});
};
const formatTime = (timestamp) => {
if (!timestamp) return '';
const date = timestamp;
// 24시간 형식 예: 14:05
return format(date, 'HH:mm');
};
// auth.user가 나중에 바뀔 수도 있으니까 watch로 감지!
// auth.profile이 로드 되어야 회원 정보가 모두 사용가능하다.
watch(
() => auth.profile,
(newUser) => {
if (newUser && newUser.userId ) {
fetchChatRooms(newUser.userId);
}
},
{ immediate: true }
);
</script>
<style scoped>
::v-deep(.mini-badge) {
font-size: 8px !important;
min-width: 16px !important;
height: 16px !important;
line-height: 16px !important;
padding: 0 4px !important;
background-color: red !important;
color: white !important;
}
</style>
chatStore
// src/stores/chatStore.js
import { defineStore } from 'pinia';
import { db } from '@/firebase';
import {
collection,
doc,
addDoc,
writeBatch,
getDoc,
query,
where,
getDocs,
increment,
serverTimestamp,
Timestamp,
} from 'firebase/firestore';
export const useChatStore = defineStore('chat', () => {
const findOrCreateChatRoom = async (currentUserId, otherUserId) => {
const chatRoomsRef = collection(db, 'chatRooms');
const q = query(chatRoomsRef, where('userIds', 'array-contains', currentUserId));
const snapshot = await getDocs(q);
// 같은 유저 조합이 있는지 확인
for (const doc of snapshot.docs) {
const userIds = doc.data().userIds;
if (userIds.includes(otherUserId)) {
return doc.id; // 기존 채팅방 ID 반환
}
}
// 없으면 새로 생성
const newRoomRef = await addDoc(chatRoomsRef, {
userIds: [currentUserId, otherUserId],
createdAt: Timestamp.now(),
});
return newRoomRef.id;
};
const sendMessage = async (chatRoomId, senderId, receiverId, text) => {
const batch = writeBatch(db);
const messagesRef = collection(db, 'chatRooms', chatRoomId, 'messages');
const newMessageRef = doc(messagesRef); // 수동으로 새 메시지 문서 생성
const timestamp = serverTimestamp();
// 메시지 객체
const messageData = {
text,
senderId,
readBy: [senderId], // 보낸 사람은 읽은 것으로 처리
createdAt: timestamp,
readAt: null
};
// 참조
const chatRoomRef = doc(db, 'chatRooms', chatRoomId);
// 1. 메시지 추가
batch.set(newMessageRef, messageData);
batch.update(chatRoomRef, {
// 2. 수신자의 안 읽은 메시지 수 증가
[`unreadCountByUser.${receiverId}`]: increment(1),
// 3. 마지막 메시지 정보 갱신
lastMessage: {
text,
senderId,
createdAt: timestamp
}
});
// 4. 배치 커밋
await batch.commit();
};
return {
findOrCreateChatRoom,
sendMessage
};
});
composables - useChatRooms.js
// src/composables/useChatRooms.js
import { collection, query, where, onSnapshot } from 'firebase/firestore';
import { useAuthStore } from '@/stores/authStore'; // ✅ authStore import
import { db } from '@/firebase';
import { ref } from 'vue';
let unsubscribe = null;
export function useChatRooms() {
const auth = useAuthStore(); // ✅ authStore 인스턴스
const chatRooms = ref([]);
const fetchChatRooms = (currentUserId) => {
if (!currentUserId)
return;
const q = query(
collection(db, 'chatRooms'),
where('userIds', 'array-contains', currentUserId)
);
unsubscribe = onSnapshot(q, async (snapshot) => {
const rooms = [];
for (const docSnap of snapshot.docs) {
const roomData = docSnap.data();
const otherUserId = roomData.userIds.find(uid => uid !== currentUserId);
const otherProfile = auth.profiles.find(p => p.id === otherUserId);
if(otherProfile) {
const otherUserName = otherProfile.name;
// 1:1 채팅방에서 서로 상대의 이름을 가져간다.
// 채팅방 리스트에 나타낸다.
rooms.push({
id: docSnap.id,
...roomData,
otherUserId,
otherUserName,
unreadCount: roomData.unreadCountByUser?.[currentUserId] || 0,
lastMessage: roomData.lastMessage || null,
});
}
}
chatRooms.value = rooms;
});
};
const stopListening = () => {
if (unsubscribe) {
unsubscribe();
unsubscribe = null;
}
};
return {
chatRooms,
fetchChatRooms,
stopListening
};
}
'Vue 3 + Firebase 기반 실시간 채팅 앱 개발' 카테고리의 다른 글
16. Vue 3 + Firebase 기반 채팅 앱 개발 - 읽지 않은 메시지 수 배지 표시 (0) | 2025.05.16 |
---|---|
15. Vue 3 + Firebase 기반 실시간 채팅 앱 v-chat 개발 - 메시지 읽은 시간 표시 (0) | 2025.05.15 |
14. Vue 3 + Firebase 기반 실시간 채팅 앱 개발 - 메시지 읽은 시간 표시 (0) | 2025.05.13 |
13. Vue 3 + Firebase 기반 실시간 채팅 앱 v-chat 개발 - 채팅 상대의 온라인 상태 (0) | 2025.05.12 |
12. Vue 3 + Firebase 기반 실시간 채팅 앱 v-chat 개발 - 채팅룸 (0) | 2025.05.11 |