
๋ฆฌ์กํธ ๋ฆฌ๋ ๋๋ง ๋ฌธ์ , FGR(Fine Grained Reactivity) ๋ก ํด๊ฒฐํ๊ธฐ
๋ฆฌ์กํธ ๋ฆฌ๋ ๋๋ง ๋ฌธ์ , FGR(Fine Grained Reactivity) ๋ก ํด๊ฒฐํ๊ธฐ ๊ด๋ จ
๋ฆฌ์กํธ์ ๋ฆฌ๋ ๋๋ง์ ์ค์ด๋ ๋ฐฉ๋ฒ์ ์๊ณ ์ถ์ผ์ ๊ฐ์?
๋ฆฌ์กํธ ์ฑ๋ฅ ์ต์ ํ ๊ฒฝํ, ๋ค๋ค ํด๋ณด์ ์ ์์ผ์ ๊ฐ์? ๊ฐ๋ฐ์ ํ๋ค ๋ณด๋ฉด ์ฑ๋ฅ ๋ฌธ์ ๋ก ๊ณจ๋จธ๋ฆฌ๋ฅผ ์์ ๊ฒฝํ์ด ํ ๋ฒ์ฏค์ ์์ ๊ฒ๋๋ค. ํนํ ์ํ ๊ด๋ฆฌ๋ฅผ ํ ๋ ๋ง์ด์ฃ .
๊ฐ๋จํ ์์๋ฅผ ํ๋ฒ ๋ณผ๊น์? ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์ ์ญ ์ํ๋ก ๊ด๋ฆฌํ๋ ์ํฉ์ ๋๋ค.
// Context๋ก ์ ์ญ ์ํ ๊ด๋ฆฌ
const UserContext = createContext();
function UserProvider({ children }) {
const [user, setUser] = useState({
name: "๊น๊ฐ๋ฐ",
age: 25,
foodPreference: "๋ถ๋จน",
theme: "dark"
});
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
}
// ์ด๋ฆ๋ง ์ฌ์ฉํ๋ ์ปดํฌ๋ํธ
function UserName() {
const { user } = useContext(UserContext);
console.log("UserName ๋ฆฌ๋ ๋๋ง!");
return <div>{user.name}</div>;
}
// ํ
๋ง๋ง ์ฌ์ฉํ๋ ์ปดํฌ๋ํธ
function ThemeButton() {
const { user } = useContext(UserContext);
console.log("ThemeButton ๋ฆฌ๋ ๋๋ง!");
return <button>{user.theme} ๋ชจ๋</button>;
}
// ์์ ์ทจํฅ๋ง ๋ฐ๊พธ๋ ์ปดํฌ๋ํธ
function FoodPreference() {
const { user, setUser } = useContext(UserContext);
const toggleFood = () => {
setUser(prev => ({
...prev,
foodPreference: prev.foodPreference === "๋ถ๋จน" ? "์ฐ๋จน" : "๋ถ๋จน"
}));
};
return <button onClick={toggleFood}>{user.foodPreference}</button>;
}
์ฌ๊ธฐ์ ๋ฌธ์ ๊ฐ ๋ญ๊น์? FoodPreference
์ปดํฌ๋ํธ์์ ์์ ์ทจํฅ๋ง ๋ฐ๊ฟ๋, UserName
๊ณผ ThemeButton
์ปดํฌ๋ํธ๊น์ง ๋ชจ๋ ๋ฆฌ๋ ๋๋ง ๋ฉ๋๋ค. ๋ถ๋ช
ํ ์ด๋ค์ foodPreference
์ ์๋ฌด ๊ด๋ จ์ด ์๋๋ฐ๋ ๋ง์ด์ฃ .
"๊ทธ๋ผ Context๋ฅผ ์ฌ๋ฌ ๊ฐ๋ก ๋๋๋ฉด ๋์ง ์๋?" ์๊ฐํ ์ ์์ง๋ง, ํ์ค์ ๊ทธ๋ ๊ฒ ๋จ์ํ์ง ์์ต๋๋ค.
// ์ด๋ ๊ฒ ๋๋๋ฉด?
const UserNameContext = createContext();
const UserThemeContext = createContext();
const UserFoodContext = createContext();
const UserAgeContext = createContext();
setState์ ๊ฐ์๋ง ๋์ด๋ ๋ฟ๋๋ฌ, ์๋ฏธ๋ก ์ ์ผ๋ก ํ๋์ฌ์ผ ํ๋ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์ต์ง๋ก ์ชผ๊ฐ์ผ ํฉ๋๋ค. ๋๊ตฐ๋ค๋ ์ฌ์ฉ์ ์ ๋ณด๋ ๋ณดํต API ์๋ต์ผ๋ก ํ๋์ ๊ฐ์ฒด๋ก ๋ฐ์์ค๋๋ฐ, ์ด๊ฑธ ์ผ์ผ์ด ๋ถ๋ฆฌํด์ ๊ด๋ฆฌํ๋ ๊ฒ๋ ๋ฒ๊ฑฐ๋กญ์ฃ . ์ด๋ณด๋ค ๋ ํฐ ๋ฌธ์ ๋ ํ์ค์ ์ธ ๊ฐ๋ฐ ํ๊ฒฝ์ ์์ต๋๋ค. ์ปดํฌ๋ํธ๋ ์๊ฐ๋ณด๋ค ์๊ฒ ์ชผ๊ฐ์ง๊ณ , ๊ธฐํ์ ์์๋ก ๋ฐ๋๋๋ค. ์ฒ์์๋ ํน์ ์ปดํฌ๋ํธ์์๋ง ์ฌ์ฉํ๋ ค๋ ๋ฐ์ดํฐ๊ฐ ๊ฐ์๊ธฐ ๋ค๋ฅธ ๊ณณ์์๋ ํ์ํด์ง์ฃ .
"์ฌ์ฉ์ ๋์ด๋ ํค๋์ ํ์ํด ์ฃผ์ธ์.", "ํ๋กํ ์ด๋ฏธ์ง๋ ์ฌ์ด๋๋ฐ์ ๋ฃ์ด์ฃผ์ธ์.", "์์ ์ทจํฅ์ ๋ฐ๋ผ ์ถ์ฒ ๋ฉ๋ด๋ฅผ ๋ค๋ฅด๊ฒ ๋ณด์ฌ์ฃผ์ธ์."... ์ด๋ฐ ์๊ตฌ์ฌํญ๋ค์ด ์์์ง๋๋ค. ๊ทธ๋ ๋ค๊ณ API๋ฅผ ๋งค๋ฒ ํธ์ถํ๊ธฐ์๋ ๋ถ๋ด์ค๋ฝ๊ณ , ํ ๋ฒ ํธ์ถํด์ ์ฌ๋ฌ ๊ณณ์ ์ฐ๊ณ ์ถ์๋ฐ... ๊ฒฐ๊ตญ ๊ธ๋ก๋ฒ ์คํ ์ดํธ๋ฅผ ๋์ ํ๊ฒ ๋ฉ๋๋ค. ๊ทธ๋ฆฌ๊ณ ๋ค์ ๋ฆฌ๋ ๋๋ง ์ง์ฅ์ ๋น ์ง๊ฒ ๋์ฃ . ๋ฆฌ๋ ๋๋ง์ด ๋ง์์ง์๋ก ์ ํ๋ฆฌ์ผ์ด์ ์ ๋๋ ค์ง๋๋ค. ํนํ ๋ชจ๋ฐ์ผ ํ๊ฒฝ์์๋ ๋์ฑ ์ฒด๊ฐ๋์ฃ . ์ฌ์ฉ์๋ ๋ฒ๋ฒ ๊ฑฐ๋ฆฌ๋ ์ฑ์ ๋ณด๋ฉฐ "์ด ์ฑ ์ ์ด๋ ๊ฒ ๋๋ ค?"๋ผ๊ณ ์๊ฐํ ๊ฒ๋๋ค.
React DevTools์ Profiler๋ฅผ ์ผ๋ณด๋ฉด, ์์ ์ด์์ผ๋ก ๋ง์ ์ปดํฌ๋ํธ๋ค์ด ๋ถํ์ํ๊ฒ ๋ฆฌ๋ ๋๋ง๋๊ณ ์๋ ๊ฑธ ํ์ธํ ์ ์์ต๋๋ค. useMemo
, useCallback
, React.memo
๋ฅผ ์จ์ ์ต์ ํ๋ฅผ ์๋ํ ์ ์์ง๋ง, ๊ทผ๋ณธ์ ์ธ ํด๊ฒฐ์ฑ
์ ์๋์ฃ . ๊ทธ๋ ๋ค๋ฉด ์ ๋ง๋ก ์ฌ์ฉํ์ง ์๋ ํ๋กํผํฐ๋ฅผ ๋ฐ๊ฟ๋ ๋ฆฌ๋ ๋๋ง์ด ์ผ์ด๋์ง ์๋ ๋ฐฉ๋ฒ์ด ์์๊น์?
๋๋๊ฒ๋ ์์ต๋๋ค. ๋ฐ๋ก โ**FGR(Fine Grained Reactivity)โ**์ ๋๋ค.
FGR์ ์๊ฐํฉ๋๋ค
๋ฆฌ์กํธ์์ ๋ฆฌ๋ ๋๋ง์ ์ค์ด๋ ๋ฐฉ๋ฒ์ ๋ค์ํฉ๋๋ค. useMemo
, useCallback
, React.memo
๊ฐ์ ์ต์ ํ ๊ธฐ๋ฒ๋ค์ ์จ๋ณด์
จ์ ํ
๋ฐ์. ์ด๋ฌํ ๋ฐฉ๋ฒ๋ค์ ๋ฆฌ์กํธ๊ฐ ์ด์ ๊ฐ๊ณผ ํ์ฌ ๊ฐ์ ๋น๊ตํด์ ๋ฆฌ๋๋๋ง์ ๊ฒฐ์ ํฉ๋๋ค.
๋ํ ํ๋ก ํธ์๋ ๊ฐ๋ฐ์ ์ํด ๋ค์ํ ์ํ ๊ด๋ฆฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์จ๋ณด์ จ์ ํ ๋ฐ์. Redux, Zustand, Jotai, Valtio ์ ๋ง ๋ง์ฃ ? ์ด๋ฐ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค๋ โselectorโ๋ผ๋ ๊ฐ๋ ์ ์ ๊ณตํฉ๋๋ค. ์๋ฅผ ๋ค์ด, Zustand์์๋ ์ด๋ ๊ฒ ์ฐ์ฃ .
const name = useStore(state => state.user.name);
const age = useStore(state => state.user.age);
์ธ๋ป ๋ณด๋ฉด user.name
๋ง ๋ฐ๋ ๋ ํด๋น ์ปดํฌ๋ํธ๋ง ๋ฆฌ๋ ๋๋ง ๋ ๊ฒ ๊ฐ์ต๋๋ค. ์ค์ ๋ก๋ ๊ทธ๋ ๊ฒ ๋์ํ๊ณ ์. ๊ทธ๋ฌ๋ ๋ด๋ถ์ ์ผ๋ก ๋ณ๊ฒฝ ์๋ฆผ์ ๋ชจ๋ ๋ฐ์ํ๊ณ , ๋จ์ง ํญ์ ๊ฐ์ ๋น๊ตํด์ ์ด์ ๊ฐ๊ณผ ๋ค๋ฅธ์ง ํ์ธํ๋ ์์
์ ๊ฑฐ์น ๋ฟ์
๋๋ค.
useStore
๋ด๋ถ ์ฝ๋๋ฅผ ๋ณด๋ฉด ์ด๋ฐ ์์ผ๋ก ๋์ํฉ๋๋ค.
// Zustand์ useStore ๋ด๋ถ (๋จ์ํ)
function useStore(selector) {
const [, forceUpdate] = useReducer(c => c + 1, 0);
const prevValueRef = useRef();
useEffect(() => {
const unsubscribe = store.subscribe((state) => {
const newValue = selector(state);
const prevValue = prevValueRef.current;
// ์ด์ ๊ฐ๊ณผ ํ์ฌ ๊ฐ ๋น๊ต
if (!isEqual(newValue, prevValue)) {
prevValueRef.current = newValue;
forceUpdate(); // ๋ฆฌ๋ ๋๋ง ๊ฐ์ ์คํ
}
});
return unsubscribe;
}, [selector]);
const currentValue = selector(store.getState());
prevValueRef.current = currentValue;
return currentValue;
}
๊ฐ ๋น๊ต๋ ์กฐ์๋์ด ํด์ฃผ๋์?
"์ด๊ฒ ๊ฒฐ๊ตญ ๋ฆฌ๋ ๋๋ง ์ ํ๋๊น ๊ด์ฐฎ์ ๊ฑฐ ์๋๊ฐ์?" ๋ง์ต๋๋ค. selector๋ฅผ ์ฌ์ฉํ๋ฉด ์ค์ ๋ก ๊ฐ์ด ๊ฐ์ ๋๋ ๋ฆฌ๋ ๋๋ง์ด ์ผ์ด๋์ง ์์ฃ . ํ์ง๋ง ๋ฆฌ๋ ๋๋ง์ ์ ํ๊ฒ ํ๋ ค๊ณ ๊ฐ ๋น๊ต๋ฅผ ํ๋ฉด, ๋ฆฌ์กํธ์ ๋ถ๋ณ์ฑ ์์น์ ๋ฐ๋ผ ๊ฐ์ฒด์ ์ผ๋ถ๋ง ๋ณ๊ฒฝํด๋, ์ ์ฒด ๊ฐ์ฒด๋ฅผ ์๋ก ๋ง๋ค์ด์ผ ํฉ๋๋ค. ๊ทธ๋์ user.name
ํ๋๋ง ๋ฐ๋์ด๋ user
๊ฐ์ฒด ์ ์ฒด๊ฐ ์๋ก ์์ฑ๋์ฃ .
์ด๋ ๊ฒ ๋ณ๊ฒฝ๋๋ฉด ํด๋น ์ํ๋ฅผ ์ฐ๋ ๊ณณ์ ์ผ๋จ ๋ชจ๋ ๋ฆฌ๋ ๋๋ง ๋์์ด ๋ฉ๋๋ค. ๊ทธ๋ฐ ๋ค์์ selector
๋ก ์ด์ ๊ฐ๊ณผ ํ์ฌ ๊ฐ์ ๋น๊ตํ๋ ๊ฒ๋๋ค. "์ด์ name๊ณผ ํ์ฌ name์ด ๊ฐ๋?" ํ๊ณ ๋ง์ด์ฃ . ๊ฐ์ด ๊ฐ์ผ๋ฉด ๋ฆฌ๋ ๋๋ง์ ํ์ง ์๊ณ , ๊ฐ์ด ๋ค๋ฅด๋ฉด ๋ฆฌ๋ ๋๋ง ํจ์๋ฅผ ํธ์ถํฉ๋๋ค.
๊ทธ๋ฐ๋ฐ ์ด ๊ฐ ๋น๊ต๋ฅผ ๋๊ฐ ํด์ฃผ๋ ๊ฑด๊ฐ์? ๊ฒฐ๊ตญ ์ปดํจํฐ๊ฐ ๋งค๋ฒ ๊ณ์ฐํด์ผ ํ๋ ์ผ์ ๋๋ค. ์ปดํฌ๋ํธ๊ฐ ๋ง์์ง์๋ก, ์ํ๊ฐ ๋ณต์กํด์ง์๋ก ์ด๋ฐ ๋น๊ต ์์ ๋ ๋์ด๋์ฃ .
์ ์ด์ ๋ณ๊ฒฝํ๋ ์๊ฐ์ "์ด๋๊ฐ ๋ณ๊ฒฝ๋์๋์ง"๋ฅผ ํํฌ์ธํธ๋ก ์ ์ ์๋ค๋ฉด ์ด๋จ๊น์? user.name
์ ๋ฐ๊พธ๋ ์๊ฐ์ "์, name์ด ๋ฐ๋์๋ค. ๊ทธ๋ผ name
์ ์ฌ์ฉํ๋ ์ปดํฌ๋ํธ๋ค๋ง ์
๋ฐ์ดํธํ์"๋ผ๊ณ ์ฆ์ ํ๋จํ ์ ์๋ค๋ฉด ๋ง์ด์ฃ . ๊ฐ ๋น๊ต๋ ํ์ ์๊ณ , ๋ถํ์ํ ์ฐ์ฐ๋ ์๊ณ , ์ ๋ง ํ์ํ ๊ณณ๋ง ์ ํํ ์
๋ฐ์ดํธํ ์ ์์ ๊ฒ๋๋ค.
์ด๋ฐ ์ ๊ทผ ๋ฐฉ์์ Fine Grained Reactivity, ์ค์ฌ์ FGR์ด๋ผ๊ณ ํฉ๋๋ค. ๊ธฐ์กด ๋ฐฉ์์ด "๋ณ๊ฒฝ ๊ฐ์ง โ ์ ์ฒด ์๋ฆผ โ ๊ฐ๋ณ ํํฐ๋ง"์ด๋ผ๋ฉด, FGR์ ์ฒ์๋ถํฐ ํน์ ๊ฐ์ ์ฌ์ฉํ๋ ๊ณณ๋ง ์ ํํ ํ๊ฒํ ํฉ๋๋ค.
๋ ์ ๋ ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋ฑ์ฅ
๊ฐ๊ฐ์ ์ํ ๊ด๋ฆฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค์ด ๋๋ฆ์ ์ฒ ํ๊ณผ ์ฅ์ ์ด ์์ง๋ง, ๋ฆฌ์กํธ์ ๊ทผ๋ณธ์ ์ธ ๋ฆฌ๋ ๋๋ง ๋ฐฉ์์ ๋ฐ๊พธ์ง๋ ๋ชปํฉ๋๋ค. ๊ทธ๋ฐ๋ฐ ์ด๋ฐ FGR ๊ฐ๋ ์ ์ ๋ง ์ ๋ฐ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ํ๋ ์์ต๋๋ค.
โLegend Stateโ๋ผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ธ๋ฐ์. ์ง์ง ์ด๋ฆ์ด "Legend"์ ๋๋ค. Expo์์ ๊ณต์์ผ๋ก ๋ฐ์ด์ฃผ๊ณ ์๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก, React Native ๊ฐ๋ฐ์ ์ํ ํ๋ซํผ์ธ๋ฐ ๋ง์ ์ฌ๋๋ค์ด ์ฌ์ฉํฉ๋๋ค. ๋ํ Legend State๋ FGR์ ๊ฐ๋ ์ ์ ๋ฐ์ํ ๊ธ๋ก๋ฒ ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ธ๋ฐ์. ๋ฆฌ์กํธ์ ๋ฆฌ๋ ๋๋ง์ ์๋์ ์ผ๋ก ์ค์ฌ์ฃผ๊ณ , ์ฑ๋ฅ์ด ๊ฐ์ฅ ๋น ๋ฅด๋ค๊ณ ํฉ๋๋ค.

๊ทธ๋์ โLegend Stateโ ์ฝ๋๋ฅผ ์ค์ฌ์ผ๋ก FGR์ ๊ตฌํ ์๋ฆฌ๋ฅผ ์์๋ณด๊ฒ ์ต๋๋ค.
FGR์ ํต์ฌ ๋์ ์๋ฆฌ
Legend State์ ์ฝ๋๋ฅผ ๋ฏ์ด๋ณด๋ฉด์ FGR์ด ์ด๋ป๊ฒ ๊ตฌํ๋๋์ง ์์๋ณด๊ฒ ์ต๋๋ค. ๋ฌผ๋ก ์ค์ ์ฝ๋๋ ํจ์ฌ ๋ณต์กํ์ง๋ง, ํต์ฌ ๊ฐ๋ ์ ์ดํดํ ์ ์๋๋ก ์ ํํํ๊ณ ๋จ์ํํด์ ์ค๋ช ํด ๋๋ฆด๊ฒ์.
FGR ๊ฐ ์ก๊ธฐ: set๋ง ํ๋ฉด effect๊ฐ ์์์ ์คํ๋๋ค
์ฐ์ FGR์ด ์ผ๋ง๋ ์ ๊ธฐํ์ง ๊ฐ์ ์ก์๋ด ์๋ค.
// Legend State ์ฌ์ฉ๋ฒ
import { observable } from '@legendapp/state';
const user = observable({
name: "๊น๊ฐ๋ฐ",
foodPreference: "๋ถ๋จน"
});
// ์ด๋ ๊ฒ๋ง ์จ๋
effect(() => {
console.log(`${user.name.get()}๋์ ${user.foodPreference.get()} ํ`);
});
// ๋์ค์ ์ด๊ฒ๋ง ํธ์ถํ๋ฉด
user.foodPreference.set("์ฐ๋จน");
// ์์ effect๊ฐ ์๋์ผ๋ก ๋ค์ ์คํ๋ฉ๋๋ค!
// ๋ณ๋ ์์กด์ฑ ๋ฐฐ์ด๋, ๊ตฌ๋
์ค์ ๋ ํ์ ์์ด์
์ด๋ป๊ฒ ์ด๊ฒ ๊ฐ๋ฅํ ๊น์?
FGR ๊ฐ์ฒด์ ๋ด๋ถ ๊ตฌ์กฐ
FGR ๊ฐ์ฒด๋ ๋๊ฐ ์ด๋ฐ ๋ด๋ถ ๊ตฌ์กฐ๋ฅผ ๊ฐ์ง๊ณ ์์ต๋๋ค.
const observableValue = {
_value: "๊น๊ฐ๋ฐ",
_handlers: [], // ์ด๋ฒคํธ ํธ๋ค๋ฌ๋ค์ด ์ ์ฅ๋๋ ๋ฐฐ์ด
get() {
// getํ ๋: ํ์ฌ ์คํ ์ค์ธ effect๋ฅผ ํธ๋ค๋ฌ๋ก ๋ฑ๋ก
if (currentTracker) {
this._handlers.push(currentTracker);
}
return this._value;
},
set(newValue) {
this._value = newValue;
// setํ ๋: ๋ฑ๋ก๋ ์ด๋ฒคํธ ํธ๋ค๋ฌ๋ค์ ํธ์ถ
this._handlers.forEach(handler => handler());
}
};
get()
์ ํธ์ถํ๋ฉด ์ด๋ฒคํธ ํธ๋ค๋ฌ๊ฐ ๋ฑ๋ก๋๊ณ , set()
์ ํธ์ถํ๋ฉด ๊ทธ ํธ๋ค๋ฌ๋ค์ด ์คํ๋๋ ๊ตฌ์กฐ์
๋๋ค.
effect์ ์ญํ
// ์ ์ญ ๋ณ์: ํ์ฌ ์คํ ์ค์ธ effect๋ฅผ ์ถ์
let currentTracker = null;
function effect(callback) {
// 1. ํ์ฌ ์คํ ์ค์ธ effect๋ก ์ค์
currentTracker = callback;
// 2. ์ฝ๋ฐฑ ์คํ (์ด๋ get()๋ค์ด ํธ์ถ๋๋ฉด์ ์๋ ๋ฑ๋ก๋จ)
callback();
// 3. ์ถ์ ์๋ฃ
currentTracker = null;
}
effect
๋ ์ ์ญ ์ถ์ ์ํ๋ฅผ ์ค์ ํ ๋ค์ ์ฝ๋ฐฑ์ ์คํํฉ๋๋ค. ์ฝ๋ฐฑ ์คํ ์ค์ get()
์ด ํธ์ถ๋๋ฉด ์๋์ผ๋ก ์ด๋ฒคํธ ํธ๋ค๋ฌ๋ก ๋ฑ๋ก๋๋ ๊ฑฐ์ฃ .
์ค์ ๋์ ์์
// 1. effect ์คํ
effect(() => {
console.log(user.name.get()); // name์ ์ด effect๊ฐ ํธ๋ค๋ฌ๋ก ๋ฑ๋ก๋จ
console.log(user.age.get()); // age์ ์ด effect๊ฐ ํธ๋ค๋ฌ๋ก ๋ฑ๋ก๋จ
// foodPreference๋ ์ฌ์ฉํ์ง ์์ โ ๋ฑ๋ก๋์ง ์์
});
// 2. ๋์ค์ ๊ฐ ๋ณ๊ฒฝ
user.name.set("๋ฐ๊ฐ๋ฐ"); // effect ์คํ๋จ
user.age.set(30); // effect ์คํ๋จ
user.foodPreference.set("์ฐ๋จน"); // effect ์คํ๋์ง ์์!
์ด๊ฒ์ด ๋ฐ๋ก Fine Grained Reactivity์ ํต์ฌ์
๋๋ค. set
์ผ๋ก ์ด๋ค ๊ฐ์ ํ ๋นํ๋๋์ ๋ฐ๋ผ ์ ํํ ํ๊ฒํ
ํ๊ธฐ ๋๋ฌธ์ ๋ถํ์ํ ์๋ณธ๊ฐ ๋น๊ต๊ฐ ์์ฃ .
๋ฆฌ์กํธ์์๋ use$
๋ก ์ฌ์ฉํฉ๋๋ค
FGR์ ์๋ฆฌ๋ฅผ ์ดํดํ์ผ๋, ์ด์ ๋ฆฌ์กํธ์์๋ ์ด๋ป๊ฒ ์ฌ์ฉํ๋์ง ์์๋ด
์๋ค. Legend State์์๋ use$
๋ผ๋ ํ
์ ์ ๊ณตํด์ FGR์ ๋ฆฌ์กํธ์ ์ฐ๊ฒฐํฉ๋๋ค.
use$
ํ
์ฌ์ฉ๋ฒ
import { observable, use$ } from '@legendapp/state';
const user = observable({
name: "๊น๊ฐ๋ฐ",
age: 25,
foodPreference: "๋ถ๋จน"
});
function UserProfile() {
const name = use$(() => user.name.get());
const age = use$(() => user.age.get());
return <div>{name} ({age}์ธ)</div>;
}
function FoodPreference() {
const preference = use$(() => user.foodPreference.get());
return <button>{preference} ํ</button>;
}
์ด์ user.name
์ด๋ user.age
๊ฐ ๋ฐ๋๋ฉด UserProfile
์ปดํฌ๋ํธ๋ง ๋ฆฌ๋ ๋๋ง๋๊ณ , user.foodPreference
๊ฐ ๋ฐ๋๋ฉด FoodPreference ์ปดํฌ๋ํธ๋ง ๋ฆฌ๋ ๋๋ง๋ฉ๋๋ค.
use$
๋ด๋ถ ๊ตฌํ
use$ ํ ์ด ์ด๋ป๊ฒ ๋์ํ๋์ง ๊ฐ๋จํ ๊ตฌํํด ๋ด ์๋ค.
function use$(callback) {
const [, forceUpdate] = useReducer(x => x + 1, 0);
useEffect(() => {
// effect ๋ด์์ forceUpdate๋ ํจ๊ป ์ฝ๋ฐฑ์ ํฌํจ
effect(() => {
callback();
forceUpdate();
});
}, [callback]);
return callback();
}
use$๋ useEffect ๋ด์์ effect๋ฅผ ํธ์ถํ๊ณ , ์ฝ๋ฐฑ๊ณผ ํจ๊ป forceUpdate๋ ๋ฃ์ด์ ๊ฐ์ด ๋ณ๊ฒฝ๋๋ฉด ์ปดํฌ๋ํธ๊ฐ ๋ฆฌ๋ ๋๋ง๋๋๋ก ํฉ๋๋ค. (์ค์ ๋ ๋ฆฌ์กํธ์ useSyncExternalStore๋ก ๊ตฌํํฉ๋๋ค.)
๊ธฐ์กด ๋ฐฉ์๊ณผ์ ๋น๊ต
๊ธฐ์กด Context API ๋ฐฉ์:
// ์ ์ฒด user ๊ฐ์ฒด๊ฐ ๋ฐ๋๋ฉด ๋ชจ๋ ์ปดํฌ๋ํธ๊ฐ ๋ฆฌ๋ ๋๋ง
const { user } = useContext(UserContext);
FGR ๋ฐฉ์:
// ์ฌ์ฉํ๋ ๊ฐ๋ง ๋ฐ๋๋ฉด ํด๋น ์ปดํฌ๋ํธ๋ง ๋ฆฌ๋ ๋๋ง
const name = use$(() => user.name.get());
const age = use$(() => user.age.get());
๋ ๋์๊ฐ: ์ปดํ์ผ๋ฌ ๊ธฐ๋ฐ ์ ๊ทผ
์ด๋ฐ FGR ๊ฐ๋ ์ ๋์ฑ ๊ทน๋จ์ ์ผ๋ก ๋ฐ๊ณ ๋๊ฐ ํ๋ ์์ํฌ๋ค๋ ์์ต๋๋ค. ๊ฐ์๋์ ์์ ์ฌ์ฉํ์ง ์๊ณ ์ปดํ์ผ ํ์์ ์ต์ ํํ๋ ๋ฐฉ์์ด์ฃ .
์๋ฅผ ๋ค์ด,
<div>{user.name}</div>
์ปดํ์ผ ํ์์ ์ด๋ ๊ฒ ๋ณํ๋ฉ๋๋ค.
// ์ปดํ์ผ๋ ๊ฒฐ๊ณผ (๋จ์ํ)
effect(() => {
element.textContent = user.name.get();
});
Svelte์์๋ ์ด๋ฐ ์ฝ๋๊ฐ ๊ฐ์๋ ๋น๊ต ๊ณผ์ ์์ด๋, ํ์ํ ๋ถ๋ถ๋ง ์ง์ ์ ๋ฐ์ดํธํ ์ ์๋ ๊ฑฐ์ฃ . ์ด๊ฒ์ด FGR์ ๊ถ๊ทน์ ์ธ ํํ๋ผ๊ณ ํ ์ ์์ต๋๋ค.
๋์ ์ ์ ์ฃผ์ํ ์ ์?
์ง๊ธ๊น์ง Fine Grained Reactivity๊ฐ ๋ฌด์์ธ์ง, ๊ทธ๋ฆฌ๊ณ Legend State๋ฅผ ํตํด ์ด๋ป๊ฒ ๊ตฌํ๋๋์ง ์ดํด๋ดค์ต๋๋ค. ์ ํฌ ์ฑ์์๋ Legend State๋ฅผ ์ฌ์ฉํ๊ณ ์์ง๋ง, ๋์ ํ๊ธฐ ์ ์ ๋ฐ๋์ ๊ณ ๋ คํด์ผ ํ ์ ๋ค์ด ์์ต๋๋ค.
์ฌ์ฉ์์๊ฒ ์ ๊ฐ๋๋ ๋ณต์ก์ฑ
FGR์ ํจ๊ณผ๋ฅผ ์ ๋๋ก ๋ณด๋ ค๋ฉด ์ฌ์ฉ์๊ฐ ์ง์ ๋ง์ ๊ฒ๋ค์ ํ๋จํด์ผ ํฉ๋๋ค. ์ด๋ ๋ถ๋ถ๊น์ง ๋ฐ์ํ ๊ฐ์ฒด๋ก ๋ง๋ค์ง, ํฐ ๊ฐ์ฒด ๋ณํ ๋น์ฉ์ ์ค์ด๊ธฐ ์ํด ์ด๋์ ์ ์ ๊ทธ์์ง ๊ฒฐ์ ํ๋ ๊ฒ์ด ์๊ฐ๋ณด๋ค ๊น๋ค๋ก์ด๋ฐ์.
ํนํ ๋ฆฌ์กํธ์์ FGR์ ์ ๋๋ก ํ์ฉํ๋ ค๋ฉด ํน์ ๊ฐ๋ง ์ ํ์ ์ผ๋ก ์ฌ์ฉํด์ผ ํฉ๋๋ค.
// ์ด๋ ๊ฒ ํ๋ฉด ์๋ฏธ ์์ (์ ์ฒด ๊ฐ์ฒด ์ฌ์ฉ)
const user = use$(() => userState.get());
// ์ด๋ ๊ฒ ํด์ผ ํจ๊ณผ ์์ (ํน์ ๊ฐ๋ง ์ ํ)
const name = use$(() => userState.name.get());
const age = use$(() => userState.age.get());
๊ฒฐ๊ตญ ๊ธฐ์กด selector ํจํด๊ณผ ๋น์ทํ ๋ณด์ผ๋ฌํ๋ ์ดํธ๊ฐ ๋ฐ์ํ๊ฒ ๋ฉ๋๋ค. ๋ชจ๋ ๊ฐ์ ๋ํด ๊ฐ๋ณ์ ์ผ๋ก ์ฌ์ฉํด์ผ ํ๋๊น์.
์ฑ๋ฅ ์ด๋ ์๋ ์๋ชป๋ ์ฌ์ฉ
๊ฐ์ฅ ์ํํ ๊ฑด ์๋ฆฌ๋ฅผ ์ ๋๋ก ์ดํดํ์ง ์๊ณ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ์ ๋๋ค. ์ ์ฒด ๊ฐ์ฒด๋ฅผ use$๋ก ์ฌ์ฉํ๋ ค๊ณ ํ๋ฉด, ๋ชจ๋ ๊ฐ์ด ๋ณํ ๋๋ง๋ค ๋ฆฌ๋ ๋๋ง๋์ด์ ์ฌ์ค์ ๋ฆฌ๋ ๋๋ง์ ์ค์ด๋ ํจ๊ณผ๊ฐ ์์ต๋๋ค. ๋ฌด๊ฑฐ์ด ๋ฐ์ํ ๊ฐ์ฒด๋ง ์ถ๊ฐ๋ก ์์ฑ๋ ๋ฟ์ ๋๋ค. ๋ํ ํฐ ๊ฐ์ฒด๋ฅผ observable๋ก ๋ณํํ๋ ๋น์ฉ๋ ๋ง๋ง์น ์์ต๋๋ค. ํ๋ก์ ๊ฐ์ฒด ์์ฒด์ ๋ฐํ์ ์ค๋ฒํค๋๋ ์๊ณ , ๊น์ ์ค์ฒฉ ๊ฐ์ฒด์ผ ๊ฒฝ์ฐ ์ด๊ธฐ ๋ณํ ์๊ฐ์ด ์๋นํ ์ ์์ด์.
๋ฆฌ์กํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ํ๊ณ
Legend State์ ๊ฒฝ์ฐ ๋ฆฌ์กํธ์ ๋ด๋ถ ๋์์ ๊ด์ฌํ์ง ์์ ์ข ๋ ์์ ์ ์ด์ง๋ง, ๋ฆฌ๋ ๋๋ง์ ์ค์ด๋ ๋ฐ ํ๊ณ๊ฐ ์์ต๋๋ค. ๊ฒฐ๊ตญ ๋ฆฌ์กํธ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ด๊ธฐ ๋๋ฌธ์ด์ฃ . ํํธ, ๋ฆฌ์กํธ์ ๋ด๋ถ๋ ์ปดํ์ผ๋ฌ ๋ฑ์ ๊ฐ์ญํด์ ์ค์ด๋ ๋ฐฉ์์ ์ข ๋ ๊ธ์ง์ ์ผ๋ก ์ค์ผ ์ ์์ผ๋, ๋ฆฌ์กํธ์ ๋ฒ์ ์ด ์ฌ๋ผ๊ฐ ๋๋ง๋ค ๋ฒ๊ทธ๊ฐ ์๊ธธ ํ๋ฅ ์ด ๋๊ณ ์. ๊ทธ๋์ SolidJS ๊ฐ์ ๋ ์์ ์ธ ํ๋ ์์ํฌ๊ฐ ๋์ค๊ธฐ๋ ํ์ฃ . FGR์ ์ ์์ ์ ์ฅ์์ ๊น๋ค๋ก์ด ๋ฐฉ์์ผ ๊ฒ๋๋ค.
๋ง์น๋ฉฐ
์ด๋ฌํ ํ๊ณ์๋ ๋ถ๊ตฌํ๊ณ FGR์ ๋ถ๋ช ํ ๋งค๋ ฅ์ ์ธ ์ ๊ทผ ๋ฐฉ์์ ๋๋ค. ๋ค๋ง ๋์ ํ๊ธฐ ์ ์ ํ์ ๊ธฐ์ ์์ค, ํ๋ก์ ํธ ๊ท๋ชจ, ๊ทธ๋ฆฌ๊ณ ์ค์ ๋ก ์ฑ๋ฅ ๊ฐ์ ์ด ํ์ํ ์ํฉ์ธ์ง๋ฅผ ์ ์คํ๊ฒ ํ๋จํด ๋ณด์ธ์. ๋ฌด์์ ๋์ ํ๊ธฐ๋ณด๋ค๋ Legend State์ ์ฝ๋๋ฅผ ์ง์ ์ฝ์ด๋ณด๋ฉด์ ์๋ฆฌ๋ฅผ ์ดํดํ๊ณ , ์์ ๋ถ๋ถ๋ถํฐ ์ ์ง์ ์ผ๋ก ์ ์ฉํด ๋ณด๋ ๊ฒ์ ์ถ์ฒํฉ๋๋ค.