2. recompose 로 컴포넌트에 기능 붙여주기

recompose는 함수를 통하여 컴포넌트에 기능을 달아주는 패턴인 HoC (Higher Order Component) 들이 있는 라이브러리입니다. HoC 를 직접 만들수도있는데, 이에 대해선 다음에 알아보고, 이 튜토리얼에선 HoC 를 통하여 어떤 작업들을 할 수 있는지 알아보겠습니다.

이 라이브러리에서 제공되는 HoC 는 정말 여러가지가 있는데요, 그 중에서 자주 사용되는 HoC 몇가지를 한번 사용해보겠습니다.

Edit Recompose 사용해보기

우리는 CodeSandbox 에서 튜토리얼을 진행해볼건데요, 만약에 실제 리액트 프로젝트에서 사용을 하고자 한다면

$ yarn add recompose

를 통하여 라이브러리를 설치해주셔야 합니다 (위 CodeSandbox 에는 이미 설치가 되어있는 상태입니다.)

현재 코드는 다음과 같습니다:

App.js

import React, { Component } from 'react';
import Counter from './Counter';
import Form from './Form';
import List from './List';

class App extends Component {
  render() {
    return (
      <div>
        <Counter />
        <hr />
        <Form />
        <List />
      </div>
    );
  }
}

export default App;

Counter.js

import React from 'react';

const Counter = () => {
  return (
    <div>
      <h1>0</h1>
      <button>+1</button>
      <button>-1</button>
      <button>초기화</button>
    </div>
  );
};

export default Counter;

Form.js

import React from 'react';

const Form = () => {
  return (
    <form>
      <input name="name" placeholder="이름" />
      <input name="description" placeholder="설명" />
      <button type="submit">추가</button>
    </form>
  );
};

export default Form;

List.js

import React from 'react';

const List = () => {
  return (
    <ul>
      <li>김민준 (설명)</li>
      <li>어쩌고 (설명)</li>
      <li>저쩌고 (설명)</li>
    </ul>
  );
};

export default List;

withState 와 withHandlers

withState 와 withHandlers 를 사용해서 카운터 기능을 구현해보겠습니다.

withState

withState 는 컴포넌트에 특정 state 와, 해당 state 의 값을 설정하는 setter 함수를 props 로 주입해줍니다.

withState(
  stateName: string,
  stateUpdaterName: string,
  initialState: any | (props: Object) => any
): HigherOrderComponent

한번 사용 예시를 보겠습니다:

Counter.js

import React from 'react';
import { withState } from 'recompose';

const Counter = ({ value, setValue }) => {
  return (
    <div>
      <h1>{value}</h1>
      <button onClick={() => setValue(value + 1)}>+1</button>
      <button onClick={() => setValue(value - 1)}>-1</button>
      <button onClick={() => setValue(0)}>초기화</button>
    </div>
  );
};

export default withState('value', 'setValue', 0)(Counter);

Edit Recompose 사용해보기

value 라는 값과, 이 값을 설정하는 setValue 라는 값을 props 로 넣어주고, value 의 기본값은 0 으로 설정했습니다.

withHandlers

withHandlers 는 특정 함수를 props 로 주입해줍니다. 예를들어, 우리가 방금 만든 Counter 에서는 setValue 를 직접 JSX 쪽에서 값을 넣어서 호출해주는 방식으로 구현을 했는데요, 이런 방식 대신에 increment, decrement, reset 이라는 함수를 만들어서 호출을 해보겠습니다.

withHandlers(
  handlerCreators: {
    [handlerName: string]: (props: Object) => Function
  } |
  handlerCreatorsFactory: (initialProps) => {
    [handlerName: string]: (props: Object) => Function
  }
): HigherOrderComponent

여러개의 HoC 를 사용하게 된다면 다음과 같은 형식으로 사용을 해야하는데요:

withHandlers(...)(withState(...)(Counter))

이렇게 계속 감싸는 형태로 구현한다면 가독성이 정말 떨어지겠죠? 그렇기에 유틸 함수로 제공되는 compose 라는 함수를 사용하면 다음과 같이 구현 할 수 있습니다.

compose(
  withState(...),
  withHandlers(...)
)(Counter)

한번 그럼 사용해볼까요?

Counter.js

import React from 'react';
import { compose, withState, withHandlers } from 'recompose';

const Counter = ({ value, increment, decrement, reset }) => {
  return (
    <div>
      <h1>{value}</h1>
      <button onClick={increment}>+1</button>
      <button onClick={decrement}>-1</button>
      <button onClick={reset}>초기화</button>
    </div>
  );
};

export default compose(
  withState('value', 'setValue', 0),
  withHandlers({
    increment: props => () => props.setValue(props.value + 1),
    decrement: props => () => props.setValue(props.value - 1),
    reset: props => () => props.setValue(0)
  })
);

저렇게 export default 하는 줄에서 바로 compose 를 사용하셔도 상관은 없지만, 보통 enhance 라는 레퍼런스에 HoC 함수를 담는 방식으로 많이 사용합니다.

const enhance = compose(
  withState('value', 'setValue', 0),
  withHandlers({
    increment: props => () => props.setValue(props.value + 1),
    decrement: props => () => props.setValue(props.value - 1),
    reset: props => () => props.setValue(0)
  })
);

export default enhance(Counter);

Edit Recompose 사용해보기

withStateHandlers

withStateHandlers는 state 를 정의하고 이 값을 바꾸는 업데이터 함수도 직접 작성 할 수 있는 HoC 입니다.

withStateHandlers(
  initialState: Object | (props: Object) => any,
  stateUpdaters: {
    [key: string]: (state:Object, props:Object) => (...payload: any[]) => Object
  }
)

만약에, 이 함수를 사용하여 Counter 를 구현한다면 다음과 같이 할 수 있습니다:

import React from 'react';
import { withStateHandlers } from 'recompose';

const Counter = ({ value, increment, decrement, reset }) => {
  return (
    <div>
      <h1>{value}</h1>
      <button onClick={increment}>+1</button>
      <button onClick={decrement}>-1</button>
      <button onClick={reset}>초기화</button>
    </div>
  );
};

const enhance = withStateHandlers(
  { value: 0 },
  {
    increment: ({ value }) => () => ({ value: value + 1 }),
    decrement: ({ value }) => () => ({ value: value - 1 }),
    reset: ({ value }) => () => ({ value: 0 })
  }
);

export default enhance(Counter);

Edit Recompose 사용해보기

이번엔, 이 함수를 사용해서 Form 컴포넌트에 기능을 붙여볼까요?

Form.js

import React from 'react';
import { withStateHandlers } from 'recompose';

const Form = ({ name, description, onChange, onSubmit }) => {
  return (
    <form onSubmit={onSubmit}>
      <input name="name" placeholder="이름" onChange={onChange} value={name} />
      <input
        name="description"
        placeholder="설명"
        onChange={onChange}
        value={description}
      />
      <button type="submit">추가</button>
    </form>
  );
};

const enhance = withStateHandlers(
  {
    name: '',
    description: ''
  },
  {
    onChange: state => e => ({
      [e.target.name]: e.target.value
    }),
    onSubmit: state => e => {
      e.preventDefault();
      return {
        name: '',
        description: ''
      };
    }
  }
);
export default enhance(Form);

Edit Recompose 사용해보기

이렇게, recompose 를 잘만 활용하면 함수형 컴포넌트만 작성해도 충분히 상태관리를 쉽게 할 수 있습니다.

list 에 항목 추가하기

이번엔 App 에서도 사용해보겠습니다! list 데이터와, 이 값을 다루는 함수를 props 로 주입해봅시다. (물론 state 와 메소드를 직접 구현하셔도 상관 없지만, 연습삼아 해보겠습니다!)

App.js

import React, { Component } from 'react';
import { withStateHandlers } from 'recompose';

import Counter from './Counter';
import Form from './Form';
import List from './List';

class App extends Component {
  render() {
    return (
      <div>
        <Counter />
        <hr />
        <Form onAdd={this.props.addToList} />
        <List list={this.props.list} />
      </div>
    );
  }
}

const enhance = withStateHandlers(
  { list: [] },
  {
    addToList: state => data => ({
      list: state.list.concat(data)
    })
  }
);

export default enhance(App);

withStateHandlers 를 통하여 list 와 addToList 를 props 로 받아와서 Form 과 List 컴포넌트에게 각각 전달을 해주었습니다. 전달해준 props 를 사용하여 기능을 마저 구현해보겠습니다.

Form.js

Form.js 에서는 onAdd 를 받아와서 사용을 하게 되는데요,

import React from 'react';
import { withStateHandlers } from 'recompose';

const Form = ({ name, description, onChange, onSubmit }) => {
  return (
    <form onSubmit={onSubmit}>
      <input name="name" placeholder="이름" onChange={onChange} value={name} />
      <input
        name="description"
        placeholder="설명"
        onChange={onChange}
        value={description}
      />
      <button type="submit">추가</button>
    </form>
  );
};

const enhance = withStateHandlers(
  {
    name: '',
    description: ''
  },
  {
    onChange: state => e => ({
      [e.target.name]: e.target.value
    }),
    onSubmit: (state, props) => e => {
      e.preventDefault();
      props.onAdd({
        name: state.name,
        description: state.description
      });
      return {
        name: '',
        description: ''
      };
    }
  }
);
export default enhance(Form);

props 로 받아온 onAdd 함수를 호출하기 위해선, onSubmit 쪽에 두번째 파라미터로 props 를 받아와서 사용하셔야 합니다.

List.js

List 컴포넌트에선 전달받은 list props 를 map 하여 JSX 로 변환해주겠습니다. 현재 고유 id 로 사용 할 key 가 없기 때문에 그냥 index 를 key 로 사용하도록 구현하겠습니다. (프로덕션에선 이렇게 index 를 key 로 사용하는것은 성능이 안 좋기 때문에 권장되지 않습니다. 단, 보여줄 데이터가 몇개 없다면 큰 상관은 없습니다.)

Edit Recompose 사용해보기

그 외의 여러 유용한 HoC 들

recompose 에서는 이 튜토리얼에서 언급한 것 말고도 굉장히 유용한 HoC 들이 제공됩니다.

lifecycle()

lifecycle 은 컴포넌트에 라이프사이클 함수를 만들어서 적용시켜줍니다.

사용 예시:

List.js

import React from 'react';
import { lifecycle } from 'recompose';

const List = ({ list }) => {
  return (
    <ul>
      {list.map((item, index) => (
        <li>
          {item.name}({item.description})
        </li>
      ))}
    </ul>
  );
};

const enhance = lifecycle({
  componentDidMount() {
    console.log('List 가 마운트되었습니다!')
  }
})
export default enhance(List);

만약에 여러분들이 함수형 컴포넌트로 무언가를 만들었는데, 라이프사이클 함수를 나중에 구현해야 될 필요가 생긴다면, 이렇게 lifecycle HoC 를 쓰면 편하겠죠?

렌더링 최적화관련

recompose 에서 제공되는 함수 중에서, shouldComponentUpdate 를 쉽게구현하기 위한 함수들 몇가지가 존재합니다. 필요할때 쓰면 정말 유용한데요, 한번 알아보겠습니다!

shouldUpdate

shouldUpdate 는 단순히 shouldComponentUpdate 를 구현하기 위한 용도의 HoC 입니다.

shouldUpdate(
  test: (props: Object, nextProps: Object) => boolean
): HigherOrderComponent

만약에 사용한다면, 이렇게 사용 할 수 있습니다.

List.js

import React from 'react';
import { shouldUpdate } from 'recompose';

const List = ({ list }) => {
  return (
    <ul>
      {list.map((item, index) => (
        <li>
          {item.name}({item.description})
        </li>
      ))}
    </ul>
  );
};

const enhance = shouldUpdate(
  (props, nextProps) => {
    return props.list !== nextProps.list
  }
);

export default enhance(List);

onlyUpdateForKeys

이 함수는 잘 쓰면 굉장히 편리한 HoC 입니다. shouldUpdate 는 단순히 shouldComponentUpdate 를 구현하는 용도였다면, 이 함수는 shouldComponentUpdate 를 훨씬 더 쉽게 구현 할 수 있게 해줍니다.

예를들어서, 특정 컴포넌트에서, 검사해야하는 props 가 여러개 있다면, 확인하고 싶은 key 를 전달해주시면 됩니다.

onlyUpdateForKeys(
  propKeys: Array<string>
): HigherOrderComponent

사용 예시:

List.js

import React from 'react';
import { onlyUpdateForKeys } from 'recompose';

const List = ({ list }) => {
  return (
    <ul>
      {list.map((item, index) => (
        <li>
          {item.name}({item.description})
        </li>
      ))}
    </ul>
  );
};

const enhance = onlyUpdateForKeys(['list', 'anyotherprops']);

export default enhance(List);

만약에 검사해야 하는 props 가 많다면 아마..

shouldComponentUpdate(props, nextProps) {
  return props.list !== nextProps.list 
    || props.anyotherprops !== nextProps.anyotherProps;
    // ....
}

이렇게 해야 하는데, 만약 onlyUpdateForKeys 가 있다면 훨씬 쉽게 구현이 가능하겠죠?

pure

pure 라는 HoC 는 props 로 받아오는 모든 것을 검사하여 실제로 바뀔때만 리렌더링 되도록 shouldComponentUpdate 를 자동으로 구현해줍니다.

얼핏 보면 다 자동으로 해준다니 좋아보일수도 있지만 이 함수는 절대로 남용하시면 안됩니다.

예를들어서, 우리가 어떤 컴포넌트에 10 종류의 props 를 전달해주고, 그중에 2개만 실제 렌더링에 영향을 끼친다고 가정을 해봅시다. (예를들어서 나머지 8개는 함수형태이거나 무조건 고정적인 값일수도 있겠죠). 그러면, 사실 검사해야하는건 두개정도밖에 없는데, 쓸데없이 리렌더링할때마다 10개의 값을 모두 비교해야 한다면, 비효율적이겠죠?

따라서, 이 pure 함수는, 컴포넌트에 전달하는 props 가 모두 렌더링에 영향을 끼치고, shouldComponentUpdate 직접구현 혹은 다른 HoC 사용을 통하여 최적화하는것이 귀찮을 시에 사용하시면 됩니다.

사용 예시:

List.js

import React from 'react';
import { pure } from 'recompose';

const List = ({ list }) => {
  return (
    <ul>
      {list.map((item, index) => (
        <li>
          {item.name}({item.description})
        </li>
      ))}
    </ul>
  );
};

export default pure(List);

renderProps 관련

recompose 에서 제공되는 HoC 중에서 toRenderPropsfromRenderProps가 있는데, fromRenderProps 는 Context API 와 함께 사용하면 굉장히 유용합니다.

renderProps 그리고 Context API 에 대해선 나중에 다뤄보게 됩니다 :) 배우고나서 여기로 다시 돌아와서 위 함수들을 살펴보시면 충분합니다~

정리

recompose 라이브러리는, 무조건 쓸 필요는 없겠지만, 잘 알아두고, 필요한 상황에 잘 쓰면 굉장히 유용합니다! recompose 에서 제공되는 HoC 는, 애초에 컴포넌트를 만들때마다 class 형태로 구현하면 딱히 쓸모가 없을수도 있습니다. 이것은 단지 취향의 차이라고 보시면 될 것 같습니다. 어떤 개발자는 모든~ 컴포넌트를 class 로 구현을 하여 나중에 이벤트 핸들러나 라이프사이클 API 를 사용하는 상황에서 그냥 구현 할 수도 있고, 어떤 개발자는 모든 컴포넌트를 함수형 컴포넌트로 작성하고 필요한곳에서 recompose 로 구현해줄수도 있겠죠.

results matching ""

    No results matching ""