본문 바로가기
FE/리액트 훅을 활용한 마이크로 상태관리

7. 사용 사례 시나리오 1: Zustand

by Toddler_AD 2025. 10. 8.
  • 지금까지 리액트에서 전역 상태를 구현하는 데 사용할 수 있는 몇 가지 기본 패턴을 알아봤다. 이번 장에서는 실제로 구현되고 공개적으로 사용 가능한 패키지인 Zustand에 대해 알아본다.
  • Zustand는 주로 리액트의 모듈 상태를 생성하도록 설계된 작은 라이브러리다. 상태 객체를 수정할 수 없고 항상 새로 만들어야 하는 불변 갱신 모델을 기반으로 한다. 렌더링 최적화는 선택자를 사용해 수동으로 한다. 또한 간단하면서도 강력한 store 생성자 인터페이스가 있다.
  • 이번 장에서는 모듈 상태와 구독이 어떻게 사용되는지 살펴보고 라이브러리 API가 어떻게 동작하는지 살펴보겠다.
  • 이번 장에서는 다음과 같은 주제를 다룬다.
    • 모듈 상태와 불변 상태 이해하기
    • 리렌더링 최적화를 위한 리액트 훅 추가하기
    • 읽기 상태와 갱신 상태 사용하기
    • 구조화된 데이터 다루기 
    • 라이브러리와 접근 방식의 장단점

모듈 상태와 불변 상태 이해하기

  • Zustand는 상태를 유지하는 store를 만드는 데 사용되는 라이브러리다. Zustand는 주로 모듈 상태를 위해 설계됐으므로 모듈에서 store를 정의하고 내보내는 것을 할 수 있다. 이 라이브러리는 상태 객체 속성을 생신할 수 없는 불변 상태 모델을 기반으로 한다. 상태를 변경하기 위해서는 새 객체를 생성해서 대처해야 하며, 수정하지 않은 객체는 재사용해야 한다. 불변 상태 보델의 장점은 상태 객체의 참조에 대한 동등성만 확인하면 변경 여부를 알 수 있으므로 객체의 값 전체를 확인할 필요가 없다는 것이다.
  • 다음은 count 상태를 만드는 아주 간단한 예제다. 초기 상태를 반환하는 store 생성자 함수가 필요하다. 
// store.ts
import { create } from "zustand";
const store = create(() => ({ count: 0, text:"hello" }));
  • store는 getState, setState, subscribe 같은 기능을 사용할 수 있다. getState를 사용해 store의 상태를 가져오고 setState를 사용해 store의 상태를 설정할 수 있다.
console.log(countState); // { count: 0, text:"hello" }
store.setState({ count: 1 });
console.log(countState2); // { count: 1, text:"hello" }
  • 상태가 불변이기 때문에 ++state.count 처럼 변경하는 것은 불가능하다. 다음은 상태의 불변성을 위반하는 잘못된 사용법이다.
const state1 = store.getState();
state1.count = 2;  // 잘못됨
store.setState(state1);
  • state.count = 2는 잘못된 사용법이므로 원하는 대로 작동하지 않는다. 이 잘못된 사용법에서는 새로운 상태가 이전 상태와 동일한 참조를 가지기 때문에 라이브러리는 변경 사항을 제대로 감지할 수 없다.
  • 상태는 반드시 store.setState({ count : 2 })와 같이 객체를 이용해 갱신해야 한다. store.setState는 함수를 통해 갱신하는 것도 가능하다.
store.setState((prev) => ({ count: prev.count + 1 }));
  • 이를 함수 갱신이라고 하며, 이전 상태를 이용해 상태를 쉽게 변경할 수 있다.
  • 지금까지는 상태에 하나의 count 속성만 가지고 있었지만 상태는 여러 속성을 가질 수 있다. 다음 예제에서  text라는 속성을 추가해보겠다.
export const store = create(() => ({
  count: 0,
  text: "hello",
}));
  • 다시 한번 강조하면, 상태는 다음과 같이 불변으로 변경돼야 한다.
store.setState({
  count: 1,
  text: "hello",
});
  • 참고로 store.setState()는 새 상태와 이전 상태를 병합한다. 따라서 설정하려는 속성만 지정해도 된다.
store.setState({
  count: 2,
});

console.log(store.getState()); // ---> {count: 2, text: 'hello'}
  • 첫 번째 console.log문은 {count: 1, text:'hello'}를 출력하고 두 번째는 {count: 2, text:''hello'}를 출력한다.
  • count만 변경하므로 text속성은 변경되지 않는다. 이것은 내부적으로 Object.assign()으로 구현된다.
Object.assign({}, oldState, newState);
  • Object.assign 함수는 oldState와 newState 속성을 병합해서 새 객체를 반환한다.
  • store 함수에서 남은 부분은 store.subscribe로, 이 함수를 사용하면 store의 상태가 변경될 때마다 호출되는 콜백 함수를 등록할 수 있다. 다음과 같이 동작한다.
store.subscribe(() => {
  console.log("store state is changed");
});
store.setState({ count: 3 });
  • store.state문을 사용하면 구독으로 인해 'store state is changed'라는 메시지가 콘솔에 출력된다. store.subscribe는 리액트 훅을 구현하기 위한 중요한 함수다.
  • 이번 절에서는 Zustand의 기본 사용법을 알아봤다. 이것은 4장 '구독을 이용한 모듈 상태 공유'에서 배운 것과 매우 유사하다는 것을 알 수 있다. 본질적으로 Zustand는 불변 상태 모델 및 구독이라는 아이디어를 중심으로 설꼐된 작고 가벼운 라이브러리다.
  • 다음 절에서는 이랙트에서 store를 사용하는 방법을 알아본다.

리액트 훅을 사용한 리렌더링 최적화

  • 전역 상태를 사용하는 경우 모든 컴포넌트가 전역 상태를 사용하는 것은 아니기 때문에 리렌더링 최적화가 필요하다. Zustand가 이 문제를 어떻게 해결하는지 알아보자.
  • 리액트에서 store를 사용하려면 사용자 정의 훅이 필요하다. Zustand의 create 함수는 훅으로 사용할 수 있는 store를 생성한다.
  • 리액트 훅의 명명 규칙을 따르기 위해 생성된 값의 이름은 store 대신 useStore로 지정한다.
import { create } from "zustand";

const useStore = create(() => ({
  count: 0,
  text: "hello",
}));
  • 다음으로 리액트 컴포넌트에서 생성된 useStore 훅을 사용해야 한다. useStore 훅이 호출되면 모든 속성을 포함한 전체 상태 객체를 반환한다. 예를 들어, store에서 count 값을 보여주는 컴포넌트를 만들어 보자.
import { useStore } from "./store.ts";

const Component = () => {
    const { count, text } = useStore();
    return <div>count: {count}</div>
};
  • 이 컴포넌트는 count 값을 보여주면서 store 상태가 변경될 때마다 리렌더링된다. 당장은 잘 작동하지만 text 값만 변경되고 count 값이 변경되지 않으면 컴포넌트는 기본적으로 동일한 JSX 요소를 출력하기에 사용자는 화면에서 변경 사항을 볼 수 없다. 한마디로 화면과 관련 없는 값을 text 값을 변경하더라도 리렌더링이 된다는 의미다.
  • 리렌더링을 피해야 하는 경우 useStore에 선택자 함수를 지정할 수 있다. 이전 코드를 다음과 같이 선택자 함수를 사용하도록 재작성한다.
const Component = () => {
  const count = useStore((state) => state.count);
  return <div>count: {count}</div>;
};
  • 이렇게 하면 count 값이 변경될 때만 컴포넌트가 리렌더링된다.
  • 이 같은 선택자 기반 리렌더링 제어를 수동 렌더링 최적화라고 한다. 리렌더링을 피하기 위해 선택자는 선택자 함수가 반환하는 결과를 비교하는 방식으로 작동한다. 리렌더링을 피하기 위해서는 안정적으로 결과를 반환하도록 선택자 함수를 정의할 때 주의해야 한다.
  • 예를 들어, 다음 예제에서는 선택자 함수가 새 객체를 포함해 새로운 배열을 생성하기 때문에 원하는 대로 작동하지 않는다.
const Component = () => {
  const [{ count }] = useStore((state) => ({ count: state.count }));
  return <div>{count}</div>;
};
  • 결과적으로 count 값이 변경되지 않은 경우에도 컴포넌트가 리렌더링된다. 이는 렌더링 최적화를 위해 선택자를 사용할 때 흔히 볼 수 있는 함정이다. 즉, 잘못된 사용법이다.
  • 선택자 기반 렌더링 최적화의 장점은 선택자 함수를 명시적으로 작성하기 때문에 동작을 정확히 예측할 수 있다는 점이다. 하지만 선택자 기반 렌더링 최적화의 단점으로 객체 참조에 대한 이해가 필요하다.
  • 이번 절에서는 Zustand로 만든 훅을 사용하는 방법과 선택자를 이용해 리렌더링을 최적화하는 방법을 알아봤다.
  • 다음으로 간단한 예제를 통해 리액트와 Zustand를 함께 사용하는 방법을 알아보자.

읽기 상태와 갱신 상태 사용하기

  • Zustand는 다양한 방식으로 사용할 수 있는 라이브러리지만 상태를 읽고 갱신하는 몇 가지 패턴이 있다. 간단한 예제를 통해 Zustand의 사용법을 배워보자.
  • 다음과 같이 count1과 count2 속성을 가진 작은 store가 있다고 가정하자.
type StoreState = {
  count1: number;
  count2: number;
};

const useStore = create<StoreState>(() => ({
  count1: 0,
  count2: 0,
}));
  • 코드를 보면 count1과 count2라는 두 개의 속성을 가지고 새로운 store를 만든다. 여기서 StoreState는 타입스크립트 문법인 type을 통해 정의되었다.
  • 다음으로 count1 값을 보여줄 Counter1 컴포넌트를 정의한다. selectCount1 선택자 함수를 미리 만든 후 useStore에 전달해서 리렌더링을 최적화할 것이다.
const selectCount1 = (state: StoreState) => state.count1;

const Counter1 = () => {
  const count1 = useStore(selectCount1);
  const inc1 = () => { useStore.setState((prev) => ({ count1: prev.count1 + 1 })
  );
 };
 
  return (
    <div>
      count1: {count1} <button onClick={inc1}>+1</button>
    </div>
  );
};
  • 보시다시피 inc1이 인라인 함수로 정의돼 있다. 자세히 보면 store에서 setState 함수를 호출한다. 이는 일반적으로 사용되는 패턴이고 더 높은 재사용성과 가독성을 위해 store에 함수를 미리 정의할 수 있다. 
  • create 함수에 전달되는 store 생성자 함수는 몇 가지 인수를 받는다. 첫 번째 인수는 store의 setState 함수다. 이 기능으로 store를 재정의해보자.
type StoreState = {
  count1: number;
  count2: number;
  inc1: () => void;
  inc2: () => void;
};

const useStore = create<StoreState>((set) => ({
  count1: 0,
  count2: 0,
  inc1: () => set((prev) => ({ count1: prev.count1 + 1 })),
  inc2: () => set((prev) => ({ count2: prev.count2 + 1 })),
}));
  • 이제 store에는 함수 속성인 inc1과 inc2라는 두 개의 새로운 속성이 있다. 첫 번째 인수의 이름을 setState의 줄임말인 set으로 지정하는 것은 좋은 규칙이다.
  • 새로운 store를 사용해 Counter2 컴포넌트를 만들어보자. 이전 Counter1 컴포넌트와 비교해보면 동일한 방식으로 리팰터링할 수 있음을 알 수 있다. 
const selectCount2 = (state: StoreState) => state.count2;
const selectInc2 = (state: StoreState) => state.inc2;

const Counter2 = () => {
  const count2 = useStore(selectCount2);
  const inc2 = useStore(selectInc2);
  return (
    <div>
      count2: {count2} <button onClick={inc2}>+1</button>
    </div>
  );
};
  • 이 예제에는 selectInc2라는 새로운 선택자 함수가 있고, inc2 함수는 useStore의 결과일 뿐이다. 마찬가지로 store에 함수를 더 추가해서 몇 개의 로직이 컴포넌트 외부에 위치하게 할 수 있다. 상태 갱신 로직을 상태 값에 가깝게 배치하는 것은 Zustand의 setState가 이전 상태와 새로운 상태를 병합하기 위해서다. '모듈 상태와 불변 상태 이해하기' 절에서 Object.assign이 어떻게 사용되는지를 알아봤다.
  • 파생 상태를 생성하려면 어떻게 해야 할까? 파생 상태에 대한 선택자를 사용하면 된다. 먼저 단순한 예제를 살펴보자. 다음은 ount1과 count2의 합계를 보여주는 새로운 컴포넌트다. 
const Total = () => {
    const count1 = useStore(selectCount1);
    const count2 = useStore(selectCount2);
    return(
        <div>
            total: { count1 + count2 }
        </div>
    );
};
  • 이것은 유효한 패턴이며 그대로 사용해도 되지만 ount1이 증가하고 count2가 같은 양만큼 감소할 때 리렌더링이 발생하는 에지 케이스가 있다. 합계는 변경되지 않지만 리렌더링된다. 이것을 피하기 위해 파생 상태에 대한 선택자 함수를 사용할 수 있다.
  • 다음 예제로 함계를 계산하는 데 사용되는 대로운 selectTotal 함수를 살펴보자.
const selectTotal = (state: StoreState) => state.count1 + state.count2;

const Total = () => {
  const total = useStore(selectTotal);
  return (
    <div>
      total: {total}
    </div>
  );
};
  • 위 코드에서는 합계가 변경될 때만 리렌더링 된다.
  • 선택자에서 total을 계산하는 이 방법도 유효한 해결책이지만 store에서 합계를 생성할 수 있는 다른 접근 방식을 살펴보자. store에서 합계를 생성할 수 있다면 결과를 기억할 수 있고 많은 컴포넌트가 값을 사용할 때 불필요한 계산을 피할 수 있다. 이것은 흔하지는 않지만 계산이 매우 많은 경우 중요하다. 단순한 방법은 다음과 같다.
const useStore = create((set) => ({
    count1: 0,
    count2: 0,
    total: 0,
    inc1: () => set((prev) => ({
        ...prev,
        count1: prev.count1 + 1,
        total: prev.count1 + 1 + prev.count2,
    })),

    inc2: () => set((prev) => ({
        ...prev,
        count2: prev.count2 + 1,
        total: prev.count2 + 1 + prev.count1,
    })),
}))
  • 이 작업을 수행하는 더 정교한 방법이 있지만 기본 아이디어는 여러 속성을 동시에 계산한 후 동기화 상태를 유지하는 것이다. 다른 라이브러리인 Jotai는 이 문제를 잘 처리한다. 자세한 내용은 8장 '사용 사례 시나리오 2: Jotai'에서 살펴보겠다.
  • 예제 애플리케이션을 실행하기 위해 마지막으로 App 컴포넌트를 만들어보자.
const App = () => (
  <>
    <Counter1 />
    <Counter2 />
    <Total />
  </>
);

 

  • 첫 번째 버튼을 클릭하면 count1 레이블과 total 레이블 뒤의 숫자가 화면에서 증가하는 것을 볼 수 있다. 두 번재 버튼도 마찬가지로 클릭하면 화면의 count2 레이블과 total 레이블 뒤 숫자가 증가하는 것을 볼 수 있다.
  • 이번 절에서는 Zustand에서 자주 사용되는 방식으로 상태를 읽고 갱신하는 방법을 알아봤다. 다음으로 구조화된 데이터를 처리하는 방법과 배열을 사용하는 방법을 알아보자.

구조화된 데이터 처리하기

  • 단순히 숫자를 다루는 예제는 상당히 쉽다. 실제로는 객체, 배열 및 이들의 조합을 처리하는 경우가 많다. 다른 예제를 통해 Zustand의 사용법을 알아보자. 이번에는 잘 알려진 Todo 애플리케이션 예제를 만들어보겠다. Todo 애플리케이션은 다음과 같은 작업을 수행할 수 있다.
    • 새로운 할일을 생성한다.
    • 할일 목록을 표시한다.
    • 할일을 완료 상태로 전환한다.
    • 할일을 제거한다.
  • 먼저 스토어를 만들기 전에 몇 가지 타입을 정해야 한다. 다음은 Todo 객체의 타입 정의다. 이 객체에는 id, title, done 속성이 있다.
type Todo = {
  id: number;
  title: string;
  done: boolean;
};
  • 이제 Todo로 StoreState 타입을 정의할 수 있다. 스토어의 값 중 하나는 할일 목록인 todos다. 이 외에도 할일 속성을 조작하는 데 사용할 수 있는 세 가지 함수인 addTodo, removeTodo, toggleTodo가 있다.
type StoreState = {
  todos: Todo[];
  addTodo: (title: string) => void;
  removeTodo: (id: number) => void;
  toggleTodo: (id: number) => void;
};
  • todo 속성은 객체의 배열이다. store 상태에 객체 배열을 두는 것은 일반적인 관행이며, 이번 절에서 중점적으로 살펴볼 부분이다.
  • 다음으로 store르 정의해야 한다. 이것은 useStore라는 훅이기도 하다. 생성할 때 store에는 빈 todos 속성과 addTodo, removeTodo, toggleTodo라는 세 가지 함수가 있다. nextId는 새 할일 목록에 고유한 id를 제공하기 위한 단순한 방법으로 create 함수 외부에 정의돼 있다.
let nextId = 0;

const useStore = create<StoreState>((set) => ({
  todos: [],
  addTodo: (title) =>
    set((prev) => ({
      todos: [...prev.todos, { id: ++nextId, title, done: false }],
    })),
  removeTodo: (id) =>
    set((prev) => ({
      todos: prev.todos.filter((todo) => todo.id !== id),
    })),
  toggleTodo: (id) =>
    set((prev) => ({
      todos: prev.todos.map((todo) =>
        todo.id === id ? { ...todo, done: !todo.done } : todo
      ),
    })),
}));
  • 코드를 보면 addTodo, removeTodo, toggleTodo 함수가 불변 방식으로 구현돼 있음을 알 수 있다. 즉, 기존 객체와 배열을 변경하지 않고 새로운 객체와 배열을 생성한다.
  • 이제 Todolist 컴포넌트를 만들기 전에 하나의 할일을 렌더링하는 TodoItem 컴포넌트를 정의한다.
const TodoItem = ({ todo }: { todo: Todo }) => {
  const removeTodo = useStore(selectRemoveTodo);
  const toggleTodo = useStore(selectToggleTodo);
  return (
    <div>
      <input
        type="checkbox"
        checked={todo.done}
        onChange={() => toggleTodo(todo.id)}
      />
      <span
        style={{
          textDecoration: todo.done ? "line-through" : "none",
        }}
      >
        {todo.title}
      </span>
      <button onClick={() => removeTodo(todo.id)}>Delete</button>
    </div>
  );
};
  • TodoItem 컴포넌트는 todo 객체를 props로 받기 때문에 상태라는 측면에서 매우 단순한 컴포넌트다. TodoItem 컴포넌트에는 removeTodo로 처리되는 버튼과 toggleTodo로 처리되는 체크박스로 두 가지 컨트롤이 있다. 각 컨트롤에 대한 store에서 제공하는 두 가지 함수인 selectRemoveTodo와 selectToggleTodo함수는 useStore 함수에 전달되어 각각 removeTodo와 toggleTodo 함수를 가져온다.
  • 다음으로 TodoItem 컴포넌트의 메모된 버전인 MemoedTodoItem을 만들어 보자.
const MemoedTodoItem = memo(TodoItem);
  • 이제 이것이 애플리케이션에 어떤 도움이 되는지 알아보기 위해 TodoItem 컴포넌트를 만들어 보겠다. 이 컴포넌트는 stoe에서 todos 속성을 선택하는 데 사용되는 함수인 selectTodos를 사용한다. 그런 다음 todos 배열을 순회하면 MemoedTodoItem을 렌더링한다.
  • 불필요한 리렌더링을 피하려면 메모된 컴포넌트를 사용하는 것이 중요하다. store 상태를 불변 방식으로 갱신하기 때문에 todos 배열에 있는 대부분의 todo 객체는 변경되지 않는다. todo 객체를 MemoedTodoItem 객체에 전달한 후 변경되지 않으면 컴포넌트가 리렌더링되지 않는다. 즉, totos 배열이 변경될 때마다 TodoList 컴포넌트는 리렌더링 되지만 MemoedTodoItem 컴포넌트는 todo 항목이 변경되는 경우에만 리렌더링된다.
  • 다음 코드는 selectTodos 함수와 Todolist 컴포넌트 구현을 보여준다.
const selectTodos = (state: StoreState) => state.todos;

const TodoList = () => {
  const todos = useStore(selectTodos);
  return (
    <div>
      {todos.map((todo) => (
        <MemoedTodoItem key={todo.id} todo={todo} />
      ))}
    </div>
  );
};
  • Todolist 컴포넌트는 todos 목록을 가져오고 각 todo 항목에 대해 MemoedTodoItem 컴포넌트를 렌더링한다.
  • 이제 남은 것은 todo를 추가하는 기능이다. NewTodo는 텍스트 상자와 버튼을 렌더링하고 버튼을 클릭할 때 addTodo 함수를 호출하는 데 사용할 수 있는 컴포넌트다. 그리고 selectAddTodo는 store에서 addTodo 함수를 선택하는 데 사용할 수 있는 함수다.
const selectAddTodo = (state: StoreState) => state.addTodo;

const NewTodo = () => {
  const addTodo = useStore(selectAddTodo);
  const [text, setText] = useState("");
  const onClick = () => {
    addTodo(text);
    setText("");
  };
  return (
    <div>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <button onClick={onClick} disabled={!text}>
        Add
      </button>
    </div>
  );
};
  • NewTodo의 동작 개선과 관련해서 언급해야 할 두 가지 사항이 있다.
    • 버튼을 클릭하면 텍스트 상자가 지워진다.
    • 텍스트 상자가 비어 있으면 버튼을 비활성화한다.
  • 마지막으로 Todo 애플리케이션을 완성하기 위해 다음과 같이 App 컴포넌트를 정의한다.
const App = () => (
  <>
    <TodoList />
    <NewTodo />
  </>
);
  • 이 앱을 실행하면 처음에는 텍스트 상자만 표시되고 [Add] 버튼이 비활성화된다.
  • 텍스트를 입력하고 [Add] 버튼을 클릭하면 다음과 같은 항목이 나타난다.
  • 체크박스를 클릭하면 할일 항목이 done 상태로 전환된다.
  • 화면에서 [Delete] 버튼을 클릭하면 해당 항목이 삭제된다.

 

  • 원하는 만큼 항목을 추가할 수 있다. 이 모든 기능은 이 절에서 설명한 코드로 구현된다. store의 불변 상태 갱신과 리액트가 제공하는 memo 함수 덕분에 리렌더링을 최적화할 수 있었다.
  • 이번 절에서는 일반적인 Todo 애플리케이션 예제를 통해 배열을 처리하는 방법을 배웠다. 다름으로 이 라이브러리의 장단점과 일반적인 접근 방식에 대해 알아본다.

이 접근 방식과 라이브러리의 장단점

  • 이번 접근 방식을 구현하기 위해 Zustand 또는 다른 라이브러리를 사용할 때의 장단점을 알아보자.
  • 간단히 요약하자면 Zustand의 읽기 및 쓰기 상태는 다음과 같다.
    • 일기 상태 : 리렌더링을 최적화하기 위해 선택자 함수를 사용한다.
    • 쓰기 상태 : 불변 상태 모델을 기반으로 한다.
  • 핵심은 리액트가 최적화를 위해 객체 불변성을 기반으로 한다는 점이다. 한 가지 예로 useState를 들 수 있다. 리액트는 불변성에 기반한 객체 참조 동등성으로 리렌더링을 최적화한다. 다음 예제를 통해 이러한 동작 방식을 살펴보자.
const countObj = { value: 0 };

const Component = () => {
    const [count, setCount] = useState(countObj);
    const handleClick = () => {
        setCount(countObj);
    };
    useEffect(() => {
        console.log("component updatede")
    });
    return{
        <>
            {count.value}
            <button onClick={handleClick}> Update </button>
        </>
    };
};
  • 여기서는 Update 버튼을 클릭해도 "component updated"라는 메시지가 표시되지 않는다. 객체 참조가 동일하다면 리액트는 countObj 값이 변경되지 않는다고 추측하기 때문이다. 다음과 같이 handleClick 함수를 변경하더라도 아무런 변화가 없다는 의미다.
const handleClick = () => {
    countObj.value += 1;
    setCount(countObj);
};
  • handleClick 함수를 호출하면 countObj 값은 변경되지만 countObj 객체는 변경되지 않기 때문에 리액트는 변경되지 않았다고 추측한다. 이것이 바로 리액트가 불변성을 기반으로 최적화 한다는 의미다. 이와 동일한 동작은 memo나 useMemo와 같은 함수에서도 볼 수 있다.
  • Zustand 상태 모델은 이러한 객체 불변성 규칙과 완전히 일치한다. 선택자 함수를 사용한 Zustand의 렌더링 최적화 역시 불변성을 기반으로 한다. 즉, 선택자 함수가 참조적으로 동일한 객체(또는 값)를 반환하면 객체가 변경되지 않은 것으로 간주하고 리렌더링을 하지 않는 것이다.
  • Zustand는 리액트와 동일한 모델을 사용해 라이브러리의 단순성과 번들 크기가 작다는 점에서 큰 이점이 있다.
  • 반면, Zustand의 한계는 선택자를 이용한 수동 렌더링 최적화다. 객체 참조 동등서을 이해해야 하며, 선택자 코드를 위해 보일러플레이트 코드를 많이 작성해야 할 필요가 있다.
  • 요약하면, Zustand 또는 이러한 방식으로 구현된 다른 라이브러리는 리액트 원칙에서 간단한 기능을 추가한 것이다. 작은 번들 크기를 가진 라이브러리가 필요하거나 참조 동등성 및 메모이제이션에 익숙하거나 수동 렌더링 최적화를 선호하는 경우에는 Zustand 라이브러리를 추천한다.

정리

  • 이번 장에서는 Zustand 라이브러리에 대해 알아봤다. 이 라이브러리는 리액트에서 모듈 상태를 사용하는 작은 라이브러리다. 라이브러리의 사용법을 파악하기 위해 카운터 예제와 할일 관리 예제를 살펴봤다. 우리는 일반적으로 이 라이브러리를 사용해 객체 참조 동등성을 이해한다. 요구사항과 이번 장에서 배운 내용에 따라 이 라이브러리 또는 유사한 접근 방식을 선택할 수 있다.
  • 이번 장에서는 store 생성자에 일부 기능을 제공할 수 있는 미들웨어와 리액트 생명주기에서 store를 생성하는 비모듈 상태 사용 등 Zustand의 다른 몇 가지 기능에 대해서는 다루지는 않았다. 이는 라이브러리를 선택할 때 고려해야 할 또 다른 사항이 될 수 있다. 더 자세한 최신 정보는 항상 공식 라이브러리 문서를 참고하는 것이 좋다.