덴티움은 상태관리를 어떻게 하고 있을까요?

profile
FE Developer - 루카스
April 25, 2023
GPT 요약
thumbnail

안녕하세요.
덴티움의 FE Developer 루카스입니다.

먼저 DENTECH TALK 시리즈의 첫 글을 작성하게 되어 정말 기쁘게 생각합니다.

덴티움의 기술 블로그를 통해 저희가 어떤 기술을 사용하고 있는지, 어떤 방식으로 개발을 하고 있는지 알려드리고자 DENTECH TALK 시리즈를 기획하게 되었습니다. 부족한 글이지만 많은 관심 부탁드립니다.

MobX의 이해

프론트엔드에서 빼놓을 수 없는 것이 상태관리입니다. 덴티움에서는 MobX를 사용하여 상태관리를 하고 이를 통해 개발 생산성을 높이고 있습니다.

왜 MobX와 같은 상태관리가 필요할까요?

두 가지 관점에서 필요성을 말씀드리고자 합니다.

  • 상태관리
  • MobX와 같은 상태관리

상태관리는 왜 필요할까요?

React로 개발을 하다 보면 작은 단위 컴포넌트를 조립해가며 개발을 하게 됩니다. 작은 컴포넌트를 모아서 조금 더 큰 컴포넌트를 만들고, 조금 큰 컴포넌트를 조립하여, 더 큰 컴포넌트를 만듭니다. 특히 이러한 특징이 두드러지는 패턴이 atomic 디자인 패턴입니다.

atomic 디자인 패턴을 사용하다 보면 필연적으로 props drilling에 대해 고민할 수밖에 없게 됩니다. 지금 진행 중인 프로젝트 또한 atomic design 패턴으로 개발 중이고 자연스럽게 props drilling에 대해 고민하게 되었습니다.

atomic design props drilling

figure 1. atomic design props drilling

props drilling 현상은 컴포넌트 간 데이터 공유의 문제가 생긴 현상입니다. 이와 같은 데이터 공유 문제를 해결하기 위해 상태를 관리할 필요성이 생기게 됩니다.

MobX와 같은 상태관리는 왜 필요할까요?

상태 관리 도구를 이용하면 얻을 수 있는 이점 중 하나는 가독성을 높일 수 있다는 점입니다. 컴포넌트를 개발하다 보면 다음과 같이 복잡한 비즈니스 코드를 작성하게 됩니다.

function App() {
  const [users, setUsers] = useState([]);
  const [posts, setPosts] = useState([]);
  useEffect(() => {
    async function getFilteredPosts() {
      const response = await axios.get('https://example.com/posts');
      const posts = response.data;
      const filteredPosts = posts.filter((post) => {
        const postDate = moment(post.date);
        const currentDate = moment();
        const isThisWeek = postDate.isSame(currentDate, 'week');
        const hasEnoughLikes = post.likes >= 5;
        return isThisWeek && hasEnoughLikes;
      });
      setPosts(filteredPosts);
    }
    getFilteredPosts();
  }, []);
  function getUserPosts(userId) {
    return posts.filter((post) => post.userId === userId);
  }
  return (
    <div>
      <UserList users={users} getUserPosts={getUserPosts} />
      <PostList posts={posts} />
    </div>
  );
}

위 코드를 보면 useEffect 훅 안에 복잡한 비즈니스 로직이 존재합니다. 복잡한 비즈니스 로직 때문에 이 컴포넌트가 어떤 역할을 하는 컴포넌트인지 쉽게 파악되지 않습니다.

비즈니스 로직을 걷어낸다면 어떨까요?

function App() {
  useEffect(() => {
    store.fetchPostsAndFilteredPosts();
  }, []);
  function getUserPosts(userId) {
    return store.posts.filter((post) => post.userId === userId);
  }
  return (
    <div>
      <UserList users={store.users} getUserPosts={getUserPosts} />
      <PostList posts={store.posts} />
    </div>
  );
}
export default observer(App);

이처럼 가독성이 좋은 형태의 컴포넌트를 얻을 수 있습니다. 또한, 컴포넌트에서 분리한 비즈니스 로직은 UI에 독립적으로 두어 테스트하기 쉬워집니다.

MobX는 이러한 과정이 쉽습니다. 특히 redux와 비교하면 보일러플레이트(boilerplate)가 적어 작성하고, 읽어야 하는 코드가 적습니다.

즉, MobX는 쉽게 UI와 상태를 디커플링(decoupling)할 수 있도록 도와주는 도구입니다.

이는 MobX의 철학과 맞닿아 있습니다.(Mobx에 대하여)

MobX는 어떤 방식으로 상태관리를 할까요?

엑셀을 보면 MobX의 동작원리가 보인다?

MobX의 동작원리를 간단하게 본다면 엑셀의 함수와 비슷합니다. 다음은 엑셀에서 SUM 함수를 이용하여 A1 값과 B1 값을 더하는 수식을, C1에 선언한 모습입니다.

SUM함수를 구현한 Excel

figure 2. SUM함수를 구현한 Excel

A1 값과 B1 값의 변화가 C1에 값을 자동으로 만들어 냅니다.

MobX 상태 변경 흐름도

figure 3. MobX 상태 변경 흐름도

MobX 상태 변경 흐름도 - MobX 공식문서

이 과정 자세하게 설명하자면, 사용자의 키보드 입력(event)으로 action이 발생하고 action은 A1이나 B1 셀의 값(observable)을 업데이트합니다. 업데이트된 값(observable)에 의해 C1셀의 값이 변경(computed)되고, 그 값을 표시(rendering)합니다.

위 과정을 간단한 MobX 코드를 통해 확인해보겠습니다.

  • 결과화면
MobX 상태 변경 흐름도

figure 4. MobX 상태 변경 흐름도

  • Store 클래스
class Store {
  a1 = 1;
  b1 = 2;
  constructor() {
    makeObservable(this, {
      a1: observable,
      b1: observable,
      changeA: action,
      changeB: action,
      c1: computed,
    });
  }
  get c1() {
    return this.#sum();
  }
  #sum() {
    return Number(this.a1) + Number(this.b1);
  }
  changeA(value) {
    this.a1 = value;
  }
  changeB(value) {
    this.b1 = value;
  }
}
const store = new Store();

우선 생성자 부분부터 확인해보겠습니다. 생성자 부분은 observable, computed, action을 정의하는 부분입니다.

Contsructor

figure 5. Contsructor

makeObservable 이라는 함수를 통해, 각 필드와 메소드의 역할을 정의합니다. 필드 a1과 b1은 observable로 역할을 정해주고, c1은 computed로, 메소드 changeA와 changeB는 action으로 정의했습니다.

Computed

figure 6. Computed

computed의 역할로 정해진 c1은 a1과 b1의 값을 더해서 출력하는 #sum 함수를 통해 값을 반환하고 있습니다. getter c1은 엑셀에서 c1 셀에 정의되어 있는 함수와 같은 역할을 하게 됩니다.

이제 store를 사용할 컴포넌트를 정의할 차례입니다.

  • App 컴포넌트
function App() {
  return (
    <div className="app">
      <div className="cell">
        <input
          className="input-box"
          value={store.a1}
          onChange={(e) => {
            store.changeA(e.target.value);
          }}
        />
      </div>
      <div className="cell">
        <input
          className="input-box"
          value={store.b1}
          onChange={(e) => {
            store.changeB(e.target.value);
          }}
        />
      </div>
      <div className="cell result">{store.c1}</div>
    </div>
  );
}
export default observer(App);

React에서 store의 변화를 인지하기 위해서는 observer메소드를 통해 컴포넌트를 감싸주기만 하면 됩니다. input의 onChange를 통해 값이 변경될 때마다 changeA, changeB(action)를 호출하여 observable 값인 a1과 b1을 변화시키고, 변경사항이 c1(computed)에 반영되어 화면에 나타나게(rendering, side-effect) 됩니다. 이 모든 과정이 자동으로 발생하게 됩니다.

MobX의 장점

Easy to use, scalable state management(사용하기 쉽고 확장 가능한 상태 관리)

MobX는 다루기 쉽고, 배우기 쉽다는 것이 큰 장점이라고 생각합니다. 다루기 쉽다는 점에서 상태관리를 위해 필드와 메소드에 역할을 부여하고 인지할 수 있는 스코프 내(observer를 감싼 컴포넌트)에서 필드의 참조 혹은 메소드 호출만 해준다면 모든 할 일이 끝난 것입니다. 그 외 모든 것은 MobX가 알아서 처리합니다.

또한, 새로운 팀원이 프로젝트에 빠르게 적응하는 것에 있어서도 MobX는 큰 도움이 될 것이라고 생각합니다. 쉽고 낮은 러닝커브(Learning Curve)로 인해 프로젝트에 새로 합류한 팀원이 빠르게 프로젝트에 적응할 수 있고, 프로젝트의 코드를 빠르게 이해할 수 있을 것입니다.

최근 고민거리

현재는 스토어를 어떻게 분리할 것인가에 대해 고민을 하고 있습니다. 현재 상태가 페이지에 의존적이라고 생각했기 때문에 상태를 관리해야 할 책임이 있는 스토어는 각 페이지 별로 분리되어야 한다고 생각했습니다. 하지만 페이지별로 분리하게 되면 같은 도메인을 공유하는 페이지의 경우, 비즈니스 로직을 페이지별 스토어에서 각각 구현해야 하므로 코드 중복이 발생하고, 로직을 재활용할 수 없게 됩니다. 현재 MobX의 공식문서에서도 도메인 객체를 관리하는 스토어와 UI에 종속된 스토어를 분리하여 관리하도록 권장하고 있습니다. (MobX 데이터 스토어의 정의)

이 문제는 현재 팀내에서 논의 중이며, 추후 결정한 내용을 경험담을 통해 공유하겠습니다.

부족한 글이지만 끝까지 읽어주셔서 감사합니다.

profile
안녕하세요 👏
FE Developer 루카스입니다.