Zustand를 뜯어보자

MountionRiver·2024년 12월 11일

1. 리액트 상태관리란?

프론트엔드에서 상태 아래의 세가지의 요건을 충족하는 값을 이야기한다.

  • 컴포넌트가 관리하는 동적인 데이터
  • 시간이 지남에 따라 변할 수 있는 값
  • 변경되면 리렌더링을 유발하는 데이터

이중 가장 중요하게 봐야 하는 부분은 변경되면 리렌더링을 유발하는 데이터라는 부분이다. 이는 결국 렌더하는데 있어서 영향을 미치는 값 이라는 것이다.
상태를 어떻게 관리하느냐에 따라 웹을 렌더하는데 영향을 미칠 수 있다. 프론트엔드 개발자에게 상태관리는 중요하다

리액트는 독립적인 컴포넌트 단위로 구성되어 있다. useStateuseReducer를 사용하여 하나의 컴포넌트에서 상태를 관리하고 props를 통해 부모-자식 간에 상태를 전파할 수 있다.

상태가 시작된 지점과 어떤 컴포넌트를 지나고, 어떻게 전달하는지, 모든 흐름을 이해하고 기억한다면 useState와 props만을 사용해도 무리가 없다. 하지만 프로젝트의 규모가 커짐에 따라 관리해야 할 상태의 개수는 늘어나고 , 늘어난 상태를 전부 기억하고 이해하기는 어렵다.

그렇기 때문에 상태관리 툴을 사용해 효율적으로 상태를 관리할 필요가 있다.

전역 상태관리를 위한 툴로는 Context API, Redux Toolkit, Recoil, Zustand, Jotai 등이 널리 사용되고 있다.

2. 상태관리 패턴: 중앙집중형(top-down)과 분산형(bottom-up)

상태관리는 크게 두가지 상태 관리 방식 이 존재하는데 중앙집중형과, 분산형이다.

중앙집중형(Centralized, Top-down)

상태를 최상위 또는 중앙 저장소에서 관리
하위 컴포넌트들이 중앙의 상태를 구독하고 사용
예시: Redux Toolkit, Context API, Zustand

분산형(Distributed, Bottom-up)

각 컴포넌트가 자신의 상태를 개별적으로 관리
필요한 경우 상태를 상위로 끌어올림(state lifting)
예시: Recoil, Jotai 등

이를 표로 나타내보자

기능/특징중앙집중형분산형
리렌더링 제어중앙 집중형, 단일 스토어분산형, 여러 개의 작은 아톰(원자) 단위
사용 패턴Flux 패턴, 중앙 스토어에서 모든 상태 관리Atomic 패턴, 각 컴포넌트에서 개별적으로 관리
상태의 범위React 외부에서도 접근 가능React 컴포넌트 외부에서 직접 접근 불가
Redux Toolkit, Context API, ZustandRecoil, Jotai

하지만 오늘은 이 중 Zustand에 대해 알아보자.

3. Zustand란?

Zustand란 상태라는 뜻을 가진 독일어이다.

단순화된 Flux 원리를 사용하는 작고 빠르며 확장 가능한 상태 관리 솔루션이다. Hooks에 기반해 편리한 API를 제공한다.

Zustand는 다음과 같은 장점을 지니고 있다.

  • 사용방법이 매우 쉽다.
    바닐라 자바스크립트를 기준으로 핵심 로직의 코드 줄 수가 약 42줄밖에 되지 않는다.
  • 상태가 변경되면 불필요한 리렌더링을 일으키지 않는다.
  • 보일러플레이트가 거의 없다.
    보일러플레이트란 최소한의 수정으로 재사용할 수 있는 코드나 구조으로 여러 곳에서 재사용되며 반복적으로 비슷한 형태를 띄는 양상을 말한다.
  • redux Devtools를 사용할 수 있어 디버깅에 용이하다.
  • React 외부에서도 상태 접근 가능이 가능하다

4. Zustand 사용법

4-1 프로젝트 생성 및 설치

프로젝트 생성하기

  • npx create-react-app "프로젝트명"

터미널에 위 명령어를 입력하여 프로젝트를 설치한다.

Zustand 설치하기

  • npm i zustand

터미널에 위 명령어를 입력하여 Zustand 라이브러리를 설치한다.

4-2 스토어 설정

스토어 생성하기

src/store/ 폴더 안에 내가 만들 Zustand 파일을 만든다

스토어 정의하기

스토어는 일반적으로 7가지 기능을 조합하여 사용한다. 조합해도 되고 단일로 사용 할 수도 있다.

  1. 기본 저장 (메모리)
  2. persist (로컬 스토리지)
  3. devtools (개발 도구)
  4. subscribe (상태 변화 구독)
  5. combine (스토어 결합)
  6. immer (불변성 관리)
  7. custom middleware (사용자 정의)

이 7가지 기능은 각각 Zustand의 미들웨어인데 이 내용은추후 Zustand의 내부구조에서 자세히 다루도록 하자.
이 중 가장 프로젝트에서 가장 많이 사용되는 것들은 기본 저장, persist 두가지이며, 추가적으로 devtools 까지 사용한다.

  • 기본형식
Copyimport { create } from 'zustand'

interface BearState {
  bears: number
  increase: () => void
}

export const useBearStore = create<BearState>((set) => ({
  bears: 0,
  increase: () => set((state) => ({ bears: state.bears + 1 }))
}))
  • presist
export const useStore = create(
  persist(
    (set) => ({
      count: 0,
      increase: () => set((state) => ({ count: state.count + 1 }))
    }),
    {
      name: 'store-name'
    }
  )
)
  • devtools
export const useStore = create(
  devtools((set) => ({
    count: 0,
    increase: () => set((state) => ({ count: state.count + 1 }))
  }))
)
  • presist + devtools
const useStore = create(
  devtools(
    persist(
      (set) => ({
        count: 0,
        increase: () => set((state) => ({ count: state.count + 1 }))
      }),
      { name: 'store' }
    )
  )
)

4-3 스토어 사용하기

스토어 불러오기

사용할 컴포넌트에서 사전에 정의한 파일을 불러온다.

import { useBearStore } from './store'

상태 구독하기

스토어에서 필요한 상태를 선택하여 가져온다. 단일 상태나 여러 상태를 한번에 구독할 수 있다.

// 단일 상태 구독
const bears = useBearStore((state) => state.bears)

// 여러 상태 구독
const { bears, increase } = useBearStore((state) => ({
  bears: state.bears,
  increase: state.increase
}))

상태 변경하기

구독한 상태 변경 함수를 호출하여 스토어의 상태를 업데이트한다.
상태가 변경되면 해당 상태를 구독하고 있는 모든 컴포넌트가 자동으로 리렌더링된다.

const increase = useBearStore((state) => state.increase)
increase() // 상태 변경 함수 호출

4-4 상태 동작 방식

상태 업데이트 과정

상태 변경 함수가 호출되면 set 함수를 통해 스토어의 상태가 업데이트되고, 이 변경사항이 구독 중인 컴포넌트들에게 전달된다.

const useBearStore = create((set) => ({
  bears: 0,
  increase: () => set((state) => ({ bears: state.bears + 1 }))
}))

리렌더링 처리

상태가 변경되면 해당 상태를 구독하고 있는 컴포넌트만 선택적으로 리렌더링된다. 불필요한 리렌더링을 방지하기 위해 필요한 상태만 정확하게 선택해서 구독해야 한다.

// 필요한 상태만 구독
const bears = useBearStore((state) => state.bears)

// 불필요한 리렌더링 발생 가능
const state = useBearStore()

최적화 방법

객체를 구독할 때는 shallow 비교를 사용하여 실제 값이 변경될 때만 리렌더링이 발생하도록 최적화할 수 있다.

import { shallow } from 'zustand/shallow'

// shallow 비교를 사용한 최적화
const { bears, fish } = useBearStore(
  (state) => ({ 
    bears: state.bears, 
    fish: state.fish 
  }),
  shallow
)

5. Zustand 내부구조

5-1 Zustand의 파일들

  1. 6개의 미들웨어와
  2. 핵심 로직 ,핵심로직을 리액트와 통합하는 로직
  3. 상태 변경을 감지할 때 사용하는 로직 ,리액트와 통합하는 로직
  4. 선택적 상태 구독과 성능 최적화하는 로직

총 11개의 파일로 이루어져있다.

실제로 내부를 확인 할경우 아래와같이 Barrel파일이 존재하나, 제외했다.

export { shallow } from './vanilla/shallow.ts'
export { useShallow } from './react/shallow.ts'

5-2 각 파일 뜯어보기

5-2-1 핵심로직과 리액트와의 통합

5-2-2 상태 변경 감지와, 리액트와의 통합

  • vailla.ts
  • react.ts

5-2-3 persist 미들웨어

5-2-4 devtools 미들웨어

  • 개발 도구와의 연동

5-2-5 subscribeWithSelector 미들웨어

  • 선택적 상태 구독

5-2-6 redux 미들웨어

  • Redux 스타일 상태관리

5-2-7 combine 미들웨어

  • 상태결합

5-2-8 immer 미들웨어

  • 불변성관리

5-2-9 선택적 상태 구독과 성능 최적화

  • traditional.ts

0개의 댓글