Zustand

박영수·2024년 7월 15일
post-thumbnail

zustand는 Redux를 대체하는 상태관리 라이브러리입니다.
Redux가 너무 어려운 사람들은 zustand로 먼저 해보고 나서 Redux를 해보면 좋지 않을까 합니다.

Redux보다는 쉽다고 하더라고요! 근데 저는 zustand도 어려웠어용 ㅎㅎ


1. Zustand의 등장배경

  • 상태관리의 중요성

    • 상태관리는 모든 리액트 애플리케이션에서 매우 중요합니다. 작은 규모의 애플리케이션에서는 상태관리가 간단하지만, 애플리케이션이 커질수록 상태관리는 점점 더 복잡해집니다.

상태(state)는 리액트 애플리케이션에서 데이터와 UI의 현재 상태를 나타냅니다.
상태가 변경되면 UI도 업데이트되어야 하죠!

  • Zustand의 필요성

    • 기존의 상태관리 라이브러리인 Redux는 제공하는 기능과 연계된 미들웨어 등 매우 강력한 퍼 포먼스를 자랑하지만, 설정과 사용법이 복잡했죠! Zustand는 상태관리 본연의 기능에 집중하여 위와 같은 복잡성을 줄이고, 보다 간단하고 직관적인 상태관리 기능을 제공해요.

공식 문서에서는 다음과 같이 소개하고 있네요!

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

Zustand는 독일어로 "상태"를 의미합니다. 매우 가볍고, 사용하기 쉬운 상태관리 라이브러리입니다.


2. 설치 및 기본 사용법

  • Zustand 설치
yarn add zustand
  • 기본 사용법
// src > zustand > bearsStore.js
import { create } from "zustand";

const useBearsStore = create((set) => ({
  bears: 0,
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
}));

export default useBearsStore;
// src > App.jsx
import "./App.css";
import useBearsStore from "./zustand/bearsStore";

function App() {
  const bears = useBearsStore((state) => state.bears);
  const increasePopulation = useBearsStore((state) => state.increasePopulation);
  return (
    <div>
      <h1>{bears} around here ...</h1>
      <button onClick={increasePopulation}>one up</button>
    </div>
  );
}
export default App;

3. Zustand와 Redux Toolkit 비교

  • Zustand: 매우 간단한 설정과 사용법을 제공해요! 우리가 이미 경험해 본 대로 상태를 정의하 고 사용하는 과정이 직관적이에요.
import { create } from "zustand";

const useStore = create(set => ({
  bears: 0,
  increasePopulation: () => set(state => ({ bears: state.bears + 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 })

장단점 비교(정리)

  • Zustand
    • 장점: 간단하고 빠르며, 설정이 매우 쉽습니다.
    • 단점: 상태가 커지면 관리가 어려울 수 있습니다.
  • Redux Toolkit
    • 장점: 구조화된 방법을 통해 대규모 애플리케이션에서도 관리가 용이합니다.
    • 단점: 설정이 복잡하고, 학습 곡선이 가파릅니다.

4. 예시코드

  • 상태 정의
// src > zustand > todosStore.js
import { create } from "zustand";

const useTodosStore = create(set => ({
  todos: [],
  addTodo: (todo) => set(state => ({ todos: [...state.todos, todo] })),
  removeTodo: (index) => set(state => ({
    todos: state.todos.filter((_, i) => i !== index)
  }))
}))

export default useTodosStore;
  • 상태 사용
// src > App.jsx
import React, { useState } from "react";
import useTodosStore from "./zustand/todosStore";

function App() {
  const todos = useTodosStore((state) => state.todos);
  const addTodo = useTodosStore((state) => state.addTodo);
  const removeTodo = useTodosStore((state) => state.removeTodo);
  const [input, setInput] = useState("");

  return (
    <div>
      <h1>Todo List</h1>
      <input value={input} onChange={(e) => setInput(e.target.value)} />
      <button
        onClick={() => {
          addTodo(input);
          setInput("");
        }}
      >
        Add Todo
      </button>
      <ul>
        {todos.map((todo, index) => (
          <li key={index}>
            {todo} <button onClick={() => removeTodo(index)}>Remove</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default App;

5. Zustand와 Immer의 결합

(1). 직접 중첩된 상태를 업데이트 했을 때의 문제점

  • Zustand의 간편한 상태 업데이트 방식은 깊은 중첩 구조를 가지는 상태를 업데이트할 때 문제가 발생할 수 있습니다.

  • 배열이나 객체의 중첩된 상태를 업데이트할 때 불변성을 유지하지 않으면 상태 반영이 제대로 이루어지지 않아, 예상치 못한 오류가 발생할 수 있습니다. 다음 예제를 통해 중첩된 상태를 업데이트할 때 발생하는 문제를 살펴봅시다!

(2). mutable vs immutable

  • Mutable 메서드

    • 불변성 유지 안됨
    • Mutable 메서드는 원본 데이터를 직접 수정하여 변경된 데이터를 반환합니다. 이 방법은 간단하지만, 상태 변화 추적이 어려워져 버그 발생 가능성이 높아집니다.
  • Immutable 메서드

    • 불변성 유지 됨
    • 원본 데이터를 변경하지 않고, 수정된 새로운 데이터를 반환합니다. 이를 통해 불변성을 유지하며, 상태 관리가 쉬워지고 예측 가능한 코드 작성을 돕습니다.
import create from "zustand";

// Zustand 스토어 생성
const useTodosStore = create((set) => ({
  todos: [],
  addTodo: (text) =>
    set((state) => {
      // 불변성을 어기는 예시: 직접 배열을 수정
      state.todos.push({ id: Date.now(), text, completed: false });
      return state;
    }),
  toggleTodo: (id) =>
    set((state) => {
      // 불변성을 어기는 예시: 직접 객체를 수정
      const todo = state.todos.find((todo) => todo.id === id);
      if (todo) {
        todo.completed = !todo.completed;
      }
      return state;
    }),
}));

export default useTodosStore;
// src > App.jsx
import useTodosStore from "./zustand/todosStore";

function App() {
  const { todos, addTodo, toggleTodo } = useTodosStore();

  return (
    <div>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            <span
              style={{
                textDecoration: todo.completed ? "line-through" : "none",
              }}
              onClick={() => toggleTodo(todo.id)}
            >
              {todo.text}
            </span>
          </li>
        ))}
      </ul>
      <button
        onClick={() => addTodo(prompt("새로운 todolist를 입력해주세요."))}
      >
        Add Todo
      </button>
    </div>
  );
}

export default App;

리스트가 추가 / 수정 되더라도 다른 요소에 의해 리렌더링이 일어나지 않는 한, UI에 반영이 안돼요

(3). immer를 이용해 해결하는 경우

Immer는 JavaScript에서 상태를 쉽게 변경할 수 있게 해주는 라이브러리입니다. 원본 데이터를 변경하지 않고도 마치 직접 수정하는 것처럼 코드를 작성할 수 있으며, Immer가 자동으로 불변성을 유지한 새 상태를 만들어줍니다.

우리가 위에서 겪은 문제를 해결해 줄 key를 가지고 있는 라이브러리에요!

  • immer 라이브러리를 설치
yarn add immer

todosStore.js를 다음과 같이 수정해주세요.

import { create } from "zustand";
import { immer } from "zustand/middleware/immer";

const useTodosStore = create(
  immer((set) => ({
    todos: [
      {
        id: 1,
        title: "Learn Zustand",
        tasks: [{ id: 1, task: "Read documentation", done: false }],
      },
    ],
    addTask: (todoId, newTask) =>
      set((state) => {
        const todo = state.todos.find((todo) => todo.id === todoId);
        if (todo) {
          todo.tasks.push(newTask); // 불변성 유지: immer가 자동으로 처리
        }
        // return { todos: state.todos }; // 변경된 참조가 기존 상태와 같아 리렌더링되지 않음
      }),
    toggleTask: (todoId, taskId) =>
      set((state) => {
        const todo = state.todos.find((todo) => todo.id === todoId);
        if (todo) {
          const task = todo.tasks.find((task) => task.id === taskId);
          if (task) {
            task.done = !task.done; // 불변성 유지: immer가 자동으로 처리
          }
        }
        // return { todos: state.todos }; // 변경된 참조가 기존 상태와 같아 리렌더링되지 않음
      }),
  }))
);

export default useTodosStore;
profile
개발자를 희망하는 백수...

0개의 댓글