(Redux) 상태 트리 설계에 대한 고민

호두파파·2021년 4월 27일
0

Redux

목록 보기
2/6

상태 트리 설계에 대한 고민

리덕스는 리듀서를 트리 구조로 분리함으로써 상태 관리에 대한 책임을 분산시킨다. 분리한 상태는 combineReduce 함수로 조합해 최사우이 리듀서가 시스템의 단일 상태트리로 조합한다. 이 지점에서 시스템의 상태 구조가 드러난다.

리듀서를 넢은 깊은 트리구조로 만들면 리듀서 결합이 복잡해지고, 상태의 흐름이 잘 안 읽힌다. 그래서 최대한 평탄한(flat) 구조로 만들어야 한다. 하지만 너무 평탄하면 상태 간의 관련성을 가독성 있게 표현하기 어렵다. 어느 지점에서 균형을 잡아야 한다.상태 트리 구성과 분류 기준을 선택하는 일은 리덕스 시스템의 큰 틀과 방향을 결정하는 중요한 의사결정이다.

import { combineReducers } from "redux";
import { routerReducer } from "react-router-redux";

import toast from "./toast/reducer.js";
import toolbar from "./toolbar/reducer.js";
import modalDialog from "./modal-dialog/reducer.js";
import components from "./components/reducer.js";
import suggest from "./suggest/reducer.js";
import metaData from "./meta-data/reducer.js";

export default combineReducers({
    toast,
    toolbar,
    modalDialog,
    components,
    suggest,
    metaData,
    routing: routerReducer
});

너무 깊은 구조를 만들지 말라는 걸 중첩 JSON 구조로 데이터를 정의하지 말라는 걸로 오해하면 곤란하다. 단지 리듀서를 지나치게 중첩하지 말라는 소리다. 리듀서가 난립하면 결합한 상태 트리의 모습을 에측하기 어려워진다.

그렇다면 상태 트리는 어떠한 기준으로 설계를 하는 것이 좋을까? 처음에는 UI를 기준으로 상태 트리를 구성해볼 수 있겠다. 헤더가 있고, 푸터가 있고, 메뉴별로 상태 트리를 만들면 어떨까?

{
    header: {},
    navigator: {},
    contents: {},
    footer: {}
}

UI 단위로 상태 트리를 구성하면 UI와 리듀서가 강하게 묶인다. 어떤 상태를 상태 트리에 추가하면 그 상태를 관리하는 리듀서가 있어야 한다. 반대 역시 마찬가지다. 결국 UI와 리듀서를 함께 끌고 다녀야 한다. UI는 변경이 잦은 부분이라 불안정한 UI와 리듀서를 엮어 버리면 리듀서 역시 불안정해진다.

기능을 기준으로 상태 트리 설계하기

기능 단위로 리듀서를 설계하면 UI와 리듀서의 관계를 약하게 만들 수 있고, UI의 변경이 미치는 파급효과를 줄일 수 있다. 상태 트리를 보고 시스템이 제공하는 기능을 유추할 수 있다는 점은 긴으 단위로 상태 트리를 설계했을때 얻을 수 있는 장점이다.

{
    toolbar : {
        isShown: false
    },
    components : {
        order: ["comp_1", "comp_2"]
        compMap :{
            "comp_1": {
                ui : {},
                doc : {}
            },
            "comp_2": {
                ui : {},
                doc : {}
            },
        }
    },
    metaData : {
        document : {}
        release : {}
    }
}

상태 트리를 기능 단위로 정리했다면, 액션은 사용자의 행위를 기준으로 정리한다. 사용자가 수행하는 하나의 동작은 다수의 기능과 결합할 수 있다. 따라서 너무 당연한 이야기지만 액션과 리듀서의 관계는 1:N이다.

리덕스를 지탱하는 두 가지 리액트 컴포넌트

리덕스를 접할 때 애플리케이션의 '모든 상태'를 리듀서에서 관리하고, 컴포넌트 자체는 무상태로 만들어야 한다고 고민하는 경우를 종종 접할 수 있다. 줄여 이야기하자면, 이는 명백한 오해다. 리덕스는 모든 상태를 리듀서, 즉 스토어에서 관리할 것을 강제하지 않는다. 컴포넌트는 지역 상태를, 리듀서는 전역 상태를 관리할 수 있고 그래야 효율적이고 유연한 애플리케이션을 만들 수 있다.

우선 표현 컴포넌트와 컨테이너 컴포넌트를 이해해야 한다.

  • 표현(Presentational) 컴포넌트
    표현 컴포넌트는 리덕스 시스템과 별개인 컴포넌트로 화면에 컴포넌트를 어떻게 렌더링할지를 결정하고, 사용자의 요청을 이벤트로 받아 상위 컴포넌트로 전달하는 역할만을 수행한다.
import React, { PropTypes } from 'react'

const Link = ({ active, children, onClick }) = {
  if (active) {
    return <span>{children}</span>
  }

  return (
    <a href="#"
      onClick={e = {
        e.preventDefault()
        onClick()
      }}>

      {children}
    </a>
  )
}

Link.propTypes = {
  active: PropTypes.bool.isRequired,
  children: PropTypes.node.isRequired,
  onClick: PropTypes.func.isRequired
}

export default Link
  • 컨테이너(Container) 컴포넌트
    컨테이너 컴포넌트는 표현 컴포넌트와 리덕스 시스템 사이의 연결 고리로 하위 컴포넌트에서 올라오는 요청을 해석해 시스템의 어느 쪽으로 요청을 전달(mapDispatchToProps)할 것인지를 결정하고, 전체 시스템의 상태 트리를 스토어로부터 넘겨받아 하위 컴포넌트로 전달(mapStateToProps)하는 책임을 수행한다.
import { connect } from 'react-redux'
import { setVisibilityFilter } from '../actions'
import Link from '../components/Link'

const mapStateToProps = (state, ownProps) = {
  return {
    active: ownProps.filter === state.visibilityFilter
  }
}

const mapDispatchToProps = (dispatch, ownProps) = {
  return {
    onClick: () = {
      dispatch(setVisibilityFilter(ownProps.filter))
    }
  }
}

const FilterLink = connect(
  mapStateToProps,
  mapDispatchToProps
)(Link)

결국 컨테이너라는 존재에 의해 컨테이너가 아닌 모든 컴포넌트는 리덕스에 얽매이지 않는, 독립적인 개체로서 자율성을 가질 수 있는 셈이다. 필요한 지점에서 컨테이너를 이용해 컴포넌트를 리덕스 시스템과 결합하면 된다.


출처

원문보기

profile
안녕하세요 주니어 프론트엔드 개발자 양윤성입니다.

0개의 댓글