리액트에서 상태는 단방향으로 흐르기 때문에 다른 컴포넌트나 페이지와 상태를 공유하기 위해서는 상태를 상위 컴포넌트로 끌어올리고 하위 컴포넌트로 내려주는 방식으로 사용해야 하는데 이렇게 되면 props drilling 현상이 발생합니다. 그러면 유지보수하기 힘들어지기 때문에 상태관리 라이브러리를 사용합니다.
Context API 문제
Context API는 Provider의 값이 변경되면 모든 하위 컴포넌트가 다시 렌더링됩니다. 특정 하위 컴포넌트만 상태를 필요로 해도 전체 트리가 영향을 받는 경우가 많아 비효율적입니다.
Zustand의 해결책
Zustand는 구독 기반 시스템을 사용하므로, 필요한 상태만 구독하여 렌더링을 최소화합니다. 상태 변경 시 관련 상태를 구독 중인 컴포넌트만 업데이트됩니다.
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
단일 함수 호출만으로 상태 생성, 업데이트, 구독을 설정할 수 있습니다.
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),
}));
Context API 문제
Context API는 전역 상태를 관리할 때 비교적 많은 코드가 필요하며, 복잡한 상태나 로직을 처리하기 어렵습니다.
예를 들어, 여러 개의 Context를 사용하는 경우 Provider가 중첩되면서 코드가 복잡해질 수 있습니다.
Zustand의 해결책
Zustand는 직관적이고 간단한 API를 제공하며, 불필요한 보일러플레이트 코드를 줄여줍니다.
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
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 });
},
}));
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' })
)
);
Context API 문제
Context API는 React에 종속적입니다. React 이외의 환경에서 사용하기 어렵습니다.
Zustand의 해결책
Zustand는 React에 의존하지 않으며, React 외부 환경에서도 사용할 수 있습니다.
상태 관리가 React에서 독립적으로 작동할 수 있으므로 더 유연합니다.
Context API 문제
Context API는 상태 변경을 추적하거나 디버깅할 수 있는 기본 도구를 제공하지 않습니다.
Zustand의 해결책
Zustand는 Redux DevTools와의 통합을 통해 상태 변경을 시각적으로 추적할 수 있습니다.
이를 통해 디버깅과 상태 추적이 훨씬 쉬워집니다.
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 문제를 해결하는 것에 그치지 않고, 다음과 같은 추가적인 장점이 있기 때문입니다:
create 함수는 Zustand의 상태 저장소를 생성합니다.
const useStore = create((set) => ({
key: initialValue,
action: () => set((state) => newState),
}));
set: 상태를 업데이트하는 함수.
state: 현재 상태를 나타내며, 업데이트를 위해 사용.
Zustand는 컴포넌트가 사용하는 상태만 구독하도록 설계되었습니다.
이는 React 컨텍스트 API에서 발생할 수 있는 불필요한 리렌더링을 방지합니다.
const count = useStore((state) => state.count);
useStore에서 콜백을 사용하여 필요한 상태만 구독
비동기 작업도 상태 업데이트에 쉽게 통합할 수 있습니다.
const useStore = create((set) => ({
data: null,
fetchData: async () => {
const response = await fetch('/api/data');
const data = await response.json();
set({ data });
},
}));
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는 상태를 모듈화하여 관리하기 위해 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),
}));
Zustand는 React 컨텍스트와 독립적이므로 외부 상태와 쉽게 통합됩니다.
import { create } from 'zustand';
const externalStore = create((set) => ({
value: 0,
setValue: (newValue) => set({ value: newValue }),
}));
flux 패턴은 데이터를 중앙 집중형 스토어에 저장하고 Action을 통해 데이터를 조작하는 패턴입니다.
Flux는 일관된 방식으로 애플리케이션의 상태를 관리하고 업데이트하는데 도움을 줍니다.
Flux 패턴의 핵심은 "단방향 데이터 흐름"입니다. 이는 데이터가 애플리케이션을 통해 한 방향으로만 흐르게 하여 상태 변경을 예측 가능하게 만듭니다. 이는 복잡한 애플리케이션에서 상태 관리를 더욱 간단하고 효과적으로 만들어줍니다.
Flux 아키텍처는 크게 4가지 주요 구성 요소로 이루어져 있다.
장점
단점
적합한 상황
장점
단점
적합한 상황
프로젝트의 규모에 따른 선택
일반적으로 권장되는 방법은 하나의 store를 사용하고 Slice로 나누는 방식입니다. 이 방법은 확장성과 유지보수성, 상태 간의 의존성 관리가 용이하기 때문에 대규모 애플리케이션에서 특히 유리합니다.
다만, 프로젝트가 작고 상태가 독립적인 경우에는 여러 개의 store를 사용하는 것이 더 직관적이고 효율적일 수 있습니다. 따라서 프로젝트의 복잡도와 상태 간의 의존성을 고려해 선택하는 것이 중요합니다.