Zustand

LEE GYUHO·2024년 1월 24일
0

상태관리 라이브러리를 쓰는 이유

1. props drilling

리액트에서 상태는 단방향으로 흐르기 때문에 다른 컴포넌트나 페이지와 상태를 공유하기 위해서는 상태를 상위 컴포넌트로 끌어올리고 하위 컴포넌트로 내려주는 방식으로 사용해야 하는데 이렇게 되면 props drilling 현상이 발생합니다. 그러면 유지보수하기 힘들어지기 때문에 상태관리 라이브러리를 사용합니다.


2. 성능 최적화

  • Context API 문제
    Context API는 Provider의 값이 변경되면 모든 하위 컴포넌트가 다시 렌더링됩니다. 특정 하위 컴포넌트만 상태를 필요로 해도 전체 트리가 영향을 받는 경우가 많아 비효율적입니다.

  • Zustand의 해결책
    Zustand는 구독 기반 시스템을 사용하므로, 필요한 상태만 구독하여 렌더링을 최소화합니다. 상태 변경 시 관련 상태를 구독 중인 컴포넌트만 업데이트됩니다.

    const useStore = create((set) => ({
      count: 0,
      increment: () => set((state) => ({ count: state.count + 1 })),
    }));

    단일 함수 호출만으로 상태 생성, 업데이트, 구독을 설정할 수 있습니다.


3. 상태 모듈화와 구조화

  • Context API 문제
    Context API를 사용할 경우 상태를 모듈화하거나 구조화하기 어렵습니다. 각 도메인에 대해 별도의 Context를 만들어야 하며, 여러 Context를 조합해 사용하려면 추가적인 관리 코드가 필요합니다.

  • Zustand의 해결책
    Zustand는 Slice 패턴을 사용해 상태를 쉽게 모듈화할 수 있습니다. 도메인별 상태를 나누어 관리하면서도 하나의 store로 통합할 수 있습니다.

const createAuthSlice = (set) => ({
  isAuthenticated: false,
  login: () => set({ isAuthenticated: true }),
  logout: () => set({ isAuthenticated: false }),
});

const createTodoSlice = (set) => ({
  todos: [],
  addTodo: (todo) => set((state) => ({ todos: [...state.todos, todo] })),
});

const useStore = create((...a) => ({
  ...createAuthSlice(...a),
  ...createTodoSlice(...a),
}));

4. 간단한 API와 사용성

  • Context API 문제
    Context API는 전역 상태를 관리할 때 비교적 많은 코드가 필요하며, 복잡한 상태나 로직을 처리하기 어렵습니다.
    예를 들어, 여러 개의 Context를 사용하는 경우 Provider가 중첩되면서 코드가 복잡해질 수 있습니다.

  • Zustand의 해결책
    Zustand는 직관적이고 간단한 API를 제공하며, 불필요한 보일러플레이트 코드를 줄여줍니다.

    const useStore = create((set) => ({
      count: 0,
      increment: () => set((state) => ({ count: state.count + 1 })),
    }));

5. 비동기 작업 처리 용이

  • Context API 문제
    Context API는 비동기 작업을 처리할 때 추가적인 로직이나 미들웨어 없이 처리하기 어렵습니다.
    상태 업데이트 로직이 복잡할 경우 관리와 유지보수가 힘들어질 수 있습니다.

  • Zustand의 해결책
    Zustand는 상태 관리에서 비동기 작업을 쉽게 통합할 수 있습니다.

    const useStore = create((set) => ({
      data: null,
      fetchData: async () => {
        const response = await fetch('/api/data');
        const data = await response.json();
        set({ data });
      },
    }));

6. 미들웨어 지원

  • Context API 문제
    Context API는 기본적으로 미들웨어 기능을 제공하지 않습니다. 상태를 영속화하거나 디버깅을 위한 추가 작업을 하려면 개발자가 직접 구현해야 합니다.

  • Zustand의 해결책
    Zustand는 다양한 미들웨어(devtools, persist, immer)를 기본적으로 지원합니다. 이를 통해 상태를 디버깅하거나 로컬 스토리지에 저장하는 작업을 쉽게 처리할 수 있습니다.

    import { create } from 'zustand';
    import { devtools, persist } from 'zustand/middleware';
    
    const useStore = create(
      devtools(
        persist((set) => ({
          count: 0,
          increment: () => set((state) => ({ count: state.count + 1 })),
        }), { name: 'counter-storage' })
      )
    );

7. React 의존성 없음

  • Context API 문제
    Context API는 React에 종속적입니다. React 이외의 환경에서 사용하기 어렵습니다.

  • Zustand의 해결책
    Zustand는 React에 의존하지 않으며, React 외부 환경에서도 사용할 수 있습니다.
    상태 관리가 React에서 독립적으로 작동할 수 있으므로 더 유연합니다.


8. 상태 변경 추적 및 디버깅

  • Context API 문제
    Context API는 상태 변경을 추적하거나 디버깅할 수 있는 기본 도구를 제공하지 않습니다.

  • Zustand의 해결책
    Zustand는 Redux DevTools와의 통합을 통해 상태 변경을 시각적으로 추적할 수 있습니다.
    이를 통해 디버깅과 상태 추적이 훨씬 쉬워집니다.


9. 불변성 관리

  • Context API 문제
    Context API에서 상태를 업데이트할 때 불변성을 수동으로 관리해야 하며, 이는 복잡한 상태에서는 번거로울 수 있습니다.

  • Zustand의 해결책
    Zustand는 immer 미들웨어를 통해 상태 불변성을 자동으로 관리할 수 있습니다.

    import { create } from 'zustand';
    import { immer } from 'zustand/middleware';
    
    const useStore = create(
      immer((set) => ({
        todos: [],
        addTodo: (todo) => set((state) => {
          state.todos.push(todo);
        }),
      }))
    );

결론

Zustand를 Context API 대신 사용하는 이유는 단순히 props drilling 문제를 해결하는 것에 그치지 않고, 다음과 같은 추가적인 장점이 있기 때문입니다:

  1. props drilling
  2. 성능 최적화 (구독 기반 상태 관리).
  3. 상태의 모듈화 및 구조화 지원.
  4. 간단한 API와 사용성.
  5. 비동기 작업과의 통합 용이성.
  6. 다양한 미들웨어 지원.
  7. React 의존성 최소화.
  8. 상태 변경 추적 및 디버깅 지원.
  9. 불변성 관리 자동화.

Zustand

  • Zustand는 이 단방향 상태흐름 규칙을 깨지 않고 상태를 편하게 관리하기 위해 등장한 flux 패턴을 다루는 상태관리 라이브러리입니다.
  • Zustand는 React 애플리케이션에서 상태를 관리하기 위한 경량 상태 관리 라이브러리입니다. 독일어로 "상태"라는 뜻을 가지고 있습니다.

Zustand의 특징

1. 가볍다

  • Zustand는 매우 가벼운 라이브러리로, 번들 크기에 미치는 영향이 작습니다.

2. 간결하다

  • Zustand는 API가 매우 간결하며, 복잡한 설정이나 보일러플레이트 코드 없이 상태 관리를 시작할 수 있습니다. 이는 코드의 가독성을 향상시키고 학습 곡선을 줄여줍니다.

3. 효율적이다

  • Zustand는 필요한 컴포넌트만 리렌더링하도록 최적화되어 있습니다. 이는 불필요한 리렌더링을 줄이고 애플리케이션의 성능을 향상시킵니다.

4. 구독 기반 상태 관리

  • 컴포넌트는 필요한 상태만 구독하기 때문에 불필요한 렌더링을 최소화합니다.

5. 미들웨어 지원

  • devtools, persist, immer와 같은 미들웨어를 사용하여 디버깅, 상태 영속성, 불변성 관리 등을 쉽게 구현할 수 있습니다.

Zustand의 주요 개념

1. 상태 저장소 생성

create 함수는 Zustand의 상태 저장소를 생성합니다.

const useStore = create((set) => ({
 key: initialValue,
 action: () => set((state) => newState),
}));

set: 상태를 업데이트하는 함수.
state: 현재 상태를 나타내며, 업데이트를 위해 사용.


2. 상태 구독

Zustand는 컴포넌트가 사용하는 상태만 구독하도록 설계되었습니다.
이는 React 컨텍스트 API에서 발생할 수 있는 불필요한 리렌더링을 방지합니다.

const count = useStore((state) => state.count);

useStore에서 콜백을 사용하여 필요한 상태만 구독


3. 비동기 상태 업데이트

비동기 작업도 상태 업데이트에 쉽게 통합할 수 있습니다.

const useStore = create((set) => ({
 data: null,
 fetchData: async () => {
   const response = await fetch('/api/data');
   const data = await response.json();
   set({ data });
 },
}));

4. 미들웨어

Zustand는 다양한 미들웨어를 제공하여 상태 관리의 확장성을 높입니다.

1) devtools
Redux 개발자 도구와 통합하여 상태 변경을 추적할 수 있습니다.

import { create } from 'zustand';
import { devtools } from 'zustand/middleware';

const useStore = create(devtools((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
})));

2) persist
상태를 로컬 스토리지나 세션 스토리지에 영구 저장합니다.

import { persist } from 'zustand/middleware';

const useStore = create(
  persist((set) => ({
    count: 0,
    increment: () => set((state) => ({ count: state.count + 1 })),
  }), { name: 'counter-storage' })
);

3) immer
상태를 불변성 유지하며 쉽게 업데이트할 수 있도록 도와줍니다.

import { immer } from 'zustand/middleware';

const useStore = create(
  immer((set) => ({
    items: [],
    addItem: (item) => set((state) => {
      state.items.push(item);
    }),
  }))
);

Zustand의 고급 기능

1. Slice 패턴

Zustand는 상태를 모듈화하여 관리하기 위해 Slice 패턴을 사용할 수 있습니다.

 const createCounterSlice = (set) => ({
   count: 0,
   increment: () => set((state) => ({ count: state.count + 1 })),
 });

 const createTodoSlice = (set) => ({
   todos: [],
   addTodo: (todo) => set((state) => ({ todos: [...state.todos, todo] })),
 });

 const useStore = create((...a) => ({
   ...createCounterSlice(...a),
   ...createTodoSlice(...a),
 }));

2. 외부 상태와 통합

Zustand는 React 컨텍스트와 독립적이므로 외부 상태와 쉽게 통합됩니다.

 import { create } from 'zustand';

 const externalStore = create((set) => ({
   value: 0,
   setValue: (newValue) => set({ value: newValue }),
 }));

Flux 패턴

  • flux 패턴은 데이터를 중앙 집중형 스토어에 저장하고 Action을 통해 데이터를 조작하는 패턴입니다.

  • Flux는 일관된 방식으로 애플리케이션의 상태를 관리하고 업데이트하는데 도움을 줍니다.

  • Flux 패턴의 핵심은 "단방향 데이터 흐름"입니다. 이는 데이터가 애플리케이션을 통해 한 방향으로만 흐르게 하여 상태 변경을 예측 가능하게 만듭니다. 이는 복잡한 애플리케이션에서 상태 관리를 더욱 간단하고 효과적으로 만들어줍니다.

  • Flux 아키텍처는 크게 4가지 주요 구성 요소로 이루어져 있다.

    • Action: 사용자 상호작용이나 서버 응답 등 애플리케이션에서 발생하는 다양한 이벤트를 나타내는 객체입니다.
    • Dispatcher: 모든 액션을 받아 등록된 콜백 함수를 실행하는 역할을 합니다. 이는 액션을 통해 상태를 업데이트하는 중앙 허브 역할을 합니다.
    • Store: 애플리케이션의 상태와 상태를 변경하는 로직을 포함한다. Store는 Dispatcher에서 액션을 받아 상태를 업데이트하고, 상태가 변경되었음을 알리기 위해 이벤트를 방출합니다.
    • View: 사용자 인터페이스를 나타내며, Store에서 상태를 가져와 렌더링합니다. 상태가 변경되면 View는 새로운 상태를 가져와 다시 렌더링합니다.

여러 개의 store 사용 VS 하나의 store를 사용하고 slice를 사용

1. 여러 개의 store를 사용하는 경우

장점

  1. 독립성
  • 각 store는 서로 독립적이므로 상태 간의 의존성이 없습니다.
  • 상태가 분리되어 있으면 특정 상태를 관리하거나 테스트하기 더 쉽습니다.
  1. 간단한 상태 관리
  • 상태가 작고 서로 관련이 없는 경우, 여러 개의 store를 사용하면 더 간단합니다.
  • 컴포넌트에서 필요한 상태만 구독하므로 불필요한 렌더링이 발생하지 않습니다.

단점

  1. 상태 간의 의존성 관리 어려움
  • 서로 다른 store 간에 상태를 공유하거나 동기화해야 하는 경우 복잡성이 증가할 수 있습니다.
  1. 구조화의 어려움
  • 상태가 많아지면 여러 store를 관리하는 것이 번거로울 수 있습니다.

적합한 상황

  • 상태가 서로 독립적이고 공유가 필요 없는 경우.
  • 소규모 프로젝트나 간단한 상태 관리가 필요한 경우.

2. 하나의 store를 사용하고 slice를 사용하는 경우

장점

  1. 상태의 중앙 집중화
  • 모든 상태를 한 곳에서 관리하므로 상태 간의 의존성을 쉽게 처리할 수 있습니다.
  • 상태 변경 흐름을 추적하기 쉽습니다.
  1. 확장성과 유지보수성
  • Slice를 사용하여 상태를 모듈화하면 코드가 깔끔하고 확장성이 높아집니다.
  • 각 Slice가 특정 도메인에만 집중하도록 설계할 수 있습니다.
  1. 미들웨어 적용 용이
  • 단일 store에 미들웨어를 적용하면 모든 상태 관리에 한 번에 적용됩니다.

단점

  1. 의존성 분리 어려움
  • Slice로 분리해도 하나의 store를 사용하기 때문에 모든 상태가 한 곳에 모여 있어, 지나치게 복잡한 상태 관리가 될 수 있습니다.
  1. 불필요한 상태 구독 가능성
  • Slice를 나눴다고 해도 잘못된 설계로 인해 필요하지 않은 상태까지 구독할 위험이 있습니다.

적합한 상황

  • 상태 간의 의존성이 크고, 상태를 공유해야 하는 경우.
  • 중대형 프로젝트에서 상태를 체계적으로 관리해야 하는 경우.
  • Redux와 유사한 패턴을 따르고 싶을 때.

권장 방법

프로젝트의 규모에 따른 선택

  1. 소규모 프로젝트
  • 상태 간의 상호작용이 거의 없고 단순한 상태 관리가 필요한 경우, 여러 개의 store를 사용하는 것이 더 간단하고 직관적입니다.
  • 예: 독립적인 카운터, 모달 상태 관리 등.
  1. 중대형 프로젝트
  • 상태 간의 의존성이 높거나 상태를 체계적으로 관리해야 하는 경우, 하나의 store를 사용하고 Slice로 나누는 것이 더 적합합니다.
  • 예: 인증 상태, 사용자 프로필, 대규모 데이터를 관리하는 애플리케이션.
  1. 실무에서 주로 사용하는 패턴
  • Slice 패턴 기반 단일 store 방식
    대부분의 프로젝트에서 단일 store를 사용하고 Slice로 모듈화하여 상태를 관리합니다. 이는 상태를 중앙 집중화하면서도 도메인별로 코드를 분리해 확장성과 유지보수성을 확보할 수 있기 때문입니다.
  1. 예외적으로 여러 개의 store를 사용하는 경우
  • 각 store가 완전히 독립적이고 상태 간의 상호작용이 필요 없는 경우, 단일 store로 관리하는 것이 오히려 복잡할 수 있으므로 여러 store를 사용하는 것이 더 적합할 수 있습니다.

결론

일반적으로 권장되는 방법은 하나의 store를 사용하고 Slice로 나누는 방식입니다. 이 방법은 확장성과 유지보수성, 상태 간의 의존성 관리가 용이하기 때문에 대규모 애플리케이션에서 특히 유리합니다.

다만, 프로젝트가 작고 상태가 독립적인 경우에는 여러 개의 store를 사용하는 것이 더 직관적이고 효율적일 수 있습니다. 따라서 프로젝트의 복잡도와 상태 간의 의존성을 고려해 선택하는 것이 중요합니다.

profile
누구나 같은 팀으로 되길 바라는 개발자가 되자

0개의 댓글

관련 채용 정보