안녕하세요, 똑똑한개발자에서 프론트엔드 개발을 맡고 있는 심재철입니다.

오늘은 ContextAPI, Redux, Mobx를 비교하는 글을 써보려고 합니다.

Context API는 왜 안쓰나요?

상태 관리 라이브러리를 사용하지 않고 리액트로만 전역 상태를 관리하려면 Context API를 사용해야합니다.

아주 소규모의 서비스라면 Context API로도 충분합니다. 하지만 사이즈가 조금만 커져도 코드가 장황해지고 관리하기가 힘들어집니다.

그 이유는, 바로 성능떄문입니다.

예를들어서,

function UserProvider({ children }) {
  const [user, setUser] = useState(null);
  const PassToChild = { user, setUser };

  return <UserContext.Provider value={PassToChild}>{children}</UserContext.Provider>;
}

이런 유저 데이터를 관리하는 hooks가 있다고 해봅시다. 이때 위와 같이 코드를 작성하는것이 괜찮을까요? 그렇지 않습니다.

실제로 잘 동작하는 코드이지만, user를 소비하고 있지 않은 컴포넌트에서도 강제적으로 리렌더링이 일어나기 때문에 쓸데없는 리렌더링이 한번 더 일어납니다.

이게 무슨 말이냐면 UserProvider 컴포넌트로 감싸진 하위 컴포넌트에서 useContextUserContext를 소비하는 순간 그 안에 들어있는 user를 실제로 쓰던말던 무조건 리렌더링이 일어난다는 말입니다.

그래서 user를 실제로 사용하는곳에만 전달해주기 위해서 user상태만 갖고 있는 컨텍스트를 별도로 만들어줘야합니다.

따라서 위 코드를 성능문제 없게 하려면 아래와 같이 Context를 두개나 만들어줘야합니다.

function UserProvider({ children }) {
  const [user, setUser] = useState(null);
  return (
    <UserContext.Provider value={user}>
      <UserUpdateContext.Provider value={setUser}>{children}</UserUpdateContext.Provider>
    </UserContext.Provider>
  );
}

유저 상태를 갖고 있는 UserContext
유저 상태를 업데이트하는 UserUpdateContext

두개나 작성해야 한다는것입니다.

이 말은 Context API로 로직을 작성할 경우 엄청나게 많은 Context가 생긴다는것을 의미하고 코드가 장황해집니다. 따라서 저는 Context API를 추천하지 않는 편입니다.

Context Provider로 제공하는 값들을 useContext로 소비하는 모든 컴포넌트가 리렌더링 됩니다.

그럼 Client Global State는 어떻게 관리하는게 좋을까요?

우선 Client Global State란 쉽게 말해 API호출을 통해 가져온 값이 아닌 리액트 내에서 전역적으로 공유되어야 하는 상태를 의미합니다.

예를들어서, 카운터가 몇번 클릭됬는지를 여러 컴포넌트에서 공유하고 싶을때 count값을 Client Global State로 관리할 수 있겠죠.

Context API는 여전히 성능 문제를 갖고 있으므로, 저는 MobxRedux를 사용하는것을 추천드립니다.

상태관리라이브러리 비교

그럼 API호출은 어디서 해야 하나요?

useEffect

가장 간단한 방법은 React에서 제공하는 useEffect를 사용하여 API호출을 하는 방식입니다.

하지만 이렇게 되면 컴포넌트 내부에 비즈니스로직UI로직이 섞이게 되어 컴포넌트가 커지고 유지보수에 악영향을 미칩니다. 그래서 이런 방법은 추천하지 않습니다.

Redux MiddleWare

대신, Redux를 사용하는 경우 Redux Saga, Redux Thunk등과 같은 Redux MiddleWare를 붙여서 사용할 수 있습니다. 하지만 Redux자체가 보일러플레이트 코드가 굉장히 긴 편이고 FSA(Flux Standard Action)구조를 학습해야하기 때문에 러닝커브가 큰 편입니다.

하지만 전세계 개발자의 50%정도가 리덕스를 사용할 만큼 풀이 크기 때문에 한번 학습하시는걸 추천드립니다. (내가 배우기 싫어도 남이 짠 코드를 유지보수해야하는 일이 반드시 옵니다.)

Mobx Store

Redux가 학습하기 어렵다면 상대적으로 러닝커브가 낮은 Mobx를 추천합니다.

컴포넌트에 observer를 달아주고 그 내부에서 사용되는 observable state(옵저버로 관찰중인 상태)의 변화가 감지되면 해당 컴포넌트를 리렌더링 시켜주는 라이브러리입니다. 마치 스타크래프트에서 옵저버로 적군(컴포넌트)의 움직임을 감지하듯이 할 수 있습니다.

Mobx를 썼을때 장점은 Redux보다 적은 코드를 작성해도 비슷한 효과를 낼 수 있다는것입니다. 물론 Redux측에서도 이런 문제를 알고 있는지 최근 Redux Toolkit이라고 하는 Redux 추상화 버전의 라이브러리가 나오긴 했지만 아직은 Mobx보다는 코드가 긴 것 같습니다.

그래도 진짜 코드가 많이 줄었으니까 앞으로는 Redux를 하실때 거의 무조건 Redux Toolkit을 사용하는것을 추천드립니다.

Mobx를 사용하면 그냥 Class의 Method내부에서 API를 호출하고 그 결과를 저장 한 다음, 여러 파생값들을 동시에 만들어내는 마법같은 일이 가능합니다.

Mobx vs Redux

두 라이브러리를 모두 사용해본 개인적인 의견으로는 적당한 규모까지는 Mobx에 손을 들어주고 싶고 큰 규모는 여전히 Redux가 더 좋다고 느껴졌습니다.

Mobx의 마법은 규모가 작을때는 좋습니다. 알아서 이것저것 다 해주기 때문이죠. 근데, 규모가 커지면 이런 마법이 오히려 디버깅을 힘들게 만들때가 있습니다.
이 값이 도대체 어디서 업데이트 됬는지 추적하기가 힘들어질 수 있다는 말입니다.

@actions을 통하지 않고 Mobx Store의 Observable State값을 여기저기서 직접 업데이트 할 경우,
해당 Observable State가 도대체 어디서 업데이트 된건지 찾는데 굉장히 오랜 시간이 걸릴 수 있습니다.

@actions을 사용해서 상태를 업데이트한다고 하더라도, 여러 액션들이 거의 동시에 하나의 observable state를 변경하는 경우 정확히 어떤 action에 의해 값이 변경됬는지 파악하기 힘들 수 있습니다.

또한, observable state의 변경에 의한 computed, autorun들의 개수가 많아지면 많아질수록 업데이트 추적이 어려워집니다.

반면 Redux의 경우 아래와 같이 데이터의 흐름이 일방향이고 중간 마법이 없습니다.

Action Dispatch -> Redux MiddleWare -> Reducer -> Redux Store Update -> Component에서 useStore로 Redux Store값 사용

또한 강력한 리덕스 개발자 도구 덕분에 시간여행이 가능합니다.

액션 하나하나가 Dispatch 되어 Store에 반영됬을때 UI가 어떤 상태였는지를 확인해 볼 수있는것이죠.

저의 라이브러리 선택 기준은 아래와 같습니다.

프로젝트의 규모가 크고 유지보수를 길게 할 것 같으면 Redux를 사용합니다.

감사합니다!

jaecheol.sim's profile image

jaecheol.sim

2021-04-28 17:00

Read more posts by this author