본문 바로가기

FrontEnd

제어의 역전(IOC : Inversion of Control) in React

반응형

 

원문 보기

사용 중인 루틴에 기능을 추가해달라는 요청을 받게 되면?

리액트 컴포넌트는 props를 추가하고,

리액트 훅은 argument를 추가할 것이다.

구현 로직은 점점 복잡해진다.

 

이같은 조치는 다음 문제들을 야기하기 쉽다

  • 성능 이슈 : 코드 사이즈 증가
  • 유지보수 문제 : 한가지 함수가 여러 기능을 하면 수정 유발 원인이 많다.
  • 구현의 어려움 : 조건문은 자주 다른 조건들과 같이 바뀜. 수정 포인트가 많이 발생함.
  • API의 복잡성 : props, argument, option 등의 플래그가 너무 많으면, 나중에 안쓰이는게 분명 발생함. 문서화도 어려움
변화에 최적화하라

Inversion Of Control

API는 추상화를 이용하여 로직을 적게 구현하고,

대신 사용자의 코드를 실행한다.

플래그를 이용해 filter 함수를 구현한다고 가정하자

// let's pretend that Array.prototype.filter does not exist
function filter(
  array,
  {
    filterNull = true,
    filterUndefined = true,
    filterZero = false,
    filterEmptyString = false,
  } = {},
) {
  let newArray = []
  for (let index = 0; index < array.length; index++) {
    const element = array[index]
    if (
      (filterNull && element === null) ||
      (filterUndefined && element === undefined) ||
      (filterZero && element === 0) ||
      (filterEmptyString && element === '')
    ) {
      continue
    }

    newArray[newArray.length] = element
  }
  return newArray
}

filter([0, 1, undefined, 2, null, 3, 'four', ''])
// [0, 1, 2, 3, 'four', '']

filter([0, 1, undefined, 2, null, 3, 'four', ''], {filterNull: false})
// [0, 1, 2, null, 3, 'four', '']

filter([0, 1, undefined, 2, null, 3, 'four', ''], {filterUndefined: false})
// [0, 1, 2, undefined, 3, 'four', '']

filter([0, 1, undefined, 2, null, 3, 'four', ''], {filterZero: true})
// [1, 2, 3, 'four', '']

filter([0, 1, undefined, 2, null, 3, 'four', ''], {filterEmptyString: true})
// [0, 1, 2, 3, 'four']

kent는 경우의 수가 25개라 했는데 16개 아닌가? (이항급수 k=4)

외부로 제공하는 api의 useCase(flag)가 증가하거나 감소하는 경우 아래와 같은 문제들을 야기한다.

- 사용하지 않게 된, 사용하지 않은 flag의 testCase관리

- 기능을 추가하거나, 삭제할 경우 테스트의 어려움 (경우의 수가 지금만 16개)

 

이미 우리는 해당 문제 해결 방법을 알고있다.

API를 사용하는 곳으로 로직 구현을 넘겨라 : 사용자의 코드를 파라미터로 받아 대신 실행한다.

이를 의존성 주입을 통한 Inversion Of Control(코드 대신 실행)이라 한다.

// let's pretend that Array.prototype.filter does not exist
function filter(array, filterFn) {
  let newArray = []
  for (let index = 0; index < array.length; index++) {
    const element = array[index]
    if (filterFn(element)) {
      newArray[newArray.length] = element
    }
  }
  return newArray
}

filter(
  [0, 1, undefined, 2, null, 3, 'four', ''],
  el => el !== null && el !== undefined,
)
// [0, 1, 2, 3, 'four', '']

filter([0, 1, undefined, 2, null, 3, 'four', ''], el => el !== undefined)
// [0, 1, 2, null, 3, 'four', '']

filter([0, 1, undefined, 2, null, 3, 'four', ''], el => el !== null)
// [0, 1, 2, undefined, 3, 'four', '']

filter(
  [0, 1, undefined, 2, null, 3, 'four', ''],
  el => el !== undefined && el !== null && el !== 0,
)
// [1, 2, 3, 'four', '']

filter(
  [0, 1, undefined, 2, null, 3, 'four', ''],
  el => el !== undefined && el !== null && el !== '',
)
// [0, 1, 2, 3, 'four']

해당 방법을 통해 나아진 점

- 사용자가 filter조건을 직접 정의하여 API 개발자의 플래그 지원 불필요

- filter 테스트는 filterFn이 true/false일 경우만 하면 됨

 

사용자 : 기존  API보다 쓰기 어려운데요?

기존 API를 wrapping해서 filter 함수와 동시 제공하면 된다.

개발자가 의존성 주입의 혜택을 누리면 되는 것이다.

function filterWithOptions(
  array,
  {
    filterNull = true,
    filterUndefined = true,
    filterZero = false,
    filterEmptyString = false,
  } = {},
) {
  return filter(
    array,
    element =>
      !(
        (filterNull && element === null) ||
        (filterUndefined && element === undefined) ||
        (filterZero && element === 0) ||
        (filterEmptyString && element === '')
      ),
  )
}

 

실전 사례 : javascript

map, reduce, filter등 Array.prototype의 메서드

 

실전 사례 : React

컴파운드 컴포넌트 패턴

버튼을 눌러 렌더링 여부를 결정하고, 상품 목록을 나열해 해당 상품의 상세 팝업을 보여주는 컴포넌트를 개발한다 하자.

하나의 컴포넌트로 만들면 아래와 같이 된다.

데이터와 컴포넌트를 주입받는다.

function App() {
  return (
    <Menu
      buttonContents={
        <>
          Actions <span aria-hidden>▾</span>
        </>
      }
      items={[
        {contents: 'Download', onSelect: () => alert('Download')},
        {contents: 'Create a Copy', onSelect: () => alert('Create a Copy')},
        {contents: 'Delete', onSelect: () => alert('Delete')},
      ]}
    />
  )
}

해당 구현의 문제는 해당 메뉴 컴포넌트 사용자가 Menu 컴포넌트 안에 다른 Rendering 요소를(원하는 위치에) 집어넣을 수 없다는 것이다.

버튼과 item사이 분리선을 하나 그리고 싶다고 생각해보자.

props 지원을 추가할 것인가? if로직이 또 추가된다.

function App() {
  return (
    <Menu>
      <MenuButton>
        Actions <span aria-hidden>▾</span>
      </MenuButton>
      <MenuList>
        <MenuItem onSelect={() => alert('Download')}>Download</MenuItem>
        <MenuItem onSelect={() => alert('Copy')}>Create a Copy</MenuItem>
        <MenuItem onSelect={() => alert('Delete')}>Delete</MenuItem>
      </MenuList>
    </Menu>
  )
}

컴파운드 컴포넌트 패턴을 사용하면, 사용자에게 rendering control을 넘길 수 있다.

추가로 Menu 컴포넌트의 상태가 사용자에게 보이지 않는 것에 주목하자.

 

그리고 위의 예처럼 Menu에 prop을 컴포넌트로 받는건 잘못된 것이 아니다.

레이아웃에 컴포넌트 주입 혹은 State의 캡슐화를 통한 Props Drilling을 피하려는 목적이라면, 괜찮다.

https://epicreact.dev/one-react-mistake-thats-slowing-you-down

컴파운드 컴포넌트는 아토믹 디자인 패턴의 유기체(섹션) 정도에 사용하는 것이 좋다.

 

 

 

참고

아래의 패턴들을 추후 게시물에서 한번씩 다룰 예정이다.

컴파운드 컴포넌트 패턴

 

React Hooks: Compound Components

How do compound components change with React hooks?

kentcdodds.com

 

State Reducer Pattern

리듀서를 오버라이딩 할 수 있게 해준다.

 

The state reducer pattern ⚛️ 🏎

A new pattern has been implemented in downshift and it's awesome. Use the state reducer pattern to make your components more useful.

kentcdodds.com

기본 리듀서의 action type을 같이 노출한 뒤, 사용자가 액션 타입에 따라 오버라이딩 할 수 있도록 한다.

function stateReducer(state, changes) {
  switch (changes.type) {
    case Downshift.stateChangeTypes.keyDownEnter:
    case Downshift.stateChangeTypes.clickItem:
      return {
        ...changes,
        // we're fine with any changes Downshift wants to make
        // except we're going to leave isOpen and highlightedIndex as-is.
        isOpen: state.isOpen,
        highlightedIndex: state.highlightedIndex,
      }
    default:
      return changes
  }
}

// then when you render the component
// <Downshift stateReducer={stateReducer} {...restOfTheProps} />
반응형