본문 바로가기
Sprint_FESI11/Project

useUserStore

by Toddler_AD 2025. 10. 27.

✅ 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: '사용자 정보를 불러오지 못했어요.' });
  }
},
 
🔸 주요 특징:
 
1. 중복 호출 방지 로직
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: '사용자 정보를 불러오지 못했어요.' });
    }
  },
}));