07. 사용 사례 시나리오 1: Zustand
zustand를 활용한 리렌더링 최적화, 구조화된 데이터 다루기
🔖 1. Zustand란?
특장점
React 애플리케이션에서 전역 상태를 간단하고 직관적으로 관리할 수 있는 경량 상태 관리 라이브러리
복잡한 설정 없이 한 두개의 훅과 간단한 함수 호출만으로 전역 상태를 정의하고 사용할 수 있다.
store 생성 시, 단순히 객체 형태로 상태와 상태를 변경하는 액션을 정의하면 된다.
필요한 상태만 구독하고, 해당 상태가 변경될 때만 리렌더링하므로 불필요한 렌더링을 줄여준다.
비동기 로직 (API 통신, 데이터 가져오기 등)을 상태 정의 로직 안에 쉽게 포함할 수 있어 side effect를 처리하기 용이하다.
예시 코드
import create from 'zustand';
// 1. 전역 상태(스토어) 정의
// create 함수로 상태와 액션(증가, 감소, 리셋)을 정의한 스토어(useStore)를 만든다.
const useStore = create((set) => ({
count: 0,
increase: () => set((state) => ({ count: state.count + 1 })),
decrease: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}));
// 상태 변경 함수인 set을 통해 count 값을 업데이트하면
// count에 의존하는 컴포넌트만 리렌더링 된다.
function Counter() {
// 2. 상태 사용
// useStore 훅을 컴포넌트 내에서 호출하면 count와 각 함수를 얻을 수 있다.
const { count, increase, decrease, reset } = useStore();
return (
<div>
<h1>{count}</h1>
<button onClick={increase}>곰 한마리 탄생!!</button>
<button onClick={decrease}>곰 한마리 사망..</button>
<button onClick={reset}>리셋</button>
</div>
);
}
export default function App() {
return (
<div>
<h2>Zustand Bear 🐻</h2>
<Counter />
</div>
);
}
Zustand 쓸까 말까
이렇듯, Zustand는 store를 정의하고, hook을 통해 필요한 상태와 함수를 가져와 쓴다.
전역 상태가 한번에 정의되고 관리되기에 상태 변경 로직을 한 곳에 모을 수 있어 규모가 커져도 유지보수가 쉬울 것으로 기대된다.
따라서 한번에 전역 상태 구조를 명확히 잡고 하나의 store를 통해 상태 관리 흐름을 단순화하고 싶다면 Zustand가 좋겠다.
Jotai는 처음부터 상태를 atom 단위로 쪼개야 해서 프로젝트 초기 단계에서 잘 사용할 수 있을지 모르겠다.
🔖 2. 불변 상태 모델
'불변 상태 모델'을 풀어 쓰기
Zustand는 상태를 관리할 때, 원래 상태 객체를 바꾸는 대신 변경된 부분만 새로 만든 객체에 담아 상태를 업데이트한다.
즉, 상태 변경 시 기존 객체를 수정하는 게 아니라, 새로운 객체를 만들어 그 객체를 기존 상태 대신 넣어준다.
그리고 변경하지 않은 부분 (e.g. 다른 필드나 내부 객체)은 그대로 이전 것을 재사용해 불필요한 렌더링을 줄일 수 있다.
비유하자면, 종이에 쓰인 내용을 지우개로 지우고 다시 쓰는 게 아니라, 새로운 종이에 바뀐 내용만 써서 교체한다. 바뀌지 않은 내용은 예전 내용을 그대로 가져와서 리소스를 낭비하지 않을 수 있다.
예시 코드
import create from 'zustand';
const useStore = create((set) => ({
user: { name: 'Ella', age: 20 },
// 상태 변경 함수 (불변성 유지)
updateUserName: (newName) => set((state) => ({
// user 객체를 직접 수정하지 않고, 새로운 객체를 return
// name만 바뀌고 age는 바뀌지 않았으니 기존 user.age를 그대로 재사용
user: { ...state.user, name: newName }
})),
items: ['apple', 'banana'],
addItem: (item) => set((state) => ({
// 기존 배열을 수정하지 않고, 새 배열을 만들어 return
items: [...state.items, item]
}))
}));
function App() {
const { user, updateUserName, items, addItem } = useStore();
return (
<div>
<p>User: {user.name}, {user.age}</p>
<button onClick={() => updateUserName('Ella')}>이름을 Chloe로 바꾸셈</button>
<p>Items: {items.join(', ')}</p>
<button onClick={() => addItem('orange')}>Orange도 주셈</button>
</div>
);
}
export default App;
예시를 통해 장점 느껴보기
이렇듯 Zustand는 상태가 어떻게 바뀌었는지 쉽게 파악할 수 있다.
새로운 객체를 반환하는 불변성 패턴을 활용하기 때문에, 이전 상태와 새로운 상태를 비교하는 과정이 단순해지는 것이다.
책에서 말하는 "상태 객체의 참조에 대한 동등성만 확인하면 변경 여부를 알 수 있으므로 객체의 값 전체를 확인할 필요가 없다" 이 문장을 풀어 이야기하면, “같은 객체냐, 다른 객체냐”를 확인하는 것만으로 상태 변경 여부를 판단할 수 있다는 것이다.
🔖 3. 수동 렌더링 최적화
선택자 기반 리렌더링 제어란?
컴포넌트가 필요로 하는 특정 부분의 상태만 subscribe하고, 그 부분이 실제로 변경되었을 때만 컴포넌트를 다시 렌더링하는 전략이다.
이렇게 하면 전역 상태의 다른 부분이 변해도 컴포넌트는 영향을 받지 않아, 불필요한 리렌더링을 줄일 수 있다.
큰 전역 상태 중 아주 일부분만 사용하는 컴포넌트는 그 일부분이 변할 때만 렌더링되기에, 성능 최적화에 유리할 것이다.
예시 코드
import create from 'zustand';
const useStore = create((set) => ({
user: { name: 'Ella', age: 20 },
items: ['apple', 'banana'],
setName: (name) => set((state) => ({
user: { ...state.user, name }
})),
addItem: (item) => set((state) => ({
items: [...state.items, item]
})),
}));
// 선택자: user의 name만 가져오기
const selectUserName = (state) => state.user.name;
// 선택자: items 배열의 길이만 가져오기
const selectItemsCount = (state) => state.items.length;
function UserNameDisplay() {
// user.name 값만 구독, name이 바뀌지 않으면 리렌더링 안 함
const name = useStore(selectUserName);
console.log('UserNameDisplay 렌더링한당');
return <div>User Name: {name}</div>;
}
function ItemsCountDisplay() {
// items.length 값만 구독, items 개수가 바뀌지 않으면 리렌더링 안 함
const count = useStore(selectItemsCount);
console.log('ItemsCountDisplay 렌더링한당');
return <div>Items Count: {count}</div>;
}
function App() {
const setName = useStore((state) => state.setName);
const addItem = useStore((state) => state.addItem);
return (
<div>
<UserNameDisplay />
<ItemsCountDisplay />
<button onClick={() => setName('Bob')}>이름을 Chloe로 바꾸셈</button>
<button onClick={() => addItem('orange')}>Orange도 주셈</button>
</div>
);
}
export default App;
Last updated