[React] HOC 정리
들어가며
최근 React에 대해 알아가면서 Hook과 더불어 Higher-Order Components (이하 HOC)에 대한 말이 자주 보인다. 따라서 이번 기회에 한번 정리를 해보려고 한다.
참고한 글
API가 아님
HOC는 API가 아니다. API처럼 보이지만 일종의 패턴으로 볼수있다. 함수형 프로그래밍의 Higher Order Function (HOF) 개념에서 유래된것이다.
HOF 개념 자체는 React뿐만 아니라 여타 다른 언어나 라이브러리에서 사용되고 있고 React의 경우 컴포넌트의 성격이 함수의 성격을 띄고 있기 때문에(특히 hooks가 react로 들어오면서 더욱) HOF와 거의 똑같은 패턴이 존재한다고 볼 수있다.
컴포넌트를 다른 컴포넌트로 변환
HOC는 입력받은 컴포넌트를 기반으로 이를 감싸 새로운 컴포넌트로 변환한다. 이때 입력된 원본 컴포넌트를 수정하지 않는것을 API문서에서 권고하고 있다.
HOC는 왜 필요한가.
아래 컴포넌트는 외부로부터 데이터를 구독하여 덧글 목록을 렌더링한다.
class CommentList extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
// DataSource는 글로벌 데이터 소스이다.
comments: DataSource.getComments(),
};
}
componentDidMount() {
// 변화를 구독(Subscribe)한다.
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
// 리스널르 제거한다.
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
//데이터 소스가 변경되면 컴포넌트를 업데이트 한다.
this.setState({
comments: DataSource.getComments(),
});
}
render() {
return (
<div>
{this.state.comments.map((comment) => (
<Comment comment={comment} key={comment.id} />
))}
</div>
);
}
}
나중에 블로그 포스트를 구독하기 위해 비슷한 패턴으로 컴포넌트를 작성한다.
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} />;
}
}
CommentList
와 BlogPost
는 다른 컴포넌트이다. DataSource
의 변화에 따라 서로 다른 메서드를 호출하며 렌더링도 다르게 한다. 하지만 구현체 관점에서 본다면 거의 동일하다.
- 마운트되면(componentDidMount) change 리스너를
DataSource
에 추가한다. - 리스너 내에서 데이터 소스가 변경되면
setState
를 호출한다. - 마운트가 해제되면(componentWillUnmount) change 리스너를 제거한다.
상상의 나래를 펼쳐서 규모가 큰 어플리케이션이라면 이런 패턴이 많이 존재할것이라고 추측할 수 있다. 그럼 동일한 코드가 곳곳에 존재할것이고 유지보수성은 선형적으로 떨어질것이다. 이 문제를 해결하기 위해 추상화가 필요한것이며 이때 HOC가 적절한것이다.
HOC 작성
DataSource
를 DataSource 를 구독하는 CommentList
나 BlogPost
같은 컴포넌트를 생성하는 함수를 작성할 수 있다. 데이터를 prop으로 구독하여 전달받는 자식컴포넌트를 파라미터 중하나로 받는 함수를 만든다. 이 함수의 이름을 withSubscription
로 지정한다.
// 이 함수는 컴포넌트를 가지고 온다.
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() {
// ... subscription을 담당...
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} />;
}
};
}
아래는 위에서 작성한 withSubscription HOC를 사용하는 예이다.
첫번째 파라미터는 래핑된 컴포넌트이다. 두번째 컴포넌트의 파라미터는 관심을 가지는 데이터를 검색한다. 여기서는 DataSource
와 현재 prosp를 전달한다.
CommentListWithSubscription
과 BlogPostWithSubscription
가 렌더링될 때 CommentList
와 BlogPost
는 DataSource
에서 가장 최근에 검색된 데이터를 data
prop으로 전달한다.
const CommentListWithSubscription = withSubscription(
CommentList,
(DataSource) => DataSource.getComments()
);
const BlogPostWithSubscription = withSubscription(
BlogPost,
(DataSource, props) => DataSource.getBlogPost(props.id)
);
HOC는 입력 컴포넌트를 수정하지 않으며 상속을 사용하여 동작을 복사하지도 않는다. HOC는 오로지 원래 컴포넌트를 컨테이너 컴포넌트에 래핑하여 조정한다. HOC는 사이드 이펙트가 없는 순수 함수이다.
주의사항
- render 메서드 안에서 고차 컴포넌트를 사용하지 말것.
- 정적 메서드는 복사할것.
Refs
는 전달 불가.
HOC의 접두사는 with
HOC의 접두사는 with 이다. 특별한 의미가 있는것은 아니다. 다만 lint가 HOC인지를 확인하는데 쓰이기 때문에 hooks의 접두사 use 처럼 쓰이는것이다.
정리
HOC를 통해 반복되는 패턴, 코드를 제거하고 추상화 시킬수 있다. 이외에도 생명주기 함수를 붙혀준다던지, HOC내에 처리 로직을 추가시킨다던지 여러가지 일들이 가능하다.
Leave a comment