2. recompose 로 컴포넌트에 기능 붙여주기
recompose는 함수를 통하여 컴포넌트에 기능을 달아주는 패턴인 HoC (Higher Order Component) 들이 있는 라이브러리입니다. HoC 를 직접 만들수도있는데, 이에 대해선 다음에 알아보고, 이 튜토리얼에선 HoC 를 통하여 어떤 작업들을 할 수 있는지 알아보겠습니다.
이 라이브러리에서 제공되는 HoC 는 정말 여러가지가 있는데요, 그 중에서 자주 사용되는 HoC 몇가지를 한번 사용해보겠습니다.
우리는 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);
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);
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);
이번엔, 이 함수를 사용해서 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);
이렇게, 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 로 사용하는것은 성능이 안 좋기 때문에 권장되지 않습니다. 단, 보여줄 데이터가 몇개 없다면 큰 상관은 없습니다.)
그 외의 여러 유용한 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 중에서 toRenderProps 와 fromRenderProps가 있는데, fromRenderProps 는 Context API 와 함께 사용하면 굉장히 유용합니다.
renderProps 그리고 Context API 에 대해선 나중에 다뤄보게 됩니다 :) 배우고나서 여기로 다시 돌아와서 위 함수들을 살펴보시면 충분합니다~
정리
recompose 라이브러리는, 무조건 쓸 필요는 없겠지만, 잘 알아두고, 필요한 상황에 잘 쓰면 굉장히 유용합니다! recompose 에서 제공되는 HoC 는, 애초에 컴포넌트를 만들때마다 class 형태로 구현하면 딱히 쓸모가 없을수도 있습니다. 이것은 단지 취향의 차이라고 보시면 될 것 같습니다. 어떤 개발자는 모든~ 컴포넌트를 class 로 구현을 하여 나중에 이벤트 핸들러나 라이프사이클 API 를 사용하는 상황에서 그냥 구현 할 수도 있고, 어떤 개발자는 모든 컴포넌트를 함수형 컴포넌트로 작성하고 필요한곳에서 recompose 로 구현해줄수도 있겠죠.