HOC(Higher Order Component)패턴은 리액트에서 같은 컴포넌트 로직을 사용하기 위해 사용되는 기술입니다. 리액트를 다루면 정말 자주 보이는 패턴이죠. 이번시간에는 HOC가 어떤식으로 동작하는지 예시를 통해 알아보겠습니다.
여기 두 개의 컴포넌트가 있습니다
<UserList /> // 유저 정보를 받아와서(fetch) 표현
<PostList /> // 포스팅 정보를 받아와서 표현
<Loading /> // 로딩중 사용될 컴포넌트
UserList와 PostList는 백엔드에서 필요한 정보를 가져온 뒤, 가져온 정보를 화면에 표시해주는 컴포넌트입니다. 리액트에서 흔히 존재하는 컴포넌트죠. 정보를 가져오는 동안 화면에 보여줄 Loading컴포넌트도 준비했습니다.
먼저 평범하게 컴포넌트를 작성해봅시다
export default UserList(){
const [userList, setUserList] = useState([]);
useEffect(() => {
fetch('http://userList.plz.com/users')
.then(response => response.json())
.then(data => setUserList(data));
}, []);
return userList.length < 1 ? <Loading /> : (
<h2>UserList</h2>
{
userList.map(user => (
<div key={user.id}>
<p>name: {user.name}</p>
<p>email: {user.email}</p>
</div>
))
}
);
}
useEffect를 이용해 유저 정보를 가져오고, 해당 정보를 받으면 유저 정보를 보여주는 간단한 컴포넌트를 만들었습니다.
이제 포스팅 컴포넌트도 만들어볼까요?
export default PostList(){
const [postList, setPostList] = useState([]);
useEffect(() => {
fetch('http://postList.plz.com/posts')
.then(response => response.json())
.then(data => setPostList(data));
}, []);
return postList.length < 1 ? <Loading /> : (
<h2>postList</h2>
{
postList.map(post => (
<div key={user.id}>
<p>{post.body}</p>
</div>
))
}
);
}
포스트 리스트도 어렵지 않게 간단히 만들 수 있습니다. 그런데 여기서 패턴이 보이시나요? 개발을 좋아하는 여러분은 반복하는 것을 매우 싫어합니다. 그리고 저 컴포넌트들은 동일한 로직이 반복되고 있죠.
- 마운트시 useEffect를 통해 데이터를 받아온다.
- 데이터를 받아오는 동안 로딩 컴포넌트를 보여준다.
- 데이터를 받아오면 데이터를 화면에 표시한다.
받아오는 "데이터의 종류"와 "표현법"은 다르지만, 동일한 로직을 처리하고 있는 것을 볼 수 있습니다. 그리고 이러한 로직은 위 2가지 외에 더 많은 컴포넌트에서 흔히 나타날 수 있는 로직이죠.
이제 우리는 이 로직을 공통으로 묶을 수 없을지 고민하게 됩니다. 이때 등장하는것이 바로 HOC이죠.
HOC는 공통된 로직을 수행하여 필요한 결과물을 표현할 수 있도록 도와줍니다.
우리가 작성한 컴포넌트들은 2가지 차이점이 있었죠.
여기서 표현법은 각각의 컴포넌트에 맡기도록 합니다. 데이터를 어떻게 표현할지는 본래 컴포넌트의 역할이니까요.
우리의 HOC는 데이터의 종류를 받고, 그 종류의 데이터를 요청한 뒤 컴포넌트에게 넘겨주는 역할을 수행합니다.
이렇게 되면 동일한 로직은 HOC에 맡기고, 컴포넌트들은 서로 다른 표현법에만 집중할 수 있겠죠.
그럼 이제 HOC를 적용해보겠습니다.
// withLoading.js
export default withLoading(WrappedComponent) {
return function({dataSource, ...otherProps}) {
const [data, setData] = useState([]);
useEffect(() => {
fetch(dataSource)
.then(response => response.json())
.then(data => setData(data));
}, []);
return data.length < 1 ? <Loading /> : <WrappedComponent data={data} {...otherProps} />
}
}
// UserList.jsx
const UserList = ({data}) => {
return data.length < 1 ? <Loading /> : (
<h2>UserList</h2>
{
data.map(user => (
<div key={user.id}>
<p>name: {user.name}</p>
<p>email: {user.email}</p>
</div>
))
}
);
}
export default withLoading(UserList);
// PostList.jsx
const PostList = ({data}) => {
return data.length < 1 ? <Loading /> : (
<h2>postList</h2>
{
data.map(post => (
<div key={user.id}>
<p>{post.body}</p>
</div>
))
}
);
}
export default withLoading(UserList);
이렇게 공통적으로 사용되는 로직을 withLoading에 넣어줍니다. 그리고 나머지 props를 받아 WrappedComponent를 돌려주도록 합니다.
이제 UserList, PostList는 dataSource만 넣어 사용할 수 있습니다.
<UserList dataSource={"http://userList.plz.com/users"} />
<PostList dataSource={"http://postList.plz.com/posts"} />
이렇게 보니 hook을 사용하는것과 비슷하죠?
HOC패턴은 이런식으로 사용할 수 있습니다.
물론 이 두가지만 보자면 컴포넌트의 로직은 줄었지만 HOC를 작성하니 어려운게 아니냐! 할 수 있죠. 하지만 동일 로직의 새로운 컴포넌트를 만든다면 얘기가 달라집니다.
// NewComponent.jsx
const NewComponent = ({data, name}) => {
return ...
}
export default withLoading(NewComponent);
// Usage
<NewComponent dataSource={"http://newDataSource.com/newData"} name={name} />
이제 우리는 HOC를 통해 동일 로직을 처리하며 간단히 컴포넌트를 생성할 수 있습니다.
오오오~~ 정말 유용하군요~~~