37. Zustand를 사용한 인증 상태 관리
2026년 1월 19일
이게머니
Zustand를 사용한 인증 상태 관리
변경 배경
이전 방식의 문제점
- 각 컴포넌트마다
useState로 독립적인 인증 상태 관리 - 상태 동기화 어려움 (컴포넌트마다 다른 상태 인스턴스)
- 비-React 코드(Interceptor)에서 상태 업데이트 불가능
- 중복 코드 발생
Zustand 도입 목적
- 전역 상태로 모든 컴포넌트가 동일한 인증 상태 공유
- Interceptor에서 직접 상태 업데이트 가능
- 코드 일관성 및 유지보수성 향상
구현 구조
프로젝트는 3개의 파일로 인증 상태를 관리합니다:
┌─────────────────────┐
│ authStore.ts │ ← 전역 상태 저장소 (Zustand)
│ (상태 저장소) │
└──────────┬──────────┘
│
┌──────┴──────┐
│ │
┌───▼────┐ ┌────▼──────────┐
│useAuth │ │authInterceptor│
│(훅) │ │(API 요청 처리) │
└────────┘ └───────────────┘
파일 역할
- authStore.ts: Zustand store - 전역 인증 상태 저장
- useAuth.ts: React 훅 - 컴포넌트에서 사용하는 인터페이스
- authInterceptor.ts: API Interceptor - 토큰 갱신 시 store 업데이트
코드 설명
1. authStore.ts - 전역 상태 저장소
export const useAuthStore = create<AuthState>((set, get) => ({ // 상태 isAuthenticated: false, isLoading: true, accessToken: undefined, refreshToken: undefined, // 액션 setTokens: (accessToken?, refreshToken?) => { set({ accessToken, refreshToken, isAuthenticated: !!accessToken, // 토큰이 있으면 자동으로 인증됨 }); }, initialize: () => { // 쿠키에서 토큰 읽어서 초기화 const cookies = document.cookie.split(';'); const accessToken = cookies.find(...)?.split('=')[1]; set({ accessToken, isAuthenticated: !!accessToken, isLoading: false, // 초기화 완료 }); }, }));
- 모든 컴포넌트가 공유하는 단일 상태 인스턴스
setTokens(): 토큰 설정 시 인증 상태 자동 계산initialize(): 앱 시작 시 쿠키에서 토큰 읽기
2. useAuth.ts - 컴포넌트용 훅
export const useAuth = () => { const { isAuthenticated, isLoading, logout, ... } = useAuthStore(); // 초기화 useEffect(() => { initialize(); // 쿠키에서 토큰 읽기 }, []); // 쿠키 변경 감지 const { accessToken, refreshToken } = useTokenCookies(); useEffect(() => { useAuthStore.getState().setTokens(accessToken, refreshToken); }, [accessToken, refreshToken]); return { isAuthenticated, isLoading, logout, ... }; };
- 기존 인터페이스 유지 (기존 코드 수정 불필요)
- 쿠키 변경 시 자동으로 store 동기화
- 컴포넌트는
useAuth()만 호출하면 됨
3. authInterceptor.ts - 토큰 갱신 통합
export const refreshAccessToken = async () => { // 토큰 갱신 성공 const { accessToken, refreshToken } = response.data; // 쿠키에 저장 document.cookie = `access_token=${accessToken}...`; // Zustand store도 즉시 업데이트 useAuthStore.getState().setTokens(accessToken, refreshToken); return accessToken; };
- Interceptor에서 직접 store 업데이트 가능
- 토큰 갱신 시 모든 컴포넌트에 즉시 반영
동작 원리
전체 흐름도
1. 앱 시작
↓
2. authStore 생성 → isLoading: true
↓
3. 컴포넌트 마운트 → useAuth() 호출
↓
4. initialize() 실행 → 쿠키에서 토큰 읽기
↓
5. isLoading: false, isAuthenticated 설정
↓
6. 모든 컴포넌트가 동일한 상태 공유
토큰 갱신 시나리오
1. API 요청 → 401 에러
↓
2. authInterceptor가 자동 감지
↓
3. refreshAccessToken() 실행
↓
4. 새 토큰 받아서 쿠키 저장
↓
5. useAuthStore.getState().setTokens() 호출
↓
6. Zustand가 모든 구독 컴포넌트에 즉시 알림
↓
7. 모든 컴포넌트의 isAuthenticated 즉시 업데이트
↓
8. 원래 API 요청 재시도
로그인/로그아웃 시나리오
[로그인]
1. 로그인 성공 → 쿠키에 토큰 저장
↓
2. useAuth의 useEffect가 쿠키 변경 감지
↓
3. setTokens() 호출 → store 업데이트
↓
4. 모든 컴포넌트에 즉시 반영
[로그아웃]
1. logout() 호출
↓
2. clearTokens() → 쿠키 삭제
↓
3. storeLogout() → store 상태 초기화
↓
4. navigate('/login') → 페이지 이동
↓
5. 모든 컴포넌트의 isAuthenticated가 false로 변경
이점
1. 전역 상태 동기화
이전: 각 컴포넌트마다 독립적인 상태
// 컴포넌트 A const [isAuthenticated, setIsAuthenticated] = useState(false); // 컴포넌트 B const [isAuthenticated, setIsAuthenticated] = useState(false); // → 서로 다른 상태 인스턴스
현재: 모든 컴포넌트가 동일한 상태 공유
// 모든 컴포넌트 const { isAuthenticated } = useAuth(); // → 하나의 전역 상태를 공유
2. 비-React 코드 접근 가능
이전: Interceptor에서 상태 업데이트 불가능
// authInterceptor.ts // ❌ React 훅 사용 불가 // ❌ 컴포넌트 상태 업데이트 불가능
현재:어디서든 접근 가능
// authInterceptor.ts useAuthStore.getState().setTokens(accessToken, refreshToken); // 즉시 모든 컴포넌트 업데이트
3. 코드 일관성
- 중복 코드 제거
- 한 곳에서 상태 관리
- 유지보수 용이
4. 메모리 사용
- 이전: 컴포넌트당 약 100-200 bytes × N개
- 현재: 약 1 KB (고정, 컴포넌트 수와 무관)
- 차이: 실질적으로 거의 없음 (1-2 KB 차이)
isLoading 관리
isLoading의 역할
"앱 시작 시 쿠키에서 토큰을 읽어서 인증 상태를 확인하는 중"을 나타냅니다.
왜 필요한가?
문제 상황 (isLoading 없이):
1. 앱 시작 → isAuthenticated: false (초기값)
↓
2. ProtectedRoute 렌더링
↓
3. isAuthenticated가 false → 즉시 /login으로 리다이렉트
↓
4. 하지만 아직 쿠키를 확인하지 않았음
(실제로는 로그인되어 있을 수 있는데)
해결 (isLoading 사용):
1. 앱 시작 → isLoading: true, isAuthenticated: false
↓
2. ProtectedRoute 렌더링 → 로딩 스피너 표시
↓
3. initialize() 실행 → 쿠키 확인
↓
4. isLoading: false, isAuthenticated: true/false 설정
↓
5. 이제 정확한 인증 상태로 판단 가능
쿠키 변경 시 isLoading을 사용하지 않는 이유
쿠키 읽기는 동기적 작업:
// 쿠키 읽기는 즉시 완료됨 (비동기 작업 아님) const cookies = document.cookie.split(';'); const accessToken = cookies.find(...)?.split('=')[1]; // → 추가 로딩 시간 없음
결론:
isLoading은 초기화 전용- 앱 시작 시:
isLoading: true→initialize()→isLoading: false - 이후 쿠키 변경: 이미 초기화 완료 상태이므로
isLoading변경 불필요 - 쿠키 변경은 즉시 처리되므로 추가 로딩 상태 불필요
실제 사용
1. ProtectedRoute - 인증이 필요한 페이지 보호
export const ProtectedRoute = ({ children }) => { const { isAuthenticated, isLoading } = useAuth(); if (isLoading) { return <FullScreenLoadingSpinner message="인증 확인 중..." />; } if (!isAuthenticated) { return <Navigate to="/login" replace />; } return <>{children}</>; };
2. SplashPage - 앱 시작 시 인증 확인
export const SplashPage = () => { const { isAuthenticated, isLoading } = useAuth(); useEffect(() => { if (!isLoading) { setTimeout(() => { if (isAuthenticated) { navigate('/home'); } else { navigate('/login'); } }, 1000); } }, [isAuthenticated, isLoading, navigate]); return ( <div> {isLoading && <LoadingSpinner />} </div> ); };
3. MyPage - 로그아웃 기능
export const MyPage = () => { const { logout } = useAuth(); const handleLogout = () => { queryClient.clear(); logout(); // 쿠키 삭제 + store 초기화 + 페이지 이동 모두 처리 }; return ( <button onClick={handleLogout}>로그아웃</button> ); };
요약
- 전역 상태 공유: 모든 컴포넌트가 동일한 인증 상태 참조
- 즉시 반영: 토큰 갱신/로그인 시 모든 컴포넌트에 즉시 반영
- 비React 접근: Interceptor에서 직접 상태 업데이트 가능
- 코드 일관성: 중복 코드 제거, 유지보수 용이
사용 방법
- 컴포넌트에서는
useAuth()훅만 사용 authInterceptor.ts는 자동으로 동작 (별도 호출 불필요)authStore.ts는 직접 사용하지 않음 (내부적으로 사용)
메모리 사용
- 실질적인 차이거의 없음 (1-2 KB)
- 하지만 상태 동기화와 코드 일관성 측면에서 이점
isLoading
- 초기화 완료 여부만 나타냄
- 쿠키 변경 시에는 사용하지 않음 (동기적 작업이므로)
그 외 이점
- 기존 코드와 호환:
useAuth()인터페이스 유지로 기존 코드 수정 불필요 - 확장성: 프로젝트가 커질수록 Zustand의 이점이 더 커짐
- 선택사항: 작은 프로젝트에서는 쿠키 직접 읽기로도 충분할 수 있음