개발 지식 모음/FE

Zustand Persist 사용 시 인증 상태가 초기화되는 문제 해결하기

calendar2 2026. 2. 5. 22:10

프로젝트에서 관리자 페이지를 구현하던 중 이상한 현상을 마주쳤다. 분명 로그인된 상태인데, 관리자 페이지에 접근하거나 새로고침을 하면 로그인 페이지로 튕겨나가는 것이다. 이 글에서는 문제의 원인과 해결 과정을 정리하고, 그 과정에서 알게 된 Hydration 개념에 대해 공유하려 한다.

문제 상황

현재 프로젝트에서는 Zustand의 persist 미들웨어를 사용해 인증 정보를 localStorage에 저장하고 있다. 로그인 후 관리자 페이지(/admin)에 접근하면 AdminGuard 컴포넌트가 인증 여부를 체크하는 구조다.

그런데 이미 로그인된 상태에서 관리자 페이지로 직접 접근하거나 새로고침을 하면, 매번 로그인 페이지로 리다이렉트되는 문제가 발생했다.

콘솔 로그를 찍어보니 원인이 보였다.

[AdminGuard] accessToken: null, user: null    // 페이지 진입 직후
[AdminGuard] accessToken: "eyJ...", user: {...}  // 약 0.3초 후

 

페이지 진입 직후에는 인증 정보가 null이었다가, 아주 짧은 시간 뒤에 정상 값이 들어오고 있었다. 문제는 AdminGuard가 이 짧은 순간에 비로그인 상태로 판단하고 리다이렉트를 실행해버린다는 것이었다.

원인 분석: Hydration이 뭐길래?

이 문제를 이해하려면 Hydration이라는 개념을 알아야 한다.

Hydration의 의미

Hydration은 원래 "건조한 것에 물을 주어 되살린다"는 의미다. 프로그래밍에서는 저장된 데이터를 런타임 상태로 복원하는 과정을 뜻한다.

Next.js를 공부했다면 SSR 맥락에서 이 용어를 들어봤을 것이다. 서버에서 생성한 정적 HTML에 JavaScript가 이벤트 핸들러를 연결해서 인터랙티브하게 만드는 과정이 바로 Hydration이다.

NextJS 사전렌더링

 

중요한 건 Hydration이 특정 프레임워크에 종속된 용어가 아니라는 점이다. "저장된 상태 → 활성 상태로 복원"이라는 개념은 다양한 곳에서 동일하게 사용된다.

맥락 무엇을 복원하나
Next.js SSR 정적 HTML → 인터랙티브 컴포넌트
Zustand Persist localStorage JSON → 스토어 상태
ORM (Hibernate 등) DB row → 객체 인스턴스

Zustand Persist의 Hydration

Zustand의 persist 미들웨어는 스토어 상태를 localStorage에 자동으로 저장하고, 페이지 로드 시 이를 다시 복원한다. 이 복원 과정이 바로 Hydration이다.

문제는 이 Hydration이 비동기적으로 일어난다는 점이다.

1. 페이지 로드 → Zustand 스토어 초기화 (초기값: null)
2. localStorage에서 데이터 읽기 시작
3. AdminGuard의 useEffect 실행 → accessToken이 null이므로 리다이렉트!
4. localStorage 데이터 복원 완료 (이미 늦음)

 

React 컴포넌트의 useEffect가 Hydration보다 먼저 실행되면서, 아직 복원되지 않은 null 상태를 기준으로 인증 체크가 이루어진 것이다.

해결 방법

해결 아이디어는 간단하다. Hydration이 완료될 때까지 인증 체크를 보류하면 된다.

1. AuthStore에 isHydrated 플래그 추가

Zustand persist는 onRehydrateStorage 콜백을 제공한다. 이 콜백은 Hydration이 완료된 시점에 호출된다.

// useAuthStore.ts
interface AuthState {
  accessToken: string | null;
  user: User | null;
  isHydrated: boolean;  // 추가
  setHydrated: (state: boolean) => void;  // 추가
  // ... 기타 상태
}

export const useAuthStore = create<AuthState>()(
  persist(
    (set) => ({
      accessToken: null,
      user: null,
      isHydrated: false,
      setHydrated: (state) => set({ isHydrated: state }),
      // ... 기타 액션
    }),
    {
      name: 'auth-storage',
      onRehydrateStorage: () => (state) => {
        state?.setHydrated(true);  // Hydration 완료 시 플래그 설정
      },
    }
  )
);

2. AdminGuard에서 Hydration 대기

이제 AdminGuard에서 isHydrated를 확인하고, Hydration이 완료되기 전까지는 인증 체크를 하지 않도록 수정한다.

// AdminGuard.tsx
export function AdminGuard({ children }: { children: React.ReactNode }) {
  const router = useRouter();
  const { accessToken, user, isHydrated } = useAuthStore();

  const isAuthenticated = !!accessToken;
  const isAdmin = user?.role === 'ADMIN';

  useEffect(() => {
    // Hydration 완료 전에는 체크하지 않음
    if (!isHydrated) return;

    if (!isAuthenticated) {
      router.replace('/login');
      return;
    }

    if (!isAdmin) {
      router.replace('/');
    }
  }, [isHydrated, isAuthenticated, isAdmin, router]);

  // Hydration 중이거나 권한 없으면 로딩 표시
  if (!isHydrated || !isAuthenticated || !isAdmin) {
    return (
      <div className="flex items-center justify-center min-h-screen">
        <Loader2 className="w-8 h-8 animate-spin" />
      </div>
    );
  }

  return <>{children}</>;
}

 

핵심은 if (!isHydrated) return; 부분이다. 이 한 줄이 Hydration이 완료되기 전에 성급하게 리다이렉트하는 것을 막아준다.

결과

수정 후에는 다음과 같은 흐름으로 동작한다.

1. 페이지 로드 → 로딩 스피너 표시 (isHydrated: false)
2. localStorage에서 데이터 복원 중...
3. Hydration 완료 → isHydrated: true
4. AdminGuard가 복원된 인증 정보로 체크
5. 인증 확인되면 관리자 페이지 렌더링

 

이제 로그인된 사용자가 관리자 페이지에서 새로고침을 해도, 불필요하게 로그인 페이지로 튕겨나가는 일이 없어졌다.

정리

이번 트러블슈팅을 통해 배운 점을 정리하면 다음과 같다.

Zustand persist를 사용할 때 페이지 로드 직후의 상태는 초기값이다. localStorage에서 복원되는 Hydration은 비동기로 일어나기 때문에, 인증 체크처럼 타이밍이 중요한 로직에서는 반드시 Hydration 완료 여부를 확인해야 한다.

또한 Hydration은 특정 프레임워크의 전유물이 아니다. "저장된 상태를 런타임으로 복원한다"는 범용적인 개념이므로, 다양한 맥락에서 마주칠 수 있다는 점도 기억해두면 좋겠다.

 

꼭 서포터즈 활동 때문은 아니지만 블로그 글에 이정환 강사님의 자료를 사용하였기에 출처 겸 서포터즈 활동으로 링크를 남겨두려 한다.

참고자료

이 링크를 통해 구매하시면 제가 수익을 받을 수 있어요. 🤗
https://inf.run/di1ro

 

한 입 크기로 잘라먹는 Next.js(v15)| 이정환 Winterlood - 인프런 강의

현재 평점 5.0점 수강생 5,207명인 강의를 만나보세요. 한입 시리즈의 3번째 작품! 세상에서 가장 친절하고 디테일 한 Next.js(15+)강의 입니다. App Router 뿐만 아니라 Page Router까지 프로젝트를 통해 살

www.inflearn.com