[React] Zustand Deep DIVE 하기
2024.11.15- -
1. 들어가며
개인적으로 Zustand 라이브러리를 여태껏 사용해보면서 굉장히 만족감을 느끼고 있고 가히 최신 상태 관리 라이브러리 중에 앞서고 있는 라이브러리가 아닐까 생각이 듭니다.
이에 Zustand의 기본적인 사용만 해보는 것이 아니라 조금 더 다양한 기능들을 알아보면서 심화된 기능들도 사용해보고자 글을 작성하게 되었습니다.
2. Zustand의 위엄
Zustand는 모던 React 라이브러리로 React 웹 개발 생태계에 상당히 뜨거운 바람을 불어 넣고 있습니다.
그 단순함은 Redux와 비교했을 때 엄청난 차이를 보여주기도 합니다.(안정성 측면은 모르겠지만...)
제가 체감했을 때는 난이도도 난이도지만 코드 양 자체가 별찍기 문제를 C언어로 푸는 것과 파이썬을 푸는 것을 비교하는 것과 비슷하다고 생각합니다.
#include <iostream>
int main()
{
int val;
std::cin >> val;
for (int i = 1; i <= val; i++)
{
for (int j = 0; j < val - i; j++)
std::cout << " ";
for (int j = 0; j < i; j++)
std::cout << "*";
std::cout << "\n";
}
}
val = int(input())
for i in range(1, val + 1):
print(" " * (val - i) + "*" * i)
그렇다면 우선 zustand의 기본 코드를 살펴봅시다.
import { create } from 'zustand';
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1})),
}));
단 몇 줄 밖에 되지 않는 코드로 전역 상태를 선언하였고 굉장히 직관적인 코드입니다.
또한 불변성과 data-UI 디커플링 등을 쉽게 활용하고 있습니다.
function App() {
const store = useStore();
return (
<div>
<div>Count: {store.count}</div>
<button onClick={store.increment}>Increment</button>
</div>
);
}
export default App;
3. 원하는 것만 가져오기
위와 같이 사용할 수도 있지만 다음과 같이 여러 컴포넌트에서 store를 공유하고 원하는 것만 선택할 수 있습니다.
function Count() {
// 'count'만 가져오기
const count = useStore((state) => state.count);
return <div>Count: {count}</div>;
}
function Controls() {
// 'increment'만 가져오기
const increment = useStore((state) => state.increment);
return <button onClick={increment}>Increment</button>;
}
여러 개의 store를 생성하여 데이터를 분산시키고 더 직관적으로 확장할 수도 있습니다. 좀만 현실적으로 생각해보면 단일 상태는 모든 경우에 합리적이지 않습니다. 캡슐화에 어긋난 것이기도 하죠. 컴포넌트의 하위 요소들이 로컬화된 전역 상태를 갖도록 하는 것이 더 자연스러운 경우가 많습니다.
// count 데이터를 다루는 전역 store
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
// 유저 input 로직을 처리하는 로컬 store
const useControlStore = create((set) => ({
input: "",
setInput: () => set((state) => ({ input: state.input })),
}));
function Controls() {
return (
<div>
<CounterInput />
<Button />
</div>
);
}
function Button() {
const increment = useStore((state) => state.increment);
const input = useControlStore((state) => state.input);
return (
<button onClick={() => increment(Number(input))}>
Increment By {input}
</button>
);
}
function CountInput() {
const input = useControlStore((state) => state.input);
return <input value={input} />;
}
4. useShallow
기존 상태가 변경되면, 즉시 업데이트되는 파생된 상태를 가져오는 강력한 방식인 useShallow()를 사용할 수 있습니다.
import { create } from "zustand";
import { useShallow } from "zustand/react/shallow";
const useLibraryStore = create((set) => ({
fiction: 0,
nonFiction: 0,
borrowedBook: {},
// ...
}));
const { fiction, nonFiction } = useLibraryStore(
useShallow((state) => ({
fiction: state.fiction,
nonFiction: state.nonFiction,
})),
);
const [fiction, nonFiction] = useLibraryStore(
useShallow((state) => [state.fiction, state.nonFiction]),
);
const borrowedBooks = useLibraryStore(
useShallow((state) => Object.keys(state.borrowedBooks)),
);
5. 특정 때에만 업데이트를 원치 않는 경우
store hook에 두 번째 인자를 전달하기만 하면 특정 때에만 업데이트를 원치 않는 경우에도 대처 가능합니다.
const user = useUserStore(
(state) => state.user,
(oldUser, newUser) => compare(oldUser.id, newUSer.id)
);
6. 이전 상태 기반 업데이트
zustand에서 상태는 기본 상태가 부분적 업데이트입니다.
import { create } from "zustand";
const useStore = create((set) => ({
user: {
username: "speardragon",
site: "cdragon.tistory.com",
color: "green",
},
premium: false,
// 'user'객체는 영향을 받지 않습니다.
// 'state'는 업데이트되기 전 현재 상태입니다.
unsubscribe: () => set((state) => ({ premium: false })),
}));
하지만 이 코드는 first level에서만 작동하며, 더 깊은 level의 업데이트는 직접 처리해주어야 합니다.
import { create } from "zustand";
const useStore = create((set) => ({
user: {
username: "speardragon",
site: "cdragon.tistory.com",
color: "green",
},
premium: false,
updateUsername: (username) =>
// 깊은 업데이트
set((state) => ({user: {...state.user, username}}));
}));
직접 전달하기를 원하지 않는 경우 두 번째 인자에 true값을 줘서 객체를 직접 전달하면 됩니다.
import { create } from "zustand";
const useStore = create((set) => ({
user: {
username: "speardragon",
site: "cdragon.tistory.com",
color: "green",
},
premium: false,
// 데이터 초기화
resetAccount: () => set({}, true);
}));
7. 비동기
zustand는 다른 라이브러리 필요 없이도 비동기를 지원합니다.
import { create } from "zustand";
const useStore = create((set) => ({
user: {
username: "speardragon",
site: "cdragon.tistory.com",
color: "green",
},
premium: false,
updateFavColor: async (color) => {
await fetch("https://api.cdragon.com", {
method: "PUT",
body: color,
});
set((state) => ({ user: { ...state.user, color } }));
},
}));
8. get 매개변수로 액션 내에서 상태를 쉽게 가져오기
import { create } from "zustand";
const useStore = create((set, get) => ({
user: {
username: "speardragon",
site: "cdragon.tistory.com",
color: "green",
},
messages: [],
sendMessage: ({ message, to }) => {
const newMessage = {
message,
to,
from: get().user.usernmae,
};
set((state) => ({
messages: [...state.messages, newMessage],
}));
},
}));
9. 상태 값을 직접 읽고 subscribe
const count = useStore.getState().count;
useStore.subscribe((state) => {
console.log(`new value: ${state.count}`);
})
따라서 속성이 많이 변경되지만 직접적인 UI가 아닌 중간 로직에 최신 값만 필요한 경우에 유용합니다.
export default function App() {
const widthRef = useRef(useStore.getState().windowWidth);
useEffect(() => {
useStore.subscribe((state) => {
widthRef.current = state.windowWidth;
});
}, []);
useEffect(() => {
setInterval(() => {
console.log(`Width is now: ${widthRef.current}`);
}, 1000);
}, []);
// ...
}
'Front-end' 카테고리의 다른 글
[React] 알아두면 쓸 데 있는 React Custom Hooks 10가지 (1) | 2024.11.19 |
---|---|
[React] Tanstack Query(구 React-Query)에서 query key 관리하는 라이브러리(query-key-factory) (0) | 2024.11.16 |
[Next.js] Sentry로 효율적인 에러 모니터링하는 법(자동화 에러 추적 도구) (2) | 2024.11.14 |
[kakao tech bootcamp] React 컴포넌트를 PDF로 만들기(react-pdf(O), react-to-pdf(X)) (10) | 2024.11.12 |
OpenAI(ChatGPT)가 Next.js에서 Remix로 전환한 이유 (1) | 2024.11.11 |
당신이 좋아할만한 콘텐츠
소중한 공감 감사합니다