예약 포털 (Vue3 + Firebase) - 서비스 오픈까지

5. 동네 (예약) 포털 (Vue 3 + Firebase) - 구글 계정으로 로그인

그랜파 개발자 2025. 5. 30. 19:50

Firebase Hosting으로 실제 웹 서비스 오픈까지 진행합니다.

서비스 오픈까지 개발을 진행하면서 계속 수정, 변경될 것입니다.

구글 계정으로 로그인

Firebase Authentication의 이메일과 비밀번호로 회원 가입을 하지 않고

구글 계정으로 로그인하는 기능을 구현합니다.

 

구글 계정으로 로그인하여 프로필 정보를 생성하면

앱에 접속을 할 때는 구글 계정으로 로그인을 해야 합니다.

만약 Firebase Authentication의 이메일과 비밀번호로 회원 가입을 한 사용자가

구글 계정으로 로그인을 하여 다른 프로필을 생성하면

이 두 계정을 각 다른 사용자로 인식합니다.

 

 

 

 

 

구글 계정으로 로그인 기능을 구현하기 위해서는 

Firebase Authentication의 GoogleAuthProvider와 signInWithPopup를 사용합니다.


1. GoogleAuthProvider

개념

  • Firebase Authentication에서 구글 로그인 기능을 구현할 때 사용하는 인증 공급자(Provider) 클래스입니다.
  • 구글 OAuth 2.0 인증 과정을 Firebase가 처리할 수 있게 도와주는 역할을 합니다.
  • 이 클래스를 통해 구글 로그인에 필요한 인증 요청 정보를 생성합니다.

주요 역할

  • 구글 로그인 시 필요한 권한 범위(scope)를 설정할 수 있습니다.
  • 사용자에게 어떤 정보를 요청할지(이메일, 프로필 등) 지정할 수 있습니다.
  • 추가 설정(예: 로그인 시 계정 선택창 강제 표시)도 가능합니다.

2. signInWithPopup

개념

  • Firebase Authentication 함수 중 하나로, 팝업 창을 이용해 사용자 로그인을 처리합니다.
  • 인증 공급자(예: GoogleAuthProvider)를 인자로 받아서 해당 공급자 방식으로 로그인 과정을 진행합니다.

작동 방식

  • 사용자가 로그인 버튼 클릭 시 팝업창이 열립니다.
  • 구글 로그인 화면이 팝업으로 나타나고, 사용자는 구글 계정으로 로그인합니다.
  • 로그인 성공 시 팝업이 닫히고, Firebase가 인증 결과(사용자 정보, 인증 토큰)를 반환합니다.

사용 예시

import { signInWithPopup, GoogleAuthProvider } from "firebase/auth";
import { auth } from "./firebase";  // Firebase 초기화 파일

const provider = new GoogleAuthProvider();

signInWithPopup(auth, provider)
  .then((result) => {
    const user = result.user; // 로그인한 사용자 정보
    const credential = GoogleAuthProvider.credentialFromResult(result);
    const token = credential.accessToken; // 구글 액세스 토큰 (선택적)
    console.log('로그인 성공:', user);
  })
  .catch((error) => {
    console.error('로그인 실패:', error);
  });

장점

  • 페이지 리다이렉트 없이 팝업창에서 간편하게 로그인 가능.
  • 사용자 경험이 자연스러움.
  • 즉시 로그인 결과를 받을 수 있음.

주의점

  • 브라우저에서 팝업 차단 설정이 되어 있으면 작동하지 않을 수 있음.
  • 모바일에서는 팝업보다는 signInWithRedirect 방식을 권장하기도 함.

 

src/stores/authStore.js

- loginWithGoogle  : 구글 계정으로 로그인

// src/stores/authStore.js
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { auth, db } from '../firebase'
import {
  createUserWithEmailAndPassword,
  signInWithEmailAndPassword,
  signOut,
  onAuthStateChanged,
  updatePassword,
  sendPasswordResetEmail,
  EmailAuthProvider,
  reauthenticateWithCredential,
  GoogleAuthProvider,
  signInWithPopup,
} from 'firebase/auth'
import { doc, setDoc, getDoc } from 'firebase/firestore'

export const useAuthStore = defineStore('auth', () => {
  const user = ref(null)
  const profile = ref(null)

  const register = async (email, password, name, aboutMe) => {
    const userCredential = await createUserWithEmailAndPassword(auth, email, password)
    user.value = userCredential.user

    const profileData = {
      uid: user.value.uid,
      name,
      aboutMe,
      email: user.value.email,
      createdAt: new Date(),
    }

    await setDoc(doc(db, 'profiles', user.value.uid), profileData)
    profile.value = profileData
  }

  const login = async (email, password) => {
    const userCredential = await signInWithEmailAndPassword(auth, email, password)
    user.value = userCredential.user

    const profileDoc = await getDoc(doc(db, 'profiles', user.value.uid))
    profile.value = profileDoc.exists() ? profileDoc.data() : null
  }

  const logout = async () => {
    await signOut(auth)
    user.value = null
    profile.value = null
  }

  const initAuth = () => {
    onAuthStateChanged(auth, async (currentUser) => {
      user.value = currentUser
      if (currentUser) {
        const profileDoc = await getDoc(doc(db, 'profiles', currentUser.uid))
        profile.value = profileDoc.exists() ? profileDoc.data() : null
      } else {
        profile.value = null
      }
    })
  }

  const changePassword = async (currentPassword, newPassword) => {
    const currentUser = auth.currentUser
    if (!currentUser || !currentUser.email) {
      throw new Error('사용자 정보가 없습니다.')
    }

    const credential = EmailAuthProvider.credential(currentUser.email, currentPassword)
    await reauthenticateWithCredential(currentUser, credential)
    await updatePassword(currentUser, newPassword)
  }

  const resetPassword = async (email) => {
    await sendPasswordResetEmail(auth, email)
  }

  const loginWithGoogle = async () => {
    try {
      const provider = new GoogleAuthProvider()
      const result = await signInWithPopup(auth, provider)
      user.value = result.user

      const profileRef = doc(db, 'profiles', user.value.uid)
      const profileDoc = await getDoc(profileRef)

      if (!profileDoc.exists()) {
        const profileData = {
          uid: user.value.uid,
          name: user.value.displayName || '',
          aboutMe: '',
          email: user.value.email || '',
          createdAt: new Date(),
        }
        await setDoc(profileRef, profileData)
        profile.value = profileData
        return { isNewUser: true }
      } else {
        profile.value = profileDoc.data()
        return { isNewUser: false }
      }
    } catch (error) {
      throw error
    }
  }

  return {
    user,
    profile,
    register,
    login,
    logout,
    initAuth,
    changePassword,
    resetPassword,
    loginWithGoogle,
  }
})

 

src/views/Login.vue 

<!-- src/views/Login.vue -->
<template>
  <v-container class="d-flex justify-center align-start" style="min-height: 100vh;">
    <v-card width="400" class="pa-4">
      <v-card-title class="text-h6">로그인</v-card-title>

      <v-form @submit.prevent="login">
        <v-text-field
          v-model="email"
          label="이메일"
          type="email"
          required
          prepend-inner-icon="mdi-email"
        />
        <v-text-field
          v-model="password"
          label="비밀번호"
          type="password"
          required
          prepend-inner-icon="mdi-lock"
        />
        <v-btn type="submit" color="primary" block class="mt-4">로그인</v-btn>
      </v-form>

      <v-btn variant="text" class="mt-2" @click="resetPassword" block>
        비밀번호를 잊으셨나요?
      </v-btn>

      <v-divider class="my-4"></v-divider>

      <!-- ✅ 구글 로그인 버튼 -->
      <v-btn
        color="white"
        class="google-login"
        block
        @click="loginWithGoogle"
      >
        <v-icon left class="mr-2">mdi-google</v-icon>
        구글 계정으로 로그인
      </v-btn>
    </v-card>
  </v-container>
</template>

<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/authStore'

const router = useRouter()
const authStore = useAuthStore()

const email = ref('')
const password = ref('')

const login = async () => {
  try {
    await authStore.login(email.value, password.value)
    router.push('/')
  } catch (error) {
    alert(error.message || '로그인에 실패했습니다.')
  }
}

const resetPassword = async () => {
  if (!email.value) {
    alert('비밀번호 재설정을 위해 이메일을 입력해주세요.')
    return
  }
  try {
    await authStore.resetPassword(email.value)
    alert('비밀번호 재설정 이메일이 전송되었습니다.')
  } catch (error) {
    alert(error.message || '비밀번호 재설정에 실패했습니다.')
  }
}

const loginWithGoogle = async () => {
  try {
    const { isNewUser } = await authStore.loginWithGoogle()
    if (isNewUser) {
      // 구글 계정으로 처음 로그인이므로 프로필 수정으로로
      router.push('/profile') 
    } else {
      router.push('/')
    }
  } catch (error) {
    alert(error.message || '구글 로그인에 실패했습니다.')
  }
}
</script>

<style scoped>
.google-login {
  border: 1px solid #ccc;
  color: #444;
  font-weight: 500;
}
</style>