logo

DowanKim

37. Zustand를 사용한 인증 상태 관리

2026년 1월 19일

이게머니

Zustand를 사용한 인증 상태 관리

변경 배경

이전 방식의 문제점

  • 각 컴포넌트마다 useState로 독립적인 인증 상태 관리
  • 상태 동기화 어려움 (컴포넌트마다 다른 상태 인스턴스)
  • 비-React 코드(Interceptor)에서 상태 업데이트 불가능
  • 중복 코드 발생

Zustand 도입 목적

  • 전역 상태로 모든 컴포넌트가 동일한 인증 상태 공유
  • Interceptor에서 직접 상태 업데이트 가능
  • 코드 일관성 및 유지보수성 향상

구현 구조

프로젝트는 3개의 파일로 인증 상태를 관리합니다:

┌─────────────────────┐
│  authStore.ts       │ ← 전역 상태 저장소 (Zustand)
│  (상태 저장소)         │
└──────────┬──────────┘
           │
    ┌──────┴──────┐
    │             │
┌───▼────┐  ┌────▼──────────┐
│useAuth │  │authInterceptor│
│(훅)     │  │(API 요청 처리)  │
└────────┘  └───────────────┘

파일 역할

  1. authStore.ts: Zustand store - 전역 인증 상태 저장
  2. useAuth.ts: React 훅 - 컴포넌트에서 사용하는 인터페이스
  3. 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: trueinitialize()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> ); };

요약

  1. 전역 상태 공유: 모든 컴포넌트가 동일한 인증 상태 참조
  2. 즉시 반영: 토큰 갱신/로그인 시 모든 컴포넌트에 즉시 반영
  3. 비React 접근: Interceptor에서 직접 상태 업데이트 가능
  4. 코드 일관성: 중복 코드 제거, 유지보수 용이

사용 방법

  • 컴포넌트에서는 useAuth() 훅만 사용
  • authInterceptor.ts는 자동으로 동작 (별도 호출 불필요)
  • authStore.ts는 직접 사용하지 않음 (내부적으로 사용)

메모리 사용

  • 실질적인 차이거의 없음 (1-2 KB)
  • 하지만 상태 동기화와 코드 일관성 측면에서 이점

isLoading

  • 초기화 완료 여부만 나타냄
  • 쿠키 변경 시에는 사용하지 않음 (동기적 작업이므로)

그 외 이점

  • 기존 코드와 호환: useAuth() 인터페이스 유지로 기존 코드 수정 불필요
  • 확장성: 프로젝트가 커질수록 Zustand의 이점이 더 커짐
  • 선택사항: 작은 프로젝트에서는 쿠키 직접 읽기로도 충분할 수 있음