'use client'; import { usePathname } from 'next/navigation'; import { useCallback, useState } from 'react'; export interface SidebarState { isMobileOpen: boolean; isDesktopCollapsed: boolean; openMobile: () => void; closeMobile: () => void; toggleDesktopCollapse: () => void; } /** * Basic 레이아웃 사이드바의 화면 로컬 상태(ViewModel 대응) — 모바일 오버레이 열림 여부와 * 데스크톱 접힘 여부만 값으로 들고 있는다. 실제로 어느 상태가 화면에 반영되는지(모바일 오버레이 vs * 데스크톱 push)는 Tailwind 반응형 클래스가 판단한다 — matchMedia로 브레이크포인트를 직접 판별하면 * 서버 렌더와 최초 클라이언트 렌더가 어긋나 hydration 불일치를 일으킬 수 있어 이 훅에서는 다루지 * 않는다. */ export function useSidebar(): SidebarState { const pathname = usePathname(); const [isMobileOpen, setIsMobileOpen] = useState(false); const [isDesktopCollapsed, setIsDesktopCollapsed] = useState(false); // 라우트가 바뀌면 모바일 오버레이는 항상 닫는다 (링크 클릭 후 열려있는 채로 남는 것 방지). // effect로 동기화하지 않는다 — 렌더 중 상태를 조정하는 React 공식 패턴(state 값 변화를 // 렌더 중 감지해 즉시 setState)을 사용해 effect발 cascading render를 피한다. const [prevPathname, setPrevPathname] = useState(pathname); if (pathname !== prevPathname) { setPrevPathname(pathname); setIsMobileOpen(false); } const openMobile = useCallback(() => { setIsMobileOpen(true); }, []); const closeMobile = useCallback(() => { setIsMobileOpen(false); }, []); const toggleDesktopCollapse = useCallback(() => { setIsDesktopCollapsed((prev) => !prev); }, []); return { isMobileOpen, isDesktopCollapsed, openMobile, closeMobile, toggleDesktopCollapse, }; }