External Store
๋ฆฌ์กํธ ์ธ๋ถ์ Store(์ ์ฅ์)๋ฅผ ๋๊ณ ์ํ๋ฅผ ๊ด๋ฆฌํจ์ผ๋ก์จ, ์ํ๋ฅผ ์ ์ดํ๋ ๊ด์ฌ์ฌ์ UI(์ปดํฌ๋ํธ)์ ๊ด์ฌ์ฌ๋ฅผ ๋ถ๋ฆฌ
React ์ปดํฌ๋ํธ ์
์ฅ์์๋ โ์ ์ญโ์ฒ๋ผ ์ฌ๊ฒจ์ง๋ค.
โProp Drillingโ ๋ฌธ์ ๋ฅผ ์ฐ์ํ๊ฒ ํด๊ฒฐํ ์ ์๋ ๋ฐฉ๋ฒ ์ค ํ๋(React๋ก ํ์ ํ๋ฉด Context๋ ์ธ ์ ์๋ค).
forceUpdate
React๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด ์๋ React ์ธ๋ถ๋ฅผ ํตํด ์ํ๊ด๋ฆฌ๋ฅผ ํ๋ ๊ฒ์ด๋ฏ๋ก useState๋ฅผ ์ฌ์ฉํ์ฌ ์ํ๊ด๋ฆฌ๋ฅผ ํ๋๊ฒ์ด ์๋๋ค.
๊ทธ๋์ ํ๋ฉด์ ๋ฆฌ๋ ๋๋ง ์ํฌ ๋ฐฉ๋ฒ์ด ํ์ํ๋ฐ ์๋์ ๊ฐ์ด ๊ฐ์ ์ ์ผ๋ก ๋ฆฌ๋ ๋๋งํ๋ ๋ฐฉ์์ ์ฌ์ฉํ๋ค.
function useForceUpdate() {
const [, setState] = useState({});
return useCallback(() => setState({}), []);
}
React ๊ณต์๋ฌธ์์ ๋์์๋ forceUpdate ์ฝ๋
๊ฐ๋ฅํ๋ฉด ํด๋น ํจํด์ ํผํ๋ผ๊ณ ์ฃผ์๋ฅผ ์ฃผ๊ณ ์๋ค.
const [ignored, forceUpdate] = useReducer(x => x + 1, 0);
function handleClick() {
forceUpdate();
}
useReducer
React์์ ์ ๊ณตํ๋ ์ํ ๊ด๋ฆฌ ํ
์ด๋ฉฐ, useState์ ์ ์ฌํ์ง๋ง ์ํ ์
๋ฐ์ดํธ ๋ก์ง์ ์ปดํฌ๋ํธ ์ธ๋ถ๋ก ๋ถ๋ฆฌํ ์ ์๋ค.
useReducer๊ฐ ๊ธฐ๋ณธํ์ด๊ณ useState๋ ๋ด๋ถ์ ์ผ๋ก useReudcer๋ฅผ ์ฌ์ฉํ๋ ๋ฐฉ์์ด๋ค.
์ฌ๋ฌ ์ปดํฌ๋ํธ์์ ๊ณต์ ๋๋ ์ํ๋ฅผ ๊ด๋ฆฌํ ๋ ๋์์ด ๋๋ค.
์ฐ๋ฆฌ๊ฐ ํํ ์๊ณ ์๋ Redux์ ๊ทธ๊ฒ๊ณผ ๋น์ทํจ
const [state, dispatch] = useReducer(reducer, initialState);
dispatch: ์ก์
์ ๋ณด๋ด๋ ํจ์๋ฅผ ๋ฐํ
reducer: ์ํ ์
๋ฐ์ดํธ ๋ก์ง์ ์ฒ๋ฆฌํ๋ ํจ์
useCallback
React์์ ์ ๊ณตํ๋ ์ฑ๋ฅ ์ต์ ํ๋ฅผ ์ํ ํ
์ด๋ฉฐ, ์ฑ๋ฅ ์ต์ ํ๋ฅผ ์ํด ํจ์๋ฅผ ์บ์ํ๊ณ ์ฌ์ฌ์ฉํ๋ค.
๋งค ๋ ๋๋ง ๋ง๋ค ์๋ก์ด ํจ์๋ฅผ ์์ฑํ์ง ์๊ณ ์ด์ ์ ์์ฑ๋ ํจ์๋ฅผ ์ฌ์ฌ์ฉํ๋๋ก ๋ณด์ฅํจ
์์์ปดํฌ๋ํธ์ props๋ก ์ฝ๋ฐฑํจ์๋ฅผ ์ ๋ฌํ ๋ props์ ๋ณํ๋ก ๋ถํ์ํ๊ฒ ๋ฆฌ๋ ๋๋ง ๋๋ ๊ฒ์ ๋ฐฉ์งํ๊ธฐ ์ํด ์ฌ์ฉ๋๋ค.
useCallback๋ง ์ฌ์ฉํ๋ค๊ณ ํด์ ์ต์ ํ ๋๋ ๊ฒ์ด ์๋๋ผ ๋ ๋ค๋ฅธ ์ฑ๋ฅ ์ต์ ํ ํ
์ธ React.memo()
์ ๊ฐ์ด ์ฌ์ฉํด์ผ ์ต์ ํ ๋๋ค๋ ๊ฒ์ ์ ์
const callbackFn = useCallback(() => {
doSomething
}, [dependency])
useEffect์ ์์กด์ฑ ๋ฐฐ์ด๊ณผ ๋น์ทํ๋ฐ ํจ์ ๋ด๋ถ์์ ์ฐธ์กฐํ๋ ์ํ๋ props ์ค์์ ๋ณ๊ฒฝ๋๋ ๊ฒ์ด ์์๋ ์๋ก์ด ํจ์๋ฅผ ์์ฑํ๋๋ก ์ง์ ํ๋ค.
Props Drilling
๋ถ๋ชจ์์ React ํธ๋ฆฌ์ ๋ชจ๋ ์ค์ฒฉ๋ ์์์๊ฒ ๋ฐ์ดํฐ๋ฅผ ์ ๋ฌํ๋ ๊ฒ์ ์๋ฏธํ๋ค.
์๋ ์ฝ๋์ ๊ฐ์ด ์ค๊ฐ ๋จ๊ณ์ ์ปดํฌ๋ํธ๊ฐ props๋ฅผ ๋จ์ํ ์์์๊ฒ ์ ๋ฌํ๊ธฐ๋ง ํ๋ ํํ๋ฅผ props drilling์ด๋ผ ํจ
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<Children count={count} setCount={setCount} />
</div>
)
}
function Children({count, setCount}) {
return (
<div>
<ChildrenChildren count={count} setCount={setCount} />
</div>
)
}
function ChildrenChildren({count, setCount}) {
return (
<div>
<p>{`count: ${count}`}</p>
<button type="button" onClick={() => setCount((prev) => prev + 1)}>Increase</button>
</div>
)
}
Props Drilling์ ๋ฌธ์ ์
์ปดํฌ๋ํธ ๊ตฌ์กฐ๊ฐ ๋ณ๊ฒฝ๋๋ฉด props๋ฅผ ์ ๋ฌํ๋ ๋ชจ๋ ์ปดํฌ๋ํธ๋ฅผ ์์ ํด์ผ ํ๋ฏ๋ก ๋ฒ๊ฑฐ๋กญ๋ค.
์ฝ๋๊ฐ ๊ธ๋ฐฉ ๋ณต์กํด์ง๊ณ ์ค์ํ๊ธฐ ์ฝ๋ค.
Context API
React์์ ์ ๊ณตํด์ฃผ๋ Context API Hooks๋ก Props Drilling์ ์ต์ํํ ์ ์๋ค.
import {createContext, useContext, useState, useMemo} from 'react';
interface CounterContextValue {
count: number;
setCount: () => void;
}
export const CounterContext = createContext({} as CounterContextValue);
export function useCounter() {
const contextValue = useContext(CounterContext);
if (!contextValue) {
throw new Error('useCounter must be used within CounterProvider');
}
return contextValue;
}
export default CounterProvider({children}: React.PropsWithChildren) {
const [count, setCount] = useState(0);
const value = useMemo(() => ({count, setCount}), [count, setCount])
return (
<CounterContext.Provider value={value}>
{children}
</CounterContxt.Provider>
)
}
Context ์ ์ฉ ์ฝ๋
๋๊ฒจ์ฃผ๋ Props๋ค์ ๋ชจ๋ ์ ๊ฑฐํ๋ ๊น๋ํด์ก๋ค.
function Parent() {
return (
<CounterProvider>
<div>
<Children/>
</div>
</CounterProvider>
)
}
function Children() {
return (
<div>
<ChildrenChildren/>
</div>
)
}
function ChildrenChildren() {
const {count, setCount} = useCounter();
return (
<div>
<p>{`count: ${count}`}</p>
<button type="button" onClick={() => setCount((prev) => prev + 1)}>Increase</button>
</div>
)
}
Context API + useReduecr ์กฐํฉ์ผ๋ก ์ํ๊ด๋ฆฌ๋ฅผ ํ๋ด๋ผ์ ์์ง๋ง ๊ด๋ฆฌ๋๋ ์ปจํ
์คํธ ๊ฐ ์ค ์ผ๋ถ๊ฐ ๋ณ๊ฒฝ์ด ๋์์ ๋ ๋ณ๊ฒฝ๋์ง ์์ ๋ค๋ฅธ ์ํ๊ฐ์ ์ฐ๊ฒฐ๋ ์ปดํฌ๋ํธ ๋ํ ๋ฆฌ๋ ๋๋ง์ด ๋ฐ์ํ๋ Side Effect๊ฐ ์๋ค.
External Store ์ง์ ๊ตฌํํด๋ณด๊ธฐ with Tsyringe
์ฌ๋ฌ ์คํ ์ด๋ฅผ ํตํด ๊ด๋ฆฌ๋๋ extenral store๋ฅผ ์ง์ ๊ตฌํ ํด๋ณด์.
ObjectStore.
์ฌ๋ฌ ์คํ ์ด์ ๋ฒ ์ด์ค๊ฐ ๋๋ ๋ถ๋ชจ ํด๋์ค
type Listener = () => void;
export default abstract class ObjectStore {
private listeners = new Set<Listener>();
protected publish() {
this.listeners.forEach((listener) => listener());
}
addListener(listener: Listener) {
this.listeners.add(listener);
}
removeListener(listener: Listener) {
this.listeners.delete(listener);
}
}
CounterStore
import { singleton } from 'tsyringe';
import ObjectStore from './objectStore';
@singleton()
export default class CounterStore extends ObjectStore {
counter = 0;
increase(step = 1) {
this.counter += step;
this.publish();
}
decrease(step = 1) {
this.counter -= step;
this.publish();
}
}
useObjectStore
import { useEffect } from 'react';
import ObjectStore from '../stores/objectStore';
import useForceUpdate from './useForceUpdate';
export default function useObjectStore<T extends ObjectStore>(store: T): T {
const forceUpdate = useForceUpdate();
useEffect(() => {
store.addListener(forceUpdate);
return () => {
store.removeListener(forceUpdate);
};
}, [store, forceUpdate]);
return store;
}
useCounterStore
import { container } from 'tsyringe';
import CounterStore from '../stores/counterStore';
import useObjectStore from './useObjectStore';
export default function useCounterStore() {
const store = container.resolve(CounterStore);
return useObjectStore(store);
}
Usage
CounterController.tsx
import useCounterStore from '../hooks/useCounterStore';
export default function CounterController() {
const store = useCounterStore();
const handleClickIncrease = (step?: number) => () => {
store.increase(step);
};
const handleClickDecrease = (step?: number) => () => {
store.decrease(step);
};
return (
<div>
<button type="button" onClick={handleClickIncrease(10)}>
Increase 10
</button>
<button type="button" onClick={handleClickIncrease()}>
Increase
</button>
<button type="button" onClick={handleClickDecrease(10)}>
Decrease 10
</button>
<button type="button" onClick={handleClickDecrease()}>
Decrease
</button>
</div>
);
}
Counter.tsx
import useCounterStore from '../hooks/useCounterStore';
export default function Counter() {
const { counter } = useCounterStore();
return (
<div>
<p>{`counter: ${counter}`}</p>
</div>
);
}
์ด๋ฐ ์ ๊ทผ์ ์ ํ๋ฉด, React๊ฐ UI๋ฅผ ๋ด๋นํ๊ณ , ์์ํ TypeScript(๋๋ JavaScript)๊ฐ ๋น์ฆ๋์ค ๋ก์ง์ ๋ด๋นํ๋, ๊ด์ฌ์ฌ์ ๋ถ๋ฆฌ(Separation of Concerns)๋ฅผ ๋ช
ํํ ํ ์ ์๋ค. ์์ฃผ ๋ฐ๋๋ UI ์์์ ๋ํ ํ
์คํธ ๋์ , ์ค๋ ์ ์ง๋๋(๋ฐ๋๋ฉด ์น๋ช
์ ์ธ) ๋น์ฆ๋์ค ๋ก์ง์ ๋ํ ํ
์คํธ ์ฝ๋๋ฅผ ์์ฑํด ์ ์ง๋ณด์์ ๋์์ด ๋๋ ํ
์คํธ ์ฝ๋๋ฅผ ์น๋ฐํ๊ฒ ์์ฑํ ์ ์๋ค.