1. 스토어 정의 파일 (src/store/useProfileStore.ts)
// src/store/useProfileStore.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
// 1. 개인 정보 데이터 모델 인터페이스 정의
export interface Profile {
id: string;
name: string;
email: string;
phone: string;
address: string;
}
// 2. 스토어 상태 및 액션 인터페이스 정의
interface ProfileState {
profile: Profile | null;
isLoading: boolean;
error: string | null;
// Actions (CRUD)
fetchProfile: () => Promise<void>; // Read (조회)
saveProfile: (newProfile: Omit<Profile, 'id'>) => Promise<void>;// Create / Save (최초 저장)
updateProfile: (updatedFields: Partial<Profile>) => void; // Update (일부 수정)
deleteProfile: () => void; // Delete (삭제 / 초기화)
clearError: () => void;
}
// 3. Zustand 스토어 생성 (새로고침 시 데이터 유지를 위해 persist 적용)
export const useProfileStore = create<ProfileState>()(
persist(
(set, get) => ({
profile: null,
isLoading: false,
error: null,
// [READ] 개인정보 조회 (비동기 API 통신 시뮬레이션)
fetchProfile: async () => {
set({ isLoading: true, error: null });
try {
// API 호출 시뮬레이션
await new Promise((resolve) => setTimeout(resolve, 800));
const mockProfile: Profile = {
id: 'prof_1001',
name: '김철수',
email: 'chulsoo@example.com',
phone: '010-1234-5678',
address: '서울특별시 강남구 테헤란로 123',
};
set({ profile: mockProfile, isLoading: false });
} catch (err) {
set({ error: '개인 정보를 불러오는데 실패했습니다.', isLoading: false });
}
},
// [CREATE/SAVE] 개인정보 등록/저장
saveProfile: async (newProfileData) => {
set({ isLoading: true, error: null });
try {
await new Promise((resolve) => setTimeout(resolve, 500));
const createdProfile: Profile = {
id: `prof_${Date.now()}`,
...newProfileData,
};
set({ profile: createdProfile, isLoading: false });
} catch (err) {
set({ error: '개인 정보 저장에 실패했습니다.', isLoading: false });
}
},
// [UPDATE] 개인정보 업데이트 (부분 수정)
updateProfile: (updatedFields) => {
const currentProfile = get().profile;
if (!currentProfile) {
set({ error: '수정할 프로필이 존재하지 않습니다.' });
return;
}
set({
profile: {
...currentProfile,
...updatedFields,
},
});
},
// [DELETE] 개인정보 삭제 / 초기화
deleteProfile: () => {
set({
profile: null,
error: null,
});
},
clearError: () => set({ error: null }),
}),
{
name: 'user-profile-storage', // localStorage 저장 키
storage: createJSONStorage(() => localStorage),
}
)
);
2. 리액트 컴포넌트에서의 사용 (src/components/ProfileManagement.tsx)
// src/components/ProfileManagement.tsx
import React, { useState } from 'react';
import { useProfileStore } from '../store/useProfileStore';
import { exportProfileToCsv, checkProfileInUtils } from '../utils/profileUtils';
export default function ProfileManagement() {
// 스토어 상태 및 액션 선택적 구독 (성능 최적화)
const profile = useProfileStore((state) => state.profile);
const isLoading = useProfileStore((state) => state.isLoading);
const error = useProfileStore((state) => state.error);
const fetchProfile = useProfileStore((state) => state.fetchProfile);
const saveProfile = useProfileStore((state) => state.saveProfile);
const updateProfile = useProfileStore((state) => state.updateProfile);
const deleteProfile = useProfileStore((state) => state.deleteProfile);
// 로컬 폼 입력 상태
const [formData, setFormData] = useState({
name: '',
email: '',
phone: '',
address: '',
});
// 입력 변경 핸들러
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData({
...formData,
[e.target.name]: e.target.value,
});
};
// 신규 저장 제출
const handleCreate = async (e: React.FormEvent) => {
e.preventDefault();
if (!formData.name || !formData.email) {
alert('이름과 이메일은 필수 입력 항목입니다.');
return;
}
await saveProfile(formData);
setFormData({ name: '', email: '', phone: '', address: '' });
};
// 전화번호만 빠른 수정 예시
const handlePhoneUpdate = () => {
const newPhone = prompt('새로운 전화번호를 입력하세요:', profile?.phone || '');
if (newPhone) {
updateProfile({ phone: newPhone });
}
};
return (
<div style={{ padding: '20px', maxWidth: '500px', margin: '0 auto', fontFamily: 'sans-serif' }}>
<h2>👤 개인정보 관리 (Zustand CRUD 예제)</h2>
{error && <p style={{ color: 'red' }}>⚠️ {error}</p>}
{/* 1. 개인정보가 없는 경우: 등록 폼 또는 서버 불러오기 */}
{!profile ? (
<div>
<button onClick={fetchProfile} disabled={isLoading} style={{ marginBottom: '15px' }}>
{isLoading ? '불러오는 중...' : '서버에서 기존 프로필 불러오기'}
</button>
<h3>새 개인정보 등록</h3>
<form onSubmit={handleCreate} style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<input
type="text"
name="name"
placeholder="이름"
value={formData.name}
onChange={handleChange}
/>
<input
type="email"
name="email"
placeholder="이메일"
value={formData.email}
onChange={handleChange}
/>
<input
type="text"
name="phone"
placeholder="전화번호"
value={formData.phone}
onChange={handleChange}
/>
<input
type="text"
name="address"
placeholder="주소"
value={formData.address}
onChange={handleChange}
/>
<button type="submit" disabled={isLoading}>
{isLoading ? '저장 중...' : '개인정보 저장'}
</button>
</form>
</div>
) : (
/* 2. 개인정보가 존재하는 경우: 조회, 수정, 삭제 기능 */
<div style={{ border: '1px solid #ccc', padding: '15px', borderRadius: '8px' }}>
<h3>📋 등록된 개인정보</h3>
<p><strong>식별 ID:</strong> {profile.id}</p>
<p><strong>이름:</strong> {profile.name}</p>
<p><strong>이메일:</strong> {profile.email}</p>
<p><strong>전화번호:</strong> {profile.phone}</p>
<p><strong>주소:</strong> {profile.address}</p>
<div style={{ display: 'flex', gap: '8px', marginTop: '15px', flexWrap: 'wrap' }}>
<button onClick={handlePhoneUpdate}>전화번호 변경</button>
<button onClick={() => updateProfile({ address: '서울특별시 마포구 월드컵북로 456' })}>
주소 임의 변경
</button>
<button onClick={deleteProfile} style={{ backgroundColor: '#ff4d4d', color: '#fff' }}>
개인정보 삭제 (초기화)
</button>
</div>
<hr style={{ margin: '20px 0' }} />
{/* 컴포넌트 외부 유틸리티 테스트 버튼 */}
<h4>🛠️ 외부 유틸리티(JS/TS) 연동 테스트</h4>
<div style={{ display: 'flex', gap: '8px' }}>
<button onClick={checkProfileInUtils}>외부 유틸에서 정보 조회</button>
<button onClick={exportProfileToCsv}>CSV 내보내기 (외부 사용)</button>
</div>
</div>
)}
</div>
);
}
3. 컴포넌트 외부(유틸 함수 등)에서의 사용 (src/utils/profileUtils.ts)
// src/utils/profileUtils.ts
import { useProfileStore } from '../store/useProfileStore';
/**
* 컴포넌트 외부에서 Zustand 스토어에 접근하는 유틸 함수 예시입니다.
* React 컴포넌트 밖(일반 유틸, Axios 인스턴스, 로깅 서비스 등)에서 호출 가능합니다.
*/
// 1. 외부에서 스토어의 현재 상태 읽기 및 에러/수정 조작
export const checkProfileInUtils = () => {
// useProfileStore.getState()로 현재 상태에 직접 접근
const currentProfile = useProfileStore.getState().profile;
if (!currentProfile) {
alert('[유틸리티] 현재 등록된 개인정보가 없습니다.');
return;
}
alert(`[유틸리티 확인]\n이름: ${currentProfile.name}\n이메일: ${currentProfile.email}`);
};
// 2. 외부에서 파일 다운로드 기능 처리 시 스토어 데이터 활용 예시
export const exportProfileToCsv = () => {
const profile = useProfileStore.getState().profile;
if (!profile) {
alert('[오류] 내보낼 개인정보가 없습니다.');
return;
}
const csvContent = `ID,Name,Email,Phone,Address\n${profile.id},${profile.name},${profile.email},${profile.phone},"${profile.address}"`;
console.log('--- 생성된 CSV 데이터 ---');
console.log(csvContent);
alert('개인정보 CSV 데이터를 콘솔에 출력했습니다.');
};
// 3. 외부 API 응답 처리 중 개인정보 탈퇴/삭제가 필요한 경우 액션 직접 호출
export const handleUserAccountDeletion = () => {
console.warn('회원 탈퇴 처리로 인해 개인정보를 삭제합니다.');
// 외부에서 스토어의 액션 함수 실행
useProfileStore.getState().deleteProfile();
};
.