본문 바로가기

카테고리 없음

Study_240614 ( Zustand )

Zustand의 주요 특징

  • 간결함
    • 매우 간단한 API를 제공하기 때문에, 학습 곡선이 완만함
    • 매우 적은 설정 코드 및 적용 코드를 이용하여 상태관리 기능을 구현 가능
  • 성능
    • 불필요한 리렌더링을 방지하는 등, 성능 최적화가 잘 되어 있음
    • Zustand는 상태가 변경될 때, 해당 상태를 **구독(subscribe)**하고 있는 컴포넌트만 리렌더링함
    • 구독은 상태의 변경을 감지하고, 해당 변경에 반응하는 컴포넌트만 업데이트하는 메커니즘을 의미. 마치 유튜브 채널을 구독한다면, 채널에서 새로운 영상이나 공지가 올라오면 알림이 오는 것과 같은 개념.
    • 상태의 일부가 변경되었을 때 그 상태를 사용하는 컴포넌트만 업데이트되므로, 애플리케이션 전체가 리렌더링되지 않음. 이를 통해 성능을 최적화하고, 리렌더링으로 인한 성능 저하를 방지
  • React와의 통합
    • React의 훅(Hook)과 완벽하게 통합됨
    • Zustand는 React의 useState, useEffect와 같은 기본 훅을 사용하는 것처럼 간편하게 사용 가능
    • 예시 코드 const bears = useBearsStore((state) => state.bears);
    • 상태를 정의하고 이를 React 컴포넌트에서 쉽게 사용할 수 있어, 기존 React 개발 경험을 그대로 활용 가능

Zustand의 장점과 단점

장점

  • 간편한 사용: 간단한 API와 직관적인 사용법을 제공함
  • 성능 최적화: 불필요한 리렌더링을 방지함
  • React와의 완벽한 통합: React의 훅과 잘 통합되어 있음
  • 미들웨어 지원: 로깅, 퍼시스턴스 등 다양한 미들웨어를 사용 가능
  • 유연성: 필요한 부분만 선택적으로 사용 가능
  • 커뮤니티와 자료: 예전엔 다른 대형 라이브러리에 비해 상대적으로 커뮤니티와 자료가 부족할 수 있었지만 꾸준한 인기로 많은 자료가 확보되고 있음

단점

  • 상태가 커지면 관리 어려움: 상태가 많아지면 관리가 복잡해질 수 있음

Redux Toolkit과의 비교

  • 설정과 사용법: Zustand는 설정과 사용이 간단하며, Redux Toolkit은 더 구조화된 방법을 제공함
  • 성능: 두 라이브러리 모두 성능 최적화가 잘 되어 있지만, Zustand는 불필요한 리렌더링을 방지하는 데 더 초점을 맞춤
  • 유연성: Zustand는 필요한 부분만 선택적으로 사용할 수 있지만, Redux Toolkit은 보다 강력한 구조화된 방법을 제공
  • 커뮤니티와 자료: Redux Toolkit은 대형 커뮤니티와 풍부한 자료를 가지고 있으며, Zustand는 상대적으로 부족할 수 있지만 현재는 많은 인기에 힘입어 늘어나고 있음

장단점 비교(정리)

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

Zustand 예시 코드

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

 

Mutable 메서드

원본 데이터를 직접 수정하여 변경된 데이터를 반환. 간단하지만, 상태 변화 추적이 어려워 버그 발생 확률 높음

Immutable 메서드

원본 데이터를 변경하지 않고, 수정된 새로운 데이터를 반환. 불변성을 유지하며, 상태 관리가 쉬워지고 예측 가능한 코드 작성에 도움됨.

 

순번          특징                                                        Mutable                          Immutable

1 배열 추가 push concat
2 배열 제거 pop slice
3 배열 앞에 추가 unshift [newItem, ...arr]
4 배열 앞에서 제거 shift slice(1)
5 배열의 요소 변경 splice slice + concat
6 배열 정렬 sort Array.from + sort
7 배열 반전 reverse slice().reverse()
8 배열 요소 제거 splice filter
9 객체 속성 추가 직접 할당 Object.assign
10 객체 속성 제거 delete Object.keys + reduce
11 객체 속성 변경 직접 할당 Object.assign
12 문자열 병합 직접 할당 concat

 

Immer

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

 

미들웨어 psersist

localStroage ( sessionStorage 등 ) 이용하여 새로고침해도 데이터 유지 가능

 

예시코드

import useTodosStore from "./zustand/todosStore.js";

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;
// 불변성을 유지 안하는 예시
import create from "zustand";
import { immer } from "zustand/middleware/immer";
import { persist } from "zustand/middleware";

// Zustand 스토어 생성
const useTodosStore = create(
  persist(
    immer((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;
        }),
    })),
    {
      name: "todos-store",
      getStorage: () => sessionStorage,
    }
  )
);

export default useTodosStore;

// 불변성을 유지하는 예시
// 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;

 

persist 적용 예시

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 }],
        },
      ],
      addTodo: (todoId, newTask) =>
        set(
          produce((state) => {
            const todo = state.todos.find((todo) => todo.id === todoId);
            if (todo) {
              todo.tasks.push(newTask); // 불변성 깨짐: 직접 수정
            }
            // return { todos: state.todos }; // 변경된 참조가 기존 상태와 같아 리렌더링되지 않음
          })
        ),
      toggleTodo: (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;