🟠 원래 생각했던 방식:
새로고침이나 페이지 이동 시 zustand 상태 초기화 -> useEffect()로 수동 복원
- 작동 방식:
- 상태가 브라우저 스토리지(localStorage/sessionStorage)에 저장됨.
- 페이지 새로고침이나 이동 시 zustand 상태가 초기화됨.
- useEffect()를 사용해 브라우저 스토리지에서 상태를 가져와 zustand 상태를 수동으로 복원.
장점:
- 상태 복원의 흐름을 직접 제어할 수 있어 디버깅이 직관적.
- 특정 조건에 따라 복원 로직을 유연하게 커스터마이징 가능.
단점:
- 불필요한 보일러플레이트 코드 증가
- 매번 useEffect()로 상태를 복원해야 하므로, 코드가 장황해지고 중복 발생 가능.
- 유지보수 어려움
- 상태 복원 로직이 여러 컴포넌트에 분산되면, 수정 시 모든 관련 코드를 검토해야 함.
- 비효율성
- 상태 복원이 수동으로 이루어지므로, 불필요한 로직 실행으로 인한 성능 저하 가능
🟢 강사님이 제안한 방식:
Zustand의 persist() 미들웨어로 자동 관리
- 작동 방식:
- 상태 저장 및 복원을 zustand의 persist() 미들웨어가 자동으로 처리.
- 상태를 브라우저 스토리지에 저장하고, 새로고침/페이지 이동 시 자동으로 복원.
- 개발자는 별도의 useEffect()로 상태 복원 로직을 구현할 필요가 없음.
장점:
- 코드 간소화 및 유지보수성 향상
- zustand의 설정만으로 상태 저장과 복원이 이루어지므로, 복잡한 로직 작성 불필요.
- 전역 상태 관리 용이
- 프로젝트 전반에서 상태를 공유하고 관리할 수 있어, 데이터 동기화가 쉬움.
- 확장성
- 저장소(localStorage, sessionStorage, IndexedDB 등)를 유연하게 선택 가능.
- 자동 복원
- 페이지 이동, 새로고침 후에도 상태가 자동으로 복원되므로, 사용자 경험이 향상됨.
- 중복 제거
- 복원 로직이 중복될 일이 없어, 코드가 더 깔끔하고 유지보수가 쉬움.
단점:
- 사실상 단점이 거의 없음.
단, 복잡한 조건에 따른 상태 복원이 필요하다면 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를 사용하면 런타임 중 특정 조건에 따라 옵션을 변경할 수 있습니다.
- 전역 상태 동기화 및 유지
zustand의 persist() 미들웨어를 사용하면 user 상태가 모든 컴포넌트에서 일관되게 유지됩니다.- 페이지 이동이나 새로고침 시에도 상태가 초기화되지 않고 자동으로 복원됩니다.
- 전역 상태 관리의 통일성을 보장하므로, 수동으로 상태를 복원할 필요가 없습니다.
- 스토리지 설정 간소화
setOptions()를 활용하면 autoLogin 값에 따라 localStorage와 sessionStorage를 동적으로 설정할 수 있습니다.- persist() 미들웨어의 setOptions() 메서드를 통해 런타임 시 동적으로 스토리지 종류를 변경할 수 있습니다.
- 이 접근법은 유저의 로그인 상태(autoLogin)에 따라 필요한 스토리지를 효율적으로 설정할 수 있어, 코드의 유연성과 유지보수성을 높여줍니다.
- 효율적이고 간결한 코드
- zustand 미들웨어는 상태 저장과 복원을 자동으로 처리하여 useEffect()로 상태를 수동 복원하는 번거로움을 제거합니다.
- 코드의 간결성과 유지보수성이 대폭 향상되어 더 직관적이고 관리하기 쉬운 상태 관리 로직을 구현할 수 있습니다.
💡 결론
zustand의 persist()와 createJSONStorage()는 상태 관리의 복잡성을 줄이고, 전역적으로 일관된 상태를 유지할 수 있는 강력한 도구입니다. 특히, 동적 스토리지 설정이 필요한 경우 setOptions를 활용하면 효율적인 동작과 유연성을 동시에 확보할 수 있습니다.
*setOptions()는 zustand의 persist() 미들웨어와 함께 사용하여 동적 스토리지 설정을 가능하게 하며, createJSONStorage()는 기본적으로 스토리지를 생성하는 데 사용됩니다. 동적 설정이 필요한 경우 setOptions()를 꼭 활용해야 합니다.
<최종 요약>
전역 상태 관리의 핵심은 zustand의 persist() 미들웨어 활용
- 변경 전: create()만 사용 → 전역 상태 불일치, 수동 복원 필요 → 비효율적.
- 변경 후: persist()와 createJSONStorage() 사용 → 상태 자동 복원, 일관된 전역 상태 관리 → 효율적이고 전문적인 코드 완성.
- persist() 미들웨어는 전역 상태를 통합적으로 관리하며, 상태 저장과 복원을 자동화.
- setOptions()로 동적 스토리지 설정을 지원하여 유저의 요구에 맞춘 유연한 상태 관리 가능.
- 변경된 접근 방식은 효율적이고 전문적인 코드로, 상태 관리의 복잡성을 크게 줄임.
💥 변경 전 문제점
- 스토어 생성 방식
create() 함수만 사용하여 상태 관리.- 컴포넌트 간 전역 상태의 불일치 발생.
- 한 컴포넌트에서 설정된 user 값이 다른 컴포넌트에서 초기화(null)되는 오류 발생.
- 상태 복원 문제
새로고침 또는 페이지 이동 시, 상태 초기화 문제 해결을 위해 useEffect()로 수동 복원.- 코드가 복잡하고 비효율적이며 유지보수 어려움.
✅ 변경 후 해결책
- persist() 미들웨어 활용
- 전역적으로 상태를 자동으로 저장 및 복원.
- 모든 컴포넌트에서 일관된 전역 상태 관리 가능.
- 동적 스토리지 설정
- setOptions()를 활용해 유저의 autoLogin 값에 따라 **localStorage 또는 sessionStorage**를 동적으로 설정 가능.
- 런타임 시 스토리지 종류를 변경하여 유연하고 효율적인 상태 관리를 지원.
- 코드 간소화
- 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;
'Final Project (React)' 카테고리의 다른 글
<Navigate> vs useNavigate() (0) | 2025.01.31 |
---|---|
자동로그인 시 액세스 토큰 및 리프레시 토큰 관리 (0) | 2025.01.12 |
파일 업로드 API 사용법 (0) | 2025.01.10 |
스크롤 위치가 이전 페이지 상태를 유지하거나 내려간 상태로 이동하는 문제 (1) | 2025.01.09 |
Query Parameters와 Path Parameters의 차이 (0) | 2025.01.07 |