본문 바로가기
Sprint_FESI11/Project

useAuthStore

by Toddler_AD 2025. 10. 16.

🧩 이 훅(useAuthStore)이 하는 일은 “로그인 상태 기억하기”예요.


1️⃣ “로그인하면 생기는 토큰”이란?

  • 우리가 로그인하면 서버가 “이 사람 진짜 로그인했어요!”라는 **증표(=토큰)**를 줍니다.
  • 이건 마치 놀이공원 입장권 같은 거예요.
    → 이걸 들고 있어야 안에서 자유롭게 놀 수 있죠 🎢

2️⃣ 근데, 이 토큰에는 유효기간이 있어요.

  • 입장권도 “오늘까지만 유효”하듯이,
    토큰도 “1시간 동안만 유효” 같은 만료시간이 있습니다.

3️⃣ 그래서 이 훅은 “입장권 보관함”이에요.

  • token : 실제 입장권 번호
  • tokenExpiry : 입장권 만료 시각
  • isAuthenticated : 아직 입장권이 유효한지 (로그인 중인지)

이 세 가지를 한데 묶어서 Zustand(주스탠드) 라는 저장소에 저장해둡니다.
(= 컴퓨터 메모리 속 “내 로그인 상태 상자”)


4️⃣ 그런데 새로고침하면? 🤔

  • 우리가 페이지를 새로고침하면 메모리에 있던 데이터는 다 사라져요.
  • 그래서 토큰을 localStorage(브라우저 안의 보관함)에 복사본으로 저장해둡니다.

새로고침해도 “아까 로그인했었지!” 하고 기억해주는 역할이에요.


5️⃣ 주요 버튼 역할로 보면 👇

함수 이름 하는 일 비유
setToken() 로그인 시 토큰 저장 + 만료시각 계산 새 입장권 받기
logout() 토큰 지우기 입장권 반납하기
readFromStorage() 브라우저에 저장된 토큰 불러오기 잃어버린 입장권 다시 꺼내보기

6️⃣ “SSR 안전”이란?

  • SSR(Server Side Rendering)은 서버에서 미리 화면을 만드는 방식이에요.
  • 서버에는 localStorage가 없어요 (그건 브라우저 전용).
  • 그래서 if (typeof window === 'undefined')로 “지금 서버야? 브라우저야?”를 구분해서
    서버에서는 실수로 localStorage를 쓰지 않도록 막은 거예요.

쉽게 말해: “서버에서는 localStorage 만지면 안 돼! 위험해!” 하는 안전벨트 🚨


✅ 한 줄 요약

이 훅은 “내 로그인 상태(입장권)”를 기억하고 관리하는 저장소예요.
새로고침해도 로그인 유지하고, 만료되면 자동으로 끊기게 해주는 똑똑한 보관함입니다. 🧠✨

 

 

왜 이 스토어가 필요한가 (요약)

  • 목표: “로그인 상태”를 앱 전역에서 안전하게 관리하고, 새로고침해도 유지하며, 만료 시간이 지나면 끊어주기.
  • 핵심 데이터:
    • token : 서버가 준 로그인 증표
    • tokenExpiry : 토큰이 더 이상 유효하지 않게 되는 시각(ms)
    • isAuthenticated : 지금 시점에 로그인 상태가 유효한지 ( Date.now() < tokenExpiry )
  • 핵심 동작:
    • 로그인 성공 → setToken(token, expiryMs)로 저장 (+ localStorage 동기화)
    • 앱 시작/새로고침 → localStorage에서 복구(readFromStorage)
    • 로그아웃 → logout()으로 로컬 저장소/메모리에서 모두 삭제
  • SSR 안전: 서버에서는 localStorage가 없으므로, typeof window !== 'undefined'로 브라우저에서만 접근.

흐름 시각화 (Mermaid 순서도)

flowchart TD
  A[페이지 로드/앱 시작] --> B{서버? 브라우저?}
  B -- 서버(SSR) --> C[localStorage 접근 금지] --> D[초기 상태: token=null, tokenExpiry=null]
  B -- 브라우저 --> E[readFromStorage()]
  E -->|토큰/만료시각 존재| F{Date.now()<tokenExpiry?}
  F -- 예 --> G[isAuthenticated=true]
  F -- 아니오 --> H[만료 간주: isAuthenticated=false]
  E -->|없음| H

  subgraph 로그인 플로우
    I[사용자 로그인 시도] --> J[서버에서 토큰과 유효기간(예:1시간) 제공]
    J --> K[setToken(token, expiryMs)]
    K --> L[localStorage에 token/expiry 저장]
    L --> M[Zustand 상태 갱신: token, tokenExpiry, isAuthenticated=true]
  end

  subgraph 만료/로그아웃
    N[시간 경과로 만료] --> O[isAuthenticated=false 처리(추가 훅/가드 권장)]
    P[사용자 로그아웃 클릭] --> Q[logout()]
    Q --> R[localStorage 제거 + Zustand 초기화]
  end

팁: “만료 시 자동 로그아웃”은 위 기본 스토어만으로는 즉시 일어나지 않습니다(스토어는 시각 비교만 함).
보통 setTimeout/setInterval 또는 라우트 가드/인터셉터로 추가 구현합니다(아래 개선 섹션 참조).


코드 상세 해설 (줄 단위로 핵심 포인트)

아래는 원본과 로직 동일하면서, 이해를 돕기 위해 주석을 매우 촘촘히 단 버전입니다.
(파일 상단 경로 주석, // 형식 통일 ✅)

// -----------------------------------------------------------------------------
// src/stores/useAuthStore.ts
// -----------------------------------------------------------------------------
// NOTE: 인증 전역 스토어 (Zustand)
//       - token: 현재 액세스 토큰(없으면 null)
//       - tokenExpiry: 만료 타임스탬프(ms). null이면 만료 관리 없음
//       - isAuthenticated: 로그인 여부 (토큰 존재 + 만료 전이면 true)
//       - setToken(token, expiryMs): 토큰/만료 등록 + localStorage 동기화
//       - logout(): 토큰/만료 제거 + 상태 초기화
//       - checkTokenValidity(): 만료된 토큰 자동 제거
//       - 초기화 시(localStorage → state) 복구 로직 포함
// -----------------------------------------------------------------------------

import { create } from 'zustand'; // ✅ Zustand의 create 함수: 전역 상태 생성용 훅

// -----------------------------------------------------------------------------
// AuthState 인터페이스
//  - 전역 인증 상태의 구조를 정의
// -----------------------------------------------------------------------------
interface AuthState {
  token: string | null; // 현재 로그인 사용자의 JWT 토큰
  tokenExpiry: number | null; // 토큰 만료 시간 (timestamp, ms)
  isAuthenticated: boolean; // 인증 상태 (true = 로그인 중)
  setToken: (token: string | null, expiryMs?: number) => void; // 토큰 등록/해제 함수
  logout: () => void; // 강제 로그아웃 함수
  checkTokenValidity: () => boolean; // 만료 확인 및 자동 로그아웃
}

// -----------------------------------------------------------------------------
// readFromStorage()
//  - 클라이언트(localStorage)에 저장된 토큰/만료 정보를 불러오는 헬퍼 함수
//  - SSR 환경(서버)에서는 localStorage 접근이 불가하므로 null 반환
// -----------------------------------------------------------------------------
const readFromStorage = () => {
  // SSR(서버) 환경일 경우: localStorage 접근 불가 → 초기값 null
  if (typeof window === 'undefined') {
    return { token: null as string | null, tokenExpiry: null as number | null };
  }

  // 브라우저 환경일 경우 localStorage에서 토큰 및 만료 시간 불러오기
  const token = localStorage.getItem('access_token');
  const expiryStr = localStorage.getItem('token_expiry');

  // 문자열을 숫자로 변환 (없거나 NaN이면 null로 처리)
  const parsed = expiryStr ? Number(expiryStr) : null;
  const tokenExpiry = parsed && !isNaN(parsed) ? parsed : null;

  return { token, tokenExpiry };
};

// -----------------------------------------------------------------------------
// useAuthStore()
//  - Zustand를 이용한 전역 인증 스토어 생성
//  - Flux 구조에서 Store 역할 수행
// -----------------------------------------------------------------------------
export const useAuthStore = create<AuthState>((set, get) => {
  // 앱 시작 시(localStorage) 저장된 값 복구
  const { token, tokenExpiry } = readFromStorage();

  // ---------------------------------------------------------------------------
  // 초기 상태 구성
  // ---------------------------------------------------------------------------
  return {
    // 초기 토큰/만료값 설정
    token,
    tokenExpiry,

    // 토큰이 존재하고, 만료되지 않았으면 true로 설정
    isAuthenticated: !!token && !!tokenExpiry && Date.now() < tokenExpiry,

    // -------------------------------------------------------------------------
    // setToken()
    //  - 새 토큰 등록 혹은 제거 (로그인/로그아웃 시 사용)
    //  - localStorage와 상태를 동기화
    // -------------------------------------------------------------------------
    setToken: (newToken, expiryMs) => {
      // 서버 환경에서는 localStorage를 사용할 수 없으므로 분기 처리
      if (typeof window !== 'undefined') {
        // ✅ 토큰이 존재할 경우 → 로그인 성공 시
        if (newToken) {
          // 만료 시간(ms): 직접 지정하거나 기본 1시간(60*60*1000)
          const expiry =
            typeof expiryMs === 'number' ? Date.now() + expiryMs : Date.now() + 60 * 60 * 1000;

          // localStorage에 토큰과 만료 시간 저장
          localStorage.setItem('access_token', newToken);
          localStorage.setItem('token_expiry', String(expiry));

          // 상태 갱신
          set({ token: newToken, tokenExpiry: expiry, isAuthenticated: true });
        }
        // ✅ newToken이 null인 경우 → 명시적 로그아웃 시
        else {
          // localStorage에서 토큰 제거
          localStorage.removeItem('access_token');
          localStorage.removeItem('token_expiry');

          // 상태 초기화
          set({ token: null, tokenExpiry: null, isAuthenticated: false });
        }
      }
      // SSR 환경에서 실행될 때 (window 없음)
      else {
        set({ token: newToken, tokenExpiry: null, isAuthenticated: !!newToken });
      }
    },

    // -------------------------------------------------------------------------
    // checkTokenValidity()
    //  - 토큰 만료 여부를 검사하고, 만료 시 자동 로그아웃 처리
    // -------------------------------------------------------------------------
    checkTokenValidity: () => {
      // 현재 스토어 상태 안전하게 가져오기 (get()으로 순환참조 방지)
      const state = get();

      // ✅ 토큰이 존재하고 만료 시간이 지났다면 → 만료 처리
      if (state.token && state.tokenExpiry && Date.now() >= state.tokenExpiry) {
        // localStorage에서 삭제 (클라이언트 환경에서만)
        if (typeof window !== 'undefined') {
          localStorage.removeItem('access_token');
          localStorage.removeItem('token_expiry');
        }

        // 상태 초기화
        set({ token: null, tokenExpiry: null, isAuthenticated: false });
        return false; // 토큰이 유효하지 않음
      }

      // ✅ 여전히 유효한 경우 true 반환
      return !!state.token && !!state.isAuthenticated;
    },

    // -------------------------------------------------------------------------
    // logout()
    //  - 사용자 강제 로그아웃
    //  - localStorage 및 상태 초기화
    // -------------------------------------------------------------------------
    logout: () => {
      // 브라우저 환경에서만 localStorage 접근
      if (typeof window !== 'undefined') {
        localStorage.removeItem('access_token');
        localStorage.removeItem('token_expiry');
      }

      // 상태 초기화
      set({ token: null, tokenExpiry: null, isAuthenticated: false });
    },
  };
});

핵심 체크포인트

    • 초기화 시점: create()가 실행될 때 readFromStorage()로 브라우저 저장값 복구.
    • isAuthenticated 계산: “지금 시간 vs 만료 시각” 비교로 즉시 판정.
    • SSR 안전: 서버에서 window/localStorage에 접근하지 않도록 가드.
    • setToken 다형성:
      • newToken이 있으면 로그인/갱신
      • newToken이 없으면 로그아웃/무효화

 

🧩 전체 구조 요약 (Flux 관점)

역할 이 코드에서의 구현  위치설명
Action setToken(), logout(), checkTokenValidity() 상태를 변경하는 모든 트리거
Store useAuthStore 전역 인증 상태 관리 (token, expiry, login 여부)
View (Component) Header, AuthProvider, LoginForm 등 useAuthStore를 구독하여 UI 업데이트
Dispatcher set() 호출 내부 Zustand가 자동으로 액션을 상태 변경으로 전달

 

💡 이해 포인트

  • readFromStorage() → “초기 상태 복구”
  • setToken() → “로그인 성공 or 토큰 갱신 시 상태 변경”
  • checkTokenValidity() → “만료 시점 관리 (자동 로그아웃)”
  • logout() → “사용자 명시적 로그아웃 처리”