프로젝트를 진행하면서 Firebase의 Authentication를 이용한 email,password 구현했고 나중에 참고하고자 firebase 로직들을 정리해두려고 한다.
우선 Router에서 isAuthEnticated(로그인이 되었는지 아닌지 판별하는 state)를 props로 받아 분기처리를 했고
// Router.tsx
import { Routes, Route, Navigate } from "react-router-dom";
import MainPage from "pages/main";
import PostList from "pages/posts";
import PostDetail from "pages/posts/detail";
import PostCreate from "pages/posts/create";
import PostEdit from "pages/posts/edit";
import Profile from "pages/profile";
import LoginPage from "pages/login";
import SignupPage from "pages/signup";
interface RouterProps {
isAuthEnticated: boolean;
}
export default function Router({ isAuthEnticated }: RouterProps) {
return (
<Routes>
{isAuthEnticated ? (
<>
<Route path="/" element={<MainPage />} />
<Route path="/posts" element={<PostList />} />
<Route path="/posts/:id" element={<PostDetail />} />
<Route path="/posts/create" element={<PostCreate />} />
<Route path="/posts/edit/:id" element={<PostEdit />} />
<Route path="/profile" element={<Profile />} />
<Route path="*" element={<Navigate replace to="/" />} />
</>
) : (
<>
<Route path="/login" element={<LoginPage />} />
<Route path="/signup" element={<SignupPage />} />
<Route path="*" element={<LoginPage />} />
</>
)}
</Routes>
);
}
App에서는 getAuth
함수를 이용해 isAuthEnticated 값을 초기화한다.
import { useEffect, useState } from "react";
import { app } from "firebaseApp";
import { getAuth, onAuthStateChanged } from "firebase/auth";
import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import Router from "./components/Router";
import Loading from "components/Loading";
function App() {
const auth = getAuth(app);
// auth를 체크하지 전에 (initialize 전)에는 loader를 띄워주는 용도(로그인이 되어있고 새로고침 시 router가 인식하지 못해 login페이지를 보여줬다가 main으로 가지는 이슈)
const [init, setInit] = useState<boolean>(false);
// auth의 currentUser가 있으면 true 없으면 false
const [isAuthEnticated, setIsAuthEnticated] = useState<boolean>(
!!auth?.currentUser
);
useEffect(() => {
onAuthStateChanged(auth, (user) => {
if (user) {
setIsAuthEnticated(true);
} else {
setIsAuthEnticated(false);
}
setInit(true);
});
}, [auth]);
return (
<>
<ToastContainer />
{init ? <Router isAuthEnticated={isAuthEnticated} /> : <Loading />}
</>
);
}
export default App;
콘솔로 auth를 찍어보면 수많은 프로퍼티 중에 currentUser가 있고 !!를 이용해 값을 판별한다.
firebaseApp.ts 에서는 app을 지역변수로 export하고 try,catch문을 이용해 initializeApp을 한다. 왜냐면 import 할 때마다 initialize를 하면 비효율적이기 때문이다.
import { initializeApp, FirebaseApp, getApp } from "firebase/app";
import "firebase/auth";
// 지역변수로 선언후 try,catch로 할당
export let app: FirebaseApp;
// Your web app's Firebase configuration
const firebaseConfig = {
apiKey: process.env.REACT_APP_API_KEY,
authDomain: process.env.REACT_APP_AUTH_DOMATIN,
projectId: process.env.REACT_APP_PROJECT_ID,
storageBucket: process.env.REACT_APP_STORAGE_BUCKET,
messagingSenderId: process.env.REACT_APP_MESSAGING_SENTER_ID,
appId: process.env.REACT_APP_APP_ID,
};
try {
app = getApp("app");
} catch (error) {
app = initializeApp(firebaseConfig, "app");
}
// Initialize Firebase
const firebase = initializeApp(firebaseConfig);
export default firebase;
// auth의 currentUser가 있으면 true 없으면 false
const [isAuthEnticated, setIsAuthEnticated] = useState<boolean>(
!!auth?.currentUser
);
useEffect(() => {
onAuthStateChanged(auth, (user) => {
if (user) {
setIsAuthEnticated(true);
} else {
setIsAuthEnticated(false);
}
setInit(true);
});
}, [auth]);
// loginForm
import { Link, useNavigate } from "react-router-dom";
import "../styles/components/Form.style.css";
import { useState } from "react";
import { app } from "firebaseApp";
import { toast } from "react-toastify";
import { getAuth, signInWithEmailAndPassword } from "firebase/auth";
import Loading from "./Loading";
interface LoginState {
email: string;
password: string;
}
export default function LoginForm() {
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string>("");
const [loginInfo, setLoginInfo] = useState<LoginState>({
email: "",
password: "",
});
const validRegex =
/^[0-9a-zA-Z]([-_\.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_\.]?[0-9a-zA-Z])*\.[a-zA-Z]{2,3}$/i;
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const {
target: { name, value },
} = e;
if (name === "email") {
if (!value?.match(validRegex)) {
setError("이메일 형식이 올바르지 않습니다.");
} else {
setError("");
}
}
if (name === "password") {
if (value?.length < 8) {
setError("비밀번호는 8자리 이상으로 입력해주세요");
} else {
setError("");
}
}
setLoginInfo({ ...loginInfo, [name]: value });
};
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setLoading(true);
try {
const auth = getAuth(app);
await signInWithEmailAndPassword(
auth,
loginInfo.email,
loginInfo.password
);
toast.success("로그인에 성공했습니다.");
setLoginInfo({
email: "",
password: "",
});
} catch (error: any) {
toast.error(error?.code);
} finally {
setLoading(false);
}
};
if (loading) return <Loading />;
return (
<form onSubmit={onSubmit} className="form form--lg">
<h1 className="form__title">로그인</h1>
<div className="form__block">
<span className="email">이메일</span>
<input
type="email"
name="email"
id="email"
required
autoComplete="off"
value={loginInfo.email}
onChange={onChange}
/>
</div>
<div className="form__block">
<span className="password">비밀번호</span>
<input
type="password"
name="password"
id="password"
required
autoComplete="off"
value={loginInfo.password}
onChange={onChange}
/>
</div>
{error && error?.length > 0 && (
<div className="form__block">
<div className="form__error">{error}</div>
</div>
)}
<div className="form__block">
계정이 없으신가요?
<Link to="/signup" className="form__link">
회원가입하기
</Link>
</div>
<div className="form__block">
<input
type="submit"
value="로그인"
className="form__btn--submit"
disabled={error?.length > 0}
/>
</div>
</form>
);
}
// signup form
import { useState } from "react";
import { Link } from "react-router-dom";
import { app } from "firebaseApp";
import { getAuth, createUserWithEmailAndPassword } from "firebase/auth";
import { toast } from "react-toastify";
import Loading from "./Loading";
interface InfoState {
email: string;
password: string;
password_confirm: string;
}
export default function SignupForm() {
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string>("");
const [signupInfo, setSignupInfo] = useState<InfoState>({
email: "",
password: "",
password_confirm: "",
});
const validRegex =
/^[0-9a-zA-Z]([-_\.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_\.]?[0-9a-zA-Z])*\.[a-zA-Z]{2,3}$/i;
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
// const { name, value } = e.target;
const {
target: { name, value },
} = e;
if (name === "email") {
if (!value?.match(validRegex)) {
setError("이메일 형식이 올바르지 않습니다.");
} else {
setError("");
}
}
if (name === "password") {
if (value?.length < 8) {
setError("비밀번호는 8자리 이상으로 입력해주세요");
} else if (
signupInfo.password_confirm?.length > 0 &&
value !== signupInfo.password_confirm
) {
setError("비밀번호와 비밀번호 확인 값이 다릅니다. 다시 확인해주세요.");
} else {
setError("");
}
}
if (name === "password_confirm") {
if (value?.length < 8) {
setError("비밀번호는 8자리 이상으로 입력해주세요");
} else if (value !== signupInfo.password) {
setError("비밀번호와 비밀번호 확인 값이 다릅니다. 다시 확인해주세요.");
} else {
setError("");
}
}
setSignupInfo({ ...signupInfo, [name]: value });
};
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setLoading(true);
try {
const auth = getAuth(app);
await createUserWithEmailAndPassword(
auth,
signupInfo.email,
signupInfo.password
);
toast.success("회원가입에 성공했습니다.");
setSignupInfo({
email: "",
password: "",
password_confirm: "",
});
} catch (error: any) {
toast.error(error?.code);
} finally {
setLoading(false);
}
};
if (loading) return <Loading />;
return (
<form onSubmit={onSubmit} className="form form--lg">
<h1 className="form__title">회원가입</h1>
<div className="form__block">
<span className="email">이메일</span>
<input
type="email"
name="email"
id="email"
required
onChange={onChange}
autoComplete="off"
value={signupInfo.email}
/>
</div>
<div className="form__block">
<span className="password">비밀번호</span>
<input
type="password"
name="password"
id="password"
required
onChange={onChange}
autoComplete="off"
value={signupInfo.password}
/>
</div>
<div className="form__block">
<span className="password_confirm">비밀번호 확인</span>
<input
type="password"
name="password_confirm"
id="password_confirm"
required
onChange={onChange}
autoComplete="off"
value={signupInfo.password_confirm}
/>
</div>
{error && error?.length > 0 && (
<div className="form__block">
<div className="form__error">{error}</div>
</div>
)}
<div className="form__block">
계정이 이미 있으신가요?
<Link to="/login" className="form__link">
로그인하기
</Link>
</div>
<div className="form__block">
<input
type="submit"
value="로그인"
className="form__btn--submit"
disabled={error?.length > 0}
/>
</div>
</form>
);
}
signOut 함수를 이용해서 logout
import "../styles/components/Profile.style.css";
import { getAuth, signOut } from "firebase/auth";
import { app } from "firebaseApp";
import { toast } from "react-toastify";
export default function Profile() {
const auth = getAuth(app);
const onsignOut = async () => {
try {
const auth = getAuth(app);
await signOut(auth);
toast.success("로그아웃 되었습니다.");
} catch (error: any) {
toast.error(error?.code);
}
};
return (
<div className="profile__box">
<div className="flex__box-lg">
<div className="profile__image" />
<div>
<div className="profile__email">{auth?.currentUser?.email}</div>
<div className="profile__name">
{auth?.currentUser?.displayName ||
auth?.currentUser?.email?.split("@")[0]}
</div>
</div>
</div>
<div role="presentation" className="profile__logout" onClick={onsignOut}>
로그아웃
</div>
</div>
);
}