본문 바로가기
Programming/React

zustand 사용 예 (ts) - 로그인 인증 샘플 - Persist 사용 O

by web data 2026. 10. 16.

1. 스토어 정의 파일 (src/store/useAuthStore.ts)

// src/store/useAuthStore.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';

// 1. 데이터 모델 및 스토어 인터페이스 정의
export interface User {
  id: string;
  name: string;
  email: string;
}

interface AuthState {
  user: User | null;
  token: string | null;
  isAuthenticated: boolean;
  isLoading: boolean;
  error: string | null;

  // Actions
  login: (email: string, password: string) => Promise<boolean>;
  logout: () => void;
  updateUserName: (newName: string) => void;
  clearError: () => void;
}

// 2. Zustand 스토어 생성 (실무에서 자주 쓰는 persist 미들웨어 적용: 새로고침해도 상태 유지)
export const useAuthStore = create<AuthState>()(
  persist(
    (set, get) => ({
      user: null,
      token: null,
      isAuthenticated: false,
      isLoading: false,
      error: null,

      // 비동기 로그인 액션
      login: async (email, password) => {
        set({ isLoading: true, error: null });
        try {
          // TODO: 실제 API 호출 (예: axios.post('/api/login', { email, password }))
          await new Promise((resolve) => setTimeout(resolve, 1000));

          // Mock 응답 데이터
          const mockUser: User = { id: 'usr_12345', name: '홍길동', email };
          const mockToken = 'mock_jwt_token_abcdef12345';

          set({
            user: mockUser,
            token: mockToken,
            isAuthenticated: true,
            isLoading: false,
          });
          return true;
        } catch (err) {
          set({
            error: '이메일 또는 비밀번호가 올바르지 않습니다.',
            isLoading: false,
          });
          return false;
        }
      },

      // 로그아웃 액션
      logout: () => {
        set({
          user: null,
          token: null,
          isAuthenticated: false,
          error: null,
        });
      },

      // 상태 일부 변경 액션 (get()을 활용하여 현재 상태 참조 가능)
      updateUserName: (newName) => {
        const currentUser = get().user;
        if (!currentUser) return;

        set({
          user: {
            ...currentUser,
            name: newName,
          },
        });
      },

      clearError: () => set({ error: null }),
    }),
    {
      name: 'auth-storage', // localStorage에 저장될 키 이름
      storage: createJSONStorage(() => localStorage), // 세션 스토리지를 원한다면 sessionStorage로 변경 가능
      // 파기하고 싶은 민감한 필드가 있다면 partialize를 이용해 제외할 수도 있습니다.
      partialize: (state) => ({
        user: state.user,
        token: state.token,
        isAuthenticated: state.isAuthenticated,
      }),
    }
  )
);

 

2. 컴포넌트 내부에서의 사용 (src/components/UserProfile.tsx)

// src/components/UserProfile.tsx
import React, { useState } from 'react';
import { useAuthStore } from '../store/useAuthStore';
import { apiCallExample } from '../utils/apiClient'; // 외부 유틸 함수 연동 예시

export default function UserProfile() {
  // 1. 필요한 상태와 액션 선택적 구독 (TypeScript가 타입을 자동 추론합니다)
  const user = useAuthStore((state) => state.user);
  const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
  const isLoading = useAuthStore((state) => state.isLoading);
  const error = useAuthStore((state) => state.error);
  
  const login = useAuthStore((state) => state.login);
  const logout = useAuthStore((state) => state.logout);
  const updateUserName = useAuthStore((state) => state.updateUserName);

  // 로컬 폼 상태
  const [emailInput, setEmailInput] = useState('');
  const [passwordInput, setPasswordInput] = useState('');
  const [newNameInput, setNewNameInput] = useState('');

  const handleLoginSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!emailInput || !passwordInput) return;
    await login(emailInput, passwordInput);
  };

  return (
    <div style={{ padding: '20px', border: '1px solid #ddd', borderRadius: '8px', maxWidth: '400px' }}>
      <h2>🔐 사용자 인증 상태 관리</h2>

      {error && <p style={{ color: 'red' }}>{error}</p>}

      {!isAuthenticated ? (
        <form onSubmit={handleLoginSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
          <input
            type="email"
            placeholder="이메일 입력"
            value={emailInput}
            onChange={(e) => setEmailInput(e.target.value)}
          />
          <input
            type="password"
            placeholder="비밀번호 입력"
            value={passwordInput}
            onChange={(e) => setPasswordInput(e.target.value)}
          />
          <button type="submit" disabled={isLoading}>
            {isLoading ? '로그인 중...' : '로그인'}
          </button>
        </form>
      ) : (
        <div>
          <p>환영합니다, <strong>{user?.name}</strong>님! ({user?.email})</p>
          
          <div style={{ margin: '15px 0', display: 'flex', gap: '8px' }}>
            <input
              type="text"
              placeholder="변경할 이름"
              value={newNameInput}
              onChange={(e) => setNewNameInput(e.target.value)}
            />
            <button onClick={() => { updateUserName(newNameInput); setNewNameInput(''); }}>
              이름 수정
            </button>
          </div>

          {/* 컴포넌트 외부 유틸 함수 호출 버튼 */}
          <div style={{ margin: '15px 0' }}>
            <button onClick={apiCallExample} style={{ background: '#e0e0e0' }}>
              외부 유틸에서 토큰 가져와 API 요청 테스트
            </button>
          </div>

          <button onClick={logout} style={{ background: '#ff4d4d', color: '#fff' }}>
            로그아웃
          </button>
        </div>
      )}
    </div>
  );
}

 

3. 컴포넌트 외부(유틸 함수, API 인터셉터 등)에서의 사용 (src/utils/apiClient.ts)

React 컴포넌트 생명주기 밖인 일반 TS 파일에서 스토어의 현재 값을 읽거나 상태를 조작(예: 토큰 만료 시 강제 로그아웃 처리)

 

// src/utils/apiClient.ts
import { useAuthStore } from '../store/useAuthStore';

/**
 * 실무 활용 예시:
 * Axios 인터셉터나 일반 유틸 함수 안에서 React 훅(useAuthStore)을 호출하면 에러가 납니다.
 * 이럴 때 `useAuthStore.getState()` 또는 `useAuthStore.setState()`를 사용합니다.
 */
export const apiCallExample = async () => {
  // 1. 컴포넌트 외부에서 현재 상태(Token) 안전하게 가져오기
  const token = useAuthStore.getState().token;
  const isAuthenticated = useAuthStore.getState().isAuthenticated;

  if (!isAuthenticated || !token) {
    alert('인증 정보가 없습니다. 로그인해주세요.');
    return;
  }

  console.log('API 요청 전송 중... 사용중인 토큰:', token);

  // 시뮬레이션: 만약 서버에서 401(인증 만료) 응답을 받았다고 가정
  const isTokenExpired = false; // 실제로는 API 응답 코드 체크

  if (isTokenExpired) {
    console.warn('토큰이 만료되었습니다. 강제 로그아웃을 실행합니다.');
    
    // 2. 컴포넌트 외부에서 액션 함수 직접 호출하기
    useAuthStore.getState().logout();
    
    // 또는 직접 상태 강제 주입하기
    // useAuthStore.setState({ error: '세션이 만료되었습니다. 다시 로그인해주세요.' });
  } else {
    alert('API 요청 성공! (콘솔 확인)');
  }
};