규모가 큰 애플리케이션에서 동일한 로직(데이터를 받아와서, 렌더링한다)이 반복되고 이를 많은 컴포넌트에서 사용한다면 추상화의 필요성이 생긴다. 이러한 경우에 고차 컴포넌트를 사용하자.
class CommentList extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
// "DataSource" 는 글로벌 데이터 소스입니다.
comments: DataSource.getComments()
};
}
componentDidMount() {
// 변화감지를 위해 리스너를 추가합니다.
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
// 리스너를 제거합니다.
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
// 데이터 소스가 변경될때 마다 comments를 업데이트합니다.
this.setState({
comments: DataSource.getComments()
});
}
render() {
return (
<div>
{this.state.comments.map((comment) => (
<Comment comment={comment} key={comment.id} />
))}
</div>
);
}
}
BlogPost
class BlogPost extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
blogPost: DataSource.getBlogPost(props.id)
};
}
componentDidMount() {
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
this.setState({
blogPost: DataSource.getBlogPost(this.props.id)
});
}
render() {
return <TextBlock text={this.state.blogPost} />;
}
}
DataSource
를 구독하는 CommentList
와 BlogPost
같은 컴포넌트를 생성하는 함수를 작성한다. 구독한 데이터를 prop으로 전달받는 자식 컴포넌트를 파라미터 중 하나로 받는 함수를 만든다. 이 함수를 withSubscription
라고 하자.
const CommentListWithSubscription = withSubscription(
CommentList,
(DataSource) => DataSource.getComments()
);
const BlogPostWithSubscription = withSubscription(
BlogPost,
(DataSource, props) => DataSource.getBlogPost(props.id)
);
첫번째 파라미터는 래핑된 컴포넌트다. 두 번째 파라미터는 DataSource
와 현재 props를 가지고 컴포넌트에서 관심 있는 데이터를 검색한다.
CommentListWithSubscription
와 BlogPostWithSubscription
가 렌더링될 때 CommentList
와 BlogPost
는 DataSource
에서 가장 최근에 검색된 데이터를 data
prop으로 전달한다.
// 이 함수는 컴포넌트를 매개변수로 받고..
function withSubscription(WrappedComponent, selectData) {
// ...다른 컴포넌트를 반환하는데...
return class extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
data: selectData(DataSource, props)
};
}
componentDidMount() {
// ... 구독을 담당하고...
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
this.setState({
data: selectData(DataSource, this.props)
});
}
render() {
// ... 래핑된 컴포넌트를 새로운 데이터로 랜더링 합니다!
// 컴포넌트에 추가로 props를 내려주는 것에 주목하세요.
return <WrappedComponent data={this.state.data} {...this.props} />;
}
};
}
고차 컴포넌트는 원본 컴포넌트를 컨테이너 컴포넌트로 Wrapping하여 조합한다. 이는 사이드 이펙트가 없는 순수 함수이다.
고차 컴포넌트는 데이터의 사용 이유와 방법, 어디서부터 왔는지 알 필요가 없다.
결국 같은 일을 하는 컴포넌트를 고차 컴포넌트로 추상화 하고 prop을 통해서만 계약을 맺었다. 이는 래핑된 컴포넌트에 동일한 prop만 전달한다면 다른 고차 컴포넌트를 만들 수 있다. 이는 객체지향에서 OCP를 지키면서 유연성을 확보한 전략과 동일해 보인다.
원본 컴포넌트를 변경하여 사용하기 보다, 새로운 컴포넌트를 반환하는 방식을 이용해라.
function logProps(WrappedComponent) {
return class extends React.Component {
componentDidUpdate(prevProps) {
console.log('Current props: ', this.props);
console.log('Previous props: ', prevProps);
}
render() {
// 들어온 component를 변경하지 않는 container입니다. 좋아요!
return <WrappedComponent {...this.props} />;
}
}
}