도움말 - 글감 수집하기 (인용)

도움말 - 부분 리뷰 작성하기

리액트(React) 이해 4 - Higher Order Component(HOC)로 컴포넌트 재사용 하기

리액트에서 컴포넌트를 만들 때 가장 많이 사용되는 패턴은 컴포지션(composition) 패턴이다. 컴포지션을 활용하여 리액트 컴포넌트를 만드는 방법은 다양하지만, 이 중에서 이번 글에서는 Higher Order Component(HOC) 패턴을 활용하여 컴포넌트를 만드는 방법에 대해서 알아보도록 하겠다.

Higher Order Component(HOC)를 굳이 번역 한다면 고차 컴포넌트라고 할 수 있는데, 여기에서는 HOC로 통일하여 사용하도록 하겠다.

이 글에서는 HOC가 무엇인지 간단하게 설명하고, 실제로 어디에 활용 할 수 있는지 샘플 예제를 통해 알아 볼 것이다. 또한, HOC를 만들 때 알아두면 좋은 규칙들과 주의 할 점에 대해서 이야기 해보도록 하겠다.

Higher Order Component(HOC)란?

HOC는 자바스크립트의 HOF(Higher Order Function)에서 F(Function) 대신 C(Component)를 리턴 하는 것이다. 이것을 정의해보자면, HOC는 리액트 컴포넌트를 인자로 받아서 새로운 리액트 컴포넌트를 리턴하는 함수이다.

pseudo code로는 이렇게 표현 할 수 있겠다:

const HOC = ReactComponent => EnhancedReactComponent;

여기에서 EnhancedReactComponent는 ReactComponent의 props를 변경 한다거나, ReactComponent에 새로운 props를 추가하여 전달한다거나 아예 새로운 컴포넌트를 return 할 수 있다.

어디에 쓰나?

 HOC를 활용 할 수  있는 방법은 다양한데, 빈번히 사용 되는 때는 아래와 같다:

  • Container 컴포넌트와 Presentational 컴포넌트 분리: 비지니스 로직을 담당하는 컴포넌트(Container 컴포넌트)와 디스플레이를 담당하는 컴포넌트(Presentational 컴포넌트)를 분리하여 사용 할 때, 컨테이너 컴포넌트를 HOC를 만들어서 사용 할 수 있다.
  • 로딩 중 화면 표시: 보통 SPA(Single Page App)에서 화면이 로딩 중일 때, Skeleton 화면을 보여주고, 로딩이 완료되면 데이터를 보여줄 때 사용 할 수 있다.
  • 유저 인증 로직 처리: 컴포넌트 내에서 권한 체크나 로그인 상태를 체크하기 보다는 인증 로직을 HOC로 분리하면 컴포넌트 재사용성도 높일 수 있고, 컴포넌트에서 역할 분리도 쉽게 할 수 있다.
  • 에러 메세지 표시: 컴포넌트 내에서 분기문(if/else 등)을 통해 처리 할 수도 있지만, 분기문을 HOC로 만들어 처리 하면 컴포넌트를 더욱 깔끔하게 사용 할 수 있다.

당연히 이 외에도 다양한 방법으로 HOC를 활용 할 수 있겠지만 여기선 이정도만 다뤄보자.

HOC를 만들 때 지키면 좋은 규칙

네이밍

Container HOC를 제외하고, 보통 HOC를 통해 새로운 prop을 주입 할 때 많이 사용 하는 규칙은 `with`로 시작하여 `withNewPropName` 식으로 네이밍하는게 좋다. 예를들면, withLoading, withAuth 등으로 주입 될 prop 명을 적어주는 식으로 말이다. 이렇게하면 실제로 HOC가 사용되는 컴포넌트에서 prop을 확인 할 때, 이 prop이 어디에서 왔는지 명확히 알 수 있기 때문이다.  

Display Name

React Dev Tool 등의 툴에서 디버깅을 위해 displayName을 명시 해주자. 

render 메소드에 HOC를 사용하면 안된다

render 메소드에 HOC를 사용 하게 되면 매번 render 메소드가 실행 될 때마다 아래처럼 EnhancedComponent가 새로 만들어지게 된다. 그렇기 때문에 HOC는 컴포넌트 외부에 선언 되어야 한다. 

render() {
// A new version of EnhancedComponent is created on every render
// EnhancedComponent1 !== EnhancedComponent2
const EnhancedComponent = enhance(MyComponent);
// That causes the entire subtree to unmount/remount each time!
return <EnhancedComponent />;
}

기타 HOC에 대해서 주의해야 할 점은 여기를 참고하자.

이론적인 부분은 이정도로 하고, 실제로 HOC를 만들어 보자.

HOC를 만들어 보자

HOC를 만드는 방법에는 크게 두가지가 있다. HOC는 Class 기반 컴포넌트와 Function 기반 컴포넌트를 리턴 할 수 있다.

Functional Component를 리턴

const withHOC = WrappedComponent => {
const newProps = {
loading: false,
};
return props => {
return <WrappedComponent {...props} {...newProps} />
}
};

Class Component를 리턴

const withHOC = WrappedComponent => {
const newProps = {
loading: false,
};
return class extends React.Component {
render() {
return <WrappedComponent {...this.props} {...newProps} />
}
}
};

HOC를 사용 할 땐

export default withHOC(AnyComponent);

자, 이제 HOC를 만들 수 있는 방법을 알았으니, 실제로 컴포넌트를 만들어보고 어떻게 활용 할 수 있는지 알아보자.

HOC 예제와 실전 활용

가장 간단한 예제로 withLoading HOC를 만들어보자. 이 HOC는 isLoading prop을 받아서 이 값이 true 일 땐 로딩 컴포넌트를 렌더링 하고, false 일 때는 원하는 컴포넌트를 렌더링 해주는 HOC이다:

const withLoading = (WrappedComponent) => (props) =>
props.isLoading
? <div>Loading ...</div>
: <WrappedComponent { ...props } />

사용 예:

export default withLoading(TodoList);

TodoList 컴포넌트를 withLoading HOC로 래핑해서, 로딩 중일때는 로딩 화면을 보여주도록 처리 했다. 하지만, 컨디셔널 렌더링을 하기전에 전처리 또는 옵션 값 처리가 필요 할 때는 어떻게 해야 할까? 예를 들어, 로딩 화면의 타입(type)이 여러가지여서 옵션(option)으로 타입을 정하게 하고 싶다면 말이다:

export default withLoading({ type: 'circle' })(TodoList); // 어떻게 구현 해야 할까?

정답은 의외로 간단하다. 컴포넌트를 리턴하기 전에 먼저 함수를 리턴하고 그 후에 컴포넌트를 리턴하면 된다: 

 const withLoading = options => WrappedComponent => props => {
if (props.isLoading) {
if (options.type === 'circle') {
return <CircularLoading />
}
return <LinearLoading />
}
return <WrappedComponent {...props} />
};

withLoading HOC를 리팩토링 해서, 위에서 처럼 WrappedComponent를 리턴하기 전에, `options`를 파라미터로 받는 함수를 먼저 리턴하게 했다. 이렇게 함으로써 isLoading props 값이 true일 때, options의 type 값에 따라 CircularLoading 또는 LinearLoading 컴포넌트를 선택에 따라 렌더링 할 수 있게 됐다. 이 같은 방법은 리액트 라이브러리에서 많이 사용 되는 패턴인데, react-redux의 connect HOC를 사용 할 때 가장 많이 사용 된다. 이렇게 함으로써, 조금 더 유연한 HOC를 만들 수 있다. 여기선 간단하게 options 값을 받아서 분기를 해주는 정도로만 사용 했지만, 전 처리로 props 값을 처리 하는 등 상황에 따라 다양하게 사용 될 수 있다(이런 패턴을 가장 많이 활용한 라이브러리는 recompose이다).

아래의 예제 소스들은 실제로 Stack Stack 사이트와 실제 서비스에서 사용 되는 소스를 단순화 한 소스 이다. 소스를 보면서 더 얘기 해보자.

withAuth.js

import get from 'lodash/get';
import { gql, graphql, compose } from 'react-apollo';
const meQuery = gql`
query {
me {
id
name
}
}
`;
export default WrappedComponent => {
return compose(
graphql(meQuery, {
props: ({ data }) => {
const userId = get(data, 'me.id');
return {
auth: {
isLoggedIn: Boolean(userId && localStorage.getItem('auth0IdToken')),
currentUserId: userId,
currentUserName: get(data, 'me.name'),
}
};
},
})
)(WrappedComponent);
};

withAuth HOC는 WrappedComponent에 auth prop을 주입 해준다. 유저가 로그인을 했는지 여부와 유저의 아이디와 이름을 auth 객체로 주입한다. 컴포넌트에서 로그인한 유저의 정보를 보여줘야 한다거나 로그인 한 유저에게만 기능을 동작하게 한다거나 할 때 사용 할 수 있다.

withOnClickIfNotLoggedIn.js

import { connect } from 'react-redux';
import { openDialog } from '/imports/state/ui';
import { AUTH_DIALOG } from '/imports/constants/ui';
import { gql, graphql, compose } from 'react-apollo';
import { get } from 'lodash';
const ME_QUERY = gql`
query currentUser {
me {
_id
}
}
`;
const mapDispatchToProps = (dispatch, ownProps) => {
return {
onClickIfNotLoggedIn: originalOnClick => {
return () => {
if (isNotLoggedIn(ownProps)) {
return dispatch(openDialog({ dialogType: AUTH_DIALOG }));
}
originalOnClick();
};
},
};
};
const isNotLoggedIn = ({ currentUserData }) => {
return !get(currentUserData, 'me._id');
};
export default WrappedComponent =>
compose(graphql(ME_QUERY, { name: 'currentUserData' }), connect(null, mapDispatchToProps))(
WrappedComponent
);

사용 예:

<CardHeader
title={currentUser.profile.name}
onClick={onClickIfNotLoggedIn(this.handleOpen)} // 로그인 하지 않았다면, 로그인 다이얼로그를 띄운다.
/>

withOnClickIfNotLoggedIn HOC는 redux와 react-apollo를 mix한 HOC이다. 이 HOC는 onClick 이벤트 핸들러를 hijack해서 로그인을 안했다면, 로그인 다이얼로그(모달)를 띄워주는 HOC이다. 이 HOC가 쓰이는 경우는 로그인 한 유저와 로그인 안한 유저에게 동일한 UI를 제공하고 기능을 사용하고 싶다면 유저에게 로그인을 유도하도록 하는 방식으로 많이 사용 될 수 있다. 예를 들어, 좋아요 버튼의 경우 로그인 안한 유저에게도 좋아요 버튼을 누를 수 있게 버튼은 그대로 노출 시키고, 버튼을 눌렀을 때 로그인 할 수 있는 다이얼로그(모달)을 보여주는 식으로 활용 할 수 있다.


지금 까지 리액트의 HOC에 대해서 알아 보았다. 이 HOC를 활용 할 수 있는 방법은 다양하지만, 이 글에서는 HOC의 기본적인 개념과 예제 그리고 실전에서 활용 할 수 있는 방법, 그리고 마지막으로 실제 서비스에서 사용 하고 있는 소스 중 일부분을 확인하면서 HOC에 대해 이해 하는 시간을 가졌다. 이를 통해, 조금 더 유연한 컴포넌트를 만들어 나갔으면 좋겠다.


References:


http://rea.tech/reactjs-real-world-examples-of-higher-order-components/


리뷰