zustand - persist 미들웨어 문제인 줄 알았던 것...

지헌·2025년 4월 9일

Banbok

목록 보기
6/8

해결될 때까지 주절주절 쓰는 글...

로그아웃 시 api를 통해 쿠키 값은 제대로 삭제되었지만 localStorage.removeItem("auth-storage")는 삭제 된 후 다시 생성되는 오류가 발생하였습니다.

이는 Zustand의 persist 미들웨어가 상태를 다시 저장하려고 시도하기 때문에 문제가 발생한건데 이를 해결해볼려고 합니다.

Zustand + persist 미들웨어의 함정인 줄 알았던 것...

🧠 핵심 원인: Zustand는 “스토어 인스턴스가 살아 있는 한, 다시 저장한다”
즉...
persist.clearStorage()는 로컬스토리지만 지움 (메모리의 store는 그대로 살아 있음)

근데 그 살아 있는 store가 set된 상태를 다시 저장함 → 또 로컬스토리지에 들어감 😱

  1. logout 시 zustand store 자체를 reset 하기 -> 실패
  2. 그래서 persist 를 제거해봤지만 똑같은 문제 발생

새로 코드를 짜는데 일단 착각했던 내용 zustand를 사용해서 상태 관리를 zustand 값을 로컬스토리지에 저장해서 상태 관리를 해주어야한다고 생각했는데 굳이 그럴 필요없이 zustand에 저장(로컬스토리지 대신) 그 zustand state를 이용하여 데이터를 뿌려주면서 관리할 수 있다.

아무튼 persist 사용하지 않는 방법으로 구현해도 로그아웃 후 데이터 삭제 console.log 확인 null 찍힘 하지만 새로고침하면 원상복구

zustand는 메모리 기반이라서 페이지가 새로고침되면 상태가 초기화되는데 새로고침 후에도 로그인 상태가 복구되는 건 다른 방식으로 작동 중이란 의미로 판단....

🤔 정리 2: 새로고침 후 상태가 복구되는 이유는?

👉 layout.tsx에서 로그인 상태를 서버에서 다시 받아오기 때문!
근데 왜? 분명 로그아웃 api를 통해 액세스 토큰이 삭제된 것을 확인이 되는데 대체 로그인 상태를 어디서 받아오는거지??

❗️문제 내용
✅ 서버는 httpOnly 쿠키로 access token을 줌
✅ 클라이언트는 /v1/members/me를 호출해서 로그인 상태 판단
✅ 로그아웃 시 /v1/auth/logout 호출함 (서버에 토큰 삭제 요청)
❌ 그런데 로그아웃 후 새로고침하면 또 로그인된 상태가 된다?
➡️ 이건 비정상이야. 절대로 정상적인 플로우가 아니야.

????????chat GPT도 그렇다는데???????????????

현재 00시 21분 서버 문제인지 체크 중....

일단 access token이 없는데, 어떻게 auth를 뚫었는지?
CustomUserDetail이 어떻게 값을 알고있는지 알아야함
일단 access token이 없는 경우, 아예 에러를 반환하도록 코드를 추가했음.
이러니까 일단 해결은 됐는데...
우리가 궁금한건 ?
access token이 없었는데 어떻게 유저의 ID를 스프링이 알고있었는지?

👉 이는 스프링 시큐리티가 이전 로그인 했던 세션 정보를 가지고 있어서 토큰이 없더라고 로그인 된 것처럼 인식을 하는 에러였던 것!

궁금적 해결 방안

  1. 토큰이 없을 때 에러를 반환하는 방법(임시 방편인거 같아 더 좋은 방법으로 수정)
  2. 스프링 시큐리티가 세션 정보를 유지하지 않도록 수정하여 해결(정답)

프론트 코드 수정

이제 zustand에서 persist를 다시 추가해줘야한다.

zustand로 충분히 상태를 전역 관리 할 수 있지만 persist 미드웨어를 사용하면 상태를 로컬 스토리지를 통해 브라우저에서 영구적으로 저장할 수 있어서 페이지를 새로고침을 해도 상태가 유지되도록 UX를 개선할 수 있기 때문입니다.

✅ persist를 사용하면 생기는 차이점

항목persist ❌persist 사용 시
상태 저장 위치메모리 (휘발성)localStorage, sessionStorage 등 (비휘발성)
새로고침 시 상태초기화됨 (null로 리셋)그대로 유지됨
로그인 상태 유지새로고침 시 사라짐새로고침 후에도 유지됨
최초 로딩 속도fetchUser로 서버 호출 필요저장된 유저 정보로 빠른 렌더링 가능

persist를 추가해서 해결한 UX로는 로그인 / 로그아웃 버튼 같은 경우에 새로고침을 누르면 깜빡거리면서 로그인 버튼이나 로그아웃 버튼이 잠시 보이는데 이를 그냥 빈 칸으로 보여주도록 개선하였습니다.

주의 할 점

persist를 사용하더라도 보안적 이슈가 발생할 수 있는 토큰은 저장하면 안됩니다.

또한 , 쿠키 기반 인증이면 persist는 필수가 아니지만 지금처럼 http-only 쿠키로 인증을 하고 있다면, 새로고침 시에도 fetchUser()를 백그라운드에서 호출하여 로그인이 유지시키는 방식을 사용하면 되기 때문에 필요가 없지만, UX 개선 차원에 persist를 추가해준 것입니다.

그래서 persist로 상태를 저장해주고 클라이언트에서 hydration가 완료된 이후에만 원하는 버튼을 보여주도록 UX를 개선했습니다.

"use client";

import { useAuthStore } from "../../_store/authStore";
import LoginButton from "../Button/LoginButton";
import LogoutButton from "../Button/LogoutButton";
import { useState, useEffect } from "react";

const LoginStatusButton = () => {
  const user = useAuthStore((state) => state.user);
  const [isHydrated, setIsHydrated] = useState(false);

  useEffect(() => {
    setIsHydrated(true);
  }, []);

  if (!isHydrated) return null; // 아직 hydration 안 됐으면 아무것도 안 보여줌

  const isAuthenticated = !!user;

  return (
    <nav className="flex items-center gap-4">
      {isAuthenticated ? <LogoutButton /> : <LoginButton />}
    </nav>
  );
};

export default LoginStatusButton;

사실 코드만 봤을 때 그냥 상태값에 "false"을 주면 해결되는거 아닌가 싶어서 zustand에서 persist를 제거를 하고 새로고침을 눌러보니 아주 미세하게 로그인 버튼이 보이는 현상이 발생했습니다. 그래서 persist를 통한 상태관리를 했을 때 깔끔한게 구현이 된 것을 확인 했습니다.

profile
차곡차곡 그만 쌓아올리고 취업해서 부딪쳐보고 싶은

0개의 댓글