Firebase Authentication

심채운·2024년 2월 15일
0

기능

목록 보기
2/2

Firebase Auth

프로젝트를 진행하면서 Firebase의 Authentication를 이용한 email,password 구현했고 나중에 참고하고자 firebase 로직들을 정리해두려고 한다.

Router

우선 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

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가 있고 !!를 이용해 값을 판별한다.

firebase

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;

onAuthStateChanged

  • 인증 상태가 변경될 때마다 호출되는 리스터 설정(로그인, 로그아웃)
  • 사용자 객체를 인자로 받는 콜백 함수 등록
  • 로그인 상태일 때는 사용자의 정보를, 아니라면 null 리턴
// 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]);

form

// 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>
  );
}

logout

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>
  );
}
profile
불가능, 그것은 사실이 아니라 하나의 의견일 뿐이다. - 무하마드 알리

0개의 댓글