본문 바로가기

세미나

Redux와 Mobx 비교 체험기

728x90

안녕하세요. 유저인사이트 이현준입니다.

 

이 글의 주제

React에서 사용하는 상태 관리 라이브러리중 Redux와 Mobx의 개념을 이해하고 두 라이브러리의 비교해 보려고 합니다.

또한 Redux와 Mobx를 각각 사용해 본 경험을 토대로 느낀점을 말씀드리려고 합니다.

 

 목차

  1. State(상태) 란?
  2. 상태 관리 라이브러리가 필요한 이유
  3. Flux 디자인 패턴
  4. Redux기초
  5. Mobx기초
  6. Redux와 Mobx 비교 및 느낀점

 

1. State(상태) 란?


State는 컴포넌트의 렌더링 결과에 영향을 주는 데이터를 관리하는 객체입니다.

State는 컴포넌트 별로 관리되며 State가 변경되면 컴포넌트가 리렌더링 됩니다.

 

 

 

2. 상태 관리 라이브러리가 필요한 이유


프로젝트 규모가 커지면서 컴포넌트 내부의 데이터(state)를 자식 컴포넌트에 props를 통해 전달해야 하는 경우가 생깁니다.

하지만, 자식 컴포넌트는 전달받은 state를 사용해서 렌더링을 할 수는 있지만 state값을 직접 수정하는 작업은 할 수 없습니다. 자식 컴포넌트에서 전달받은  state값을 수정하기 위해서는 state를 변경하는 함수 역시 전달받아야 합니다.

 

그런데 컴포넌트의 수가 많아질수록 위와같은 작업을 수행해야 하는 횟수가 많아지고, 현재 컴포넌트에서 필요하지 않은 데이터라도 하위 컴포넌트에서 필요하여 데이터를 넘겨 받는 등 비효율적인 상황이 발생합니다.

 

prop drilling

 

이러한 컴포넌트간의 비효울적인 데이터 전달 과정을 prop drilling 이라고 하며, 컴포넌트 간에 공유되는 state가 어디서 생성, 변경되었는지 확인하기 어렵게 만드는 요소입니다.

 

이처럼 경우에 필요한 것이 바로 상태 관리 라이브러리 입니다.

 

 

 

3. Flux 디자인 패턴


Redux의 동작을 이해하기 위해서는 우선 Flux의 흐름을 이해할 필요가 있습니다.

 

Flux 디자인 패턴

 

Flex는 단방향 데이터 흐름을 통해 데이터의 변화를 예측하기 쉽게 만들어 시스템의 복잡도를 줄이는데 의의를 두고 있습니다. 위의 이미지 처럼 항상 Dispatcher를 통해 Store의 데이터를 변경합니다.

 

 

 

4. Redux 기초


간단한 코드를 구현하며 Redux의 기초를 배워보도록 하겠습니다.

 

1. Store 생성

 

import { configureStore } from '@reduxjs/toolkit'

export default configureStore({
  reducer: {}
})

 

우선 state를 관리하는 store를 생성합니다. 그런데 reducer이라는 생소한 용어가 하나 보입니다.

나중에 설명할 예정이니 우선은 store에 등록해야 하는 무언가 라고 생각하시면 될 것 같습니다.

 

2. Provider

 

import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import store from './app/store'
import { Provider } from 'react-redux'

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

 

React에서 Store을 사용하기 위해서는 위처럼 Provider에 Store를 등록할 필요가 있습니다.

Provider은 하위 컴포넌트에 props를 통해 스토어를 전달하므로 상위 컴포넌트에 두는 것이 좋습니다.

 

3. Slice 생성 (Reducer 생성)

Slice는 또 뭔가 싶으실 수 있습니다. 간단하게 설명드리면 State와 State에 접근 가능한 함수, 그리고 함수를 식별 가능하게 해주는 Action을 통합한 요소라고 말씀드릴 수 있습니다.

 

import { createSlice } from '@reduxjs/toolkit'

export const counterSlice = createSlice({
  name: 'counter',
  initialState: {
    value: 0
  },
  reducers: {
    increment: state => {
      state.value += 1
    },
    decrement: state => {
      state.value -= 1
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload
    }
  }
})

export const { increment, decrement, incrementByAmount } = counterSlice.actions

export default counterSlice.reducer

 

createSlice를 통해 State를 초기화 하고 reducer함수를 생성, Action을 등록하게 됩니다.

createSlice내부의 reducers는 state에 접근할 수 있는 reducer함수를 나타냅니다.

또한 reducer함수의 함수 명처럼 보이는 'increment, decrement, incrementByAmount'는 Reducer함수를 식별하는 Action입니다.

 

위의 createSlice를 통해 생성된 객체가 바로 Reducer입니다.

이게 무슨말인가 싶으시겠지만, createSlice는 기존의 Reducer생성, Action생성, Reducer에 Action등록 등의 복잡한 과정을 한번에 처리하기 위해 Redux ToolKit에서 지원해주는 함수이지 Slice라는 객체가 따로 있는것은 아닙니다.

 

 

즉, Reducer란 State를 저장하고, Action의 수행을 통해 state에 접근가능한 객체입니다.

4. Store에 Reducer등록

 

import { configureStore } from '@reduxjs/toolkit'
import counterReducer from '../features/counter/counterSlice'

export default configureStore({
  reducer: {
    counter: counterReducer
  }
})

 

생성해준 Reducer 이전에 생성한 Store에 등록하여 줍니다.

 

5. 컴포넌트에서 State에 접근

 

import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { decrement, increment } from './counterSlice'
import styles from './Counter.module.css'

export function Counter() {
  const count = useSelector(state => state.counter.value)
  const dispatch = useDispatch()

  return (
    <div>
      <div>
        <button
          aria-label="Increment value"
          onClick={() => dispatch(increment())}
        >
          Increment
        </button>
        <span>{count}</span>
        <button
          aria-label="Decrement value"
          onClick={() => dispatch(decrement())}
        >
          Decrement
        </button>
      </div>
    </div>
  )
}

 

React Redux Hook인 useSelector을 통해 state를 참조하고 있으며, useDispatch를 통해 Reducer에 Action을 넘겨줌으로써 State데이터에 접근하도록 구현되었습니다.

 

 

 

 

4. Mobx 기초


 

Mobx 프로세스

 

Mobx는 Redux와 마찬가지로 단방향 데이터 흐름을 가지는 Flux패턴을 따라 동작합니다.

다만 세세한 용어나 구현방식이 다르기 때문에 간단한 코드를 구현하며 Mobx의 기초를 배워보도록 하겠습니다.

 

1. Mobx 용어

코드를 구현하기 전에 Mobx에서 사용하는 기본적인 용어들을 알아보도록 하겠습니다.

  • observable : state를 저장하는 필드를 정의합니다.
  • action : state를 수정하는 setter함수입니다.
  • computed : state값이나 state로부터 계산된 값을 캐싱하여 가져오는 getter함수입니다.
  • flow : 외부의 데이터를 비동기로 가져오는 함수입니다.
  • reaction : observable state의 변경을 감지하고 그에 대한 trigger을 수행하는 개념

 

2. observable

state필드를 정의하는 observable에 대해 알아보겠습니다.

 

import { makeObservable, observable, computed, action, flow } from "mobx"

class Doubler {
    value

    constructor(value) {
        makeObservable(this, {
            value: observable,
            double: computed,
            increment: action,
            fetch: flow
        })
        this.value = value
    }

    get double() {
        return this.value * 2
    }

    increment() {
        this.value++
    }

    *fetch() {
        const response = yield fetch("/api/value")
        this.value = response.json()
    }
}

 

makeObservable함수를 사용하여 Class 내부의 필드와 함수들에 어노테이션(observable, computed 등...)을 달아줍니다.

이로 인해 value는 state로써 관리되고, 각각의 함수들도 computed, action, flow로써의 역할을 수행할 수 있습니다.

 

3. action

action에는 두가지의 특징이 있습니다.

 

하나는 action을 통해서만 state를 변경할 수 있다는 것입니다.

이는 Flux패턴에서 dispatcher을 통해서만 store에 접근가능한 것과 같은 개념으로, 이를 통해 코드에서 state 변경이 일어나는 위치를 명확히 확인 할 수 있습니다.

 

다른 하나는 action동작이 transaction내부에서 실행된다는 것입니다.

action실행 중의 중간값 또는 불완전한 값은 action이 완료될 때까지 observer가 인식하지 못합니다.

 

import { makeObservable, observable, action } from "mobx"

class Doubler {
    value = 0

    constructor(value) {
        makeObservable(this, {
            value: observable,
            increment: action
        })
    }

    increment() {
        this.value++
        this.value++
    }
}

 

위 코드의 increment action은 내부에서 state를 두번 변경하나 transaction내부에서 실행 되기에 중간 state는 observer가 확인이 불가능 합니다.

4. computed

computed는 state를 조회하는 getter함수로, state에 저장된 값을 캐싱하여 조회합니다.

 

import { makeObservable, observable, computed } from "mobx"

class OrderLine {
    price = 0
    amount = 1

    constructor(price) {
        makeObservable(this, {
            price: observable,
            amount: observable,
            total: computed
        })
        this.price = price
    }

    get total() {
        console.log("Computing...")
        return this.price * this.amount
    }
}

 

위처럼 computed 함수에서 state를 계산하여 조회하는 경우, state가 변경되지 않은 경우에는 이전에 계산된 캐싱값을 가져올 수 있습니다.

5. reaction

reaction은 state값의 변경을 감지하며 등록된 사이드 이펙트를 수행합니다. 이를 통해 state가 변경되었을 때 리렌더링을 수행 할 수 있습니다.

reaction을 autorun유틸리티를 사용하여 수행할 수 있습니다.

 

import { makeObservable, autorun } from "mobx"

class Count {
    value

    constructor(value) {
    	makeObservable(this, {
            value: observable,
            add: action,
        })
        this.value = value
    }
    
    add() {
    	this.value += 1
    }
}

const count = new Count(0);

autorun(() => {
	document.getElementById("count").innerText = count.value
})

for (let i = 0; i < 10; i++) {
    count.add()
}

 

위처럼 state의 변경을 감지하여 element의 텍스트를 변경하는 작업을 수행 할 수 있습니다.

 

 

 

6. Redux와 Mobx 비교 및 느낀점


이상으로 각각의 공식문서를 기반으로 한 코드들을 작성해 보았습니다.

기본적으로 state를 관리하는 개념 자체는 비슷하다고 느꼈습니다. 다만 Store관리나, state에 접근하는 방식에서 많은 차이를 느꼈던 것 같습니다.

 

1. Redux

Redux는 전역적으로 하나의 Store을 관리합니다. (Store내부에서 여러개의 등록된 Reducer에서 state를 관리)

컴포넌트에서 Store를 subscribe하거나, React Redux Hook를 통해 state의 변경을 감지할 수 있습니다.

 

2. Mobx

Mobx는 여러개의 Store를 관리 할 수 있으며, reaction 개념을 통해 state변경에 대한 사이드 이펙트를 수행 할 수 있습니다. 구조 설계가 쉬우며 이번에는 사용하지 않았지만 데코레이터를 통해 더욱 쉽게 구조 설계가 가능합니다.

 

느낀점

개인적으로는 Redux보다 Mobx가 더 이해하기 쉬웠습니다.

아키텍쳐 구조가 단순한 것도 있었지만, 특히 Class형태의 객체지향적으로 동작하여 이해가 더 쉬웠습니다.

 

또한 Mobx를 사용한 많은 사람들이 말하듯이 스프링 프레임워크와 유사한 아키텍쳐 구조로 동작하였습니다. Store <=> Service, Model <=> Entity, Repository <=> Repository 로 각각의 레이어가 매칭되는 구조이기에 스프링을 주로 개발하는 입장에서 Mobx를 이해하는데 많은 도움이 된 것 같습니다.

 

물론 Redux와 Mobx의 편의성을 지원해 주는 라이브러리들이 많기에 실제 업무에 적용하려면 더 많이 알아보아야 겠지만, 혹시나 다음에 React 프로젝트를 진행할 일이 있다면 Redux대신 Mobx를 사용하는 것도 고려해 볼만할 것 같습니다.

 

이상으로 글을 마치겠습니다. 감사합니다.

728x90