React 19.0.0
TypeScript 5.7.3
Firebase 11.3.1
1️⃣ 파이어베이스
에 접속하여 프로젝트를 생성하고, React에 Firebase SDK를 추가하기 위해 Firebase를 설치한다.
npm i firebase
2️⃣ src
폴더 안에 firebase.tsx
파일을 생성하여 아래 내용을 넣어준다. (프로젝트 설정 탭에서 복사할 수 있다.)
import { initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';
import { getFirestore } from 'firebase/firestore';
interface ImportMetaEnv {
VITE_FIREBASE_APP_KEY: string;
VITE_FIREBASE_AUTH_DOMAIN: string;
VITE_FIREBASE_PROJECT_ID: string;
VITE_FIREBASE_STORAGE_BUCKET: string;
VITE_FIREBASE_MESSAGING_SENDER_ID: string;
VITE_FIREBASE_APP_ID: string;
VITE_FIREBASE_MEASUREMENTID: string;
}
declare global {
interface ImportMeta {
readonly env: ImportMetaEnv;
}
}
const firebaseConfig = {
apiKey: import.meta.env.VITE_FIREBASE_APP_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,
measurementId: import.meta.env.VITE_FIREBASE_MEASUREMENTID,
};
// Initialize Firebase
export const app = initializeApp(firebaseConfig);
export const auth = getAuth(app); // 인증 객체
export const db = getFirestore(app); // 데이터베이스
1️⃣ 회원가입 기능을 구현하기 위해 Authentication
- 시작하기
를 클릭한다.
2️⃣ 로그인 방법을 추가하라는 창이 뜰텐데, 나는 이메일/비밀번호
를 추가하여 사용 설정을 활성화해줬다.
파이어베이스 공식 문서를 보면, 신규 사용자 가입의 경우 createUserWithEmailAndPassword
메서드를 제공하고 있다고 한다.
실제 작성한 코드를 기반으로 설명을 해보자면,
(참고로, 나는 react-hook-form
라이브러리를 사용하여 form을 구현하였다.)
import { FirebaseError } from 'firebase/app';
import { auth } from '@/firebase';
import { createUserWithEmailAndPassword } from 'firebase/auth';
const handleSignUp = async (data: SignUpType) => {
try {
await createUserWithEmailAndPassword(
auth,
data.email,
data.password
);
navigate('/');
} catch (error) {
const firebaseError = error as FirebaseError;
switch (firebaseError.code) {
case 'auth/email-already-in-use':
setError('email', { message: '이미 가입된 이메일입니다.' });
break;
case 'auth/invalid-email':
setError('email', { message: '이메일 형식이 잘못되었습니다.' });
break;
case 'auth/weak-password':
setError('password', {
message: '비밀번호를 6자 이상 입력해 주세요.',
});
break;
default:
setError('email', {
message: '회원가입 중 문제가 발생했습니다. 다시 시도해 주세요.',
});
break;
}
}
};
파이어베이스의 인증 객체를 가져오기 위해 firebase.tsx
에서 getAuth
메서드를 사용하여 export 해준 auth
를 가져온다.
export const auth = getAuth(app);
입력받은 사용자의 이메일 주소와 비밀번호를 createUserWithEmailAndPassword
메서드의 인자로 넣어주어 유저를 생성한다.
await createUserWithEmailAndPassword(
auth,
data.email,
data.password
);
함수가 비동기로 실행되게 만들고, 회원가입 시에 발생할 수 있는 오류를 처리하기 위해 try-catch
구문을 사용하였다. 파이어베이스에서는 사용자가 입력한 이메일 주소와 비밀번호의 유효성을 검사하여 적절한 message를 제공해주고 있다. (자세한 오류 코드 목록은 여기서 확인할 수 있다.)
폼을 제출하게 되면 사용자가 생성되어 Authentication - 사용자
탭에 정상적으로 저장이 된 것을 확인할 수 있다!
💡 여기서 추가로 알고 있어야 할 점은, Firebase에서는 사용자가 회원가입을 하면 해당 사용자로 자동 로그인이 된다는 것이다. 즉, 회원가입 직후 추가적인 로그인 절차 없이도 사용자가 인증된 상태로 앱을 이용할 수 있도록 설계되어 있다.
파이어베이스에서는 사용자의 표시 이름 및 프로필 사진 URL 등의 기본 프로필 정보를 업데이트할 수 있도록 updateProfile
메서드를 제공해준다. 이를 통해 사용자 정보를 업데이트할 수 있다.
await createUserWithEmailAndPassword(
auth,
data.email,
data.password
);
if (createdUser.user) {
await updateProfile(auth.currentUser, { displayName: "Jane Q. User", photoURL: "https://example.com/jane-q-user/profile.jpg"});
}
(currentUset
속성을 통해 현재 로그인한 사용자 정보를 가져올 수 있다. 사용자가 로그인 상태가 아니라면 currentUser
값이 null이다.)
위처럼 Firebase Authentication만 사용하여 회원가입을 진행하게 되면, 이메일/비밀번호
로만 회원가입이 이루어지기 때문에 이외에 커스텀한 정보들을 저장할 수 없게 된다. 😣
그래서,
Firestore Database를 통해 데이터베이스에 추가로 정보를 저장할 수 있다.
데이터베이스 시작하기
를 클릭하여 데이터베이스를 테스트모드에서 시작한다. (데이터베이스의 위치는 현재 위치에서 가장 가까운 곳을 선택하면 된다.)
내 프로젝트에서는 직책, 부서, 입사일 등의 정보를 특정 사용자마다 추가로 저장을 해줘야 했기에 사용자에게 입력받은 위 정보들을 데이터베이스에 setDoc
메서드를 통해 저장을 해주었다. (getDoc
, addDoc
, updateDoc
, deleteDoc
등 CRUD 관련 메서드는 파이어베이스 공식 문서를 통해 더 자세히 확인해보자!)
try {
const createdUser = await createUserWithEmailAndPassword(
auth,
data.email,
data.password
);
if (createdUser.user) {
await updateProfile(createdUser.user, { displayName: data.name });
}
const user = createdUser.user;
const userData = {
name: user.displayName,
email: user.email,
position: data.position, // 직책
department: data.department, // 부서
joinDate: data.joinDate, // 입사일
};
// Firestore에 유저 정보 저장
await setDoc(doc(db, 'users', user.uid), userData);
navigate('/');
} catch (error) {
// 예외 처리
}
이번에도
Firestore Database - 데이터
탭에 정상적으로 저장이 된 것을 확인할 수 있다!
이제 회원가입을 마쳤으니, 로그인을 진행해보자.
로그인의 경우 signInWithEmailAndPassword
메서드를 통해 로그인을 진행할 수 있다고 한다.
이번에도 실제 작성한 코드를 기반으로 설명해보자면,
try {
await signInWithEmailAndPassword(
auth,
data.email,
data.password
);
navigate('/');
} catch (error) {
try-catch
구문을 사용하였고, 입력받은 사용자의 이메일 주소와 비밀번호를 signInWithEmailAndPassword
메서드의 인자로 넣어주어 로그인을 진행한다.그렇다면 로그인된 유저의 상태는 언제까지 유효할까? 🤔
Firebase는 기본적으로 사용자가 명시적으로 로그아웃하지 않는 한, 인증 상태를 지속적으로 유지한다고 한다. 이는 사용자가 브라우저를 닫거나 페이지를 새로고침해도 로그인 상태가 유지됨을 의미한다. 이러한 동작은 Firebase의 인증 상태 지속성 설정에 따라 관리되며, 기본 설정은 local
로, 영구적으로 인증 상태를 유지한다. 따라서, 로그아웃을 통해서만 로그인 상태가 해제된다.
(로그아웃은 auth.signOut()
로 아주 쉽게 처리할 수 있다.)
추가로, onAuthStateChanged
메서드를 통해 로그인 상태 변경을 감지하여 로그인 접근 제한 로직을 구현하였다.
import { JSX, ReactNode, useEffect, useState } from 'react';
import { Navigate } from 'react-router-dom';
import { onAuthStateChanged, User } from 'firebase/auth';
import { auth } from '@/firebase';
interface AuthRouteProps {
children: ReactNode;
type: 'protected' | 'public';
}
export const AuthRoute = ({ children, type }: AuthRouteProps): JSX.Element => {
const [isLoading, setIsLoading] = useState(true);
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (user) => {
setUser(user);
setIsLoading(false);
});
return () => unsubscribe();
}, []);
if (isLoading) {
return <div>Loading...</div>; // 임시 로딩
}
if (type === 'protected' && !user) {
return <Navigate to="/login" replace />;
}
if (type === 'public' && user) {
return <Navigate to="/" replace />;
}
return <>{children}</>;
};
type="protected"
→ 로그인된 유저만 접근 가능, 아니면 로그인 페이지로 이동type="public"
→ 로그인 안 한 유저만 접근 가능, 아니면 홈으로 이동 <Route
path="/login"
element={
<AuthRoute type="public">
<LoginPage />
</AuthRoute>
}
/>
<Route
path="/signup"
element={
<AuthRoute type="public">
<SignUpPage />
</AuthRoute>
}
/>
<Route
element={
<AuthRoute type="protected">
<NavBar />
</AuthRoute>
}
>
currentUser
속성을 사용하지 않고 왜onAuthStateChanged
를 사용했을까?
currentUser
와 onAuthStateChanged
모두 Firebase Authentication에서 사용자 정보를 가져오는 방법이지만, 사용하는 상황과 안전성에서 차이점이 있다.
auth.currentUser
는 현재 로그인된 사용자 정보를 가져오는 속성이다. 하지만, 즉시 값이 설정되지 않을 수도 있다고 한다. Firebase는 앱이 처음 로드될 때, 사용자의 인증 상태를 비동기적으로 복원하기 때문에, auth.currentUser
가 null일 수도 있다는 것이다.
그렇다면 언제 사용해야 할까?
사용자가 이미 로그인한 상태일 때 즉시 currentUser
를 사용해서 정보를 업데이트하는 경우이다.
예를 들어, 사용자가 계정을 생성한 후 바로 프로필을 업데이트할 때는 auth.currentUser
를 사용해도 문제가 없다.
하지만 앱이 처음 실행되었을 때 로그인 상태를 정확하게 감지하려면 onAuthStateChanged
를 사용하는 것이 안전하다. Firebase가 사용자 인증 상태를 비동기적으로 복원하는 과정이 끝난 후, 로그인 상태가 변할 때마다 감지하는 역할을 하기 때문이다.
✅ 사용하면 적절한 경우
onAuthStateChanged
는 Firebase가 인증 정보를 완전히 불러온 후 실행되므로, 로그인 상태가 정확하게 반영된다.
참고문헌
https://firebase.google.com/docs/auth?hl=ko
https://jjang-j.tistory.com/48