React & Typescript

📝 Zustand를 활용한 React 관리 시스템의 통합 메뉴 및 UI 상태 관리 전략

창따오 2025. 12. 9. 13:40
728x90

복잡한 관리 시스템(Admin System)을 React로 구축할 때, **데이터(메뉴 트리)**와 **UI 상태(활성 탭, 사이드바 확장)**를 효율적으로 관리하는 것은 매우 중요합니다.

이 글에서는 경량의 상태 관리 라이브러리인 Zustand를 사용하여, 백엔드에서 가져온 계층형 메뉴 데이터와 사용자 상호작용 상태를 통합 관리하는 전략을 분석합니다.

 

1. 🚀 Zustand Store 설계 개요

useMenuStore는 단순한 데이터 저장소가 아니라, 애플리케이션의 핵심 네비게이션 로직을 담고 있는 중앙 허브 역할을 합니다.

🔑 핵심 기술: create와 persist

import { create } from "zustand/react";
import { persist } from "zustand/middleware"; // 상태 지속성 확보

export const useMenuStore = create(
    persist( // ✅ persist 미들웨어 적용
        (set, get) => ({
            // ... 상태 정의 및 액션 ...
        }),
        {
            name: "menu-state", // localStorage에 저장될 키
        }
    )
);
  • Zustand의 간결함: set과 get 함수만으로 상태를 쉽게 조작할 수 있어 보일러플레이트 코드가 적습니다.
  • 상태 지속성 (persist): persist 미들웨어를 사용하여 tabs, activeTab, openGroups 등의 UI 상태를 **브라우저의 localStorage**에 자동으로 저장하고 로드합니다.
    • UX 이점: 사용자가 페이지를 새로고침하거나 브라우저를 닫았다가 다시 열어도 기존에 열려 있던 탭 상태를 그대로 유지하여 뛰어난 사용성을 제공합니다.

2. 🌲 메뉴 데이터 전역 관리 (menuGroups)

백엔드에서 Fetch Join과 메모리 재구성을 통해 가져온 메뉴 트리 데이터는 전역 상태에 저장되어 여러 컴포넌트에서 참조됩니다.

🔑 핵심 상태: menuGroups

// === ✅ 메뉴 트리 전역 관리 ===
menuGroups: [],
setMenuGroups: (groups) => set({ menuGroups: groups }),

 

  • 단일 진실 공급원 (Single Source of Truth): 백엔드에서 불러온 모든 메뉴 데이터는 이 menuGroups에 저장됩니다. 좌측 트리 뷰(MenuTreePanel)와 기타 메뉴 관련 로직은 모두 이 상태를 구독합니다.
  • 최적화된 갱신: setMenuGroups 액션을 통해, 메뉴 추가/수정/삭제 등 CRUD 작업이 완료된 후 딱 한 번 최신 트리 데이터를 주입하여 불필요한 리렌더링을 최소화합니다.

3. 📄 탭 관리 로직 (tabs, activeTab)

메뉴 관리 시스템에서 사용자가 여러 기능을 동시에 사용할 수 있도록 지원하는 탭 시스템을 효율적으로 구현했습니다.

🔑 핵심 액션: addTab

// ✅ 탭 추가 (menu.name 기준)
addTab: (menu) => {
    const { tabs } = get();
    const exists = tabs.some((t) => t.path === menu.path); // 경로 중복 체크

    if (!exists) {
        set({
            tabs: [
                ...tabs,
                { ...menu, title: menu.name }, // 새로운 탭 추가
            ],
        });
    }
    set({ activeTab: menu.path }); // 활성화 (새로 추가했든, 이미 존재했든)
},

 

  • 경로 기반 관리: 탭을 식별하는 고유 키로 메뉴의 path를 사용합니다. 이는 라우팅 시스템과 연동할 때 가장 자연스러운 방식입니다.
  • 중복 방지 및 활성화:
    1. path가 이미 존재하는지 확인하여 탭 중복 생성을 방지합니다.
    2. 새 탭이 추가되었든, 기존 탭이 클릭되었든 관계없이 해당 탭의 path를 activeTab에 설정하여 즉시 해당 탭이 활성화되도록 합니다.

🔑 기타 중요한 액션

액션 역할 로직 설명
removeTab 탭 제거 및 활성 탭 전환 제거된 탭이 활성 탭이었다면, 남아 있는 탭 중 가장 마지막 탭을 새로운 활성 탭으로 지정하여 사용자 경험의 끊김을 방지합니다.
setActiveTab 활성 탭 변경 탭 헤더 클릭 시 간단하게 activeTab을 업데이트합니다.
resetTabs 탭 전체 초기화 로그아웃 등 특정 이벤트 발생 시 모든 탭을 닫아 상태를 초기화합니다.

4. 🧭 사이드바 상태 관리 (openGroups)

메뉴 트리 컴포넌트(MenuTreePanel)에서 사용자가 어떤 상위 그룹을 확장했는지 상태를 관리합니다.

🔑 핵심 액션: toggleGroup

// ✅ 그룹 토글 (menu.name 기준)
toggleGroup: (menuName) => {
    const { openGroups } = get();
    set({
        openGroups: {
            ...openGroups,
            [menuName]: !openGroups[menuName], // 토글
        },
    });
},

 

  • 동적 키 사용: menuName을 객체의 키로 사용하여 어떤 메뉴 그룹이 열려있는지 상태를 관리합니다.
  • UX 이점: 이 상태(openGroups) 역시 persist 미들웨어를 통해 저장되므로, 페이지를 새로고침해도 사용자가 마지막으로 열어 놓았던 사이드바 상태가 그대로 유지됩니다.

🚀 결론: 통합 상태 관리의 힘

useMenuStore는 단순히 데이터를 저장하는 것을 넘어, 메뉴 데이터 로딩부터 라우팅(탭) 상태, 사이드바(그룹) 상태까지 관리 시스템의 핵심 UI 로직을 Zustnad로 통합하고 persist 미들웨어로 지속성을 부여한 모범적인 설계입니다.

이러한 통합 상태 관리를 통해 개발자는 컴포넌트 간의 복잡한 프롭스 드릴링(Props Drilling) 없이 효율적으로 기능을 구축할 수 있으며, 최종 사용자에게는 안정적이고 끊김 없는 사용자 경험을 제공할 수 있습니다.

백엔드에서 MenuTree구조를 Dto로 받아와 Drawer랑 Droptip으로 관리

 

 

메뉴 클릭 시 상세 메뉴내용을 확인할 수있고 수정가능