로그인에 성공을 하면 user 객체를 돌려 받고 이것을 state의 user 변수에 저장합니다.
이 user 변수가 null인지 확인을 하면 로그인 상태인지 로그아웃 상태인지 확인할 수 있습니다.
로그인 상태에 따라 Drawer의 메뉴 항목을 다르게 할 수 있습니다.
또 AppBar의 로그인, 로그아웃 아이콘 버튼을 로그인 상태에 따라 나타낼 수 있습니다.
로그인 창에 비밀번호 리셋 기능을 둡니다.
비밀번호 리셋은 비밀번호를 잊었을 때 사용합니다.
![](https://blog.kakaocdn.net/dn/dacXOH/btsMaBHEwh5/gkUSypWJb5b5LIxAj1Qi1K/img.png)
![](https://blog.kakaocdn.net/dn/ccKwzp/btsMa5n9AL9/KbabFoztq3RrqqSWLBRpQ1/img.png)
![](https://blog.kakaocdn.net/dn/dbkOpy/btsMbWw5zwa/iuFKMi4Cxn4aytnhEqFmHK/img.png)
![](https://blog.kakaocdn.net/dn/qnbo2/btsMcfDcyzS/bVZlCWEUhqLuivWMQxKQU0/img.png)
로그인
로그인은 Firebase Authentication에서 제공하는 메서드 signInWithEmailAndPassword를 사용하며,
로그인을 성공하면 UserCredential 객체를 받아 이것을 state의 user 변수에 저장합니다.
// --로그인
async login({ commit, dispatch }, { email, password }) {
try {
const userCredential = await signInWithEmailAndPassword(auth, email, password);
commit("setUser", userCredential.user);
dispatch("fetchProfile", userCredential.user); // 로그인한 회원의 계정 설정 정보
} catch (error) {
alert(error.message); // 에러 메시지 표시
}
},
로그아웃
로그아웃은 Firebase Authentication에서 제공하는 메서드 signOut을 사용하며,
현재 로그인한 사용자를 로그아웃시키는 기능을 합니다.
이 메서드를 호출하면 Firebase 인증 세션이 종료됩니다.
// 로그아웃
async logout({ commit }) {
try {
await signOut(auth);
commit("setUser", null);
alert("로그아웃 했습니다!");
} catch (error) {
alert("Error logging out:" + error.message);
}
},
자동 로그인
자동 로그인은 Firebase Authentication에서 제공하는 메서드 onAuthStateChanged를 사용하며,
사용자의 로그인 상태 변화를 감지하는 기능을 합니다.
즉, 로그인 / 로그아웃 / 인증 상태 변화가 발생하면 자동으로 콜백 함수가 실행됩니다.
// -- 앱을 시작하면 자동 로그인을 설정한다.
async initializeAuth({ commit, dispatch }) {
onAuthStateChanged(auth, (user) => {
if (user) {
commit("setUser", user);
} else {
commit("setUser", null);
}
});
},
main.js
// src/main.js
import Vue from 'vue'
import App from './App.vue'
import './registerServiceWorker'
import router from './router'
import store from './store'
import vuetify from './plugins/vuetify'
Vue.config.productionTip = false
new Vue({
router,
store,
vuetify,
render: h => h(App),
created() {
// Set up Firebase auth state change listener
const { dispatch } = this.$store;
// Initialize Firebase authentication to check for the logged-in user
dispatch('auth/initializeAuth');
}
}).$mount('#app')
비밀번호 재설정
비밀번호 재설정은 Firebase Authentication에서 제공하는 메서드 sendPasswordResetEmail을 사용하며,
사용자가 비밀번호를 잊었을 때 비밀번호 재설정 이메일을 전송하는 기능을 제공합니다.
// 비밀번호 재설정
async PasswordReset({}, email) {
sendPasswordResetEmail(auth, email)
.then(() => {
alert("비밀번호 재설정 이메일이 발송되었습니다.");
})
.catch((error) => {
alert("비밀번호 재설정 이메일 발송 중 오류 발생:", error);
});
},
로그인, 로그아웃 아이콘 - App.vue template
![](https://blog.kakaocdn.net/dn/dBfzWt/btsMb2cZGPS/cJGRKiEKuu5NcdcqOKGtv1/img.png)
Drawer 메뉴 항목 - App.vue script
![](https://blog.kakaocdn.net/dn/cpd1SG/btsMdnHbAmX/3uvf1KUDa3byVfzwY5Mql1/img.png)
App.vue
<!-- src/App.vue -->
<template>
<v-app>
<!-- App Bar -->
<v-app-bar color="primary" dark app>
<v-app-bar-nav-icon @click="toggleDrawer" />
<v-toolbar-title>마이 블로그</v-toolbar-title>
<v-spacer></v-spacer>
<div @click="doLogout" v-if="user" style="cursor: pointer">
<v-icon left class="ml-1">mdi-logout</v-icon>
</div>
<div @click="doLogin" v-if="!user" style="cursor: pointer">
<v-icon left class="ml-1">mdi-login</v-icon>
</div>
</v-app-bar>
<!-- Navigation Drawer -->
<v-navigation-drawer v-model="drawer" app :clipped="$vuetify.breakpoint.lgAndUp">
<v-list>
<v-list-item-group>
<v-list-item v-for="(item, index) in GetMenuItems"
:key="index"
@click="$router.push(item.path).catch(() => { drawer = !drawer });">
<v-list-item-icon>
<v-icon>{{ item.icon }}</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>{{ item.title }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list-item-group>
</v-list>
</v-navigation-drawer>
<!-- Main Content -->
<v-main>
<v-container>
<router-view /> <!-- 라우터가 여기에 컴포넌트를 렌더링 -->
</v-container>
</v-main>
<!-- Footer -->
<v-footer app color="secondary" dark>
<v-btn icon v-if="$route.name !== 'Home'" @click="$router.go(-1)">
<v-icon>mdi-arrow-left</v-icon>
</v-btn>
<v-spacer></v-spacer>
<router-link to="/" style="cursor: pointer">
<v-icon>mdi-home</v-icon>
</router-link>
</v-footer>
</v-app>
</template>
<script>
import { mapState, mapActions } from 'vuex';
import router from '@/router';
export default {
data() {
return {
drawer: false,
menuItems: [
],
};
},
computed: {
...mapState('auth', ['user']),
GetMenuItems() {
if(this.user) {
return [
{ title: '홈', icon: 'mdi-home', path: '/' },
{ title: '글쓰기', icon: 'mdi-pencil', path: '/write' },
{ title: '블로그', icon: 'mdi-post', path: '/blog/myblog' },
{ title: '구독', icon: 'mdi-account-heart', path: '/subscription' },
{ title: '독자', icon: 'mdi-account-details', path: '/readers' },
{ title: '검색', icon: 'mdi-file-search-outline', path: '/search' },
{ title: '알림 요청', icon: 'mdi-bell', path: '/notification' },
{ title: '계정 설정', icon: 'mdi-account-box-edit-outline', path: '/profile' },
{ title: '카테고리 관리', icon: 'mdi-cog', path: '/category' },
{ title: '마이블로그는?', icon: 'mdi-information', path: '/about' },
]
} else {
return [
{ title: '홈', icon: 'mdi-home', path: '/' },
{ title: '로그인', icon: 'mdi-login', path: '/login' },
{ title: '검색', icon: 'mdi-file-search-outline', path: '/search' },
{ title: '계정 만들기', icon: 'mdi-account-box-edit-outline', path: '/register' },
{ title: '마이블로그는?', icon: 'mdi-information', path: '/about' },
]
}
}
},
methods: {
...mapActions('auth', ['logout']),
toggleDrawer() {
this.drawer = !this.drawer;
},
doLogin() {
if(this.$route.name !== 'login')
router.push("/login"); // home으로
},
doLogout() {
this.logout();
if(this.$route.name !== 'Home')
router.push("/"); // home으로
},
},
};
</script>
store의 auth 모듈
// src/store/modules/auth.js
import router from '@/router';
import { auth, db, collection, doc, getDoc, getDocs, addDoc, updateDoc,
query, where, arrayUnion } from "@/firebase";
import { createUserWithEmailAndPassword, signInWithEmailAndPassword,
signOut, sendPasswordResetEmail, onAuthStateChanged,
GoogleAuthProvider, signInWithPopup } from "firebase/auth";
const state = {
loading: false,
user: null, // 현재 로그인한 회원
profile: null, // 계정설정 정보
profiles: [], // 모든 회원의 계정 설정 정보
};
const mutations = {
setLoading(state, loading) {
state.loading = loading;
},
setUser(state, user) {
state.user = user;
},
setProfile(state, profile) {
state.profile = profile;
},
setProfiles(state, profiles) {
state.profiles = profiles;
},
};
const actions = {
// -- 계정 만들기
async register({ commit, dispatch }, { email, password }) {
try {
const userCredential = await createUserWithEmailAndPassword(auth, email, password);
commit('setUser', userCredential.user);
// 계정 설정 정보 저장 - 공란으로 저장하고, 계정 설정에서 정보를 입력한다.
const profile = {
userId: userCredential.user.uid,
email:email,
name: '',
blogName: '',
aboutMe: '',
createdAt: new Date(),
uids: [userCredential.user.uid]
}
dispatch('registerProfile', profile);
} catch (error) {
alert("register : " + error);
}
},
// 계정 설정 정보를 저장한다.
async registerProfile({ dispatch }, profile) {
try {
const profileRef = await addDoc(collection(db, "profiles"), {
userId: profile.userId,
email: profile.email,
name: profile.name,
blogName: profile.blogName,
aboutMe: profile.aboutMe,
createdAt: profile.createdAt,
uids: profile.uids
});
// profile 문서 id를 추가한다.
//profile.id = profileRef.id;
//dispatch('fetchProfile', profile.id);
// 전체 계정 설정 정보를 다시 로드한다.
// 개별 profile 로드도 포함되어 있다.
dispatch('fetchProfiles');
} catch (error) {
alert("Failed to register user. : " + error);
}
},
// 기존의 계정 설정 정보를 수정 한다.
async updateProfile({ dispatch, commit, state }, profile) {
//console.log(state.profile);
try {
commit('setLoading', true);
const userDoc = doc(db, "profiles", state.profile.id);
await updateDoc(userDoc, profile);
//dispatch('fetchProfile', profile.id);
// 전체 계정 설정 정보를 다시 로드한다.
// 개별 profile 로드도 포함되어 있다.
dispatch('fetchProfiles');
commit('setLoading', false);
alert("계정 정보를 수정하였습니다.");
} catch (error) {
alert("계정 정보 수정 실패: " + error.message);
}
},
// -- 계정 설정 정보 로드
async fetchProfile({ commit }, profileId) {
try {
const profileRef = doc(db, "profiles", profileId);
const profileSnap = await getDoc(profileRef); // 문서 가져오기
if (profileSnap.exists()) {
//console.log('profile: ', profileSnap.data());
// 계정 설정 정보로 로그인한 사용자 저장
commit("setProfile", {id:profileId, ...profileSnap.data()});
} else {
console.log("fetchProfile : " + userId + " 계정 설정 정보가 없습니다.");
}
} catch (error) {
alert("Failed to fetch user. : " + error.message);
}
},
// 전체 회원의 계정 설정 정보를 로드한다.
async fetchProfiles({ commit, dispatch, state }) {
try {
const profiles = [];
const profileRef = collection(db, "profiles");
const querySnapshot = await getDocs(profileRef);
querySnapshot.forEach((doc) => {
profiles.push({ id: doc.id, ...doc.data() });
});
commit('setProfiles', profiles);
// login 사용자가 있으면 pfofile 로드
if(state.user) {
const profile = state.profiles.find(profile => profile.uids && profile.uids.includes(state.user.uid));
//console.log(profile);
if(profile)
dispatch("fetchProfile", profile.id);
}
} catch (error) {
console.log('fetchProfiles : ' + error.message);
}
//commit('setLoading', false);
},
// --로그인
async login({ commit, dispatch }, { email, password }) {
try {
const userCredential = await signInWithEmailAndPassword(auth, email, password);
commit("setUser", userCredential.user);
// 계정 uid로 profile 문서를 구한다.
const profile = state.profiles.find(profile => profile.uids && profile.uids.includes(userCredential.user.uid));
dispatch("fetchProfile", profile.id);
} catch (error) {
alert("login: " + error.message); // 에러 메시지 표시
}
},
// 로그아웃
async logout({ commit }) {
try {
await signOut(auth);
commit("setUser", null);
alert("로그아웃 했습니다!");
} catch (error) {
alert("Error logging out:" + error.message);
}
},
// -- 앱을 시작하면 자동 로그인을 설정한다.
// onAuthStateChanged는 각 OAuth의 user 객체를 사용
async initializeAuth({ commit }) {
onAuthStateChanged(auth, (user) => {
if (user) {
commit("setUser", user);
} else {
commit("setUser", null);
}
});
},
// 비밀번호를 변경한다.
async changePassword({}, { oldPassword, newPassword }) {
const credential = EmailAuthProvider.credential(
auth.currentUser.email,
oldPassword
);
reauthenticateWithCredential(auth.currentUser, credential)
.then(() => {
updatePassword(auth.currentUser, newPassword)
.then(() => {
alert("비밀번호가 성공적으로 변경되었습니다.");
}).catch((error) => {
alert("비밀번호 변경 실패 : " + error.message);
});
})
.catch((error) => {
alert("재인증 실패 : " + error.message);
});
},
// 비밀번호 재설정
async PasswordReset({}, email) {
sendPasswordResetEmail(auth, email)
.then(() => {
alert("비밀번호 재설정 이메일이 발송되었습니다.");
})
.catch((error) => {
alert("비밀번호 재설정 이메일 발송 중 오류 발생:", error);
});
},
// -- OAuth(구글 계정) 로그인
// 구글 계정의 uid를 profile의 uids 배열에 넣는다.
async addUidToProfile({ state }, newUid) {
try {
const profileDoc = doc(db, "profiles", state.profile.id);
updateDoc(profileDoc, {
uids: arrayUnion(newUid)
});
} catch (error) {
alert('Error adding UID to user:' + error);
}
},
async addGoogleAccount({ dispatch, state }) {
try {
const provider = new GoogleAuthProvider();
// 이 메서드는 onAuthStateChanged를 호출한다.
// 이로 인해 상태 변수 user가 변경된다.
const { user } = await signInWithPopup(auth, provider);
try {
// 이미 연동된 회원의 경우 알림 메시지 출력한다.
//console.log(state.profile);
const profile = state.profiles.find(profile => profile.uids && profile.uids.includes(user.uid));
//console.log(profile);
if (profile) {
// 이미 연동되어 있다.
alert('이미 구글 계정에 연동되어 있습니다.');
} else {
// profile의 uids에 구글 계정 uid를 추가한다.
dispatch('addUidToProfile', user.uid);
}
} catch (error) {
alert('addGoogleAccount: ' + 'Error adding google uid');
}
} catch (error) {
alert('addGoogleAccount: ' + error.message);
}
},
// 구글 계정으로 로그인
async googleLogin({ commit, dispatch, state }) {
try {
// 구글 계정에 로그인
const provider = new GoogleAuthProvider();
const { user } = await signInWithPopup(auth, provider)
if(user) {
// 구글계정의 uid로 profile을 가져온다.
// 웹앱 계정은 사이트에 접속할 때 전체 회원 정보를 로드하였으므로
// 이미 로드된 회원 리스트에서 구글 계정 uid를 가진 profile을 가져온다.
const profile = state.profiles.find(profile => profile.uids && profile.uids.includes(user.uid));
if (profile) {
// 이 경우 Firebase Auth가 아닌 Google Account임을 고려해야 한다.
commit('setUser', user);
dispatch('fetchProfile', profile.id);
router.push("/"); // home으로
} else {
alert('addGoogleAccount: ' + '등록된 회원이 아닙니다.');
}
}
} catch (error) {
alert('googleLogin: ' + error.message);
}
},
};
const getters = {
};
export default {
namespaced: true,
state,
mutations,
actions,
getters
};
LoginView.vue
<!-- src/views/LoginView.vue -->
<template>
<v-card>
<v-card-title>로그인</v-card-title>
<v-card-text>
<v-form ref="form" v-model="valid" lazy-validation>
<v-text-field label="이메일" v-model="email" :rules="[rules.required, rules.email]"
prepend-icon="mdi-email" type="email" required></v-text-field>
<v-text-field label="비밀번호" v-model="password" :rules="[rules.required]"
prepend-icon="mdi-lock" type="password" required></v-text-field>
</v-form>
</v-card-text>
<v-card-actions>
<v-btn @click="doPasswordReset"> 비밀번호 재설정 </v-btn>
<v-spacer></v-spacer>
<v-btn color="primary" @click="doLogin" :disabled="!valid"> 로그인 </v-btn>
</v-card-actions>
</v-card>
</template>
<script>
import { mapActions } from 'vuex';
import router from '@/router';
export default {
data() {
return {
email: '',
password: '',
valid: false,
rules: {
required: (value) => !!value || "Required.",
email: (value) =>
/.+@.+\..+/.test(value) || "E-mail must be valid.",
},
};
},
methods: {
...mapActions('auth', ['login', 'PasswordReset']),
async doLogin() {
if (this.$refs.form.validate()) {
await this.login({ email: this.email, password: this.password });
router.push("/"); // home으로
}
},
async doPasswordReset() {
if (!this.email) {
alert("비밀번호 재설정을 위한 이메일을 입력하세요.");
return;
}
await this.PasswordReset(this.email);
},
},
};
</script>