- claudedocs 폴더 구조 재정리: archive/sessions, guides/migration·mobile·universal-list, refactoring 분류 - 오래된 세션 컨텍스트/체크리스트 문서 정리 (아카이브 이동 또는 삭제) - AuthContext → authStore(Zustand) 전환 시작, RootProvider 간소화 - GenericCRUDDialog 공통 다이얼로그 컴포넌트 추가 - PermissionDialog 삭제 → GenericCRUDDialog로 대체 - RankDialog/TitleDialog GenericCRUDDialog 기반으로 리팩토링 - toast-utils.ts 삭제 (미사용) - fileDownload.ts 개선, excel-download.ts 정리 - menuStore/themeStore Zustand 셀렉터 최적화 - useColumnSettings/useTableColumnStore 기능 보강 - 세금계산서/견적/작업자화면/결재 등 소규모 개선 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
7.3 KiB
7.3 KiB
Phase 1-5: Zustand 셀렉터 훅 추가 (3개 스토어)
난이도: 저 | 영향도: 🟡 리렌더 최적화 | 예상 변경: 3 스토어 + 4 컨슈머
현황 요약
셀렉터 없이 전체 스토어를 구독하면, 무관한 상태 변경에도 컴포넌트가 리렌더됩니다.
| 스토어 | 셀렉터 훅 | 사용처 | 문제 |
|---|---|---|---|
✅ masterDataStore |
usePageConfig() 등 |
다수 | 양호 |
✅ authStore |
useCurrentUser() 등 |
4곳 | 양호 (방금 추가) |
❌ useTableColumnStore |
없음 | 1곳 | 전체 스토어 구독 |
❌ useMenuStore |
없음 | 15곳 | 일부 전체 구독 |
❌ useThemeStore |
없음 | 2곳 | 전체 구독 |
작업 내역
Step 1: src/stores/useTableColumnStore.ts — 셀렉터 훅 추가
파일 끝에 추가:
// ===== 셀렉터 훅 =====
/** 특정 페이지의 컬럼 설정만 구독 */
export const usePageColumnSettings = (pageId: string) =>
useTableColumnStore((state) => state.pageSettings[pageId] ?? DEFAULT_PAGE_SETTINGS);
/** 특정 페이지의 숨김 컬럼만 구독 */
export const useHiddenColumns = (pageId: string) =>
useTableColumnStore((state) => state.pageSettings[pageId]?.hiddenColumns ?? []);
/** 특정 페이지의 컬럼 너비만 구독 */
export const useColumnWidths = (pageId: string) =>
useTableColumnStore((state) => state.pageSettings[pageId]?.columnWidths ?? {});
주의: DEFAULT_PAGE_SETTINGS 객체는 파일 내에 이미 정의되어 있음 (line 30-33).
컨슈머 변경 — src/hooks/useColumnSettings.ts:
// Before (line 17)
const store = useTableColumnStore(); // 전체 스토어 구독
const settings = store.getPageSettings(pageId);
// After
const settings = usePageColumnSettings(pageId); // 해당 페이지 설정만 구독
const { setColumnWidth: storeSetWidth, toggleColumnVisibility: storeToggle, resetPageSettings } = useTableColumnStore.getState();
// 또는 액션만 별도 구독 (액션은 참조 안정적이라 리렌더 유발 안 함):
const setColumnWidth = useTableColumnStore((s) => s.setColumnWidth);
const toggleColumnVisibility = useTableColumnStore((s) => s.toggleColumnVisibility);
const resetPageSettings = useTableColumnStore((s) => s.resetPageSettings);
Step 2: src/stores/menuStore.ts — 셀렉터 훅 추가
파일 끝에 추가:
// ===== 셀렉터 훅 =====
/** 사이드바 접힘 상태만 구독 */
export const useSidebarCollapsed = () =>
useMenuStore((state) => state.sidebarCollapsed);
/** 활성 메뉴 ID만 구독 */
export const useActiveMenu = () =>
useMenuStore((state) => state.activeMenu);
/** 메뉴 아이템 목록만 구독 */
export const useMenuItems = () =>
useMenuStore((state) => state.menuItems);
/** 하이드레이션 완료 여부만 구독 */
export const useMenuHydrated = () =>
useMenuStore((state) => state._hasHydrated);
컨슈머 변경 대상:
2-A. src/layouts/AuthenticatedLayout.tsx (line 99) — 🔴 핵심
현재: 전체 스토어 디스트럭처링
const { menuItems, activeMenu, setActiveMenu, setMenuItems, sidebarCollapsed, toggleSidebar, _hasHydrated } = useMenuStore();
변경:
const menuItems = useMenuItems();
const activeMenu = useActiveMenu();
const sidebarCollapsed = useSidebarCollapsed();
const _hasHydrated = useMenuHydrated();
// 액션은 참조 안정적이므로 별도 셀렉터:
const setActiveMenu = useMenuStore((s) => s.setActiveMenu);
const setMenuItems = useMenuStore((s) => s.setMenuItems);
const toggleSidebar = useMenuStore((s) => s.toggleSidebar);
2-B. src/components/production/WorkerScreen/index.tsx (line 327)
현재:
const { sidebarCollapsed } = useMenuStore(); // 전체 구독
변경:
const sidebarCollapsed = useSidebarCollapsed();
2-C. src/components/layout/CommandMenuSearch.tsx (line 68)
현재:
const { menuItems } = useMenuStore(); // 전체 구독
변경:
const menuItems = useMenuItems();
2-D. 나머지 sidebarCollapsed 사용 파일 (이미 셀렉터 패턴)
아래 파일들은 이미 useMenuStore((state) => state.sidebarCollapsed) 패턴을 사용 중이므로 변경 불필요:
ItemDetail.tsx,ChecklistDetail.tsx,PriceDistributionDetail.tsxStepDetail.tsx,PermissionDetailClient.tsx,BoardDetail/index.tsxProcessDetail.tsx,PricingTableForm.tsx,DynamicItemForm/index.tsxItemDetailClient.tsx,ClientDetail.tsx,DetailActions.tsx
단, 셀렉터 훅이 추가되면 이 파일들도 향후 useSidebarCollapsed()로 전환 가능 (선택).
Step 3: src/stores/themeStore.ts — 셀렉터 훅 추가
파일 끝에 추가:
// ===== 셀렉터 훅 =====
/** 현재 테마만 구독 */
export const useTheme = () =>
useThemeStore((state) => state.theme);
/** setTheme 액션만 구독 */
export const useSetTheme = () =>
useThemeStore((state) => state.setTheme);
컨슈머 변경 대상:
3-A. src/layouts/AuthenticatedLayout.tsx (line 100)
현재:
const { theme, setTheme } = useThemeStore();
변경:
const theme = useTheme();
const setTheme = useSetTheme();
3-B. src/components/ThemeSelect.tsx (line 24)
현재:
const { theme, setTheme } = useThemeStore();
변경:
const theme = useTheme();
const setTheme = useSetTheme();
검증
npx tsc --noEmit
셀렉터 훅은 기존 API에 추가만 하는 것이므로 기존 코드에 영향 없음. 컨슈머 변경은 import 경로와 호출 패턴만 바뀌므로 타입 에러 가능성 낮음.
변경 파일 총 정리
| # | 파일 | 작업 | 내용 |
|---|---|---|---|
| 1 | src/stores/useTableColumnStore.ts |
추가 | 셀렉터 훅 3개 (usePageColumnSettings, useHiddenColumns, useColumnWidths) |
| 2 | src/stores/menuStore.ts |
추가 | 셀렉터 훅 4개 (useSidebarCollapsed, useActiveMenu, useMenuItems, useMenuHydrated) |
| 3 | src/stores/themeStore.ts |
추가 | 셀렉터 훅 2개 (useTheme, useSetTheme) |
| 4 | src/hooks/useColumnSettings.ts |
수정 | useTableColumnStore() → 셀렉터 패턴 |
| 5 | src/layouts/AuthenticatedLayout.tsx |
수정 | menuStore/themeStore 전체 구독 → 셀렉터 |
| 6 | src/components/production/WorkerScreen/index.tsx |
수정 | useMenuStore() → useSidebarCollapsed() |
| 7 | src/components/layout/CommandMenuSearch.tsx |
수정 | useMenuStore() → useMenuItems() |
| 8 | src/components/ThemeSelect.tsx |
수정 | useThemeStore() → useTheme() + useSetTheme() |
참고: Zustand 셀렉터가 중요한 이유
// ❌ 전체 구독 — menuItems 변경 시 sidebarCollapsed만 쓰는 컴포넌트도 리렌더
const { sidebarCollapsed } = useMenuStore();
// ✅ 셀렉터 — sidebarCollapsed 변경 시에만 리렌더
const sidebarCollapsed = useMenuStore((state) => state.sidebarCollapsed);
// 또는
const sidebarCollapsed = useSidebarCollapsed();
Zustand는 Object.is로 반환값을 비교. 셀렉터가 원시값(string, boolean, number)을 반환하면 참조 비교로 정확히 변경 감지.
객체를 반환하는 셀렉터(예: usePageColumnSettings)는 같은 참조를 반환하므로 해당 pageId의 설정이 변경될 때만 리렌더.