zustand와 친해지기 프로젝트 시작한지 어연 2달차...
나는 아직까지.. zustand를 다알지 못한다는걸 깨달았다....
어쩌다가 만난 zustand shallow ...
넌 누구냐?

Zustand 로 캐시 저장, 로컬 스토리지 저장 하기
zustand에 대한 소개는 윗글에서 했으니 넘어가고~
오늘은 shallow에 심도있게 알아보고자 한다.
사전적 의미로는 얕은 이라는 의미인데
일명 자바스크립트에서 행해지는 얕은 비교를 수행하는 친구이다.
자바스크립트에서는 원시값비교와 참조값 비교로 진행되는데
원시값 비교로 값이 같은걸 비교하는 반면
const a = 5;
const b = 5;
console.log(a === b); // true (값이 같음)
참조값 비교는
const obj1 = { x: 1 };
const obj2 = { x: 1 };
console.log(obj1 === obj2); // false (다른 참조)
const obj3 = obj1;
console.log(obj1 === obj3); // true (같은 참조)
오브젝트 내부를 비교하는 것이 아니라 실제로 같은 곳에서 만들어지고 같은 위치 인지를 비교한다.
위에서 obj1 과 obj2는 서로 다른 메모리 공간에 있는 별개의 객체이므로 ===으로 비교를 해도 false라는 값이 떨어진다.
그러면 참조값비교에서는 얉은 비교 (Shallow) 와 깊은 비교 (Deep)가 나뉘게 된다.
const obj1 = { a: 1, b: 2 };
const obj2 = { a: 1, b: 2 };
console.log(obj1 === obj2); // false
앞에서와 동일하게 내용이 같아 보이지만 참조가 다르기때문에 동일하다고 판단이 불가하다.
그렇다면 깊은 비교는?
function deepEqual(obj1, obj2) {
if (obj1 === obj2) return true; // 같은 참조면 바로 true
if (typeof obj1 !== 'object' || typeof obj2 !== 'object') return false; //둘중 하나라도 object가 아니면 false
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) return false; // 두 오브젝트의 키 개수가 다르면 바로 false
for (let key of keys1) { //obj1의 키를 하나씩 순회하면서 obj2와 비교시작 => 모든 속성을 하나씩 확인
if (!keys2.includes(key)) return false;
if (typeof obj1[key] === 'object' && typeof obj2[key] === 'object') {
if (!deepEqual(obj1[key], obj2[key])) return false; // 재귀적으로 깊게 비교
} else if (obj1[key] !== obj2[key]) {
return false; // 객체가 아닌경우 두 값이 다르면 false 반환
}
}
return true; // 모든 비교를 통과하면 true 반환
}
console.log(deepEqual({ a: 1, b: 2 }, { a: 1, b: 2 })); // true
console.log(deepEqual({ a: 1, nested: { x: 10 } }, { a: 1, nested: { x: 10 } })); // true
재귀적으로 deepEqual에서 계속해서 deepEqual를 호출하면서 참조값의 내부까지 비교한다.
그럼 갑자기 zustand의 Shallow 얘기하다가 왜 자바스크립트 이야기를 하냐고?
zustand Shallow는 이름부터 알 수 있듯이 얕은 비교만을 행하기 때문이다!
기존상황
const BearAndFishCounter = () => {
const { bears, fish } = useBearStore((state) => ({
bears: state.bears,
fish: state.fish,
}));
console.log('BearAndFishCounter 렌더링!');
return (
<div>
Bears: {bears}, Fish: {fish}
</div>
);
};
increaseBears를 호출해서 bears만 바뀌어도, 셀렉터는 매번 새로운 객체를 만들어내기 때문에 zustand는 이 객체가 "바뀌었다"고 판단해서 컴포넌트를 리렌더링한다.
심지어 fish는 전혀 바뀌지 않았는데도!
왜냐? 자바스크립트에서 { bears: 1, fish: 0 }와 { bears: 2, fish: 0 }는 내용이 비슷해도 다른 참조(reference)이기 때문이다.
\=== 비교로는 같다고 안 나온다. 이게 바로 불필요한 리렌더링의 원인이 된다.
그럼 여기서 Shallow를 사용한다면?
import { useShallow } from 'zustand/react/shallow';
const BearAndFishCounter = () => {
const { bears, fish } = useBearStore(
useShallow((state) => ({
bears: state.bears,
fish: state.fish,
}))
);
console.log('BearAndFishCounter 렌더링!');
return (
<div>
Bears: {bears}, Fish: {fish}
</div>
);
};
이제 increaseBears를 호출하면 bears만 바뀌고 fish는 그대로이다.
useShallow가 { bears: 1, fish: 0 }와 { bears: 2, fish: 0 }를 얕은 비교로 보면, fish 값은 여전히 같다고 판단해서 리렌더링을 막아줄 수 있다.
실제로 바뀐 부분(bears)에 대해서만 반응하도록 최적화가 된다!
간단한 예제로 비교하자면
import { create } from 'zustand';
const useStore = create((set) => ({
a: 0,
b: 0,
setA: () => set((state) => ({ a: state.a + 1 })),
setB: () => set((state) => ({ b: state.b + 1 })),
}));
const App = () => {
const { a, b, setA, setB } = useStore((state) => ({
a: state.a,
b: state.b,
setA: state.setA,
setB: state.setB,
}));
console.log('App 렌더링!');
return (
<div>
<p>A: {a}</p>
<p>B: {b}</p>
<button onClick={setA}>A 증가</button>
<button onClick={setB}>B 증가</button>
</div>
);
};
export default App;
위와 같은 코드에서는 "A증가" 라는 버튼을 누르면 a의 값만 바뀌지만 컴포넌트가 매번 리렌더링 된다. (콘솔에 App 렌더링! 이 계속 찍힘)
import { create } from 'zustand';
import { useShallow } from 'zustand/react/shallow';
const useStore = create((set) => ({
a: 0,
b: 0,
setA: () => set((state) => ({ a: state.a + 1 })),
setB: () => set((state) => ({ b: state.b + 1 })),
}));
const App = () => {
const { a, b, setA, setB } = useStore(
useShallow((state) => ({
a: state.a,
b: state.b,
setA: state.setA,
setB: state.setB,
}))
);
console.log('App 렌더링!');
return (
<div>
<p>A: {a}</p>
<p>B: {b}</p>
<button onClick={setA}>A 증가</button>
<button onClick={setB}>B 증가</button>
</div>
);
};
export default App;
하지만 useShallow를 활용한다면 setA를 호출했을때 a만 바뀌면 리렌더링이 필요한 상황에서만 발생한다.
하지만 useShallow가 만능은 아니다.
useShallow는 얕은 비교만 하니까 객체안에 객체가 중첩된 경우에는 작동이 안될수도 있다.
import { create } from 'zustand';
import { useShallow } from 'zustand/react/shallow';
const useStore = create((set) => ({
data: {
count: 0,
nested: {
value: 10,
},
},
increaseNestedValue: () => set((state) => ({
data: {
...state.data,
nested: {
value: state.data.nested.value + 1, // 새 객체 안 만듦
},
},
})),
}));
const Component = () => {
const { count, nested } = useStore(
useShallow((state) => ({
count: state.data.count,
nested: state.data.nested,
}))
);
const increaseNestedValue = useStore((state) => state.increaseNestedValue);
console.log('Component 렌더링!');
return (
<div>
<p>Count: {count}</p>
<p>Nested Value: {nested.value}</p>
<button onClick={increaseNestedValue}>Nested Value 증가</button>
</div>
);
};
export default Component;
increaseNestedValue를 호출하면 nested.value가 10에서 11로 바뀌지만, useShallow는 nested 참조가 바뀌었는지(얕은 비교)만 보니까:
count: 안 바뀜.
nested: 새 객체로 교체되니까 참조 바뀜 → 리렌더링
하지만 객체나 배열 관리에는 최적화에는 이만한 기능이 없다.😎
세상은 넓고 내가 모르는건 너무 많구나~