
์ต๊ทผ ๊ฐ๋จํ ํ ์ด ํ๋ก์ ํธ๋ฅผ ์งํํ๋ฉด์ Context API์ฒ๋ผ props๋ฅผ ์ด์ฉํ์ง ์๋ ์ํ ์ ํ ๋ก์ง์ด ํ์ํด์ก๋ค.
๊ธฐ์กด Context API๋ฅผ ์ฌ์ฉํ๋ฉด ์ฌ์ ํ ๋ถํ์ํ ๋ฆฌ๋ ๋๋ง ์ด์๋ฅผ ์์ ํ ํผํ๊ธฐ ์ด๋ ค์ ๋ค.
๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์์กดํ๋ฉด ํธ๋ฆฌํ์ง๋ง, ์์ ํ ์ด ํ๋ก์ ํธ์ zustand๋ฅผ ์ค์นํ๋ ๊ฑด ๊ณผํ๋ค๋ ์๊ฐ์ด ๋ค์๋ค.
๊ทธ๋ฌ๋ ์ค, ํ์ตํ๋ฉด์ ์ป์ Observer ํจํด ๊ธฐ๋ฐ์ ์ํ ๊ด๋ฆฌ ์์ด๋์ด๋ฅผ ๊ตฌํํ๋ฉด์ ์ฒดํ์์ผ๋ณด์๋ ๊ฒฐ์ฌ์ด ๋ค๊ฒ ๋์๋ค.
์ฐธ๊ณ : Observer ํจํด์ ๋ํ ์์ธํ ๋ด์ฉ์ Refactoring.Guru์ ๊ธ ๋ฅผ ์ถ์ฒํ๋ค.
์์ ์ ์ฝ์๋ ๊ธ์ '๋์์ธ ํจํด์ ๊ณต๋ถํ๋ ๊ฒ์ ์ ๋ ์๋ํ ๊ฐ๋ฐ์๋ค์ ๋ฉ์ง ๊ฐ๋ฐ ๋ฐฉ์์ ์ด๊นจ๋์ด์์ ๋น๋ ค๋ค ์ธ ์ ์๋ ์ข์ ๋ฐฉ๋ฒ์ด๋ค.' ๋ผ๋ ๊ธ์ด ๊ธฐ์ต๋๋ค.
ํจํด ์ถ์ข ์๊น์ง ์๋์ด๋, ์ด๋์ ๋๋ ์ด๋ฐ ๊ฐ๋ฐ ๋ฐฉ์์ ํจํดํ๋จ์ ๋ฐ๋ผํ๋ ๊ฒ์ด ๋งค์ฐ ์ ์ฉํ๋ค๋ ๊ฒ์ ๊ทผ๋ ๋ง์ด ๋๋ผ๊ณ ์๋ค.
ํจํด ์ ์
Subject ๊ตฌ์กฐ ์ ์
๋์ ํ๋ฆ
update() ๋ฉ์๋ ์คํ โ ํ์ํ ๋์ ์ํ ์ฅ๋จ์
๋ฆฌ์ํธ์์๋ "Re-rendering" ์ด๋ผ๋ ํน์ฑ์ ํญ์ ์ํ๊ด๋ฆฌ์ ์์ด ๋ฆฌ์ํธ์ ํ๊ฒฝ์ ์ข ์๋ ์ ์๋ค๋ ์ ์ ๋ ๊ณ ๋ คํ ์๋ฐ์ ์๋ค.
๋ํ์ ์ผ๋ก, ๋ฆฌ์กํธ ์ํ๊ฐ ์ ๋ฐ์ดํธ ๋์ง ์์ผ๋ฉด ๋ฆฌ๋๋๋ง์ด ๋์ง ์์ ์นํ์ด์ง์ ๋ณ๋์ ๋ณผ ์ ์๋ค๋ ์ ์ด ๊ทธ ๋ํ์ ์ธ ์๋ค.
๊ทธ๋ ๋ค๋ฉด Zustand์ ๊ฐ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ์ด๋ป๊ฒ ๋ชจ๋ ์ค์ฝํ ๋ด์ ์๋ ์ธ๋ฉ๋ชจ๋ฆฌ ์ํ๊ฐ์ ์ด์ฉํด์ ๋ฆฌ์กํธ์ ์ฐ๋์ ์ํจ ๊ฒ์ผ๊น?
๊ทธ๊ฒ์ ํด๋ต์๋ Observer์ ์ด์ฉํ ๋ฆฌ๋๋๋ง ์์ฒญ ์ ํ ํ๋ก์ธ์ค์ ๊ทธ ํค๊ฐ ์๋ค.
Zustand์ ๊ตฌ์กฐ์๋ฆฌ๋ฅผ ๊ฐ๋จํ๊ฒ ๋ฐ๋ผํด๋ณธ ์์ ์ฝ๋๋ฅผ ์๋ก ๋ค์ด๋ณด๋ ค๊ณ ํ๋ค.
๐ก ์ด๋๊น์ง๋ ์ต๋ํ ๊ฐ๋ตํ๊ฒ ๋ง๋ ์์์ด๋ค. ์ค์ ๋ก ํจ์ฌ ๋ณต์กํ๊ณ ์์ ์ฑ์ ์ํ ์ฝ๋๋ ๋ง๋ค.
import { useReducer, useEffect, useRef, useMemo } from 'react';
/********************************************
*
* @createStore
*
********************************************/
type Subscriber = () => void;
type Unsubscribe = () => void;
type PartialState<T> = Partial<T> | ((prev: T) => T);
export function createStore<T>(initialState: T) {
let state = initialState;
const subscribers = new Set<Subscriber>();
return {
// 1) ํ์ฌ ์ํ ์กฐํ
getState: () => state,
// 2) ์ํ ๋ณ๊ฒฝ ๋ฐ ๊ตฌ๋
์ ์๋ฆผ
setState: (partial: PartialState<T>) => {
state =
typeof partial === 'function' ? (partial as (s: T) => T)(state) : { ...state, ...partial };
for (const fn of subscribers) {
fn?.();
}
},
// 3) ๊ตฌ๋
ํ๊ธฐ (์ปดํฌ๋ํธ๊ฐ ์ฌ๊ธฐ์ ๋ฑ๋ก)
subscribe: (fn: Subscriber): Unsubscribe => {
subscribers.add(fn);
return () => {
subscribers.delete(fn);
};
},
};
}
์์ ๋ด์ฉ์ Observer์ ๊ด๋ฆฌํ๋ Subject ์์ฑ ํจ์์ด๋ค.
์ด ํจ์๋ฅผ ํธ์ถํ ๊ฒฝ์ฐ ํน์ ๋ชจ๋ ์ค์ฝํ ๋ด์์ ์ด๊ธฐํ๋์ด ๋ฆฌ์กํธ์ ๋ฆฌ๋๋๋ง ์ฌ์ดํด๊ณผ๋ ๋ฌด๊ดํ ์์ญ์์ ์ ์ญ์ ์ผ๋ก ์ ์ง๊ฐ ๋๋ค.
export interface MainLayoutStoreState {
headerMenuOpen: boolean;
}
// ํด๋น ์คํ ์ด๋ ๋ชจ๋ ์ค์ฝํ ๊ธฐ์ค์ผ๋ก ์๋ช
์ฃผ๊ธฐ๊ฐ ์ ์ง๋๋ ์ธ๋ฉ๋ชจ๋ฆฌ ๋ฐ์ดํฐ์ด๋ค.
export const mainLayoutStore = createStore<MainLayoutStoreState>({
headerMenuOpen: false,
});
๊ทธ๋ ๋ค๋ฉด ์ด์ ์ฐ๋ฆฌ์๊ฒ ํ์ํ ๊ฒ์ ์ด ๋จ์ํ ์ธ๋ฉ๋ชจ๋ฆฌ ์ํ์ ์คํ ์ด์ ์ํ ์ ๋ฐ์ดํธ๋ฅผ react์ ๋๋๋ง ์ฌ์ดํด๊ณผ ์ฐ๊ฒฐ์ง์ ์ ์๋ Store-Binding Hook์ด ํ์ํ๊ฒ ๋๋ค.
๊ทธ๊ฒ์ react hook๊ณผ react state๋ฅผ ์ด์ฉํด์ ๊ตฌํ์ ํ๋ฉด ๋๋ค.
/********************************************
*
* @useStore
*
********************************************/
export function useStore<T>(store: ReturnType<typeof createStore<T>>): T;
export function useStore<T, U>(store: ReturnType<typeof createStore<T>>, sel: (state: T) => U): U;
export function useStore<T, U>(
store: ReturnType<typeof createStore<T>>,
sel?: (state: T) => U,
): T | U {
const selector = useMemo(() => sel ?? ((state: T) => state as unknown as U), [sel]);
const [, forceUpdate] = useReducer((x: number) => x + 1, 0);
const lastSelected = useRef<U>(selector(store.getState()));
// ํ
ํธ์ถ ํ store ์
๋ฐ์ดํธ์ ๊ฐ์ง์ ๋ฐ์ํ์ฌ ๋ฆฌ๋๋๋งํ๋ ์ต์ ๋ฒ ๋ฑ๋ก
useEffect(() => {
const checkForUpdates = () => {
const next = selector(store.getState());
if (!Object.is(lastSelected.current, next)) {
lastSelected.current = next;
forceUpdate();
}
};
const unsubscribe = store.subscribe(checkForUpdates);
checkForUpdates();
return unsubscribe;
}, [store, selector]);
return lastSelected.current;
}
์ด ํ ์ ํต์ฌ์ ๋ฐ๋ก ์์ ์ ๋ฆฌ๋๋๋ง์ ์ ๋ฐ์ํฌ ์ ์๋ Observer์ props๋ก ์ฃผ์ ๋ฐ๋ observer ๊ฐ์ฒด์ subscribe์ผ๋ก ๋ฑ๋ก์ํจ๋ค๋ ์ ์ ์๋ค.
๋ง์ฝ ์ด ํ ์ ์ด๋ค ํน์ ์ปดํฌ๋ํธ๊ฐ ํธ์ถํ๋ค๊ณ ๊ฐ์ ํ์.
function WebMenu() {
const { headerMenuOpen } = useStore(mainLayoutStore);
...
์ด ํ ์ด ์ปดํฌ๋ํธ ๋ด์์ ํธ์ถ๋์๋ค๋ ์๋ฏธ๋ ๋ฐ๋ก
"๋ด๊ฐ ๋ฆฌ๋๋๋ง์ ์ํฅ์ ๋ฐ์ง ์์ ref ์ํ(lastSelected.current) ๋ฅผ ๋ณด์ ํ๊ณ ์์๊ฑด๋ฐ,
๋ง์ฝ ์ธ๋ถ์ ์์นํ store์ ์ํ๊ฐ ์ ๋ฐ์ดํธ๊ฐ ๋๋ฉด ๋ด๊ฐ ๋ฑ๋กํด๋๋ observer์ธ checkForUpdates ๋ฅผ ํธ์ถํด์ฃผ์ธ์.
์คํ ์ด ์ํ์ ๋ด ref ์ํ๋ฅผ ๋น๊ตํด๋ด์ ์ฐจ์ด๋๋ฉด ๋ฆฌ๋๋๋ง ํด๋ฒ๋ฆด๊ฑฐ์์." ํ๊ณ ์ ์ธํ๋ ๊ฒ์ด๋ค.
์ด๋ก ์ธํด์ ์ Zustand๊ฐ ์์ ์ "๊ตฌ๋ ํ" ์ํ๊ด๋ฆฌ๋ผ๊ณ ์๊ฐํ๋์ง ์ดํดํ ์ ์๋ค.
์ปดํฌ๋ํธ๋ ์์ ์ ๋ฆฌ๋๋๋ง์ ๊ฒฐ์ ํด์ค ์ ์๋ ์ต์ ๋ฒ ๋ฑ๋ก ํ
"useStore" ๊ฐ ํธ์ถ๋์ง ์๋๋ค๋ฉด ๋ถ๋ชจ๊ฐ ๋ฆฌ๋๋๋งํ์ง ์๋ ์ด์
๋ณธ์ธ์ ๋ฆฌ๋๋๋ง์ ํ์ง ์๊ธฐ ๋๋ฌธ์ด๋ค.
// ๐ซ ์๋ ์ปดํฌ๋ํธ๋ store ์
๋ฐ์ดํธ์ ๋ํด ๋ฐ์ํ์ง ์๋๋ค.
// store์ ์
๋ฐ์ดํธ ๋๋๋ผ๋, ์ด๋ฅผ ๊ฐ์งํ ์ต์ ๋ฒ๋ฅผ ๋ฑ๋กํ์ง ์๊ธฐ ๋๋ฌธ์ด๋ค.
function WebMenu() {
const { headerMenuOpen } = mainLayoutStore.getState();
...
// โ
์๋ ์ปดํฌ๋ํธ๋ store ์
๋ฐ์ดํธ์ ๋ํด ๋ฐ์ํ์ฌ ๋ฆฌ๋๋๋งํ๋ค.
// useStore์ ํธ์ถ์ด ๋ฐ๋ก ์๊ธฐ ์์ ์ ๋ฆฌ๋๋๋ง ์ ๋ฌด๋ฅผ ๊ฐ์งํ ์ต์ ๋ฒ๋ฅผ ๋ฑ๋กํ๋ ๋์์ด๊ธฐ ๋๋ฌธ์ด๋ค.
function WebMenu() {
const { headerMenuOpen } = useStore(mainLayoutStore);
...
์ฆ, ๋ค์๋งํด ์ต์ ๋ฒ๋ฅผ ๋ฑ๋กํ๋ ํ์ === store ์ํ๋ณํ๋ฅผ "๊ตฌ๋ " ํ๋ ํ์! ๊ฐ ๋๋ ๊ฒ์ด๋ค.
๋์์ธ ํจํด์ ์๋ํด.
๊ทธ๋ฆฌ๊ณ , ๋ฐฐ์ฐ๊ณ ์ค์ ๋ก ์ ์ฉํด๋ณผ์๋ก ์์ผ์ค๋ฌ์ด ๊ฐ๋ ์ฒ๋ผ ๋ณด์ด๋ ๊ฒ์ด ๋ ํฌ๊ฒ ๋ค๊ฐ์ค๋ ๊ฒ ๊ฐ๋ค.
๊ทธ๋ฅ ๋จธ๋ฆฟ์์ผ๋ก ์ดํดํ๊ณ "์ ๊ทธ๋" ํ๊ณ ๋์ด๊ฐ ์ ์๋ ๋ด์ฉ๋ ๋ค์ ์ฌ๋ฌ๋ฒ ๋ณด๊ฒ ๋๋ฉด ์๋ก์ด ๊นจ๋ฌ์์ด ์ค๋ ์๊ฐ์ด ์๋ค.
๋์ฑ ๊น์ ๊นจ๋ฌ์์ ์ป์ ์ ์๋๋ก ๋ ธ๋ ฅํ๋ ์์ธ๋ก ์์ด์ผ ํ๊ฒ ๋ค.