🧩 이 훅(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() → “사용자 명시적 로그아웃 처리”
'Sprint_FESI11 > Project' 카테고리의 다른 글
| Flux와 Redux 개념을 UPDO 프로젝트에 대입 (0) | 2025.10.27 |
|---|---|
| 프로젝트 전반에서 어떤 React 훅들이 어디에 사용되고 있는가? (0) | 2025.10.16 |
| HttpClient → PolymorphicHttpClient → Service → Component 계층 구조 (0) | 2025.10.16 |
| 소켓(Socket) vs HTTP Client (0) | 2025.10.16 |
| CSR vs SSR vs SSG vs Edge (0) | 2025.10.16 |