리액트 훅을 활용한 마이크로 상태 관리

Minboy·2025년 1월 3일
25
post-thumbnail

이미지 출처 - 교보문고

상태(state)는 프론트엔드에서 너무나 중요한 개념이고, 그렇기에 늘 관심있게 지켜보는 주제였다. 그 와중에 좋은 책을 하나 선물 받아서 해당 책을 읽으면서 새로 배우고 정리한 내용들을 기록하려고한다.

저자분은 프론트 개발자라면 정말 친숙한 라이브러리들인 Zustand, Jotai, Valtio를 제작하고 maintain중이신 Dashi Kato님이다. 책을 읽기 전부터 저자분의 명성에 벌써 내용이 신뢰가 갔다.

요약

  • 상태란 무엇인가, 지역 상태 vs 전역 상태
  • 전역 상태를 구현하는 방법들
  • 리렌더링을 최적화하는 방법들
  • 각 상태 관리 라이브러리들의 철학과 특징들

상태 관리에 관해서 기초적인 내용부터 깊은 내용까지 코드예시와 함께 잘 설명되어 있기 때문에 대부분의 프론트엔드 개발자라면 큰 도움이 될 책이라고 생각된다.

내용 정리

여기서부터는 책을 읽으며 개인적으로 정리한 내용들이다. 모든 내용을 정리한 것은 아니고 인상 깊었거나 새로 배운 내용등을 정리했다.

잘못된 내용이 있다면 알려주시면 감사하겠습니다!

01 _ 리액트 훅을 이용한 마이크로 상태 관리

마이크로 상태 관리 이해하기

리액트에서 상태사용자 인터페이스(UI)를 나타내는 모든 데이터를 말한다. 상태는 시간이 지남에 따라 변할 수 있으며 리액트는 상태와 함께 렌더링할 컴포넌트를 처리한다. -p4

책에서는 위와 같이 상태를 정의하였다. 나도 나만의 상태에 대한 정의를 내려보았다.


상태란 컴포넌트 동작과 UI에 영향을 미치는 데이터로써, 세 가지 특별한 점이 존재한다.

1. 상태의 변경은 해당 상태에 의존하는 컴포넌트들의 리렌더링을 유발한다.

이를 위해 리액트 라이브러리가 상태의 변경을 추적하고 관리해준다.

2. 상태는 컴포넌트와 별개로 React 라이브러리가 독립적으로 관리한다.

컴포넌트의 리렌더링, 즉 함수의 재호출에도 상태가 초기화 되지 않을 수 있는 이유이다.

3. 상태는 불변성을 기반으로 관리되며, 값을 직접 변경할 수 없고 새로운 값을 생성하여 업데이트된다.

이를 통해 React는 단순한 참조값 비교(shallow comparison)만으로도 상태 변경을 효율적으로 추적할 수 있고, 동작을 쉽게 예측할 수 있다.


이미지 출처 - Medium

리액트 훅이 나오기 전까지는 Redux와 같은 중앙 집중형 상태 관리 라이브러리를 사용하는 것이 일반적이었다. 하지만 이는 사용되지 않는 기능 까지 포함될 수 있어 과한 측면이 있었다.

=> Hooks가 등장하면서 상태를 생성하는 새로운 방법이 생겼고, 특정 목적에 따라 다른 해결책을 제공할 수 있게 되었다.

  • 폼(form) 상태는 전역 상태와 별도로 처리해야 하는데, 이는 단일 상태로는 해결할 수 없다.
  • 서버 캐시 상태는 다른 상태와는 다른 리페칭(refetching, 다시 불러오기) 같은 몇 가지 고유한 특성이 있다.
  • 내비게이션 상태는 원 상태가 브라우저에 있다는 특수한 요건이 있기 때문에 단일 상태는 적합하지 않다. - p4

이미지 출처 - React Hook Form, React Query

Hooks의 등장으로 폼 상태는 react-hook-form이나 Formik, 서버 캐시 상태는 React QueryuseSWR과 같은 목적 지향형 상태 관리 라이브러리들이 각 문제에 대한 해결책으로 제시되었다.

하지만 여전히 특정 목적 지향적인 방법이 아닌 범용적 상태 관리에 대한 수요도 존재했고, 당연히 각 상황마다 범용적 상태 관리가 필요한 작업의 비율은 다를 것이다.

따라서 범용적인 상태 관리를 위한 방법은 가벼워야하고, 각 상태 관리 방법마다 서로 다른 기능을 가져 개발자가 요구사항에 따라 적절한 방법을 선택할 수 있어야 한다. (+추가로 완만한 학습 곡선까지)

저자는 이를 마이크로 상태 관리라는 개념으로 정의했다.

Poimandres를 처음 봤을 때, Zustand, Jotai, Valtio 등 여러 상태 관리 라이브러리들이 함께 있는 것을 보고 의아한 적이 있었는데, 여기서 그 이유를 찾은 것 같다. 결국 그 라이브러리들 모두 철학과 쓰임새가 달랐던 것이다.

리액트 훅 사용하기

마이크로 상태 관리를 하기 위해서는 리액트 훅이 필수다. useState, useReducer 훅을 이용하면 지역 상태를 생성할 수 있다.

사용자 정의 훅(커스텀 훅)을 이용하면 UI 컴포넌트에서 로직을 추출할 수 있다. 로직이 분리됨으로써 컴포넌트를 건드리지 않고 기능을 추가할 수 있다.

전역 상태 탐구하기

이미지 출처 - Freepik

컴포넌트에서 정의되고 컴포넌트 트리 내에서 사용되는 상태를 지역 상태라고 부른다. 이를 생성하기 위해 리액트는 useState와 같은 기본적인 훅을 제공한다.

반면 전역 상태는 애플리케이션 내 서로 멀리 떨어져 있는 여러 컴포넌트에서 사용하는 상태이다.

리액트의 컴포넌트 모델에서는 각 컴포넌트들이 격리돼야하고 재사용 가능해야한다는 지역성(locality)이 중요하기 때문에 전역 상태를 구현하는 것은 쉬운일이 아니다.

컴포넌트 재사용은 컴포넌트가 독립적인 경우에만 가능하고, 컴포넌트가 외부에 의존하는 경우 동작이 일관되지 않아 재사용이 불가능할 수 있다. 따라서 엄밀하게 말하면 컴포넌트 자체는 전역 상태에 가급적 의존하지 않는 것이 좋다.

리액트는 전역 상태에 대한 직접적인 해결책을 제공하지 않기 때문에 이는 개발자와 커뮤니티의 몫이다. Context API를 떠올릴 수도 있지만, 이것이 완벽한 해결책이 되지 않는다는 것을 뒤에서 설명한다. 이어서 여러 해결책들과 각각의 장단점을 논의할 것이다.

02 _ 지역 상태와 전역 상태 사용하기

상태는 컴포넌트와 독립적으로 리액트가 관리하는 값이므로 사실 전역 상태가 아니더라도 지역 상태, 즉 컴포넌트 내에서 상태를 사용하게 되면 해당 컴포넌트는 순수하지 않다. (Pure function)

하지만 상태가 컴포넌트 내에서만 사용된다면 다른 컴포넌트에 영향을 미치지 않고, 이러한 특성을 저자는 억제됨(contained)라고 표현한다.

리액트는 개념적으로 상태를 사용자 인터페이스(UI)로 변환하는 함수다. -p31

지역 상태의 한계

지역성을 제공하고 싶지 않을 때, 즉 함수 컴포넌트 외부에서 상태를 변경해야 한다면 전역 상태가 필요하다.
함수 외부에서 함수의 동작을 제어할 때 전역 변수는 유용하게 사용될 수 있다. 마찬가지로 전역 상태는 컴포넌트 외부에서 컴포넌트의 동작을 제어할 때 유용하게 사용할 수 있지만 컴포넌트 동작을 예측하기 어렵게 만든다는 단점이 존재한다.

따라서 지역 상태를 기본으로 사용하고, 전역 상태는 보조 수단으로 사용하는 것이 좋다.

전역 상태 사용하기

  • 지역 상태 - 개념적으로 하나의 컴포넌트에 속하고 컴포넌트에 의해 캡슐화된 상태
  • 전역 상태 - 지역 상태가 아닌 상태, 하나의 컴포넌트에만 속하지 않으며 여러 컴포넌트에서 사용할 수 있다.

만약 모든 컴포넌트가 의존하는 애플리케이션 차원의 지역 상태가 존재한다면 해당 지역 상태는 전역 상태라고 볼 수 있다. 이처럼 지역 상태와 전역 상태는 명확히 나누기 힘든데, 대부분 경우 상태가 개념적으로 속한 곳이 어디인지 생각해보면 상태가 지역 상태인지 전역 상태인지 구분할 수 있다.

전역 상태의 두가지 측면
1. 싱글턴 - 특정 컨텍스트에서 상태가 하나의 값만을 가짐
2. 공유 상태 - 상태 값이 다른 컴포넌트 간 공유되며, 꼭 단일 값일 필요는 없음(싱글턴이 아닌 전역 상태는 여러 값을 가질 수 있다.)

좀 더 부연 설명을 하자면 전역 상태가 싱글턴일때는 메모리에 하나의 값으로만 존재하지만, 싱글턴이 아닌 경우에는 컴포넌트 트리의 다른 부분(하위 트리)에 대해 여러 값을 가질 수 있다. (ex. 여러 Provider를 가지고 그에 따라 다른 값 가질 수 있는 것)

전역 상태를 사용하는 상황

  • prop을 전달하는 것이 적절하지 않을 때 - prop drilling
  • 이미 리액트 외부에 상태가 있을 때 - 리액트 없이 획득한 사용자 인증 정보처럼 이미 리액트 외부에 존재하는 전역 상태를 연결

03 _ 리액트 컨텍스트를 이용한 컴포넌트 상태 공유

이미지 출처 - LinkedIn

컨텍스트 이해하기

컨텍스트 공급자(Provider)가 새로운 컨텍스트 값을 갖게 되면 모든 컨텍스트 소비자는 새로운 값을 받고 리렌더링 된다. 이는 공급자의 값이 모든 소비자에게 전파된다는 것을 의미한다.

컨텍스트에 객체를 사용할 때의 한계점

객체에는 여러 가지 값을 포함할 수 있으며, 컨텍스트 소비자는 모든 값을 사용하지 않을 수 있다.

const CountContext = createContext({ count1: 0, count2: 0 });

const Counter1 = () => {
  const { count1 } = useContext(CountContext);
  ...
}
  
const Counter2 = () => {
  const { count2 } = useContext(CountContext);
  ...
}
  
const App = () => {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);
  
  return (
    <CountContext.Provider value={{ count1, count2 }}>
      ...

Counter1 컴포넌트는 count1만, Counter2 컴포넌트는 count2만 사용한다. 따라서 이상적으로는 Counter1count1이 변경될 때만 리렌더링돼야한다. 하지만 위 예제에서는 count2만 변경하더라도 Counter1또한 리렌더링된다. 이는 불필요한 연산이고, 리액트 컨텍스트를 활용할 때 반드시 알아둬야할 리렌더링 문제이다. 이를 해결하기 위한 몇 가지 일반적인 패턴에 대해 알아보자.

작은 상태 조각 만들기

첫번째로 전역 상태를 합쳐진 큰 객체 대신 여러 조각으로 나누어 사용하는 방법이 있다.

const Count1Context = createContext([0, () => {}]);

const Count2Context = createContext([0, () => {}]);

const Count1Provider = ({ children }) => {
  const [count1, setCount1] = useState(0);
  return (
    <Count1Context.Provider value={[count1, setCount1]}>
      {children}
    </Count1Context.Provider>
  );
};

const Count2Provider = ({ children }) => {
  const [count2, setCount2] = useState(0);
  return (
    <Count2Context.Provider value={[count2, setCount2]}>
      {children}
    </Count2Context.Provider>
  );
};

const Count1 = () => {
  const [count1, setCount1] = useContext(Count1Context);
  ...
  
const Count2 = () => {
  const [count2, setCount2] = useContext(Count2Context);
  ...

const App = () => (
  <Count1Provider>
    <Count2Provider>
  ...

이런 식으로 각 상태에 쪼개고 그에 대한 공급자를 만들어주면 Counter1Counter2 컴포넌트는 각각 count1count2가 변경될 때만 리렌더링된다.

useReducer로 하나의 상태를 만들고 여러 개의 컨텍스트로 전파하기

두 번째 해결책은 단일 상태를 만들고 여러 컨텍스트로 상태 조각을 배포하는 것이다. 이때 상태를 갱신하는 함수는 별도의 컨텍스트로 배포해야한다.

const Count1Context = createContext(0);
const Count2Context = createContext(0);
const DispatchContext = createContext(() => {});

const Counter1 = () => {
  const count1 = useContext(Count1Context);
  const dispatch = useContext(DispatchContext);
  ...
  
const Counter2 = () => {
  const count2 = useContext(Count2Context);
  const dispatch = useContext(DispatchContext);
  ...

const Provider = ({ children }) => {
  const [state, dispatch] = useReducer(
    ( prev, action ) => {
      if (action.type === "INC1") {
        return { ...prev, count1: prev.count1 + 1 };
      }
      if (action.type === "INC2") {
        return { ...prev, count2: prev.count2 + 1 };
      }
      throw new Error("no matching action");
    },
    {
      count1: 0,
      count2: 0,
    }
  );
  
  return (
    <DispatcherContext.Provider value={dispatch}>
      <Count1Context.Provider value={state.count1}>
        <Count2Context.Provider value={state.count2}>
          ...

하나의 단일 상태를 중첩된 공급자가 각 상태 조각과 하나의 실행 함수로 제공한다. 이 방식 또한 불필요한 렌더링을 없앨 수 있다. 여러 상태를 사용하는 것보다 단일 상태를 사용할 때의 장점은 단일 상태에서는 하나의 액션으로 여러 조각을 갱신할 수 있다는 것이다.

컨텍스트 사용을 위한 모범 사례

const Count1Context = createContext(null);

const Count1Provider = ({ children }) => {
  <Count1Context.Provider value={useState(0)}>
    {children}
  </Count1Context.Provider>
};

const useCount1 = () => {
  const value = useContext(Count1Context);
  if (value === null) throw new Error("Provider missing");
  return value;
};

const Count1 = () => {
  const [count1, setCount1] = useCount1();
  ...
}
  
const App = () => (
  <Count1Provider>
  ...

위와 같이 커스텀 훅을 이용해 컨텍스트의 기본값인 null 체크로 공급자 사용을 확인할 수 있다. 또한 공급자에서 valueuseState를 직접 넣어주는 것으로 코드를 줄일 수 있다.

04 _ 구독을 이용한 모듈 상태 공유

이미지 출처 - Freepik

앞서 살펴본 컨텍스트는 싱글턴 패턴을 피하고 각 하위 트리에 서로 다른 값을 제공하기 위한 기능이다. 전역 상태를 싱글턴과 유사하게 만들고 싶다면 모듈 상태를 사용하는 것이 싱글턴 값으로 메모리에 할당되기 때문에 더 좋다.

모듈 상태란?
모듈 상태의 엄격한 정의는 ECMAScript(ES) 모듈 스코프에 정의된 상수 또는 변수다. 여기서는 단순하게 모듈상태는 전역적이거나 파일의 스코프 내에서 정의된 변수라고 가정한다.

const createStore = <T extends unknown>(initialState: T): Store<T> => {
  let state = initialState;
  const callbacks = new Set<() => void>();
  const getState = () => state;
  const setState = (nextState: T | ((prev: T) => T)) => {
    state =
      typeof nextState === "function"
        ? (nextState as (prev: T) => T)(state)
        : nextState;
    callbacks.forEach((callback) => callback());
  };
  const subscribe = (callback: () => void) => {
    callbacks.add(callback);
    return () => callbacks.delete(callback);
  };
  return { getState, setState, subscribe };
};

const store = createStore({ count: 0 });

위처럼 구독을 활용한 전역 스토어를 구현할 수 있다. 스토어는 callbacks라는 Setstate가 변경될 때 수행할 작업들을 subscribe 메소드를 통해 등록한다. statesetState를 통해 변경되면 callbacks에 담겨있던, 즉 등록된 작업들이 수행된다.

const useStore = (store) => {
  const [state, setState] = useState(store.getState());

  useEffect(() => {
    const unsubscribe = store.subscribe(() => {
      setState(store.getState());
    });
    setState(store.getState());
    return unsubscribe;
  }, [store]);

  return [state, store.setState];
};

다음은 store의 상태와 React 컴포넌트의 상태를 동기화하는 useStore 훅이다. 여기서 useState로 생성한 setState 변경 함수를 state에 구독시키고, 이를 통해 스토어의 상태변경시에 해당 컴포넌트가 리렌더링 될 수 있게되는 것이다.

눈 여겨볼 점으로는 store.subscribe로 상태 변경 구독 후에 setState(store.getState())를 호출하여 store의 최신 상태로 React 상태를 강제로 업데이트한다. 이는 useEffect가 뒤늦게 실행돼서 store가 이미 새로운 상태를 가지고 있을 가능성이 있기 때문이다.

const Component1 = () => {
  const [state, setState] = useStore(store);
  const inc = () => {
    setState((prev) => ({ ...prev, count: prev.count + 1 }));
  };

  return (
    <div>
      <h1>Component1</h1>
      <p>Count: {state.count}</p>
      <button onClick={inc}>Increment</button>
    </div>
  );
};

function App() {
  return (
    <>
      <Component1 />
    ...

이제 useStore 훅을 활용해 컴포넌트에서 전역 상태를 활용할 수 있다.

선택자와 useSubscription 사용하기

useStore는 현재 상태 객체 전체를 반환한다. 이는 일부분만 변경되더라도 모든 useStore 훅에 전달되기 때문에 불필요한 리렌더링을 발생시킬 수 있다. 이를 피하기 위해 컴포넌트가 필요로 하는 상태의 일부분만 반환하는 선택자(selector)를 도입할 수 있다.

const useStoreSelector = <T, S>(
  store: Store<T>
  selector: (state: T) => S
) => {
  const [state, setState] = useState(() => selector(store.getState()));
  
  useEffect(() => {
    const unsubscribe = store.subscribe(() => {
      setState(selector(store.getState()));
      setState(selector(store.getState()));
    });
    setState(selector(store.getState()));
    return unsubscribe;
  }, [store, selector]);
  
  return state;
}

useStore와 다르게 useState훅을 상태의 전체 내용으로 초기화하는 것이 아닌 selector의 반환값을 가지도록 설정하자.

const Component1 = () => {
  const state = useStoreSelector(
    store,
    useCallback((state) => state.count1, []),
  );
  
  const inc = () => {
    store.setState((prev) => ({
      ...prev,
      count1: prev.count1 + 1,
    }));
    
...

하지만 이 경우에는 상태를 갱신할때는 store.setState()를 직접 호출해야한다. 또한 useStoreSlectoruseEffect에서 두 번째 인수에 선택자 함수가 지정돼있으므로 선택자 함수에 아무런 변화가 없더라도 Component1을 렌더링할 때마다 store를 구독 해제하고 구독하는 것을 반복하게된다. 따라서 useCallback을 사용해 selector를 넘겨주어야한다.

위와 같이 선택자 함수를 통해 필요로하는 상태의 일부분만 가져옴으로써 불필요한 렌더링을 줄일 수 있다.

여기서 주의해야할 점이 하나 있다. store 또는 selector가 변경될 때 useEffect는 조금 늦게 실행되기 때문에 재구독될 때까지는 갱신되기 이전 상태 값을 반환한다. 따라서 최신 값을 반영하지 않을 가능성이 있다.

다행히 리액트 팀은 이러한 문제를 해결하기 위한 use-subscription이라는 공식적인 훅을 제공한다.

useSyncExternalStore

책에서는 use-subscription이라는 훅을 통해 상기한 문제를 해결하는 예제를 보여주는데, React18에서 useSubscription을 계승하는 useSyncExternalStore라는 훅을 제공한다. 따라서 직접 해당 훅을 이용한 예제로 변경해보았다.

const Component1 = () => {
  const state = useSyncExternalStore(
    store.subscribe,
    useCallback(() => store.getState().count, [])
  );

  const inc = () => {
    store.setState((prev) => ({ ...prev, count: prev.count + 1 }));
  };

  return (
    <div>
      <h1>Component1</h1>
      <p>Count: {state}</p>
      <button onClick={inc}>Increment</button>
    </div>
  );
};

위처럼 useStoreSlector 훅 대신 컴포넌트에서 useSyncExternalStore훅에 store의 구독 메서드와 선택자 함수를 넘겨주는 것으로 항상 최신 데이터임을 보장해주는 state를 가져올 수 있다.

05 _ 리액트 컨텍스트와 구독을 이용한 컴포넌트 상태 공유

앞서 전역 상태를 구현하기 위해 리액트 컨텍스트를 활용한 방법과 구독을 사용한 방법 두 가지를 알아보았다. 리액트 컨텍스트는 하위 트리마다 서로 다른 값을 제공할 수 있고 구독은 불필요한 리렌더링을 막을 수 있다.

  • 컨텍스트는 하위 트리에 전역 상태를 제공할 수 있고 컨텍스트 공급자를 중첩하는 것이 가능하다. 컨텍스트를 사용하려면 리액트 컴포넌트 생명 주기 내에서 useState와 같은 훅으로 전역 상태를 제어할 수 있다.
  • 반면 구독을 사용하면 단일 컨텍스트로는 불가능한 리렌더링 문제를 해결할 수 있다.

이제 하위 트리마다 다른 값을 가질 수 있고 불필요한 리렌더링을 피할 수 있다는 두 가지 이점을 모두 확보하기 위해 두 방식을 적절히 섞어볼 수 있다.

모듈 상태의 한계

모듈 상태는 리액트 컴포넌트 외부에 존재하는 전역으로 정의된 싱글턴이기 때문에 컴포넌트 트리나 하위 트리마다 다른 상태를 가질 수 없다는 한계가 있다.

따라서 여러 스토어로 격리하여 한 컴포넌트를 재사용할 수 없다는 문제가 존재한다. 예를들어 storecount라는 상태를 보여주는 Counter 컴포넌트가 있을 때, count2라는 상태를 새로운 store로 생성 후 컴포넌트로 보여주려면 Counter컴포넌트를 재사용하지 못하고 Counter2 컴포넌트를 새로 만들 수 밖에 없다는 것이다.

storeprops로 전달해주면 Counter 컴포넌트를 재활용할 수 있지 않을까 싶지만 컴포넌트가 깊게 중첩되면 prop drilling이 발생할 것이고, 모듈 상태를 소개한 주된 이유가 prop drilling을 피하기 위한 것이었기 때문에 안될 말이다.

이를 해결하기 위해 컨텍스트를 이용할 수 있다.

const Component = () => {
  <StoreProvider>
    <Counter />
  </StoreProvider>
}

const Component2 = () => {
  <StoreProvider2>
    <Counter />
  </StoreProvider2>
}

const Component3 = () => {
  <StoreProvider3>
    <Counter />
  </StoreProvider3>
}

컨텍스트를 활용하면 위와 같이 Counter 컴포넌트를 재활용하면서도 각기 다른 state를 가질 수 있다.

컨텍스트와 구독 패턴 사용하기

type State = { count: number; text?: string };

const StoreContext = createContext<Store<State>>(
  createStore<State>({ count: 0, text: "hello" })
);

createStore는 구독 패턴을 알아볼 때 사용했던 것을 그대로 사용한다.

const StoreProvider = ({
  initialState,
  children,
}: {
  initialState: State;
  children: ReactNode;
}) => {
  const storeRef = useRef<Store<State>>();
  if (!storeRef.current) {
    storeRef.current = createStore(initialState);
  }
  
  return (
    <StoreContext.Provider value={storeRef.current}>
      {children}
    </StoreContext.Provider>
  );
};

useRef를 통해 스토어 객체가 첫 번째 렌더링에서 한 번만 초기화되게 만들 수 있다.

이제 스토어 객체를 사용하기 위해 useSelector 훅을 구현해보자.

const useSelector = <S extends unknown>(selector: (state: State) => S) => {
  const store = useContext(StoreContext);

  return useSyncExternalStore(
    store.subscribe,
    useCallback(() => selector(store.getState()), [selector, store])
  );
};

이 패턴의 핵심은 useContext와 함께 useSyncExternalStore을 사용하는 것이다. 이를 통해 컨텍스트와 구독의 이점을 모두 누릴 수 있다.

다만 모듈 상태와 다르게 컨텍스트를 사용해서 상태를 갱신하는 방법을 제공할 필요가 있다.

const useSet State = () => {
  const store = useContext(StoreContext);
  return store.setState;
}

const selectCount = (state: State) => state.count;

const Component = () => {
  const count = useSelector(selectCount);
  const setState = useSetState();
  const inc = () => {
    setState((prev) => ({ ...prev, count: prev.count + 1 }));
  };

  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={inc}>Increment</button>
    </div>
  );
};

이제 위처럼 사용하면된다. 여기서 Component가 특정 스토어 객체에 연결돼 있지 않다는 점에 주목해야한다. Component는 다른 스토어와 사용할 수 있는 것이다.

function App() {
  return (
    <>
      <h1>Using default store</h1>
      <Component />
      <Component />
      <StoreProvider initialState={{ count: 10 }}>
        <h1>Using store provider</h1>
        <Component />
        <Component />
        <StoreProvider initialState={{ count: 20 }}>
          <h1>Using nested store provider</h1>
          <Component />
          <Component />
        </StoreProvider>
      </StoreProvider>
    </>
  );
}

위와 같이 여러 공급자와 함께 사용한 결과는 다음과 같다.

동일한 store 객체를 사용하는 컴포넌트는 동일한 count값을 보여주지만, 다른 컴포넌트 트리의 컴포넌트는 다른 store를 사용하므로 다른 count 값을 표시하는 것을 확인할 수 있다.

이렇게 컨텍스트와 구독을 함께 사용해 전역 상태를 구현하는 방법을 알아보았다. 컨텍스트로 하위 트리에서의 상태를 분리하고, 구독으로 리렌더링 문제를 피할 수 있었다.

이후의 내용은 지금까지 나온 컨텍스트와 모듈 등을 기반으로 설계된 여러 상태관리 라이브러리들과 그들의 특징을 알아본다. 해당 내용은 차후에 각 라이브러리들의 실제 프로젝트 적용 사례와 함께 더 깊게 살펴보겠다.

맺음말

해당 책을 통해 전역 상태관리를 구현하는 과정에서의 문제점들과 해결책에 대해 깊게 알아볼 수 있었다. 지금까지는 무작정 컨텍스트나 상태관리 라이브러리들을 이용했는데, 이들이 어떤 식으로 작동하는지, 또 왜 필요한지 공부해볼 수 있는 좋은 기회였다. 책의 흐름이 문제를 인식하고 해결해나가는 과정으로 작성되어 있어 직관적으로 이해하기 너무 좋았던 것 같다.

학습을 위해서 인터넷 자료들을 찾아보는 것도 좋지만, 이렇게 검증된 내용으로 작성된 도서를 읽는 것이 양질의 정보를 얻을 수 있는 좋은 방법이라는 것을 다시 한 번 체감했다.

profile
🐧

2개의 댓글

comment-user-thumbnail
2025년 1월 6일

이런 좋은 책을 선물해준 사람이 누구인지는 몰라도 멋진 사람일 것 같네요 ~ 😏

답글 달기
comment-user-thumbnail
2025년 1월 14일

이 남자한테 리액트 과외받고 싶다

답글 달기

관련 채용 정보