목표
Google 팝업 로그인을 구현하고, Google Auth의 상태와 profile(firebase db유저)을 분리해
전역으로 관리하는 패턴을 소개합니다. 또한 하나의 헤더 버튼으로 로그인 / 로그아웃을 토글하는 UX까지 다룹니다.
1. 왜 Firebase Auth인가?
기본적으로 이 프로젝트는 백엔드가 없는 서버리스 프로젝트라 firebase서비스에 포함된 Firebase Authentication을 이용하게 되었습니다. 게다가 Firebase Authentication은 앱에서 사용자 인증시 필요한 백엔드 서비스와 쉬운 SDK를 제공하고 google, facebook 등의 인증이 지원되기 때문에 로그인 기능을 더욱 쉽게 구현할 수 있게 되어 있습니다.
2. 이 글의 범위
- google Auth와 블로그에서 필요한 Profile의 분리
- popup 기반의 google 로그인 기능
- 유저 정보의 전역 상태관리 ( context )
- 헤더 토글 버튼
3. 사용 스택
- React + Vite + TypeScript
- Firebase
- module.css
전체 아키텍처, 플로우 한눈에 보기
[Header: 로그인 버튼]
│ click (isLoginOpen = true)
▼
[LoginModal] --handleGoogleLogin--> [signInWithGoogle()]
│
├─ signInWithPopup(auth, Google)
│ ( 구글 인스턴스 생성 후 로그인, Auth생성 )
▼
[onAuthStateChanged(auth)]
│ setAuthUser(user)
├─ getOrCreateUserProfile(user, OWNER_UID)
│ └─ users/{uid} => Profile생성, role 계산
└─ loading=false
│
▼
[subscribeUserProfile(uid)] -- onSnapshot -->
│ setProfile(profile) => AuthContext
│ context에서 Auth구독 후 context에 저장
▼
[Header] re-render → authUser 존재 → “로그아웃” 노출
- 로그인 UI 구현
- Header의 로그인 버튼 클릭 → 로그인 유형 선택 Modal → 로그인 방법 선택 후 실행
- Firebase의 Authentication 인증 기능을 사용하여 로그인 구현 ( Firebase의 Auth 설정 )
- 로그인 기능 실행
- 구글 로그인 API 실행 signInWithGoogle() → 유저의 정보 받음 ⇒ Auth
- 현재 서비스에 필요한 유저 정보 DB에 저장 getOrCreateUserProfile() ⇒ profile
- 로그인 유지 및 전역에서 유저 데이터 사용
- 유저 정보 ( auth/ profile ) 를 구독하여 전역에서 사용할 수 있도록 함
- 위 내용을 기반으로 context 생성 ( Auth, Profile, isLoading 등 상태 저장 )
- 각 컴포넌트에서 hooks을 사용해 유저 정보 사용
구현 설명
1. Firebase 초기화
.env 파일에 firebase config 값 저장해 사용
// <https://firebase.google.com/docs/web/setup#available-libraries>
import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";
import { getAuth } from "firebase/auth";
// Your web app's Firebase configuration
const firebaseConfig = {
apiKey: import.meta.env.VITE_FIREBASE_apiKey,
authDomain: import.meta.env.VITE_FIREBASE_authDomain,
databaseURL: import.meta.env.VITE_FIREBASE_databaseURL,
projectId: import.meta.env.VITE_FIREBASE_projectId,
storageBucket: import.meta.env.VITE_FIREBASE_storageBucket,
messagingSenderId: import.meta.env.VITE_FIREBASE_messagingSenderId,
appId: import.meta.env.VITE_FIREBASE_appId
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
const auth = getAuth(app);
export { db, auth };
2. 로그인 서비스 함수
firebase의 Auth로 로그인 실행, DB에 user profile 저장
// Header.tsx
export const Header = () => {
const { profile,authUser } = useAuth();
const [isLoginOpen, setIsLoginOpen] = useState(false);
return (
{authUser ? (
<button onClick={handleLogout}>로그아웃</button>
) : (
<button onClick={() => setIsLoginOpen(true)}>로그인</button>
)}
{isLoginOpen && <LoginModal onClose={() => setIsLoginOpen(false)} />}
)
}
// LoginModal.tsx
export const LoginModal = ({ onClose }: { onClose: () => void }) => {
const handleGoogleLogin = async () => {
try {
const userProfile = await signInWithGoogle();
if (userProfile) {
alert(`환영합니다♥ ${userProfile.username}님!`);
}
onClose();
} catch (error) {
console.log(error);
alert("로그인 오류입니다.");
}
};
return (
<button onClick={handleGoogleLogin}>Google 로그인</button>
)
}
// google login ( Firebase Auth )
import { GoogleAuthProvider, signInWithPopup, signOut } from "firebase/auth";
import { auth } from "./firebase";
import { getOrCreateUserProfile } from "../apis/users";
const BLOG_OWNER_UID = import.meta.env.VITE_BLOG_OWNER_UID as string;
export const signInWithGoogle = async () => {
// google Provider인스턴스 생성
const provider = new GoogleAuthProvider();
const result = await signInWithPopup(auth, provider);
const googleUser = result.user;
const profile = await getOrCreateUserProfile(googleUser, BLOG_OWNER_UID);
return profile;
};
// DB에 유저 Profile 생성
export const getOrCreateUserProfile = async (
googleUser: FirebaseUser,
isOwnerUid: string
) => {
const ref = doc(db, USERS, googleUser.uid);
const snapshot = await getDoc(ref);
// db에 유저 프로필이 없으면 새로 생성
if (!snapshot.exists()) {
const baseUser = toProfile(googleUser.uid, googleUser);
const role = isOwnerUid && isOwnerUid === googleUser.uid ? "owner" : "user";
const userProfileData = {
...baseUser,
role,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
await setDoc(ref, userProfileData, { merge: true }); // merge: true는 기존 데이터에 덮어쓰지 않고 병합
console.log("firebase db 저장 데이터 ", userProfileData);
return userProfileData as UserProfile;
} else {
// 있으면 기존 프로필을 반환
return snapshot.data() as UserProfile;
}
};
3. context생성
// context > AuthProvider.tsx
import { createContext,ReactNode,useContext,useEffect,useMemo,useState} from "react";
import { onAuthStateChanged, User as FirebaseUser } from "firebase/auth";
import { auth } from "../service/firebase";
import { UserProfile } from "../types/user";
import { subscribeUserProfile } from "../apis/users";
type AuthContextType = {
authUser: FirebaseUser | null; // Firebase Auth User (토큰/UID 등 필요할 때)
profile: UserProfile | null; // 서비스용 Profile (전역 UI는 이걸 쓰자)
loading: boolean;
};
export const AuthContext = createContext<AuthContextType>({
authUser: null, profile: null, loading: true
});
export const AuthProvider = ({ children }: { children: ReactNode }) => {
const [authUser, setAuthUser] = useState<FirebaseUser | null>(null);
const [profile, setProfile] = useState<UserProfile | null>(null);
const [loading, setLoading] = useState(true);
// 1) Firebase Auth 상태 구독
useEffect(() => {
const unsub = onAuthStateChanged(auth, (current) => {
setAuthUser(current);
setLoading(false);
});
return () => unsub();
}, []);
// 2) authUser가 있으면 Firestore Profile 실시간 구독
useEffect(() => {
if (!authUser) {
setProfile(null);
return;
};
subscribeUserProfile(authUser.uid, setProfile);
return;
}, [authUser]);
const value = useMemo(
() => ({ authUser, profile, loading }),
[authUser, profile, loading]
);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};
4. useAuth() hooks 생성
// hooks > useAuth.ts
import { useContext } from "react";
import { AuthContext } from "../contexts/AauthProvider";
export const useAuth = () => {
const userContext = useContext(AuthContext);
if (!userContext) throw new Error("userContext가 없습니다");
return userContext;
};
5. useAuth() 를 통해 전역에서 profile 사용하기
export const Header = () => {
**const { profile,authUser } = useAuth();**
return (
{ authUser ? <button>로그아웃</button> : <button>로그인</button> }
)
}
다음 포스팅 예고
이번 포스팅에서는 인증과 서비스에서 사용할 유저 프로필을 분리하는 기능을 중심으로 로그인 흐름을 구현하였습니다. 다음 포스팅에서는 vercel로 프로젝트를 배포하고, 그 과정에서 발생한 에러를 해결하는 내용을 담아보겠습니다.
'개발공부_Blog > Project' 카테고리의 다른 글
| 하드코딩 된 카테고리, 중앙 관리로 리팩토링[2] (0) | 2025.09.18 |
|---|---|
| 하드코딩 된 라우트 경로, 중앙 관리로 리팩토링[1] (0) | 2025.09.18 |
| Vercel로 [ Vite + React + Firebase ] 프로젝트 배포하기 (4) | 2025.08.17 |
| React를 사용해서 만든 서비스를 어플로 다운 받을 수 있다고? ( PWA로 프로젝트 하기 ) (0) | 2024.12.11 |
| 프로젝트를 할거야. React와 Vite로. (1) | 2024.12.10 |
댓글