구글 계정으로 로그인
OAuth(Open Authorization)는 제3자 서비스에서 사용자의 비밀번호를 공유하지 않고 인증 및 권한을 위임할 수 있도록 하는 표준 프로토콜입니다. 예를 들어, 웹사이트나 앱에서 Google, Facebook, GitHub 등의 계정으로 로그인하는 기능이 OAuth를 사용한 사례입니다.
OAuth의 핵심 개념
1. 사용자의 로그인 정보를 직접 제공하지 않음
앱이 직접 사용자의 아이디/비밀번호를 저장하지 않음
대신, OAuth 제공자(예: Google, Facebook) 를 통해 사용자를 인증
2. 제한된 권한 부여
사용자가 특정 권한(예: 이메일 접근, 프로필 정보, 파일 읽기/쓰기 등) 을 앱에 허용할 수 있음
앱이 사용자 대신 특정 작업을 수행할 수 있도록 토큰(Token)을 발급 받음
3. 토큰 기반 인증
OAuth에서는 Access Token(접근 토큰) 을 발급하여 인증을 처리
Access Token은 일정 시간이 지나면 만료됨(보안 강화)
signInWithPopup()이란?
signInWithPopup()은 Firebase Authentication에서 제공하는 소셜 로그인 방식 중 하나입니다.
Google의 제3자 로그인 제공자(OAuth Provider) 를 사용할 때 유용합니다.
새 창(Popup)을 띄워 사용자가 로그인하도록 유도하고
OAuth 인증 방식을 사용하여, Google, Facebook, GitHub 등의 계정을 통해 로그인 가능합니다.
signInWithPopup()을 사용할 때 필요한 설정
소셜 로그인을 사용하려면 Firebase 콘솔에서 제공자를 활성화해야 합니다.
Firebase 콘솔 설정
- Firebase Console에 접속
- Authentication > 로그인 제공자 선택
- 원하는 제공자(Google, Facebook 등)를 선택하고 사용 설정
- 필요한 API 키 또는 앱 ID 입력 후 저장
myBlog에서 구글 계정으로 로그인
OAuth를 이용한 로그인을 허용한다면 한 사람의 사용자가 여러 계정으로 로그인할 수 있습니다.
OAuth의 여러 계정이 같은 사용자라면 한 사람의 사용자로 통합하여야 합니다.
![](https://blog.kakaocdn.net/dn/bOBUgj/btsMfFioRa5/I5DCq2XqY5n1BdI2bKQ0P0/img.png)
![](https://blog.kakaocdn.net/dn/en1T9L/btsMgUr1AOR/QwX7bz2Ktzj6FX8R3Kzah0/img.png)
myBlog 앱은 이를 위하여 계정과는 별도의 계정 설정 정보를 Firestore의 profiles 컬렉션으로 관리합니다.
profiles 컬렉션의 각 profile 문서에는 uids 배열 항목이 있고,
이 uids 배열에 같은 사용자의 각 다른 계정 uid를 저장하여 같은 사용자임을 보증합니다.
각 사용자의 계정 정보 문서 항목은 다음과 같습니다.
- userId : 로그인 Id입니다. 계정 만들기에 등록된 정보입니다.
- email : 로그인할 때 이메일입니다. 이미 계정 생성에서 등록된 정보이므로 계정 설정에서는 입력하지 않습니다.
- name : 사용자의 이름 또는 별명입니다.
- blogName : 사용자의 블로그 이름입니다.
- aboutMe : 사용자의 현재 상태 정보입니다.
- createdAt : 계정 정보 생성일입니다. 계정 설정 정보를 저장할 때 시스템 시간을 자동으로 얻어 저장합니다.
- uids[] : 여러 OAuth를 이용한 로그인을 위한 것입니다.
구글 계정 연동
로그인 한 사용자의 profile 정보는 상태변수 profile에 저장되어 있습니다.
signInWithPopup() 메서드로 로그인한 사용자 정보를 얻어 이중 uid를 profile문서의 항목 uids에 저장합니다.
async addGoogleAccount({ dispatch, state }) {
try {
const provider = new GoogleAuthProvider();
// 이 메서드는 onAuthStateChanged를 호출한다.
// 이로 인해 상태 변수 user가 변경된다.
const { user } = await signInWithPopup(auth, provider);
try {
// 이미 연동된 회원의 경우 알림 메시지 출력한다.
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);
}
},
// 구글 계정의 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);
}
},
구글 로그인
signInWithPopup 메서드로 로그인을 하여
받은 user의 uid로 로그인 한 사용자의 profile 문서를 찾아
있으면 로그인 설정을 합니다.
// 구글 계정으로 로그인
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);
}
},
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 로드도 포함되어 있다.
dispatch('fetchProfiles');
} catch (error) {
alert("Failed to register user. : " + error);
}
},
// 기존의 계정 설정 정보를 수정 한다.
async updateProfile({ dispatch, commit, state }, profile) {
try {
commit('setLoading', true);
const userDoc = doc(db, "profiles", state.profile.id);
await updateDoc(userDoc, profile);
// 전체 계정 설정 정보를 다시 로드한다.
// 개별 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));
if(profile)
dispatch("fetchProfile", profile.id);
}
} catch (error) {
console.log('fetchProfiles : ' + error.message);
}
},
// --로그인
async login({ commit, dispatch }, { email, password }) {
try {
const userCredential = await signInWithEmailAndPassword(auth, email, password);
const user = userCredential.user;
commit("setUser", user);
// 계정 uid로 profile 문서를 구한다.
const profile = state.profiles.find(profile => profile.uids && profile.uids.includes(user.uid));
if(profile)
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
};
'토이 프로젝트 - Vue, Firebase로 서버리스 PWA 개발' 카테고리의 다른 글
14. Vue(with Vuetify)와 Firebase로 서버리스 PWA myBlog 개발 - 블로그 글쓰기 (0) | 2025.02.14 |
---|---|
13. Vue(with Vuetify)와 Firebase로 서버리스 PWA myBlog 개발 - 블로그 카테고리 (0) | 2025.02.14 |
11. Vue(with Vuetify)와 Firebase로 서버리스 PWA myBlog 개발 - 계정 설정 (0) | 2025.02.11 |
10. Vue(with Vuetify)와 Firebase로 서버리스 PWA myBlog 개발 - Firestore (0) | 2025.02.11 |
9. Vue(with Vuetify)와 Firebase로 서버리스 PWA myBlog 개발 - 로그인 구현 (0) | 2025.02.09 |