[React + Firebase] Context API를 통한 인증 상태 공유하기

Hadam Cho·2022년 11월 22일
1

Project

목록 보기
4/4
post-thumbnail

Firebase Authentication을 통한 로그인을 구현하고 나니, 인증 상태와 유저 정보를 모든 페이지에서 쉽게 접근하여 사용하고 싶다는 생각이 들었습니다.

Context 생성

먼저 src 폴더 아래에 contexts 폴더를 만들고 AuthContext.tsx 파일을 생성했습니다.

먼저 createContext로 context를 생성해 줍니다. 타입은 상황에 맞게 정의하시면 됩니다. 저는 Firebase Auth를 불러오는 동안 기다려주기 위해서 state를 두어 ❶ loading ❷ loaded ❸ error에 맞게 분기해 주었습니다. 또한 로드됐을 때 유저 정보가 없다면 로그인이 안 된 것이므로 isAuthentication을 통해 쉽게 확인할 수 있도록 구현했습니다.

import { createContext } from "react";
import { User } from "firebase/auth";

type AuthState =
  | { state: "loading" }
  | { state: "loaded"; isAuthentication: true; user: User }
  | { state: "loaded"; isAuthentication: false; user: null }
  | { state: "error"; error: Error };

const AuthStateContext = createContext<AuthState | undefined>(undefined);

Provider

Auth에 onAuthStateChanged observer를 등록하여 현재 로그인 한 사용자를 가져올 수 있습니다. 이전에 config 파일에서 불러온 auth 객체를 가져와 넘겨주고 onChange 함수를 구현해 줍니다.

onChange 함수가 실행되었다면 로드가 완료된 것이므로 state는 loaded로 설정해 주면 됩니다. 다만 유저의 경우 로그인이 되어있지 않다면 null이 주어지기 때문에 이를 확인하여 인증 여부와 유저 정보 상태를 설정합니다.

해당 Provider는 외부에서 사용할 것이므로 내보내 주어야 합니다.

import { createContext, useEffect, useState } from "react";
import { onAuthStateChanged, User } from "firebase/auth";
import { auth } from "../config";

...

export const AuthContextProvider = ({ children }: {
  children: React.ReactNode;
}) => {
  const [authState, setAuthState] = useState<AuthState>({
    state: "loading"
  });

  const onChange = (user: User | null) => {
    if (user) {
      setAuthState({ state: "loaded", isAuthentication: true, user });
    } else {
      setAuthState({ state: "loaded", isAuthentication: false, user });
    }
  };
  const setError = (error: Error) => {
    setAuthState({ state: "error", error });
  }

  useEffect(() => {
    const unsubscribe = onAuthStateChanged(auth, onChange, setError);
    return () => unsubscribe();
  }, []);

  return (
    <AuthStateContext.Provider value={authState}>
      {children}
    </AuthStateContext.Provider>
  );
};

Hook

커스텀 훅을 작성해 사용하기 좀 더 편리하게 해주겠습니다.

useContext를 이용해 상태를 가져와 반환하는 코드를 작성합니다. 만약 상태가 undefined라면 Provider로 감싸지 않고 해당 훅을 사용한 것이기 때문에 에러를 발생시킵니다.

import { createContext, useContext, useEffect, useState } from "react";

...

export const useAuthState = () => {
  const authState = useContext(AuthStateContext);
  if (!authState) throw new Error("AuthProvider not found");
  return authState;
};

사용

저는 main.tsx에서 AuthContextProvider를 불러와 감싸주었습니다. 이렇게 하면 자식 컴포넌트에서는 useAuthState를 통해 인증 정보에 쉽게 접근할 수 있습니다.

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
  <BrowserRouter>
    <AuthContextProvider>
      <Routes>
        ...
      </Routes>
    </AuthContextProvider>
  </BrowserRouter>
);

로그인 여부에 따라 페이지를 다르게 보여주기 위해서는 아래와 같이 코드를 작성하면 됩니다.

import { Navigate } from "react-router-dom";
import { useAuthState } from "../contexts/AuthContext";

...

const Home = () => {
  const auth = useAuthState();

  switch (auth.state) {
    case "loading":
      return <Loading.Full />;

    case "loaded":
      if (auth.isAuthentication) {
        return <div>Home</div>;
      } else {
        return <Navigate to="/login" replace />;
      }

    case "error":
      return <Error.Default />;
  }
};

export default Home;

전체 코드

import { onAuthStateChanged, User } from "firebase/auth";
import { createContext, useContext, useEffect, useState } from "react";
import { auth } from "../config";

type AuthState =
  | { state: "loading" }
  | { state: "loaded"; isAuthentication: true; user: User }
  | { state: "loaded"; isAuthentication: false; user: null }
  | { state: "error"; error: Error };

const AuthStateContext = createContext<AuthState | undefined>(undefined);

export const AuthContextProvider = ({
  children,
}: {
  children: React.ReactNode;
}) => {
  const [authState, setAuthState] = useState<AuthState>({ state: "loading" });

  const onChange = (user: User | null) => {
    if (user) {
      setAuthState({ state: "loaded", isAuthentication: true, user });
    } else {
      setAuthState({ state: "loaded", isAuthentication: false, user });
    }
  };
  const setError = (error: Error) => setAuthState({ state: "error", error });

  useEffect(() => {
    const unsubscribe = onAuthStateChanged(auth, onChange, setError);
    return () => unsubscribe();
  }, []);

  return (
    <AuthStateContext.Provider value={authState}>
      {children}
    </AuthStateContext.Provider>
  );
};

export const useAuthState = () => {
  const authState = useContext(AuthStateContext);
  if (!authState) throw new Error("AuthProvider not found");
  return authState;
};

참고

profile
(。・∀・)ノ゙

0개의 댓글