Final Project (React)

[자동로그인 기능 구현] zustand middleware를 활용한 persist와 setOptions으로 유연하고 일관된 상태 관리

taytay 2025. 1. 13. 22:26

🟠 원래 생각했던 방식:

새로고침이나 페이지 이동 시 zustand 상태 초기화 -> useEffect()로 수동 복원

  • 작동 방식:
    1. 상태가 브라우저 스토리지(localStorage/sessionStorage)에 저장됨.
    2. 페이지 새로고침이나 이동 시 zustand 상태가 초기화됨.
    3. useEffect()를 사용해 브라우저 스토리지에서 상태를 가져와 zustand 상태를 수동으로 복원.

장점:

  • 상태 복원의 흐름을 직접 제어할 수 있어 디버깅이 직관적.
  • 특정 조건에 따라 복원 로직을 유연하게 커스터마이징 가능.

단점:

  1. 불필요한 보일러플레이트 코드 증가
    • 매번 useEffect()로 상태를 복원해야 하므로, 코드가 장황해지고 중복 발생 가능.
  2. 유지보수 어려움
    • 상태 복원 로직이 여러 컴포넌트에 분산되면, 수정 시 모든 관련 코드를 검토해야 함.
  3. 비효율성
    • 상태 복원이 수동으로 이루어지므로, 불필요한 로직 실행으로 인한 성능 저하 가능

🟢 강사님이 제안한 방식:

Zustand의 persist() 미들웨어로 자동 관리

  • 작동 방식:
    1. 상태 저장 및 복원을 zustand의 persist() 미들웨어가 자동으로 처리.
    2. 상태를 브라우저 스토리지에 저장하고, 새로고침/페이지 이동 시 자동으로 복원.
    3. 개발자는 별도의 useEffect()로 상태 복원 로직을 구현할 필요가 없음.

장점:

  1. 코드 간소화 및 유지보수성 향상
    • zustand의 설정만으로 상태 저장과 복원이 이루어지므로, 복잡한 로직 작성 불필요.
  2. 전역 상태 관리 용이
    • 프로젝트 전반에서 상태를 공유하고 관리할 수 있어, 데이터 동기화가 쉬움.
  3. 확장성
    • 저장소(localStorage, sessionStorage, IndexedDB 등)를 유연하게 선택 가능.
  4. 자동 복원
    • 페이지 이동, 새로고침 후에도 상태가 자동으로 복원되므로, 사용자 경험이 향상됨.
  5. 중복 제거
    • 복원 로직이 중복될 일이 없어, 코드가 더 깔끔하고 유지보수가 쉬움.

단점:

  • 사실상 단점이 거의 없음.
    단, 복잡한 조건에 따른 상태 복원이 필요하다면 zustand 설정을 추가적으로 커스터마이징해야 함.

결론

  • persist() 미들웨어 방식이 명확히 더 나은 선택입니다.
    코드의 간결성, 유지보수성, 확장성 면에서 모두 우위에 있으며, 단점이 거의 없는 완성도 높은 상태 관리 방법입니다.
  • 강사님의 설명처럼, 장기적인 관점에서 zustand의 persist()를 활용하는 방식은 매우 효율적인 솔루션입니다. 🎉

 

비교 요약

항목 useEffect로 수동 복원 persist() 미들웨어 사용
코드 간결성 복잡한 로직으로 인해 간결하지 않음 설정만으로 자동화, 코드 간결함
유지보수 복원로직이 분산되어 관리가 어려움 중앙화된 상태관리로 유지보수 용이
성능 불필요한 로직 실행가능 효율적이고 최적화된 상태 복원
확장성 상태 복원 로직을 커스터마이징하기 번거로움 다양한 저장소 지원 및 유연한 설정 가능
사용자 경험 새로고침 시 수동 복원이 필요, 빠른 복원 불가 자동 복원으로 사용자 경험 향상

 

menuIcon.jsx

<변경 전>

import useUserStore from "@store/userStore";
import { useEffect } from "react";
import { useNavigate } from "react-router-dom";

const MenuIcons = () => {
  const { user, resetUser, setUser } = useUserStore();
  const navigate = useNavigate();

  const handleLogout = (e) => {
    e.preventDefault();
    resetUser();
    alert(`${user.name} 님, 정상적으로 로그아웃 되었습니다.`);
    navigate("/");
  };

  // ⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️
  // 컴포넌트가 "처음" 마운트될 때 "상태를 확인"하고 UI를 갱신
  useEffect(() => {
    // 로컬스토리지에서 사용자 정보를 불러오는 작업 => 로컬저장소에 유저 정보가 남아 있다면, 그 유저정보를 가져와서 setUser를 이용해 다시 스토리지에 저장한다.
    // ✅ 이때, autoLogin: true라는 속성값도 같이 넘어오므로 다시 로컬스토리지에 저장 가능
    const localStoredUser = localStorage.getItem("user");
    if (localStoredUser) {
      setUser(JSON.parse(localStoredUser));
    }
  }, [setUser]); // setUser는 함수이고, 함수는 변하지 않기 때문에 사실상 setUser를 디펜던시 배열에 넣어도 useEffect는 한 번만 실행(= 빈 배열 넣은 것과 동일한 동작!)
  // => 함수는 디펜던시에 넣으나마나, 무조건 useEffect 내부의 함수는 한 번만 실행되므로, 여기서 setUser를 넣는 것은 선택사항이다. 하지만 리액트 최적화의 권장사항으로서 ESLink 경고가 뜨니까 일단 넣어놓은 것.
  // ⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️
  
  return (
    <div className="absolute top-4 right-6 flex space-x-6 items-center">
      {user ? (
        <form className="flex gap-[20px] items-center" onSubmit={handleLogout}>
          <a href="/order" className="text-gray-700 hover:text-primary-30">
            <i className="fas fa-user"></i>
          </a>
          <a href="/cart" className="text-gray-700 hover:text-primary-30">
            <i className="fas fa-shopping-cart"></i>
          </a>
          <a href="/search" className="text-gray-700 hover:text-primary-30">
            <i className="fas fa-search mr-2"></i>
          </a>
          {user.profile && (
            <img
              className="w-12 h-12 rounded-full object-cover"
              src={`https://11.fesp.shop${user.profile}`}
              alt="프로필 이미지"
            />
          )}
          <p> {user.name} 님 :)</p>
          <button
            type="submit"
            className="bg-primary-40 py-2 px-4 text-white hover:bg-primary-20 rounded font-gowunBold"
          >
            로그아웃
          </button>
        </form>
      ) : (
        <>
          <a href="/search" className="text-gray-700 hover:text-primary-30">
            <i className="fas fa-search mr-2"></i>
          </a>
          <a
            href="/login"
            className="bg-primary-40 px-4 py-2 rounded text-white hover:bg-primary-20 font-gowunBold"
          >
            로그인
          </a>
          <a
            href="/signup"
            className="bg-secondary-20 px-4 py-2 rounded text-white hover:bg-secondary-10 font-gowunBold"
          >
            회원가입
          </a>
        </>
      )}
    </div>
  );
};

export default MenuIcons;

 

<변경 후>

useEffect 코드 삭제 (다른 부분은 모두 동일)

import useUserStore from "@store/userStore";
import { useNavigate } from "react-router-dom";

const MenuIcons = () => {
  const { user, resetUser } = useUserStore();
  const navigate = useNavigate();

  const handleLogout = (e) => {
    e.preventDefault();
    resetUser();
    alert(`${user.name} 님, 정상적으로 로그아웃 되었습니다.`);
    navigate("/");
  };

  return (
    <div className="absolute top-4 right-6 flex space-x-6 items-center">
      {user ? (
        <form className="flex gap-[20px] items-center" onSubmit={handleLogout}>
          <a href="/order" className="text-gray-700 hover:text-primary-30">
            <i className="fas fa-user"></i>
          </a>
          <a href="/cart" className="text-gray-700 hover:text-primary-30">
            <i className="fas fa-shopping-cart"></i>
          </a>
          <a href="/search" className="text-gray-700 hover:text-primary-30">
            <i className="fas fa-search mr-2"></i>
          </a>
          {user.profile && (
            <img
              className="w-12 h-12 rounded-full object-cover"
              src={`https://11.fesp.shop${user.profile}`}
              alt="프로필 이미지"
            />
          )}
          <p> {user.name} 님 :)</p>
          <button
            type="submit"
            className="bg-primary-40 py-2 px-4 text-white hover:bg-primary-20 rounded font-gowunBold"
          >
            로그아웃
          </button>
        </form>
      ) : (
        <>
          <a href="/search" className="text-gray-700 hover:text-primary-30">
            <i className="fas fa-search mr-2"></i>
          </a>
          <a
            href="/login"
            className="bg-primary-40 px-4 py-2 rounded text-white hover:bg-primary-20 font-gowunBold"
          >
            로그인
          </a>
          <a
            href="/signup"
            className="bg-secondary-20 px-4 py-2 rounded text-white hover:bg-secondary-10 font-gowunBold"
          >
            회원가입
          </a>
        </>
      )}
    </div>
  );
};

export default MenuIcons;

 

userStore.js

<변경 전> 

setUser 함수에서 autoLogin값을 뽑아와 true냐 false냐에 따라 유저 정보를 저장할 스토리지 종류를 동적으로 설정(setItem)

대신 persist() 함수를 쓰지 않고, 단순히 zustand의 create함수만을 이용해 유저 스토어 생성. => 💥근본적인 문제 발생 원인💥

=> Persist()를 이용해 스토어를 생성하고 상태값을 관리해야, 각 컴포넌트의 구분없이 user라는 하나의 전역적인 상태값이 통일되게 공유될 수 있는데, 그렇지 못함 => 유저가 한 컴포넌트에서 다른 컴포넌트로 페이지 이동할 때 세팅되었던 user 값이 갑자기 null로 바뀌는 오류 발생 => useEffect()로 일일이 새로고침/페이지 이동 시마다 user값을 재설정해 줄 필요 있음 => 굉장히 번거롭고 멍청한 작업.. => 이 작업을 대신 해주는 zustand의 middleware인 persist, createJSONStorage 사용하는 것이 훨씬 효율적

🔦 문제점과 원인

  • setUser의 동적 스토리지 설정
    • autoLogin 값을 기준으로 true/false를 판단해 유저 정보를 저장할 스토리지를 setItem으로 동적으로 설정.
    • 그러나 zustand의 persist() 미들웨어를 사용하지 않음.
  • 스토어 생성 방식
    • 단순히 zustand의 create() 함수만 사용하여 스토어 생성.
    • 이로 인해 전역 상태 관리의 근본적인 문제 발생.

💥 근본적인 문제

  • 전역 상태의 불일치 문제
    • persist()를 사용하지 않으므로, user 상태가 전역적으로 통일되지 않음.
    • 한 컴포넌트에서 설정된 user 값이 다른 컴포넌트로 페이지 이동 시 초기화(null)되는 오류 발생.
  • 수동 복원 작업의 번거로움
    • 상태가 초기화되므로, 새로고침/페이지 이동 시마다 useEffect()로 user 상태를 일일이 재설정해야 함.
    • 이는 코드가 복잡해지고 유지보수가 어려워지는 비효율적이고 비전문적인 작업을 초래.

 
 
import { create } from "zustand";
// import { persist, createJSONStorage } from "zustand/middleware";

const UserStore = (set) => ({
  user: null,
  setUser: (user) => {
    set({ user });
    const storage = user.autoLogin ? localStorage : sessionStorage;
    // * setItem(): localStorage와 sessionStorage에서 제공하는 메서드로, 지정된 키("user")에 값을 저장한다. (브라우저 내에서 localStorage나 sessionStorage에 접근할 수 있는 곳이라면 어디서든 사용 가능)
    // * JSON.stringify(): 브라우저의 localStorage와 sessionStorage는 문자열만 저장할 수 있기 때문에, 객체나 배열 등의 데이터를 저장하려면 반드시 문자열로 변환해야 하기 때문에 user객체를 JSON 문자열로 변환하였다.
    storage.setItem("user", JSON.stringify(user));
  },
  resetUser: () => {
    set({ user: null });
    localStorage.removeItem("user");
    sessionStorage.removeItem("user");
  },
});

// ❌ autoLogin 상태값에 의해서만 스토리지 종류를 설정하고 싶다면, persist 함수 쓰지말자 ❌
// 👉 persist()의 storage 부분을 아예 제거하여, 상태값은 기본적으로 set을 통해 관리하여 두 스토리지 모두(local & session)에 저장되는 문제 해결.
// ∵ persist 함수만 써주고, storage 속성 값을 지정하지 않아도, 무조건 localStorage에 저장되는 것이 기본 동작, 그렇다고 sessionStorage라고 설정하면 자동로그인 설정 안했을 때(autoLogin: false)도 로컬 스토리지에 저장되어 브라우저창을 닫고도 유저 정보가 계속해서 유지되는 문제 발생.

// 📝 수정된 코드
// * create(): Zustand의 store를 생성할 때 사용되는 함수
const useUserStore = create(UserStore);

// 🚨 문제의 코드
// const useUserStore = create(
//   persist(UserStore, {
//     name: "user",
//     storage: createJSONStorage(() => sessionStorage), // 기본은 localStorage
//   })
// );

export default useUserStore;

 

 

 

<변경 후>

 변경 후 해결책: persist()와 createJSONStorage, setOptions()를  활용한 개선된 상태 관리

❓ setOptions 함수란?

  • zustand의 persist() 미들웨어에서 제공하는 함수로, 스토어의 persist 설정을 동적으로 변경할 수 있게 해줍니다.
  • 일반적으로 persist()를 초기화할 때 설정한 옵션(name, storage 등)은 고정되지만, setOptions를 사용하면 런타임 중 특정 조건에 따라 옵션을 변경할 수 있습니다.

  1.  
  1. 전역 상태 동기화 및 유지
    zustand의 persist() 미들웨어를 사용하면 user 상태가 모든 컴포넌트에서 일관되게 유지됩니다.
    • 페이지 이동이나 새로고침 시에도 상태가 초기화되지 않고 자동으로 복원됩니다.
    • 전역 상태 관리의 통일성을 보장하므로, 수동으로 상태를 복원할 필요가 없습니다.
  2. 스토리지 설정 간소화
    setOptions()를 활용하면 autoLogin 값에 따라 localStorage와 sessionStorage를 동적으로 설정할 수 있습니다.
    • persist() 미들웨어의 setOptions() 메서드를 통해 런타임 시 동적으로 스토리지 종류를 변경할 수 있습니다.
    • 이 접근법은 유저의 로그인 상태(autoLogin)에 따라 필요한 스토리지를 효율적으로 설정할 수 있어, 코드의 유연성과 유지보수성을 높여줍니다.
  3. 효율적이고 간결한 코드
    • zustand 미들웨어는 상태 저장과 복원을 자동으로 처리하여 useEffect()로 상태를 수동 복원하는 번거로움을 제거합니다.
    • 코드의 간결성과 유지보수성이 대폭 향상되어 더 직관적이고 관리하기 쉬운 상태 관리 로직을 구현할 수 있습니다.

💡 결론
zustand의 persist()와 createJSONStorage()는 상태 관리의 복잡성을 줄이고, 전역적으로 일관된 상태를 유지할 수 있는 강력한 도구입니다. 특히, 동적 스토리지 설정이 필요한 경우 setOptions를 활용하면 효율적인 동작과 유연성을 동시에 확보할 수 있습니다.

*setOptions()는 zustand의 persist() 미들웨어와 함께 사용하여 동적 스토리지 설정을 가능하게 하며, createJSONStorage()는 기본적으로 스토리지를 생성하는 데 사용됩니다. 동적 설정이 필요한 경우 setOptions()를 꼭 활용해야 합니다.


<최종 요약>

전역 상태 관리의 핵심은 zustand의 persist() 미들웨어 활용

  • 변경 전: create()만 사용 → 전역 상태 불일치, 수동 복원 필요 → 비효율적.
  • 변경 후: persist()와 createJSONStorage() 사용 → 상태 자동 복원, 일관된 전역 상태 관리 → 효율적이고 전문적인 코드 완성.

 

  • persist() 미들웨어는 전역 상태를 통합적으로 관리하며, 상태 저장과 복원을 자동화.
  • setOptions()로 동적 스토리지 설정을 지원하여 유저의 요구에 맞춘 유연한 상태 관리 가능.
  • 변경된 접근 방식은 효율적이고 전문적인 코드로, 상태 관리의 복잡성을 크게 줄임.

 


💥 변경 전 문제점

  1. 스토어 생성 방식
    create() 함수만 사용하여 상태 관리.
    • 컴포넌트 간 전역 상태의 불일치 발생.
    • 한 컴포넌트에서 설정된 user 값이 다른 컴포넌트에서 초기화(null)되는 오류 발생.
  2. 상태 복원 문제
    새로고침 또는 페이지 이동 시, 상태 초기화 문제 해결을 위해 useEffect()로 수동 복원.
    • 코드가 복잡하고 비효율적이며 유지보수 어려움.

✅ 변경 후 해결책

  1. persist() 미들웨어 활용
    • 전역적으로 상태를 자동으로 저장 및 복원.
    • 모든 컴포넌트에서 일관된 전역 상태 관리 가능.
  2. 동적 스토리지 설정
    • setOptions()를 활용해 유저의 autoLogin 값에 따라 **localStorage 또는 sessionStorage**를 동적으로 설정 가능.
    • 런타임 시 스토리지 종류를 변경하여 유연하고 효율적인 상태 관리를 지원.
  3. 코드 간소화
    • persist()와 createJSONStorage()를 조합하여 기본 스토리지 설정을 자동화.
    • 복잡한 setItem 로직 제거 및 useEffect()를 통한 수동 복원 불필요.
    • 가독성과 유지보수성이 크게 향상된 전문적인 코드 구현.

💡 참고 링크: https://github.com/pmndrs/zustand/blob/main/docs/integrations/persisting-store-data.md#api

import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";

const UserStore = (set) => ({
  user: null,
  setUser: (user) => {
    localStorage.setItem("autoLogin", JSON.stringify(user.autoLogin));

    // setOptions()를 사용하여 user의 autoLogin 값에 따라 저장소를 동적으로 설정가능
    useUserStore.persist.setOptions({
      storage: createJSONStorage(() =>
        user.autoLogin ? localStorage : sessionStorage
      ),
    });
    set({ user });
  },
  resetUser: () => {
    set({ user: null });
    localStorage.removeItem("user");
    sessionStorage.removeItem("user");
  },
});

const useUserStore = create(
  persist(UserStore, {
    name: "user",
    storage: createJSONStorage(() => {
      const autoLogin = localStorage.getItem("autoLogin") === "true";
      console.log(autoLogin);

      // autoLogin 값에 따라 저장소를 동적으로 선택
      return autoLogin ? localStorage : sessionStorage;
    }),
  })
);

export default useUserStore;