React에서 상태 관리를 위해 Context API를 사용하면 매번 공급자(Provider)와 사용자 정의 훅을 반복적으로 작성해야 하는 번거로움이 있었습니다.
Zustand, Jotai의 개발자인 다이시 카토(Daishi Kato) 의 책 "리액트 훅을 활용한 마이크로 상태 관리" 를 읽던 중 사용자 정의 훅을 팩토리 패턴과 결합하여 보다 범용적으로 활용하는 방법을 알게 되었습니다.
이 패턴을 활용하면 중복 코드를 줄이고 상태 관리 코드를 더욱 간결하고 유연하게 작성할 수 있고 활용도가 높아보여서 블로그에 기록합니다.
Context API를 활용한 반복적인 코드 문제
일반적으로 Context API를 사용할 때는 다음과 같은 패턴이 반복됩니다.
const CountContext = createContext<number | null>(null);
const CountProvider = ({ children }: { children: ReactNode }) => {
const [count, setCount] = useState(0);
return (
<CountContext.Provider value={[count, setCount]}>
{children}
</CountContext.Provider>
);
};
const useCount = () => {
const context = useContext(CountContext);
if (!context) throw new Error("Provider missing");
return context;
};
이 방식은 Provider와 사용자 정의 훅을 매번 정의해야 하는 번거로움이 있습니다.
이를 해결하기 위해 팩토리 패턴을 활용한 createStateContext
함수를 만들 수 있습니다.
팩토리 패턴을 활용한 createStateContext
구현
-
createStateContext
함수 정의const createStateContext = <Value, State>( useValue: (init?: Value) => State ) => { const StateContext = createContext<State | null>(null); const StateProvider = ({ initialValue, children, }: { initialValue?: Value; children?: ReactNode; }) => ( <StateContext.Provider value={useValue(initialValue)}> {children} </StateContext.Provider> ); const useContextState = () => { const value = useContext(StateContext); if (value === null) throw new Error("Provider missing"); return value; }; return [StateProvider, useContextState] as const; };
- 사용자 정의 훅 (
useValue
)을 인자로 받아useState
와 같은 내부 상태를 활용할 수 있도록 함. createContext
를 사용해 새로운 Context를 생성.StateProvider
컴포넌트를 생성해 Provider를 통해 상태를 주입하도록 함.useContextState
훅을 반환해 Context를 쉽게 사용할 수 있도록 함.
- 사용자 정의 훅 (
-
createStateContext
를 활용한 상태 관리const useNumberState = (init?: number) => useState(init || 0); const [Count1Provider, useCount1] = createStateContext(useNumberState); const [Count2Provider, useCount2] = createStateContext(useNumberState);
useNumberState
훅을 활용하여createStateContext
를 통해 각각의 상태를 별도로 관리할 수 있습니다. -
활용 예시
const Counter1 = () => { const [count1, setCount1] = useCount1(); return ( <div> Count1: {count1}{" "} <button onClick={()=> setCount1((c)=> c + 1)}>+1</button> </div> ); }; const Counter2 = () => { const [count2, setCount2] = useCount2(); return ( <div> Count2: {count2}{" "} <button onClick={()=> setCount2((c)=> c + 1)}>+1</button> </div> ); }; const App = () => ( <Count1Provider> <Count2Provider> <Counter1 /> <Counter2 /> </Count2Provider> </Count1Provider> ); export default App;
- 각 상태가 독립적으로 관리되므로,
Count1Provider
와Count2Provider
를 따로 제공 가능. - 코드를 재사용할 수 있으며, Context API를 효율적으로 관리할 수 있음.
- 각 상태가 독립적으로 관리되므로,
Provider를 왜 다 따로 만들어서 사용하는거야? value 를 한번에 넣으면 되지!
이렇게 생각이 들수도 있습니다.
const CountContext = createContext<{
count1: number;
count2: number;
setCount1: React.Dispatch<React.SetStateAction<number>>;
setCount2: React.Dispatch<React.SetStateAction<number>>;
} | null>(null);
const CountProvider = ({ children }: { children: ReactNode }) => {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
return (
<CountContext.Provider value={{ count1, setCount1, count2, setCount2 }}>
{children}
</CountContext.Provider>
);
};
const useCount = () => {
const context = useContext(CountContext);
if (!context) throw new Error("Provider missing");
return context;
};
React의 Context API는 Provider의 값이 변경될 때, 해당 Context를 구독하는 모든 컴포넌트를 리렌더링합니다. 그래서 코드의 count1 이 변경될경우 count2에 대한 컴포넌트들도 리렌더링이 되어 불필요한 리렌더링이 발생합니다.
결론
팩토리 패턴을 활용하면 Context API를 사용할 때 발생하는 반복적인 코드 문제를 해결하고 사용자 정의 훅을 보다 유연하게 활용할 수 있습니다.
이 방식은 컴포넌트별 상태를 분리해야 하는 경우 또는 유지보수성을 높이고 싶을 때 유용하게 사용할 수 있을것 같습니다.
참고
- 원본 코드: 팩토리 패턴과 사용자 정의 훅을 활용한 예제