이번 학기 전공수업 데이터베이스프로젝트 팀 프로젝트로 제가 동아리박람회때 혼자했던 프로젝트가 선정이 되어서 종합적으로 리펙토링이 필요했습니다.
먼저, 프론트엔드 인증 흐름을 개선하기 위해 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와 상태 관리 책임 분리.
- 홈 화면에서 사용자 이름을 활용해 개인화된 환영 메시지 준비.