프로젝트에서 전역상태관리 라이브러리로 Zustand를 사용하기로 했다.
Zustand를 선택한 이유는
그저 store를 만들고, 컴포넌트에서 사용하면 된다.
1import { create } from 'zustand'; 2 3interface SidebarState { 4 isSidebarVisible: boolean; 5 toggleSidebar: () => void; 6} 7 8const useSidebarStore = create<SidebarState>((set) => ({ 9 isSidebarVisible: false, 10 toggleSidebar: () => 11 set((state) => ({ isSidebarVisible: !state.isSidebarVisible })), 12})); 13 14export default useSidebarStore;
1import useSidebarStore from '../stores/sidebarStore'; 2 3const Sidebar = () => { 4 const { isSidebarVisible, toggleSidebar } = useSidebarStore(); 5 6 return ( 7 <div> 8 <button onClick={toggleSidebar}> 9 {isSidebarVisible ? 'Hide' : 'Show'} Sidebar 10 </button> 11 {isSidebarVisible && <nav>Sidebar content here</nav>} 12 </div> 13 ); 14}; 15 16export default Sidebar;
provider를 만들고 layout.tsx
에 추가해야한다.
그 이유는
서버가 동시에 여러 요청을 처리할 수 있기 때문에, 각 요청에 대해 독립적인 스토어 인스턴스를 생성해야 한다. 이를 위해 각 store마다 provider를 사용하면 안전하게 요청별 store을 관리할 수 있다.
provider를 사용하면 서버에서 렌더링될 때와 클라이언트에서 다시 렌더링될 때 동일한 초기 상태를 유지할 수 있습니다. 이를 통해 hydration 오류를 방지할 수 있다.
Next.js의 클라이언트 사이드 라우팅을 지원하기 위해, 각 컴포넌트에서 store를 초기화할 수 있는 provider를 사용하면 상태 관리를 용이하게 할 수 있다.
간단하게 말해서 Next.js의 SSR 환경에서 발생하는 동시 다발적인 요청 처리와 client-server간의 상태 일관성 유지를 위해 각각의 store를 독립적으로 관리해야 한다.
예를 들어, SSR을 사용하고, 모달의 상태를 전역으로 관리하고 싶다면
1import { createStore } from 'zustand/vanilla'; 2import { ModalType } from '@/types/types'; 3 4export type ModalState = { 5 isOpened: boolean; 6 modalType: ModalType; 7}; 8 9export type ModalActions = { 10 toggleModal: () => void; 11 setModalType: (modalType: ModalType) => void; 12}; 13 14export type ModalStore = ModalState & ModalActions; 15 16export const initModalStore = (): ModalState => { 17 return { isOpened: false, modalType: null }; 18}; 19 20export const defaultInitState: ModalState = { 21 isOpened: false, 22 modalType: null, 23}; 24 25export const createModalStore = (initState: ModalState = defaultInitState) => { 26 return createStore<ModalStore>()((set) => ({ 27 ...initState, 28 toggleModal: () => set((state) => ({ isOpened: !state.isOpened })), 29 setModalType: (modalType: ModalType) => set(() => ({ modalType })), 30 })); 31}; 32
1'use client'; 2 3import { type ReactNode, createContext, useRef, useContext } from 'react'; 4import { type StoreApi, useStore } from 'zustand'; 5import { 6 ModalStore, 7 createModalStore, 8 initModalStore, 9} from '@/stores/modalStore'; 10 11export const ModalStoreContext = createContext<StoreApi<ModalStore> | null>( 12 null, 13); 14 15export interface ModalStoreProviderProps { 16 children: ReactNode; 17} 18 19export const ModalStoreProvider = ({ children }: ModalStoreProviderProps) => { 20 const storeRef = useRef<StoreApi<ModalStore>>(); 21 if (!storeRef.current) { 22 storeRef.current = createModalStore(initModalStore()); 23 } 24 25 return ( 26 <ModalStoreContext.Provider value={storeRef.current}> 27 {children} 28 </ModalStoreContext.Provider> 29 ); 30}; 31 32export const useModalStore = <T,>(selector: (store: ModalStore) => T): T => { 33 const modalStoreContext = useContext(ModalStoreContext); 34 35 if (!modalStoreContext) { 36 throw new Error(`useModalStore must be use within ModalStoreProvider`); 37 } 38 39 return useStore(modalStoreContext, selector); 40};
layout.tsx
에 provider 추가하기1import type { Metadata } from 'next'; 2import '../styles/globals.scss'; 3import { ModalStoreProvider } from '../../providers/ModalStoreProvider'; 4 5export const metadata: Metadata = { 6 title: 'Create Next App', 7 description: 'Generated by create next app', 8}; 9 10export default function RootLayout({ 11 children, 12}: Readonly<{ 13 children: React.ReactNode; 14}>) { 15 return ( 16 <html lang='ko'> 17 <body> 18 <ModalStoreProvider> 19 {children} 20 </ModalStoreProvider> 21 </body> 22 </html> 23 ); 24}
1// src/app/page.tsx 2import { CounterStoreProvider } from '@/providers/ModalStoreProvider' 3import { HomePage } from '@/components/pages/home-page' 4 5export default function Home() { 6 return ( 7 <ModalStoreProvider> 8 <HomePage /> 9 </ModalStoreProvider> 10 ) 11}
useModalStore
- 컴포넌트에서 사용하기1export default function Floating({ token }: Props) { 2 const { isOpened, toggleModal, modalType, setModalType } = useModalStore( 3 (state) => state, 4 ); 5return ...
Zustand 공식문서 참고