avatar
mushroombud

GetX 쓰다가 Redux 새로 배우기

Flux Architecture에 대한 이해를 바탕으로 Redux의 철학, 작동방법에 대해 탐구해보자
Sep 27
·
9 min read

React에서 전역 상태관리는 뗄레야 뗄 수 없는 부분이다. 동적 웹이 대두된 이래 프론트엔드에서 이만큼 핫한 주제가 있을까 싶은데, Flutter로 프론트엔드를 처음 접한 나로서는 React 생태계의 상태관리를 보고 신기한 부분이 많았다. 일단 Flutter 자체가 나중에 나온 만큼 기존에 있었던 프레임워크들에서 괜찮아 보이는 요소들을 짬뽕해서 집어넣고 생산성을 극한까지 올려준 프레임워크인데, 상태관리 부분은 React에서 좋다는 것만을 취사선택했다고 느꼈다.

취미생 정도 수준으로 GetX를 써오던 입장에서 솔직히 상태에 대한 깊은 이해는 필요가 별로 없었다. GetX가 워낙 잘 만들어져 있기도 하고, 엄청나게 쉬운 편이기 때문이다. 그냥 '상태'라는 Variable을 하나 두고, 이 Variable을 바꿀 수 있는 규칙을 지정해준 다음 규칙대로만 하면 상태를 관찰하던 Stateful Widget이 알아서 자신을 리렌더링한다. 얼마나 간단한가?

class SimpleCounterController extends GetxController {
  var count = 0.obs;

  void increase() => count++;
  void decrease() => count--;
}

class SimpleCounter extends StatelessWidget {
  final SimpleCounterController controller = Get.put(SimpleCounterController());

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        IconButton(
          icon: Icon(Icons.remove),
          onPressed: controller.decrease,
        ),
        Obx(() => Text('Count: ${controller.count}')),
        IconButton(
          icon: Icon(Icons.add),
          onPressed: controller.increase,
        ),
      ],
    );
  }
}

출처: CherrieSSH의 블로그

아니 until blog는 Dart syntax를 따로 지원을 안하나보다. 플러터 개발자 슬프니까 빨리 만들어주셈

코드를 슬쩍 보면 onPressed일 때 controller.decrease를 통해 상태를 조작하게 되어 있다. 그리고 Obx 를 이용하면, State가 변할 때마다 위젯 리렌더링을 진행한다.

그렇다면 지금까지 Flutter을 통해 학습할 수 있었던 내용을 요약한다면,

Stateful해야 하는 기능들이 있다. 이것들은 "상태(State)"가 변화하면 자동으로 리렌더링된다.

전역 State를 관리하는 공간을 정해두고 State 내의 Attribute를 어떻게 Manipulate할지 규칙을 둔 뒤 그대로 수행하면, 이를 관찰하고 있다가 (Observer) 리렌더링을 바로 처리한다.

이게 다이다. 그런데 React로 와서 가장 많이 쓰이는 상태관리 라이브러리인 Redux를 훑다 보니 전역 상태관리에 대해서 왜 이렇게 생겨났는지를 더 깊이있게 알게 되었다.

만악의 근원은 React는 기본적으로 Child Component의 정보를 Parent Component에 전달할 수가 없다는 점이다. 그래서 Parent Component가 Child로 Props를 끝도 없이 전달하는 문제가 발생한다.

import React, { useState } from 'react';

// 최상위 컴포넌트
function App() {
  const [user, setUser] = useState({ name: "Steve" });

  return (
    <div>
      <Navbar />
      <MainPage user={user} />
    </div>
  );
}

// MainPage 컴포넌트
function MainPage({ user }) {
  return (
    <div>
      <h3>Main Page</h3>
      <Content user={user} />
    </div>
  );
}

// Content 컴포넌트
function Content({ user }) {
  return (
    <div>
      <Message user={user} />
    </div>
  );
}

// Message 컴포넌트
function Message({ user }) {
  return <p>Welcome {user.name}</p>;
}

export default App;

출처: 멋쟁이사자처럼 상태관리 세미나 자료 이만큼 문제점을 잘 표현한 자료가 없다

따라서 전역 변수와 같이 "전역 상태"의 도입 필요성이 생겨나게 되고, 그렇다면 전역 상태를 보관하는 공간과 이 안에 들어있는 전역 상태 Attribute들에 대한 제약조건이 필요하다. 이때 등장하는 개념이 Flux Architecture이다.

Flux Architecture

until-1576

Data Flow가 단방향인 것이 핵심이다. State가 Observer인 Controller-Views를 통해 View를 변화시켜야 하고, 그 반대는 안된다. Stores에 저장되는 데이터(State Attribute)를 변경하는 규칙(Dispacher)은 미리 작성되어 있어야 하고, 규칙 외의 변화는 불허한다.

Redux

Flux Architecture을 구현한 Redux의 3원칙을 살펴보자.

  • Single source of truth

  • State is read-only

  • Changes are made with pure functions

Single Source of truth야 Store을 하나만 두겠다는 뜻이고, Read-only개념도 Flux Architecture 그대로이다. Pure Function에 대해서만 조금 더 알아보면, "같은 입력값에 대해 항상 같은 결과값을 반환 & 입력값을 변화시키지 않음" 조건을 만족하는 함수이다. 비슷한 개념으로 CRUD에서 Read, Update, Delete는 계속 실행해도 결과값이 똑같으나 Create의 경우 계속 실행하면 문서가 끝없이 생성되는 것이 있다. return a+b는 가능하나, return ++a, ++b는 안 된다. (입력값에 변화를 줌)

역시나 나는 공식 Docs를 좋아하는 척을 하므로, Docs를 통해 자세히 알아보자.

출처: Redux Quick Start

import { configureStore } from '@reduxjs/toolkit'

export default configureStore({
  reducer: {},
})

npm에서 Redux를 가져온 뒤 일단 Store을 하나 생성해준다.

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

// As of React 18
const root = ReactDOM.createRoot(document.getElementById('root'))

root.render(
  <Provider store={store}>
    <App />
  </Provider>,
)

아주 간단한 React Component에다가 store을 끼워넣는다. 이제 Store 내에 있는 State를 변화시켜 보자.

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 또는 createReducer을 사용한다. createSlice가 더 최신식의 편리한 기능이고, 두 가지의 차이점은 아래 표와 같다.

until-1577

코드에서는 정말 간단하게 counter이라는 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>
  )
}

여기서 중요한 점은 useSelector이다. 좀 독특하다 싶은 기능은 몽땅 훅으로 구현하는 React답게 (Hook이라도 없었으면 그냥 JS 덩어리 수준인듯) useSelector 훅을 사용해서 우리의 타깃 State에 "Subscription"을 하고, 유튜브 구독하듯 보고 있다가 State Change가 일어나면 바로 리렌더링을 날려준다. Redux가 상태에만 집중하면 나머지는 React가 알아서 하는 셈.

지금까지 간단하게 Flux Architecture과 Redux에 대해 알아봤다.

오늘의 결론: 생산성 면에서 Flutter + GetX 조합을 이길 게 있을까?







system.out.println("코드치는버섯")