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 요청 성공! (콘솔 확인)');
}
};'Programming > React' 카테고리의 다른 글
| zustand 사용 예(ts) - 회원 정보 관리 - Persist 적용x (0) | 2026.10.16 |
|---|---|
| zustand 사용 예(ts) - 회원 정보 관리 - 기초적이고 심플한 개인정보(CRUD) 코드 샘플 (0) | 2026.10.16 |
| zustand 사용 (js) - todo list sample (0) | 2026.10.16 |
| vite 으로 리액트 프로젝트 시작하기 (0) | 2026.05.19 |
| cra 로 생성한 프로젝트에 빌드시 콘솔로그 코드 모두 제거하기 (eject 사용안함) (0) | 2025.02.26 |