- 프로젝트: SNU FESTIVAL
- 키워드: firebase auth, oauth, redux, redux persist, session storage
- 상황
- 로그인 기능은 firebase auth 에서 제공하는 google login을 사용한다.(학교 메일과 Gmail이 연동돼있어 사용하기 쉽다.)
- 웹사이트 안에서 로그인 정보(email, name, firebase uid)를 이곳저곳에서 사용해야하므로 redux에 저장한다.
- 로그인을 할 때, 사용자 정보는 firebase auth instance와 redux에 동일하게 저장되어야한다.
- 로그아웃을 할 때, firebase auth instance와 redux에 저장되어있던 사용자 정보가 사라져야한다.
- 보안을 위해 브라우저 창을 닫을 때에도 사용자 정보를 제거하여 로그아웃과 같은 효과를 내야한다.
- 해결과정
- 사용자 정보가 바뀌는 상황을 크게 3가지로 나눌 수 있다. 로그인할 때, 로그아웃할 때, 창이 꺼졌을 때.
- 먼저 로그인은 firebase auth에서 제공하는 GoogleAuthProvider 와 signInWithRedirect API 로 구현하였다.
(Persistence.SESSION 에 대한 내용은 뒤로 미룬다.)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]);
signInWithRedirect 에서 정상적으로 구글 로그인에 성공하면, 'auth' instance 의 상태가 변화한다. - 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]);
- 로그아웃도 firebase auth의 signOut 함수를 사용하여 간단하게 구현한다.
이때, reset action을 통해 redux의 유저 정보도 명시적으로 제거한다.const signOut = useCallback(async () => { dispatch(actions.setLoading(true)); dispatch(actions.reset()); await auth.signOut(); }, [dispatch]);
- 완성된 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;
- 사용자가 로그인/로그아웃 버튼을 누르지 않더라도, 브라우저 창이 꺼지면 로그인이 풀리도록 구현하고자 했다.
먼저 2번 과정에서 Persistence.SESSION을 통해 firebase auth instance의 지속성을 session 으로 설정했다. (https://firebase.google.com/docs/auth/web/auth-state-persistence#supported_types_of_auth_state_persistence)
redux도 마찬가지로 session storage를 사용하도록 redux-persist 옵션값을 설정했다.await auth.setPersistence(firebase.auth.Auth.Persistence.SESSION);
이제 브라우저 창을 닫으면 firebase auth instance와 redux storage가 모두 비워진다!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);
- 의의
- 서로 다른 객체에 담긴 정보를 동기화하는 작업을 수행해보았다.
- useAuth라는 custom hook을 만들어 어려운 로그인 로직은 숨기고, signIn/signOut 함수만 간단하게 import해 사용할 수 있도록했다.
'프론트엔드 > React' 카테고리의 다른 글
선언형 Portal, Modal 컴포넌트 구현하기 (0) | 2022.02.15 |
---|---|
Toast 컴포넌트 구현하기 (1) | 2022.01.26 |
리액트에서 달팽이 모양으로 움직이는 애니메이션 만들기 (0) | 2022.01.19 |
SVGR id collision issue 해결하기 (0) | 2021.11.30 |
useSetRecoilState로 렌더링 최적화하기 (0) | 2021.11.15 |