zustand/local storage/firebase 로그인 상태 동기화 설계

김가희·2024년 4월 22일

문제 상황

Firebase를 사용하여 사용자의 로그인 상태를 관리하는 시스템을 구현하였다. Zustand의 persist 미들웨어를 사용하여 사용자 상태를 Local Storage에 저장함으로써 페이지를 새로고침하거나 재방문해도 로그인 상태가 유지되게 했다.

그런데 테스트 중 사용자의 로그인 상태가 예상대로 동기화되지 않는 문제가 발생하였다.



문제 원인

  • 인증 토큰 갱신 실패: Firebase 인증 시스템은 사용자의 로그인 상태를 관리하기 위해 인증 토큰을 사용한다. 유지 시간은 한 시간으로, 이 토큰이 만료되거나 갱신에 실패하면 사용자가 로컬 스토리지 상으로는 로그인 상태임에도 불구하고 로그아웃으로 처리될 수 있다.


문제 해결

  • 인증 토큰 갱신 로직 강화: AuthStateObserver 컴포넌트는 Firebase의 인증 상태 변화를 감지하고, 사용자가 로그인할 때마다 Firebase Firestore에서 사용자 정보를 가져와 Zustand 상태에 업데이트한다. 이 과정은 비동기적으로 처리되며, 인증 토큰 갱신에 실패하거나 사용자 정보가 없는 경우 상태를 null로 설정하여 로그아웃 처리한다.
// @store/useUserStore.ts

import create from 'zustand';
import { persist } from 'zustand/middleware';

import { UserType } from '@/types/User';

type State = {
  user: UserType | null;
};

interface Action {
  setUser: (user: UserType | null) => void;
}

export const useUserStore = create<State & Action>()(
  persist(
    (set) => ({
      user: null,
      setUser: (user: UserType | null) => set(() => ({ user })),
    }),
    {
      name: 'user-store',
      getStorage: () => localStorage,
    },
  ),
);
// @components/auth/AuthStateObserver.tsx

import { useEffect } from 'react';
import { doc, getDoc } from 'firebase/firestore';

import { auth, db } from '@services/firebaseConfig';
import { useUserStore } from '@store/useUserStore';
import { UserType } from '@/types/User';

function AuthStateObserver() {
  const { setUser } = useUserStore();

  useEffect(() => {
    const unsubscribe = auth.onAuthStateChanged((user) => {
      if (user) {
        // 사용자가 로그인한 상태
        user
          .getIdToken()
          .then(async () => {
            const userRef = doc(db, 'users', user.uid);
            const userDoc = await getDoc(userRef);
            setUser(userDoc.data() as UserType);
          })
          .catch((error) => {
            console.error('Token renewal error:', error);
            setUser(null);
          });
      } else {
        setUser(null);
      }
    });

    return () => unsubscribe();
  }, [setUser]);

  return null;
}

export default AuthStateObserver;

0개의 댓글