Vue3, Firebase 프로젝트 - 채팅앱 VSignal

21. [개발] 채팅 기능 구현 - Vue3 Firebase 프로젝트 채팅앱 VSignal

그랜파 개발자 2025. 4. 2. 05:24

로그인한 사용자만 채팅방 접근 가능하게 만들기!

Firebase Auth 상태를 확인해서 로그인한 사용자만 채팅방에 접근가능하도록 합니다.

1. Firebase에서 로그인 상태 확인

Vue Router의 beforeEach 훅을 이용해서, 로그인하지 않은 사용자는 /chat에 접근할 수 없도록 설정할 거야.

🔥 onAuthStateChanged란?

onAuthStateChanged는 Firebase Authentication에서 제공하는 사용자 인증 상태 감지 함수입니다.
즉, 로그인, 로그아웃, 회원가입 등 사용자 인증 상태 변화를 실시간으로 감지할 수 있습니다.

// src/router.js
import { createRouter, createWebHistory } from "vue-router";
import AuthView from "@/views/AuthView.vue";
import ChatView from "@/views/ChatView.vue";
import { getAuth, onAuthStateChanged } from "firebase/auth";

const routes = [
  { path: "/", component: AuthView },
  { path: "/chat", component: ChatView, meta: { requiresAuth: true } },
];

const router = createRouter({
  history: createWebHistory(),
  routes,
});

// 네비게이션 가드 추가 (로그인한 사용자만 채팅방 접근 가능)
router.beforeEach((to, from, next) => {
  if (to.meta.requiresAuth) {
    const auth = getAuth();
    onAuthStateChanged(auth, (user) => {
      if (user) {
        next();
      } else {
        next("/");
      }
    });
  } else {
    next();
  }
});

export default router;

2. Firestore에 채팅 데이터 저장 구조

Firestore에 채팅 데이터를 저장할 거야.
1:1 채팅을 위해 다음과 같은 구조를 사용하자.

/chats/{chatId}/messages/{messageId}
 

각 1:1 채팅은 고유한 chatId를 가지며, 그 안에 메시지가 저장됨.

// 채팅 메시지 전송
const sendMessage = async (chatId, sender, message) => {
  const messagesRef = collection(db, `chats/${chatId}/messages`);
  console.log(messagesRef);
  await addDoc(messagesRef, {
    sender,
    message,
    timestamp: new Date(),
  });
};

3. 채팅 데이터 Firestore에 저장

firebase.js에 Firestore 관련 로직을 추가하자

// src/firebase.js
import { initializeApp } from "firebase/app";
import { getAuth, createUserWithEmailAndPassword, 
  signInWithEmailAndPassword, signOut } from "firebase/auth";
import { getFirestore, initializeFirestore, collection, addDoc, query, 
  where, getDocs, orderBy} from "firebase/firestore";

const firebaseConfig = {
  apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
  authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
  projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
  storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
  appId: import.meta.env.VITE_FIREBASE_APP_ID,
  databaseURL: import.meta.env.VITE_FIREBASE_DATABASE_URL,
};

console.log(import.meta.env.VITE_FIREBASE_PROJECT_ID);

// Firebase 초기화
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
const db = getFirestore(app);

// 회원가입 함수
const register = (email, password) => {
  return createUserWithEmailAndPassword(auth, email, password);
};

// 로그인 함수
const login = (email, password) => {
  return signInWithEmailAndPassword(auth, email, password);
};

// 로그아웃 함수
const logout = () => {
  return signOut(auth);
};

// 특정 유저와의 1:1 채팅 찾기 또는 생성
const getOrCreateChat = async (user1, user2) => {

  const chatRef = collection(db, "chats");
  const q = query(chatRef, where("users", "array-contains", user1));
  const snapshot = await getDocs(q);

  for (const doc of snapshot.docs) {
    const chatData = doc.data();
    if (chatData.users.includes(user2)) {
      return doc.id; // 기존 채팅방 ID 반환
    }
  }

  // 새로운 채팅방 생성
  const newChat = await addDoc(chatRef, { users: [user1, user2] });
  return newChat.id;
};

// 채팅 메시지 전송
const sendMessage = async (chatId, sender, message) => {
  const messagesRef = collection(db, `chats/${chatId}/messages`);
  console.log(messagesRef);
  await addDoc(messagesRef, {
    sender,
    message,
    timestamp: new Date(),
  });
};

export { auth, db, register, login, logout,
        getOrCreateChat, sendMessage
};

추가된 기능:
✔️ getOrCreateChat(user1, user2) → 기존 1:1 채팅방을 찾거나 새로 생성
✔️ sendMessage(chatId, sender, message) → Firestore에 채팅 메시지 저장

4. 채팅 UI 추가

이제 1:1 채팅 UI를 만들어보자.
src/views/ChatView.vue에 다음과 같이 코드를 추가해.

<!-- src/views/ChatView.vue -->
<template>
  <v-container class="fill-height d-flex flex-column">
    <v-card class="pa-5" width="400">
      <v-card-title class="text-center text-h5">1:1 채팅</v-card-title>

      <v-select v-model="selectedUser" :items="users" label="대화 상대 선택" />
      <v-divider class="my-2"></v-divider>

      <v-list v-if="messages.length > 0">
        <v-list-item v-for="msg in messages" :key="msg.id">
          <v-list-item-title>
            <strong>{{ msg.sender === currentUser ? "나" : "상대" }}:</strong>
            {{ msg.message }}
          </v-list-item-title>
        </v-list-item>
      </v-list>

      <v-text-field v-model="newMessage" label="메시지 입력" @keyup.enter="handleSendMessage" />
      <v-btn color="primary" block class="mt-3" @click="handleSendMessage">전송</v-btn>
      <v-btn color="red" block class="mt-3" @click="handleLogout">로그아웃</v-btn>
    </v-card>
  </v-container>
</template>

<script>
import { ref, watch } from "vue";
import { auth, getOrCreateChat, sendMessage } from "@/firebase";
import { useRouter } from "vue-router";

export default {
  setup() {
    const router = useRouter();
    const currentUser = auth.currentUser?.email;
    const selectedUser = ref(""); // 대화할 사용자 선택
    const users = ref(["user1@example.com", "user2@example.com"]); // 예제 사용자 리스트
    const chatId = ref(null);
    const newMessage = ref("");
    const messages = ref([]);

    // 로그아웃
    const handleLogout = async () => {
      await auth.signOut();
      router.push("/");
    };

    // 메시지 전송
    const handleSendMessage = async () => {
      if (newMessage.value.trim() === "" || !chatId.value) return;
      await sendMessage(chatId.value, currentUser, newMessage.value);
      newMessage.value = "";
    };

    // 채팅방 변경 감지
    watch(selectedUser, async (newUser) => {
      if (newUser) {
        chatId.value = await getOrCreateChat(currentUser, newUser);
      }
    });

    return { selectedUser, users, newMessage, handleSendMessage, messages, handleLogout, currentUser };
  },
};
</script>

✅ 추가된 기능

✔️ selectedUser를 선택하면 1:1 채팅방 자동 생성
✔️ handleSendMessage()로 Firestore에 메시지 저장
✔️ watch(selectedUser, ...)로 채팅방 자동 변경

5. 실행 및 테스트

npm run dev