[React 심화] Zustand란?

조아영·2025년 3월 24일

📌

Zustand는 상태관리 본연의 기능에 집중한 라이브러리.
복잡성을 줄이고, 간단하고 직관적인 상태관리 기능 제공. 단순화된 Flux 패턴 기반의 small, fast, scalable 상태관리 솔루션이며 Hooks 기반의 간편한 API 제공.

◼ 등장배경

상태관리의 중요성

기존 상태관리 라이브러리인 Redux는 강력한 기능과 다양한 미들웨어 지원하지만, 설정과 사용 방식이 복잡한 편.
상태관리는 모든 React 애플리케이션에서 핵심 요소.
작은 규모에서는 상태관리가 간단하지만, 애플리케이션이 커질수록 상태관리는 더 복잡해짐.

→ Zustand는 복잡성을 줄이고, 간단하고 직관적인 상태관리 기능 제공.

◼ 주요특징

  • 간결함 : 간단한 API 제공하기 때문에, 학습 곡선이 완만. 적은 설정 코드로 상태관리 구현 가능.
  • 성능 : 불필요한 리렌더링을 방지하는 등 성능최적화가 잘 되어 있음. 상태 변경 시 해당 상태를 구독(subscribe)한 컴포넌트만 리렌더링.
  • React와의 통합 : React의 훅(Hook)과 자연스럽게 통합 가능. 상태를 정의하고 이를 React 컴포넌트에서 쉽게 사용할 수 있어, 기존 React 개발 경험을 그대로 활용할 수 있음.

구독(subscribe)이란?
구독은 상태 변경을 감지하고, 해당 변경에 반응하는 컴포넌트만 업데이트하는 메커니즘 의미.
상태 일부가 변경되더라도, 그 값을 사용하지 않는 컴포넌트는 리렌더링되지 않음. 애플리케이션 전체 리렌더링 방지. 이를 통해 성능을 최적화하고 리렌더링으로 인한 성능 저하를 방지할 수 있음.

◼ 설치 및 기본 사용법

설치

npm install 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;

◼ Zustand vs Redux Toolkit

  • 설정과 사용법: Zustand는 간단한 설정과 직관적인 사용 방식 제공. Redux Toolkit은 더 구조화된 방법을 제공.
  • 성능: 두 라이브러리 모두 성능 최적화가 잘 되어 있지만, Zustand는 불필요한 리렌더링을 방지하는데 더 초점
  • 유연성: Zustand는 필요한 부분만 선택적으로 사용 가능. Redux Toolkit은 보다 강력한 구조화된 방법을 제공.
  • 커뮤니티와 자료: 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 장점과 단점

장점

  • 간편한 사용: 간단한 API와 직관적인 사용법 제공.
  • 성능 최적화: 불필요한 리렌더링을 방지.
  • React와의 완벽한 통합: React Hooks 기반 자연스러운 통합 지원.
  • 미들웨어 지원: persist, devtools 등.
  • 유연성: 필요한 기능만 선택 사용 가능.

단점

  • 규모가 커지면 관리 어려움: 상태가 많아지면 관리가 복잡해짐.

◼ 예시

기본 예제

상태 정의

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

Zustand와 Immer의 결합

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

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

Zustand의 간편한 상태 업데이트 방식은 깊은 중첩 구조를 가지는 상태를 업데이트할 때 문제가 발생할 수 있음.
배열이나 객체의 중첩된 상태를 업데이트할 때 불변성을 유지하지 않으면 상태 반영이 제대로 이루어지지 않아, 예상치 못한 오류가 발생할 수 있음.

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에 반영이 안됨.

immer 설치

npm install 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;

기타 유용한 패턴

선택적 상태 구독

상태의 특정 부분만 구독하여 성능 최적화 할 수 있음. 리렌더링 범위 최소화 가능.

// src/App.jsx

const todos = useTodosStore((state) => state.todos);
const addTask = useTodosStore((state) => state.addTask);
const toggleTask = useTodosStore((state) => state.toggleTask);

미들웨어 사용

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

const useTodosStore = create(
  persist(
    (set) => ({
      todos: [
        {
          id: 1,
          title: "Learn Zustand",
          tasks: [{ id: 1, task: "Read documentation", done: false }],
        },
      ],
      addTask: (todoId, newTask) =>
        set(
          produce((state) => {
            const todo = state.todos.find((todo) => todo.id === todoId);
            if (todo) {
              todo.tasks.push(newTask); // 불변성 깨짐: 직접 수정
            }
            // return { todos: state.todos }; // 변경된 참조가 기존 상태와 같아 리렌더링되지 않음
          })
        ),
      toggleTask: (todoId, taskId) =>
        set(
          produce((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; // 불변성 깨짐: 직접 수정
              }
            }
            // return { todos: state.todos }; // 변경된 참조가 기존 상태와 같아 리렌더링되지 않음
          })
        ),
    }),
    {
      name: "todos-storage", // 저장소 이름 설정.
      // getStorage: () => sessionStorage, // localStorage가 아닌 곳에 저정하고 싶다면.
    }
  )
);

export default useTodosStore;

persist를 이용해서 새로고침을 하더라도 데이터 유지. persist는 zustand 내장이므로 별도 설치할 필요가 없음.
위와 같이 하면 새로고침 이후에도 localStorage에서 데이터를 관리하기 때문에 지속성을 유지할 수 있음.

0개의 댓글