상태 관리
Lumie는 하나의 전역 스토어를 쓰지 않고 상태 계층에 따라 서로 다른 도구를 사용합니다.
상태 계층
| 상태 유형 | 주요 도구 | 현재 사용 방식 |
|---|---|---|
| 서버 상태 | TanStack Query | 세션, 목록, 상세 뷰, 대부분의 CRUD 기반 화면 |
| 폼 상태 | React Hook Form | 로그인, 회원가입, 온보딩, 설정, CRUD 다이얼로그, 다단계 폼 |
| 공유 클라이언트 UI 상태 | Zustand | 공유 인증 모달 상태와 서버에서 주입한 공개 테넌트 컨텍스트 |
| 라우트 상태 | Next.js search params | 목록 필터, 정렬, 페이지네이션, 모달 진입 파라미터 |
| 로컬 일시 UI 상태 | React 컴포넌트 상태 | 초안 입력값, 다이얼로그 상태, 대기 중 액션, 뷰 토글 |
소스 경로
| 상태 경계 | 소스 경로 |
|---|---|
| Query 기본값과 서버/브라우저 클라이언트 분리 | lumie-frontend/src/shared/lib/query-client.ts |
| 루트 Query provider | lumie-frontend/src/shared/providers/QueryProvider.tsx |
| orval fetch 브리지 | lumie-frontend/src/shared/api/orval-mutator.ts |
| 공유 API 요청과 refresh 재시도 | lumie-frontend/src/shared/api/base.ts |
| 서버 인증 상태 | lumie-frontend/src/entities/session/api/getServerUser.ts |
| 세션 캐시와 공유 인증 접근 브리지 | lumie-frontend/src/shared/lib/sessionCache.ts, lumie-frontend/src/shared/api/sessionAccessor.ts |
| 인증 리다이렉트 사유 매핑 | lumie-frontend/src/shared/lib/authRedirectReason.ts |
| 클라이언트 인증 모달 스토어 | lumie-frontend/src/shared/providers/AuthModalProvider.tsx |
| 서버에서 주입한 공개 테넌트 컨텍스트 | lumie-frontend/src/entities/tenant/providers/PublicTenantContextHydrator.tsx |
| URL 기반 목록 상태 | lumie-frontend/src/entities/student/model/search-params.ts, lumie-frontend/src/shared/lib/useUrlPageParam.ts |
TanStack Query를 이용한 서버 상태
QueryProvider는 루트에서 QueryClientProvider를 한 번만 마운트합니다.
getQueryClient()는 다음을 사용합니다.
- 서버 요청마다 새로운 query client
- 브라우저에서는 안정적인 singleton query client
기본 Query 동작은 src/shared/lib/query-client.ts에서 중앙 정의됩니다.
- 서버
staleTime은 한 번의 SSR 패스에서 중복 fetch를 피하기 위해Infinity - 브라우저
staleTime은 5분 - Query 캐시 garbage collection은 10분 동안 warm 상태 유지
- window-focus refetch는 기본 비활성화
- 오래 실행되는 polling은 opt-in이며, 제한 없이 백그라운드 refetch를 계속하는 대신 최종 상태 또는 timeout에서 멈춰야 합니다
대부분의 API 읽기와 쓰기는 src/shared/api/orval-mutator.ts로 위임하는 orval
생성 hook에서 옵니다. 이 mutator는 다시 공유 apiRequest()와 apiUpload()
헬퍼를 사용합니다. 생성된 클라이언트만으로 부족한 경우 수동 hook도 있지만,
같은 공유 fetch 기본 요소를 사용합니다.
세션 및 인증 상태
인증된 사용자 상태는 커스텀 전역 스토어에 저장하지 않습니다.
- 서버 레이아웃은 접근 제어를 위해
getServerUser()를 호출합니다. - 클라이언트 컴포넌트는
useMe()또는useMeQuery()를 사용합니다. apiRequest()는tryRefreshToken()으로 401 재시도를 처리하며, 이 함수는 단순 boolean이 아니라 타입이 있는 성공/실패 결과를 반환합니다.- 백엔드의
AUTH_017은 같은 디바이스 카테고리에서 다른 로그인이 발생해 현재 브라우저 세션이 교체되었다는 뜻입니다. 공유 API 레이어는 이 코드를 URL-safereason=session_replaced로그인 안내로 매핑합니다. sessionAccessor와sessionCache는shared가 엔티티 코드를 import하지 않고도 공유 API 코드에서 테넌트 slug, 테넌트 리다이렉트 컨텍스트를 읽고 세션 상태를 지울 수 있게 합니다.sessionCache.getTenantRedirectContext()는 현재 테넌트 query 데이터, 캐시된/v1/me의 백엔드 검증 테넌트 URL 필드, 콜드 인증 경계 리다이렉트용 브라우저 host/path 대체 경로 순서로 테넌트 컨텍스트를 선택합니다.
이렇게 하면 인증 관심사를 여러 스토어에 중복시키지 않고 Query 레이어 가까이에 둘 수 있습니다.
Zustand를 이용한 공유 클라이언트 UI 상태
Zustand는 현재 두 개의 좁은 클라이언트 브리지에 사용됩니다.
src/shared/providers/AuthModalProvider.tsx는 다음을 유지합니다.
- 로그인용 또는 회원가입용으로 모달이 열려 있는지
- 정제된
callbackUrl - 열기, 초기화, URL 기반 초기화를 위한 액션
src/entities/tenant/providers/PublicTenantContextHydrator.tsx는 현재
/:customId 랜딩 페이지에 대해 서버가 해석한 공개 테넌트를 유지합니다.
usePublicTenantAuthContext()는 브라우저의 공개 테넌트 query로 대체하기 전에
이 store를 먼저 읽기 때문에, 공유 인증 모달은 랜딩 페이지를 렌더링할 때
검증된 것과 같은 tenantSlug를 사용할 수 있습니다.
두 store의 범위는 의도적으로 좁습니다. 대부분의 페이지 수준 UI 상태는 여전히 해당 컴포넌트 로컬에 둡니다.