Next.js app router에서 Zustand 사용하기


프로젝트에서 전역상태관리 라이브러리로 Zustand를 사용하기로 했다.

Zustand를 선택한 이유는

  • redux는 약간 복잡해서 러닝 커브가 높다.
  • 가벼워서 번들 크기를 줄일 수 있다.
  • 최근 1년간 Zustand의 다운로드 수가 많다.

✨ React 프로젝트에서의 사용법

그저 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;

✨ Next.js app router에서의 사용법

provider를 만들고 layout.tsx에 추가해야한다.

그 이유는

  1. 서버가 동시에 여러 요청을 처리할 수 있기 때문에, 각 요청에 대해 독립적인 스토어 인스턴스를 생성해야 한다. 이를 위해 각 store마다 provider를 사용하면 안전하게 요청별 store을 관리할 수 있다.

  2. provider를 사용하면 서버에서 렌더링될 때와 클라이언트에서 다시 렌더링될 때 동일한 초기 상태를 유지할 수 있습니다. 이를 통해 hydration 오류를 방지할 수 있다.

  3. Next.js의 클라이언트 사이드 라우팅을 지원하기 위해, 각 컴포넌트에서 store를 초기화할 수 있는 provider를 사용하면 상태 관리를 용이하게 할 수 있다.

간단하게 말해서 Next.js의 SSR 환경에서 발생하는 동시 다발적인 요청 처리와 client-server간의 상태 일관성 유지를 위해 각각의 store를 독립적으로 관리해야 한다.


SSR을 사용하지 않고, CSR을 사용한다면, React 에서의 사용법과 같다.

예를 들어, SSR을 사용하고, 모달의 상태를 전역으로 관리하고 싶다면

1. store 만들기

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

2. provider 만들기

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};

3. 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}

3-1 route별로 store를 따로 생성

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}

4. useModalStore - 컴포넌트에서 사용하기

1export default function Floating({ token }: Props) {
2  const { isOpened, toggleModal, modalType, setModalType } = useModalStore(
3    (state) => state,
4  );
5return ...

Zustand 공식문서 참고