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

---

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를 사용할 때 발생하는 반복적인 코드 문제를 해결하고 사용자 정의 훅을 보다 유연하게 활용할 수 있습니다.

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

참고