사용자 정의 훅과 팩토리 패턴을 활용한 상태 관리

February 9, 2025

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 구현

  1. 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를 쉽게 사용할 수 있도록 함.
  2. createStateContext를 활용한 상태 관리

    const useNumberState = (init?: number) => useState(init || 0);
    
    const [Count1Provider, useCount1] = createStateContext(useNumberState);
    const [Count2Provider, useCount2] = createStateContext(useNumberState);

    useNumberState 훅을 활용하여 createStateContext를 통해 각각의 상태를 별도로 관리할 수 있습니다.

  3. 활용 예시

    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;
    • 각 상태가 독립적으로 관리되므로, Count1ProviderCount2Provider를 따로 제공 가능.
    • 코드를 재사용할 수 있으며, 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를 사용할 때 발생하는 반복적인 코드 문제를 해결하고 사용자 정의 훅을 보다 유연하게 활용할 수 있습니다.

이 방식은 컴포넌트별 상태를 분리해야 하는 경우 또는 유지보수성을 높이고 싶을 때 유용하게 사용할 수 있을것 같습니다.

참고