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

김가희·2024년 4월 22일
0

문제 상황

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개의 댓글