문제: 로그아웃했는데 이전 사용자 데이터가 보인다
구성:
- 같은 백엔드를 쓰는 쌍둥이 Next.js 프론트엔드 — 관리자 포털, 사용자 포털
- 인증 상태: Zustand
-
서버 상태: TanStack Query
- 이 분리 자체는 흔하고 합리적인 구성
-
문제 지점: 로그아웃 경로
-
useAuthStore.logout()이 초기화하는 것 — 토큰, 사용자 정보,isAuthenticated플래그 - 딱 거기까지
- React Query의 QueryClient는 미처리
증상:
- A 계정 로그아웃 → B 계정 로그인
- 대시보드·목록 화면에 A의 데이터가 잠깐 노출
- 원인: TanStack Query의 stale-while-revalidate
- 캐시의 이전 응답을 먼저 렌더 → 백그라운드 재요청 → 새 응답 도착 전까지 이전 사용자 화면 노출
- 수동 새로고침해야 B의 데이터로 전환
멀티테넌트 SPA에서 이건 UI 깜빡임이 아니라 정보 노출 버그다.
- 관리 화면 특성상 목록에 다른 조직 이름·자원 이름이 그대로 적재
원인: 캐시 수명 경계를 아무도 안 그었다
- 인증 상태와 서버 캐시를 다른 라이브러리에 위임 → “두 상태의 수명을 누가 맞추나” 질문 발생
-
그 질문에 대한 답: 미정
- Zustand의 auth 상태 수명 = 세션
-
QueryClient 캐시 수명 = QueryClient 인스턴스의 수명 = 앱이 떠 있는 동안
- QueryClient는 Providers에서 1회 생성 후 앱 전체가 공유
- 로그인/로그아웃과 무관하게 존속
- 로그아웃 시 명시적으로 비우지 않으면, 로그아웃은 캐시 입장에서 아무 사건도 아님
어떻게 고쳤나
- 사용자 포털:
NavHeader.handleLogout의finally블록에 한 줄 추가
const queryClient = useQueryClient();
// ...
try {
await logout();
} catch (error) {
// logout error handled silently
} finally {
// 이전 사용자의 React Query 캐시를 비워 새 로그인 후 다른 데이터가
// stale-while-revalidate 로 잠깐 노출되는 문제를 방지한다.
queryClient.clear();
handleMenuClose();
router.push("/");
}
- 위치가
finally인 이유: 로그아웃 API 호출 실패 시에도 클라이언트 캐시는 비워야 함 - 서버 세션 종료 실패 상황에서 브라우저에 이전 사용자 데이터 잔류 = 더 나쁨
- 관리자 포털: 로그아웃이
try/catch없이 메뉴 아이템onClick에서 동기 처리 → 그 자리에 삽입
onClick={() => {
logout();
queryClient.clear();
handleMenuClose();
router.push("/");
}}
- 이 메뉴는
useMemo로 래핑 → 의존성 배열에queryClient추가까지가 한 세트 -
queryClient는 참조가 안정적이라 재계산 증가 없음 -
의존성 누락 시 lint 경고 발생, 나중에 실제 stale closure의 근거가 됨
- 규모: 두 저장소에 1초 차이로 들어간 5줄짜리 수정
clear() vs removeQueries() vs resetQueries()
- 유일한 설계 판단: 셋 중 무엇을 쓸지
| 메서드 | 동작 | 로그아웃에 맞나 |
|---|---|---|
clear() |
캐시 전체 제거 + mutation 캐시까지 초기화 | 적합 |
removeQueries() |
필터에 맞는 쿼리만 캐시에서 제거 | 부분 정리용 |
resetQueries() |
초기 상태로 되돌리고 활성 쿼리는 즉시 refetch | 부적합 |
-
resetQueries()— 명백히 부적합 - 근거: 활성 쿼리 재요청 → 로그아웃 직후엔 토큰 부재 → 401 세례
removeQueries() 검토:
- 필터를 잘 쓰면 동작
- 단 “무엇을 남길까”를 결정해야 함
- 로그아웃 시점에 남겨도 되는 서버 캐시는 사실상 없음
- 공통 코드 목록 등으로 예외를 두는 순간, 그 예외가 사용자별 데이터를 물고 있는지 매번 검토 필요
-
화이트리스트 유지 비용 > 캐시 재조회 비용
- 결론:
clear() - 이유: “세션이 끝나면 서버에서 받아온 모든 것을 버린다”는 규칙은 예외가 없어 검토 불필요
남는 교훈
클라이언트 상태와 서버 상태를 분리하는 건 좋은 설계지만, 분리하면 경계에 규칙이 필요해진다. 이 경우 규칙은 하나다 — 캐시 수명은 세션 수명을 넘지 않는다.
-
그리고 이런 버그는 개발 중 포착이 어려움
- 로컬에서는 대체로 단일 계정 사용
- 계정 전환 시 습관적으로 새로고침
-
A로 확인 → B로 재로그인 → 재확인 흐름을 타야 비로소 노출
- 통합테스트 국면에 이런 종류의 버그가 몰려 나오는 이유