
개요
개인 프로젝트를 진행하던 중 마이페이지에서 새로고침 시 로그인 상태가 유실되어 강제로 로그인 페이지로 리다이렉트되는 문제가 발생했습니다.
로그인에 성공한 후 메인 페이지에서는 인증 상태가 정상적으로 유지되었지만, 페이지에서 새로고침을 하면 localStorage에 유효한 토큰이 남아있음에도 불구하고 사용자를 로그아웃시키는 현상이었습니다.
본 글에서는 이러한 문제에 대한 원인 분석과 해결 과정을 기록하고자 합니다.
🚨 문제 상황
- 문제 현상
- 로그인이 성공하여 메인 페이지에서는 인증 상태가 잘 유지되지만, 마이페이지에서 새로고침을 하면 강제로 로그인 페이지로 리다이렉트됨.
- 사용자 경험
- 새로고침할 때마다 다시 로그인해야 하는 치명적인 불편함 발생.
🔍 원인 분석
문제의 코드

핵심 원인
- Zustand의 메모리 기반 특성
- Zustand는 기본적으로 메모리 기반 상태 관리 라이브러리입니다.
- persist 미들웨어를 사용하면 localStorage에 상태를 저장하여 새로고침 후에도 데이터를 유지할 수 있지만, 페이지가 새로고침되는 순간 Zustand 스토어는 일단 초기값(memberId: null)으로 시작됩니다.
- Hydration 복구 지연
- 클라이언트 사이드에서 실행될 때, Zustand의
persist미들웨어가 로컬스토리지에서 데이터를 읽어와 상태를 복구(Hydration)하는 데 수 밀리초의 시간이 소요됩니다.
- 클라이언트 사이드에서 실행될 때, Zustand의
- Race Condition (경쟁 상태)
- 문제는 상태가 복구되기도 전에 useEffect 내의 인증 체크 로직이 먼저 실행된다는 점입니다
t=0ms : 페이지 새로고침
→ Zustand 스토어 초기화: memberId = null
t=1ms : useEffect 실행
→ if (!memberId) 조건 충족
→ router.push('/login') <--
t=5ms : localStorage 복구 완료
→ memberId = "user123" (하지만 이미 늦음)
t=10ms : /login 페이지로 이동 완료
즉, localStorage에 유효한 토큰이 있음에도 불구하고 복구되기 전에 검증 로직이 실행되어 사용자를 강제로 로그아웃시키는 현상이 발생한 것입니다.
Hydration이란❓
Hydration은 Next.js와 같은 SSR/SSG 프레임워크에서 발생하는 서버 렌더링 HTML과 클라이언트 자바스크립트를 연결하는 과정입니다.

Zustand의 persist 미들웨어는 4번 Hydration 단계에서 localStorage의 데이터를 읽어와 전역 상태를 복구합니다.
이 과정은 비동기적이며 수 밀리초가 소요됩니다.
즉, 유효한 인증 정보가 있음에도 복구되기 전에 검증 로직이 실행되어 사용자를 로그아웃시키는 현상입니다.
💡 해결 방법
전략: Rehydration 완료를 보장하는 가드 패턴
- Zustand의 onRehydrateStorage 라이프사이클 훅을 활용하여 복구 완료 시점을 명시적으로 추적합니다.
onRehydrateStorage 라이프사이클 훅이란❓
onRehydrateStorage는 Zustand의 persist 미들웨어가 제공하는 라이프사이클 콜백 함수입니다.
Storage에서 저장된 상태를 읽어와 Zustand 스토어에 복구(rehydrate)하는 과정의 시작과 완료 시점을 감지할 수 있게 해주는 훅입니다.
[Step 1] Store에 Hydration 상태 플래그 추가
onRehydrateStorage를 이용해 복구가 끝난 시점을 전역 상태로 관리합니다.

Store에 Hydration 완료 여부를 추적하는 _hasHydrated 플래그를 추가합니다.
onRehydrateStorage 라이프사이클 훅은 localStorage에서 데이터 복구가 완료되는 시점에 자동으로 호출되며, 이때 setHasHydrated(true)를 실행하여 복구 완료 상태를 전역적으로 알립니다.
이 플래그를 통해 컴포넌트들은 "아직 복구 중인지" 혹은 "복구가 끝났는지" 를 정확히 판단할 수 있게 됩니다.
[Step 2] 컴포넌트에 가드 로직 적용

컴포넌트에서 _hasHydrated 플래그를 활용하여 2단계 가드 로직을 구현합니다.
- 첫 번째 가드
- Hydration이
완료되지 않았다면 인증 검증을 유예하고 조기 반환합니다. - 이를 통해 복구 전의 null 상태를 "인증 실패"로 오판하는 것을 방지합니다.
- Hydration이
- 두 번째 가드
- Hydration 완료 후에만 실행되며, 이 시점에서 memberId가 없다면 진짜 미인증 상태이므로 로그인 페이지로 리다이렉트합니다.
💡 해결 방법
Next.js 환경에서 Zustand persist를 사용할 때 발생하는 Hydration Race Condition은 예상치 못한 UX 문제를 야기할 수 있습니다.
본 트러블슈팅을 통해 onRehydrateStorage 라이프사이클 훅과 _hasHydrated 플래그를 활용한 가드 패턴으로 이 문제를 근본적으로 해결할 수 있었습니다.
이제 사용자는 새로고침 시에도 안정적으로 로그인 상태를 유지할 수 있게 되었으며, 불필요한 재로그인 없이 원활한 서비스 이용이 가능해졌습니다.
클라이언트 상태 관리에서 Hydration 타이밍을 명시적으로 제어하는 것의 중요성을 다시 한번 체감할 수 있었던 경험이었습니다.
'Trouble Shooting' 카테고리의 다른 글
| [트러블슈팅] POI명 vs 실제 주소: Tmap 경로 검색 400 에러 해결하기 (0) | 2026.02.11 |
|---|---|
| [트러블 슈팅] TMAP API 다중 경로 조회 : Mono.zip으로 동시 요청 처리하기 (0) | 2026.02.06 |
| [트러블 슈팅] 할인 계산 로직 개선: 쿠폰 할인 적용 오류 수정 (0) | 2024.10.19 |
| [트러블 슈팅] 사용자 직접 취소 시 다중 결과 반환 트러블 슈팅 (0) | 2024.10.11 |
| [트러블 슈팅] BeanCreationException 빈 충돌 해결을 위한 @ConditionalOnProperty 활용 (0) | 2024.10.11 |