☁️ goormTIL | Zustand #52

매루·2025년 11월 21일

goormTIL

목록 보기
50/67
post-thumbnail

📅 2025-11-21

➡️ Zustand에 대해 새롭게 알게 된 것 또는 헷갈리는 부분 정리


🔎 학습 리마인드

📌 Zustand

💡 상태 관리의 중요성

  • 리액트 애플리케이션에서 상태(state)는 화면(UI)에 보여지는 데이터의 현재 상태를 의미
    상태가 변경되면, 그에 맞게 UI도 함께 업데이트 되어야 함

  • 규모가 커질수록 문제 등장

    • 여러 컴포넌트가 같은 상태를 공유해야 함
    • 페이지 간 동일한 상태 접근 필요
    • 로그인 정보, 사용자 정보 같은 전역 상태 증가
  • 단순 useState 로는 복잡한 애플리케이션을 관리하기 어려움 → 전역 상태 관리 필요


💡 Zustand의 필요성

가장 대표적인 상태 라이브러리 Redux

  • 장점

    • 강력한 에코시스템(미들웨어, DevTools 등)
  • 단점

    • 액션/리듀서/스토어 설정 등 보일러플레이트 코드가 많음
    • 학습해야 할 개념이 많아 학습 비용이 큼
    • 작은 앱에서도 구조가 과한 편

Zustand의 등장

🔗 https://zustand-demo.pmnd.rs/

단순화된 Flux 패턴을 사용하는 작고(small) 빠르고(fast) 확장가능한(scalable) 상태관리 솔루션이며, Hooks에 기반으로하는 간편한 API가 존재함


💡 Zustand의 특징

  1. 간결한 API

    • create 하나로 스토어를 만들고, Hook처럼 불러다 쓰는 구조
    • 상태와 액션을 한 파일에서 쉽게 정의 가능 → Redux처럼 복잡한 설정 필요 없음
  2. 성능 최적화 - 구독 기반 렌더링

    • Zustand는 state의 특정 부분을 구독하여 사용
      • 상태가 변경되더라도 그 상태를 실제로 사용하는 컴포넌트만 리렌더링
  3. 리액트와의 자연스러운 통합

    • useState, useEffect를 사용하는 것과 비슷하게 Hook처럼 사용
    const bears = useBearsStore((state) => state.bears);
  4. 빠른 성장과 점유율

    • Redux는 여전히 전통적인 강자로 많이 사용되지만, 상태 관리 라이브러리 중 Zustand의 성장세와 점유율이 매우 빠르게 증가하고 있음
    • 간결함과 편의성 덕분에 새로운 프로젝트에서 선호되는 경우가 많음

💡 설치 및 기본 사용법

설치

npm i zustand

기본 사용법

  • 스토어 생성
    import create from 'zustand';
    
    // 스토어 생성
    const useStore = create((set) => ({
      count: 0, // 상태
      increment: () => set((state) => ({ count: state.count + 1 })), // 상태를 업데이트하는 메서드
      decrement: () => set((state) => ({ count: state.count - 1 })), // 상태를 업데이트하는 메서드
    }));
    
    export default useStore;
  • 스토어 사용
    import useStore from './store';  // Zustand 스토어를 불러옴
    
    function Counter() {
      const count = useStore((state) => state.count);  // 상태를 불러옴
      const increment = useStore((state) => state.increment);  // 상태 변경 함수
      const decrement = useStore((state) => state.decrement);  // 상태 변경 함수
      
      // 또는 구조 분해 할당
      // const { count, increment, decrement } = useStore()
    
      return (
        <div>
          <h1>카운터: {count}</h1>
          <button onClick={increment}>증가</button>
          <button onClick={decrement}>감소</button>
        </div>
      );
    }
    
    export default Counter;
    • create: Zustand 스토어 생성하는 함수로, 상태와 그 상태를 업데이트 하는 액션을 정의하여 전역 상태 훅을 반환
    • set: 상태를 업데이트하는 내장 함수
    • 상태(count) 와 액션(increment, decrement)을 한 곳에서 정의
    • 컴포넌트에서는 원하는 상태만 선택적으로 구독 → 최적화 자동 처리

💡 Zustand와 Redux Toolkit 비교

  1. Zustand

    import { create } from "zustand";
    
    const useStore = create((set) => ({
      bears: 0,
      increasePopulation: () =>
        set((state) => ({ bears: state.bears + 1 })),
    }));
    
    export default useStore;
    • 스토어와 액션을 한 파일에서 정의할 수 있어 구조가 단순함

    • Provider, reducer, action 타입 등 불필요

    • 필요한 상태만 선택적으로 구독 → 불필요한 리렌더링 방지

    • 장점

      • 매우 간단한 설정, 빠른 적용
      • 파일 단위로 스토어를 만들기 쉬움
      • 구독 기반 구조로 성능 최적화에 유리
    • 단점

      • 규모가 커질수록 스토어 분리·구조 설계를 직접 정의해야 함
      • 팀 컨벤션이 없다면 스토어 구조가 제각각이 될 수 있음

  1. Redux Toolkit 예시

    import { configureStore, createSlice } from "@reduxjs/toolkit";
    
    const slice = createSlice({
      name: "counter",
      initialState: { value: 0 },
      reducers: {
        increment: (state) => {
          state.value += 1;
        },
      },
    });
    
    const store = configureStore({ reducer: slice.reducer });
    
    export default store;
    • store, slice, reducer, provider 구조가 명확함

    • 장점

      • 대규모 프로젝트에서 일관성 유지에 매우 강함
      • 미들웨어, DevTools, 비동기 처리(createAsyncThunk) 등 풍부한 생태계
      • 팀 개발에서 역할 분담과 코드 구조가 명확함
    • 단점

      • 초기 설정과 개념 학습에 드는 비용이 큼
      • 간단한 앱에 사용하기에는 다소 무거울 수 있음

💡 중첩 상태와 Immer의 중요성

  • Zustand의 set함수는 기본적으로 불변성을 지키는 업데이트를 전제로 동작

  • 하지만 중첩된 객체/배열을 직접 수정하면, 참조가 그대로라 React가 상태 변경을 감지하지 못함

    // 권장되지 않는 패턴 (불변성 깨짐)
    const useTodosStore = create((set) => ({
      todos: [],
      addTodo: (text) =>
        set((state) => {
          state.todos.push({
            id: Date.now(),
            text,
            completed: false,
          }); // 원본 배열 직접 수정
          return state; // 같은 참조를 그대로 반환 → 리렌더링 안됨
        }),
    }));
    • React는 state가 “같은 객체”라고 판단해서 UI 업데이트를 하지 않음
  • immer를 이용한 해결

    • 직접 객체를 수정하는 것처럼 코드를 작성하되, 내부적으로 불변성을 깨지 않도록 새로운 상태를 만들어줌

      import { create } from "zustand";
      import { immer } from "zustand/middleware/immer";
      
      const useTodoStore = create(
        immer((set) => ({
          todos: [],
          addTodo: (todo) => set((state) => ({ todos: [...state.todos, todo] })),
          removeTodo: (index) =>
            set((state) => ({
              todos: state.todos.filter((_, i) => i !== index),
            })),
        }))
      );
      
      export default useTodoStore;
      addTodo: (todo) =>
        set((state) => {
          state.todos.push(todo) // 불변성 걱정 없이 직접 변경 가능
        }),
      
      removeTodo: (index) =>
        set((state) => {
          state.todos.splice(index, 1) // 불변성 걱정 없이 직접 변경 가능
        }),
    • 불변성 신경 안써도 됨

    • 코드 간결 & 실수 줄어둚

    • 복잡한 중첩 구조에도 유리함


💡 미들웨어 (Middleware)

  1. persist - 상태 영구 저장

    • persist는 상태를 localStorage / sessionStorage에 저장하여 새로고침하거나 브라우저를 닫아도 상태를 유지할 수 있게 해줌
    • zustand/middleware에서 제공
    persist(스토어, 옵션)
    • 옵션
      • name: 저장될 key 이름
        • 필수 옵션
      • getStorage: 저장소 지정
        • 기본 값 localStorage
        • sessionStorage에 저장하고 싶을 때 → getStorage : () ⇒ sessionStorage
  • persist + immer 기본 패턴
    import { create } from "zustand";
    import { persist } from "zustand/middleware";
    import { immer } from "zustand/middleware/immer";
    
    const useTodoStore = create(
      persist(
        immer((set) => ({
          todos: [],
    
          addTodo: (todo) =>
            set((state) => {
              state.todos.push(todo);
            }),
    
          removeTodo: (index) =>
            set((state) => {
              state.todos.splice(index, 1);
            }),
        })),
        {
          name: "todo-storage", // localStorage key 이름
          getStorage : ()=> seessionStorage // 세션 스토리지에 저장하고 싶을때
        }
      )
    );
    
    export default useTodoStore;
    • 보통 persistimmer 구조로 하는게 일반적임

  1. Devtools

    🔗 https://chromewebstore.google.com/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd

    • Zustand도 Redux DevTools와 연동 가능하며, 액션 이름을 명시해주면 디버깅이 매우 쉬워짐
    import { create } from "zustand";
    import { persist, devtools } from "zustand/middleware";
    import { immer } from "zustand/middleware/immer";
    
    const useTodoStore = create(
      devtools(
        persist(
          immer((set) => ({
            todos: [],
    
            addTodo: (todo) =>
              set((state) => {
                state.todos.push(todo);
              }, false, "ADD_TODO"),        // DevTools에 표시될 액션명
    
            removeTodo: (index) =>
              set((state) => {
                state.todos.splice(index, 1);
              }, false, "REMOVE_TODO"),        // DevTools에 표시될 액션명
          })),
          { name: "todo-storage" }
        )
      )
    );
    
    export default useTodoStore;

0개의 댓글