Firebase Authentication을 통한 로그인을 구현하고 나니, 인증 상태와 유저 정보를 모든 페이지에서 쉽게 접근하여 사용하고 싶다는 생각이 들었습니다.
먼저 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);
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>
);
};
커스텀 훅을 작성해 사용하기 좀 더 편리하게 해주겠습니다.
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;
};