본문 바로가기
카테고리 없음

zustand 사용 예(ts) - 회원 정보 관리 - Persist 적용 O

by web data 2026. 10. 16.

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();
};

 

.