카테고리 없음

COW-CAMPUS-CONNECT. 전역 상태 관리 + Protected Route + AuthStore 리팩토링 기록

Frisbeen 2025. 9. 30. 21:39

 

 

 

 

이번 학기 전공수업 데이터베이스프로젝트 팀 프로젝트로 제가 동아리박람회때 혼자했던 프로젝트가 선정이 되어서 종합적으로 리펙토링이 필요했습니다.

 

먼저, 프론트엔드 인증 흐름을 개선하기 위해 Zustand 전역 상태 관리 + ProtectedRoute 도입 + AuthStore 리팩토링을 진행했습니다.


1. 문제 상황

  • 로그인 후 상태가 유지되지 않아 새로고침 시 인증 정보가 사라짐.
  • 로그인 없이도 특정 페이지 접근이 가능했음.
  • 로그인/회원가입 플로우가 복잡해지면서 상태 관리와 라우팅 분리가 필요.

2. Zustand 전역 상태 구축

2.1 AuthStore 정의

import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { login as apiLogin } from '../api/api';

export type AuthUser = { id: number; name: string; gender?: string };

type AuthState = {
  user: AuthUser | null;
  loading: boolean;
  login: (p: { id: number; name: string }) => Promise<void>;
  logout: () => void;
};

export const useAuthStore = create<AuthState>()(
  persist(
    (set) => ({
      user: null,
      loading: false,
      login: async ({ id, name }) => {
        set({ loading: true });
        try {
          const u = await apiLogin({ id, name });
          set({
            user: { id: u?.id ?? id, name: u?.name ?? name, gender: u?.gender },
            loading: false,
          });
        } catch (e) {
          set({ loading: false });
          throw e;
        }
      },
      logout: () => set({ user: null }),
    }),
    {
      name: 'cow-auth',
      storage: createJSONStorage(() => sessionStorage),
      partialize: (s) => ({ user: s.user }),
    }
  )
);

2.2 핵심 포인트

  • persist: Zustand 상태를 sessionStorage에 저장하여 새로고침에도 유지.
    • localStorage 대신 sessionStorage를 쓴 이유: 탭 단위 인증 유지.
  • partialize: 저장할 데이터 필드를 제한 (user만 저장, loading은 제외).
  • set 함수:
    • set({ key: value }) → 상태 병합 업데이트.
    • 비동기 액션(login)에서 먼저 loading: true → API 호출 성공 시 user 업데이트 → loading: false.

 정리: AuthStore는 **“현재 로그인 사용자 정보 + 인증 관련 액션”**만을 전역으로 관리.


3. ProtectedRoute 구현

로그인이 안되어있을 시, 허용하지 않는 페이지으로의 라우팅 보호 로직을 추가했습니다.

3.1 코드

import { Navigate, useLocation } from 'react-router-dom';
import { useAuthStore } from '../stores/auth';

export default function ProtectedRoute({ children, role, redirectTo }: any) {
  const user = useAuthStore((s) => s.user);
  const location = useLocation();

  if (!user) {
    const target = redirectTo ?? location.pathname + location.search;
    const loginPath = role
      ? `/login?role=${encodeURIComponent(role)}&next=${encodeURIComponent(target)}`
      : `/login?next=${encodeURIComponent(target)}`;

    return <Navigate to={loginPath} replace state={{ toast: 'auth_required' }} />;
  }

  return children;
}

3.2 기능 요약

  • user가 없으면 → 로그인 페이지로 강제 리다이렉트.
  • 원래 가려던 URL(next)을 쿼리에 담아두어 UX 보존.
  • state.toast = 'auth_required' → 로그인 페이지에서 토스트로 “로그인이 필요한 서비스입니다!” 안내.

4. Login 컴포넌트 리팩토링

  • 기존에는 api.ts login 호출 결과를 바로 사용 → 상태 관리 분리 안 됨.
  • 리팩토링 후:
    • useAuthStore().login() 호출.
    • 성공 시 user가 전역 상태에 반영됨.
    • 토스트 띄우고 홈 화면에서 user.name으로 환영 메시지.

API 호출 로직은 스토어 액션에 넣고, 컴포넌트는 상태만 구독하도록 역할을 명확히 분리.


5. persist + set 깊게 이해하기

5.1 persist

  • 미들웨어.
  • 지정한 스토리지를 사용해 특정 필드(partialize)를 직렬화/복원.
  • 앱 시작 시 sessionStorage에서 불러와서 초기화.

5.2 set

  • Zustand의 상태 업데이트 핵심.
  • 화살표 함수 안에서 객체를 return 하여, 초기 스토어를 정의함.
    • 스토어 정의 시 초기값 + 액션을 한 객체로 리턴하기 위함.
  • 액션 내부에서는 set({ ... })만으로 업데이트 가능.

👉 즉, 스토어 정의 (set) => ({ ... }) 구조, 액션 내부 set() 호출.


6. 요약

  • 전역 Auth 상태를 구축하여 로그인/로그아웃 흐름 일관성 확보.
  • sessionStorage 기반 persist로 새로고침에도 로그인 유지.
  • ProtectedRoute로 미인증 접근 차단.
  • Login 리팩토링으로 API와 상태 관리 책임 분리.
  • 홈 화면에서 사용자 이름을 활용해 개인화된 환영 메시지 준비.