✅ 1. useUserStore의 목적
useUserStore는 전역적으로 “현재 로그인된 사용자 정보를 관리” 하는 스토어입니다.
즉, 로그인 여부가 아닌 — “누가 로그인했는가?”에 대한 데이터 계층을 담당합니다.
useAuthStore가 “인증(세션)”,
useUserStore는 “개인정보(프로필)” 를 책임집니다.
✅ 2. 주요 상태(state)
user: IUser | null
isLoading: boolean
error: string | null
| 상태 | 설명 |
| user | 서버로부터 받은 현재 로그인 사용자의 상세 정보 (이름, 이메일, 직업 등) |
| isLoading | 사용자 정보를 서버에서 불러오는 중 여부 |
| error | 사용자 정보 로딩 실패 시 메시지 (ex: "사용자 정보를 불러오지 못했어요.") |
즉,
- user는 “현재 로그인된 사람의 정체성(identity)”
- isLoading은 “데이터 가져오는 중인지”
- error는 “요청 실패 상태”를 관리합니다.
✅ 3. 주요 메서드(actions)
| 메서드 | 역할 | 특징 |
| setUser(u) | 서버 응답으로 받은 사용자 객체를 그대로 저장 | 외부에서 직접 주입 |
| updateUser(patch) | 낙관적(optimistic) 업데이트 — 부분 필드만 병합하여 UI 즉시 반영 | 서버 호출 없이 즉시 반영 가능 |
| patchUser(patch) | 과거 호환용 alias (updateUser와 동일) | 유지보수 편의용 |
| clear() | 사용자, 로딩, 에러 상태를 초기화 | 로그아웃/세션만료 시 호출 |
| fetchMe() | 서버 /auths/user 엔드포인트로 현재 사용자 정보 요청 | 최초 1회 로딩 또는 새로고침 시 실행 |
✅ 4. 작동 원리 (Flow)
useUserStore는 useAuthStore의 토큰을 기반으로 작동합니다.
🔹 ① 로그인 성공
LoginForm → authService.signin() → token 저장 (useAuthStore.setToken)
↓
useUserStore.fetchMe()
↓
서버의 /auths/user → { id, name, email, ... }
↓
useUserStore.setUser(me)
즉,
로그인이 완료되면 fetchMe()를 통해 사용자 정보를 서버에서 가져와
user 상태를 채웁니다.
🔹 ② 새로고침 / 페이지 첫 진입
App 시작 → AuthProvider 렌더링
↓
useAuthStore.checkTokenValidity() → 유효한 토큰이면 true
↓
useUserStore.fetchMe() → /auths/user 요청
↓
user 상태 복원
즉, 앱 초기 진입 시에도 AuthStore가 살아있으면 자동으로 사용자 정보를 복구합니다.
(서버 세션이 아니라 클라이언트 토큰 기반 복구)
🔹 ③ 로그아웃 / 토큰 만료
useAuthStore.logout() → token 제거
↓
useUserStore.clear() → user=null
사용자 정보와 인증 상태가 동시에 초기화되어
모든 UI에서 로그인 상태가 해제됩니다.
✅ 5. 내부 구현 상세 분석
아래 코드 기반으로 핵심 동작 포인트를 단계별로 짚겠습니다 👇
fetchMe: async () => {
if (get().user || get().isLoading) return; // 중복 호출 방지
set({ isLoading: true, error: null });
try {
const me = await authService.getUser(); // ✅ 서버 호출 (/auths/user)
set({ user: me as IUser, isLoading: false });
} catch {
// 서버 인증 실패 or 네트워크 오류
set({ user: null, isLoading: false, error: '사용자 정보를 불러오지 못했어요.' });
}
},
if (get().user || get().isLoading) return;
→ 이미 user가 있거나 로딩 중이면 다시 요청하지 않음 (불필요한 API 호출 방지)
2. 로딩 상태 관리
set({ isLoading: true, error: null });
→ UI에서 로딩 스피너 표시 등과 연동 가능
3. API 호출 후 정상 처리
const me = await authService.getUser();
set({ user: me, isLoading: false });
→ 서버에서 받은 JSON 객체를 user에 그대로 저장
4. 예외 처리
set({ user: null, isLoading: false, error: '사용자 정보를 불러오지 못했어요.' });
→ 로그인 만료나 네트워크 장애 시 UI에서 대응 가능
✅ 6. “낙관적 업데이트(Optimistic Update)”의 의미
updateUser: patch => {
const cur = get().user;
if (!cur) return;
const next = { ...cur, ...patch } as IUser;
const isSame = JSON.stringify(cur) === JSON.stringify(next);
if (!isSame) set({ user: next });
},
즉,
서버에 요청을 보내기 전에 UI에 바로 반영하는 방식입니다.
예를 들어,
사용자가 “닉네임 변경”을 했다고 가정하면:
// 버튼 클릭 → 서버 호출 전 즉시 UI 반영
updateUser({ name: '새 닉네임' });
await authService.updateUser({ name: '새 닉네임' });
이렇게 하면 서버 응답을 기다리지 않아도 즉시 화면이 바뀌어,
UX가 훨씬 부드럽게 느껴집니다.
✅ 7. UserStore가 필요한 이유
만약 useUserStore 없이 AuthStore만 있다면,
우리는 단순히 “로그인 됨/안됨”만 알 뿐
누가 로그인했는지, 어떤 정보를 보여줘야 하는지를 알 수 없습니다.
즉, 다음과 같은 문제들이 생깁니다:
| 문제 | 이유 |
| 헤더에 “홍길동님 환영합니다” 표시 불가 | user 정보가 없음 |
| 마이페이지 접근 시 사용자 정보 없음 | 프로필 데이터 관리 불가 |
| 사용자 데이터 변경 후 UI 자동 갱신 안 됨 | 전역 state 부재 |
그래서 useUserStore는 AuthStore 위에 “사용자 컨텍스트”를 얹는 레이어 역할을 합니다.
✅ 8. 다른 도메인과의 관계
AuthStore ─┬─> UserStore ─┬─> GatheringStore
│ ├─> ReviewStore
│ └─> ToastStore (UX 알림)
- AuthStore : 로그인 세션 관리
- UserStore : 로그인된 사용자 정보 제공
- GatheringStore / ReviewStore 등은 user.id를 참조하여 데이터 요청 수행
즉, useUserStore.user는
다른 도메인 스토어들이 현재 사용자 컨텍스트를 인식하는 진입점 역할을 합니다.
✅ 9. 정리 요약표
| 구분 | 내용 |
| 핵심 역할 | 로그인된 사용자의 정보를 전역적으로 보관 및 갱신 |
| 상호작용 | AuthStore와 연결 (토큰 유효 시 fetchMe 수행) |
| 호출 시점 | 로그인 성공 직후 / 페이지 새로고침 / 세션 복구 시 |
| 주요 기능 | 사용자 정보 로딩, 낙관적 업데이트, 상태 초기화 |
| 사용 예시 | 헤더 유저명 표시, 마이페이지, 프로필 수정 등 |
| 데이터 유지 범위 | 로그인 유지 중에만 (로그아웃 시 clear) |
✅ 10. 한 줄 요약
🔹 useAuthStore는 "로그인 상태를 판별",
🔹 useUserStore는 "로그인된 사람의 정보 관리".즉, AuthStore는 “세션”, UserStore는 “정체성(Identity)”을 다룹니다.
✅ 11. CODE
// src/stores/useUserStore.ts
'use client'; // 이 파일을 클라이언트 컴포넌트 컨텍스트로 강제 지정 (Zustand 훅/브라우저 API 사용을 위해 필요)
// ----------------------------------------------------------------------------
// NOTE: 사용자 전역 스토어 (Zustand)
// - user: 로그인된 사용자 정보 (없으면 null)
// - isLoading: 사용자 정보 요청 중 여부
// - error: 사용자 정보 로딩 실패 시 에러 메시지
// - setUser(u): 사용자 전역 상태를 통째로 교체 (서버 응답 그대로 반영할 때 사용)
// - updateUser(patch): 부분 업데이트(낙관적 업데이트) - 기존 user와 병합
// - patchUser(patch): 과거 호환용 (updateUser alias)
// - clear(): 사용자 상태 및 로딩/에러 초기화
// - fetchMe(): 최초 진입 시 1회 사용자 정보 로딩 (중복 호출 방지)
// ----------------------------------------------------------------------------
import { create } from 'zustand'; // Zustand의 전역 상태 생성 함수
import type { IUser } from '@/types/auths'; // 사용자 타입 정의 (id, name 등 도메인 인터페이스)
import { authService } from '@/services/auths/authService'; // 사용자 정보 조회 API 래퍼
// 전역 스토어에서 관리할 상태(데이터)와 액션(함수)들을 타입으로 정의
interface UserState {
user: IUser | null; // 현재 로그인한 사용자 정보 (로그아웃 상태면 null)
isLoading: boolean; // 사용자 정보 로딩 중인지 여부
error: string | null; // 사용자 정보 로딩 실패 시 에러 메시지
setUser: (u: IUser | null) => void; // user 상태를 외부에서 직접 교체할 때 사용
updateUser: (patch: Partial<IUser>) => void; // 부분 필드만 갱신하는 낙관적 업데이트
patchUser: (patch: Partial<IUser>) => void; // 과거 호환용 alias (updateUser로 위임)
clear: () => void; // user/로딩/에러 상태를 모두 초기화
fetchMe: () => Promise<void>; // 최초 진입 시 1회 사용자 정보 로딩 (중복 호출 방지)
}
// create<UserState>()로 전역 스토어 훅을 생성
export const useUserStore = create<UserState>()((set, get) => ({
// ---------------------------
// 초기 상태 (앱 시작 시 기본값)
// ---------------------------
user: null, // 아직 사용자 정보를 모르는 상태
isLoading: false, // 로딩 중 아님
error: null, // 에러 없음
// --------------------------------------------------------
// setUser(u)
// - 외부(서비스/컴포넌트)에서 user를 통째로 교체할 때 사용
// - 예: 프로필 수정 API 이후 서버 응답을 그대로 반영
// --------------------------------------------------------
setUser: u => set({ user: u }),
// --------------------------------------------------------
// updateUser(patch)
// - user 일부 필드만 변경하는 "낙관적 업데이트" 도우미
// - 현재 user가 없으면 아무 것도 하지 않음 (로그아웃 상태 보호)
// - 이전 user와 병합된 next가 기존과 동일하면 set을 생략해 불필요 렌더 방지
// (참조 동일성 비교가 아닌 JSON 문자열 비교를 사용 -> 간단하지만 깊은 객체에선 비용↑)
// --------------------------------------------------------
updateUser: patch => {
const cur = get().user; // 현재 사용자 상태 스냅샷
if (!cur) return; // 로그인 전이면 업데이트할 대상 없음
const next: IUser = { ...cur, ...patch } as IUser; // 얕은 병합으로 변경점 반영
const isSame = JSON.stringify(cur) === JSON.stringify(next); // 변경 여부 간단 비교
if (!isSame) set({ user: next }); // 실제 변경이 있을 때만 상태 갱신
},
// --------------------------------------------------------
// patchUser(patch)
// - 과거 코드 호환을 위해 남긴 alias
// - 내부적으로 updateUser로 위임
// --------------------------------------------------------
patchUser: patch => get().updateUser(patch),
// --------------------------------------------------------
// clear()
// - 사용자/로딩/에러 상태를 한 번에 초기화
// - 로그아웃, 세션 만료, 401 응답 시 등에서 호출
// --------------------------------------------------------
clear: () => set({ user: null, isLoading: false, error: null }),
// --------------------------------------------------------
// fetchMe()
// - 서버의 "내 정보" API를 호출하여 user를 채우는 비동기 액션
// - 최초 진입 시 1회만 호출되도록 가드 (user가 이미 있거나, 로딩 중이면 리턴)
// - 성공: user 채움 / 실패: user=null + 에러 메시지
// --------------------------------------------------------
fetchMe: async () => {
// 이미 user가 있거나, 현재 호출이 진행 중이면 중복 호출을 방지
if (get().user || get().isLoading) return;
set({ isLoading: true, error: null }); // 로딩 시작, 기존 에러 초기화
try {
const me = await authService.getUser(); // 서버로부터 현재 사용자 정보 수신
set({ user: me as IUser, isLoading: false }); // 성공 시 사용자 상태 갱신
} catch {
// 실패 시 사용자 정보를 비우고, 에러 메시지를 제공
set({ user: null, isLoading: false, error: '사용자 정보를 불러오지 못했어요.' });
}
},
}));'Sprint_FESI11 > Project' 카테고리의 다른 글
| useUserStore VS useAuthStore (0) | 2025.10.27 |
|---|---|
| Flux와 Redux 개념을 UPDO 프로젝트에 대입 (0) | 2025.10.27 |
| 프로젝트 전반에서 어떤 React 훅들이 어디에 사용되고 있는가? (0) | 2025.10.16 |
| useAuthStore (0) | 2025.10.16 |
| HttpClient → PolymorphicHttpClient → Service → Component 계층 구조 (0) | 2025.10.16 |