본문 바로가기

프론트엔드/React

Firebase auth 정보를 redux로 관리하기

  • 프로젝트: SNU FESTIVAL
  • 키워드: firebase auth, oauth, redux, redux persist, session storage
  • 상황
    1. 로그인 기능은 firebase auth 에서 제공하는 google login을 사용한다.(학교 메일과 Gmail이 연동돼있어 사용하기 쉽다.)
    2. 웹사이트 안에서 로그인 정보(email, name, firebase uid)를 이곳저곳에서 사용해야하므로 redux에 저장한다.
    3. 로그인을 할 때, 사용자 정보는 firebase auth instance와 redux에 동일하게 저장되어야한다.
    4. 로그아웃을 할 때, firebase auth instance와 redux에 저장되어있던 사용자 정보가 사라져야한다.
    5. 보안을 위해 브라우저 창을 닫을 때에도 사용자 정보를 제거하여 로그아웃과 같은 효과를 내야한다.
  • 해결과정
    1. 사용자 정보가 바뀌는 상황을 크게 3가지로 나눌 수 있다. 로그인할 때, 로그아웃할 때, 창이 꺼졌을 때.
    2. 먼저 로그인은 firebase auth에서 제공하는 GoogleAuthProvider 와 signInWithRedirect API 로 구현하였다.
        export const auth = firebase.auth();
      
      const signIn = useCallback(async () => {
        await auth.setPersistence(firebase.auth.Auth.Persistence.SESSION);
        const provider = new firebase.auth.GoogleAuthProvider();
      
        dispatch(actions.setLoading(true));
        try {
          await auth.signInWithRedirect(provider);
        } catch {
          toast('인터넷이 불안정합니다. 다시 시도해주세요.');
        }
      }, [dispatch]);
      (Persistence.SESSION 에 대한 내용은 뒤로 미룬다.)
      signInWithRedirect 에서 정상적으로 구글 로그인에 성공하면, 'auth' instance 의 상태가 변화한다.

    3. auth 상태가 변할 때 onAuthStateChanged observer를 이용해 유저 정보가 있다면(로그인할 때) redux에 유저 정보를 저장한다. 유저 정보가 없다면(로그아웃할 때) redux의 유저 정보를 null로 명시해서 버그를 사전에 방지한다.
      useEffect(() => {
        auth.onAuthStateChanged((currentUser) => {
          if (currentUser) {
            dispatch(actions.setValue('email', currentUser.email));
            dispatch(actions.setValue('uid', currentUser.uid));
          } else {
            dispatch(actions.setValue('email', null));
            dispatch(actions.setValue('uid', null));
          }
          dispatch(actions.setLoading(false));
        });
      }, [dispatch]);
    4. 로그아웃도 firebase auth의 signOut 함수를 사용하여 간단하게 구현한다.
        const signOut = useCallback(async () => {
        dispatch(actions.setLoading(true));
        dispatch(actions.reset());
        await auth.signOut();
      }, [dispatch]);
      이때, reset action을 통해 redux의 유저 정보도 명시적으로 제거한다.

    5. 완성된 useAuth hook 코드이다.
      import { useCallback, useEffect } from 'react';
      import { useDispatch } from 'react-redux';
      import { auth } from '@U/initializer/firebase';
      import { actions } from '@/redux/user/state';
      ...
      import firebase from 'firebase/app';
      ...
      
      const useAuth = () => {
        const dispatch = useDispatch();
      
        useEffect(() => {
          auth.onAuthStateChanged((currentUser) => {
            if (currentUser) {
              dispatch(actions.setValue('email', currentUser.email));
              dispatch(actions.setValue('uid', currentUser.uid));
            } else {
              dispatch(actions.setValue('email', null));
              dispatch(actions.setValue('uid', null));
            }
            dispatch(actions.setLoading(false));
          });
        }, [dispatch]);
      
        const signIn = useCallback(async () => {
          await auth.setPersistence(firebase.auth.Auth.Persistence.SESSION);
          const provider = new firebase.auth.GoogleAuthProvider();
      
          dispatch(actions.setLoading(true));
          try {
            await auth.signInWithRedirect(provider);
          } catch {
            toast('인터넷이 불안정합니다. 다시 시도해주세요.');
          }
        }, [dispatch]);
      
        const signOut = useCallback(async () => {
          dispatch(actions.setLoading(true));
          dispatch(actions.reset());
      
          try {
            await auth.signOut();
          } finally {
            ...
          }
        }, [dispatch]);
      
        return { signIn, signOut };
      };
      export default useAuth;
    6. 사용자가 로그인/로그아웃 버튼을 누르지 않더라도, 브라우저 창이 꺼지면 로그인이 풀리도록 구현하고자 했다.
      먼저 2번 과정에서 Persistence.SESSION을 통해 firebase auth instance의 지속성을 session 으로 설정했다. (https://firebase.google.com/docs/auth/web/auth-state-persistence#supported_types_of_auth_state_persistence)
        await auth.setPersistence(firebase.auth.Auth.Persistence.SESSION);​
       redux도 마찬가지로 session storage를 사용하도록 redux-persist 옵션값을 설정했다.
        import { persistStore, persistReducer } from 'redux-persist';
      import sessionStorage from 'redux-persist/lib/storage/session';
      
      const rootPersistConfig = {
        key: '',
        storage: sessionStorage,
        whitelist: [],
      };
      const persistedReducer = persistReducer(rootPersistConfig, reducer);​
      이제 브라우저 창을 닫으면 firebase auth instance와 redux storage가 모두 비워진다!

  • 의의
    1. 서로 다른 객체에 담긴 정보를 동기화하는 작업을 수행해보았다.
    2. useAuth라는 custom hook을 만들어 어려운 로그인 로직은 숨기고, signIn/signOut 함수만 간단하게 import해 사용할 수 있도록했다.