프론트엔드에서 상태 아래의 세가지의 요건을 충족하는 값을 이야기한다.
이중 가장 중요하게 봐야 하는 부분은 변경되면 리렌더링을 유발하는 데이터라는 부분이다. 이는 결국 렌더하는데 있어서 영향을 미치는 값 이라는 것이다.
상태를 어떻게 관리하느냐에 따라 웹을 렌더하는데 영향을 미칠 수 있다. 프론트엔드 개발자에게 상태관리는 중요하다
리액트는 독립적인 컴포넌트 단위로 구성되어 있다. useState나 useReducer를 사용하여 하나의 컴포넌트에서 상태를 관리하고 props를 통해 부모-자식 간에 상태를 전파할 수 있다.
상태가 시작된 지점과 어떤 컴포넌트를 지나고, 어떻게 전달하는지, 모든 흐름을 이해하고 기억한다면 useState와 props만을 사용해도 무리가 없다. 하지만 프로젝트의 규모가 커짐에 따라 관리해야 할 상태의 개수는 늘어나고 , 늘어난 상태를 전부 기억하고 이해하기는 어렵다.
그렇기 때문에 상태관리 툴을 사용해 효율적으로 상태를 관리할 필요가 있다.
전역 상태관리를 위한 툴로는 Context API, Redux Toolkit, Recoil, Zustand, Jotai 등이 널리 사용되고 있다.
상태관리는 크게 두가지 상태 관리 방식 이 존재하는데 중앙집중형과, 분산형이다.
상태를 최상위 또는 중앙 저장소에서 관리
하위 컴포넌트들이 중앙의 상태를 구독하고 사용
예시: Redux Toolkit, Context API, Zustand
각 컴포넌트가 자신의 상태를 개별적으로 관리
필요한 경우 상태를 상위로 끌어올림(state lifting)
예시: Recoil, Jotai 등
| 기능/특징 | 중앙집중형 | 분산형 |
|---|---|---|
| 리렌더링 제어 | 중앙 집중형, 단일 스토어 | 분산형, 여러 개의 작은 아톰(원자) 단위 |
| 사용 패턴 | Flux 패턴, 중앙 스토어에서 모든 상태 관리 | Atomic 패턴, 각 컴포넌트에서 개별적으로 관리 |
| 상태의 범위 | React 외부에서도 접근 가능 | React 컴포넌트 외부에서 직접 접근 불가 |
| 툴 | Redux Toolkit, Context API, Zustand | Recoil, Jotai |
하지만 오늘은 이 중 Zustand에 대해 알아보자.
Zustand란 상태라는 뜻을 가진 독일어이다.
단순화된 Flux 원리를 사용하는 작고 빠르며 확장 가능한 상태 관리 솔루션이다. Hooks에 기반해 편리한 API를 제공한다.
Zustand는 다음과 같은 장점을 지니고 있다.
npx create-react-app "프로젝트명"터미널에 위 명령어를 입력하여 프로젝트를 설치한다.
npm i zustand터미널에 위 명령어를 입력하여 Zustand 라이브러리를 설치한다.
src/store/ 폴더 안에 내가 만들 Zustand 파일을 만든다
스토어는 일반적으로 7가지 기능을 조합하여 사용한다. 조합해도 되고 단일로 사용 할 수도 있다.
이 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 }))
}))
export const useStore = create(
persist(
(set) => ({
count: 0,
increase: () => set((state) => ({ count: state.count + 1 }))
}),
{
name: 'store-name'
}
)
)
export const useStore = create(
devtools((set) => ({
count: 0,
increase: () => set((state) => ({ count: state.count + 1 }))
}))
)
const useStore = create(
devtools(
persist(
(set) => ({
count: 0,
increase: () => set((state) => ({ count: state.count + 1 }))
}),
{ name: 'store' }
)
)
)
사용할 컴포넌트에서 사전에 정의한 파일을 불러온다.
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() // 상태 변경 함수 호출
상태 변경 함수가 호출되면 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
)
총 11개의 파일로 이루어져있다.
실제로 내부를 확인 할경우 아래와같이 Barrel파일이 존재하나, 제외했다.
export { shallow } from './vanilla/shallow.ts'
export { useShallow } from './react/shallow.ts'