리액트 고급 가이드 - 고차 컴포넌트 (계속)

개요

고차 컴포넌트는 리액트의 재사용 컴포넌트 로직의 고급 기술이다. 고차 컴포넌트는 리액트 API는 아니다. 고차 컴포넌트는 구성상의 자연으로부터 탄생한 패턴이다.
Concretely, 고차 컴포넌트는 컴포넌트를 받아 새 컴포넌트를 반환하는 함수이다.

const EnhancedComponent = higherOrderComponent(WrappedComponent);

 component가 UI로 props을 변환하는 반면, 고차 component는 component를 다른 component로 변환한다.
HOC는 Redux의 connect 및 Relay의 createFragmentContainer와 같은 써드 파티 React 라이브러리에서 일반적으로 사용된다.
이 문서에서는 고차 component가 유용한 이유와 직접 component를 작성하는 방법에 대해 설명한다.

Use HOCs For Cross-Cutting Concerns

컴포넌트는 React에서 코드 재사용의 기본 단위이다. 그러나 일부 패턴은 기존 구성 요소에 적합하지 않다.
예를 들어, 주석 목록을 렌더링하기 위해 외부 데이터 소스를 구독하는 CommentList 컴포넌트가 있다고 가정 해보자:

class CommentList extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {
      // "DataSource" 는 어떤 global data 소스
      comments: DataSource.getComments()
    };
  }

  componentDidMount() {
    // 변화하기 위한 스크라이브?!
    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>
    );
  }
}

이후, 너는 단일 블로그 포스트를 구독하기 위해 비슷한 패턴을 따라하는 컴포넌트를 쓴다:

   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에서 다른 메소드를 호출하고 다른 출력을 렌더링한다. 그러나 구현의 대부분은 동일하다.
마운트시 DataSource에 변경 리스너를 추가하라.  리스너 내부에서 데이터 소스가 변경 될 때마다 setState를 호출한다. 마운트 해제시 변경 리스너를 제거하라. 큰 응용 프로그램에서 DataSource를 구독하고 setState를 호출하는 동일한 패턴이 반복해서 발생한다고 상상할 수 있다. 우리는이 논리를 단일 장소에서 정의하고 여러 컴포넌트간에 공유 할 수있게 해주는 추상화가 필요하다. 이 것이 고차원 구성 요소가 탁월한 지점이다.
DataSource를 구독하는 CommentList 및 BlogPost와 같은 컴포넌트를 만드는 함수를 작성할 수 있다. 이 함수는 구독 된 데이터를 받는 자식 컴포넌트를 인수 중 하나로 받아들인다. 함수를 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에서 가장 최근 데이터가 검색된 데이터 props이 전달된다.

// 이 함수는 컴포넌트를 갖는다.
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() {
      // ... that takes care of the 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} />;
    }
  };
}

HOC는 입력 컴포넌트를 수정하지 않으며 상속을 사용하여 동작을 복사하지도 않는다. 오히려 HOC는 원래 컴포넌트를 컨테이너 구성 요소에 래핑하여 작성한다. HOC는 부작용이없는 순수 함수이다.
and that's it! 래핑 된 컴포넌트는 컨테이너의 모든 props과 함께 output을 렌더링하는데 사용되는 새로운 props 데이터를 받는다. HOC는 데이터가 사용되는 방법 또는 이유와 관련이 없으며 랩핑 된 컴포넌트는 데이터가 어디서 왔는지와 관련이 없다.
withSubscription은 일반 함수이기 때문에 원하는만큼 인수를 추가 할 수 있다. 예를 들어 랩핑 된 컴포넌트에서 HOC를 더 분리 시키려면 데이터 props의 이름을 configurable하게 만들 수 있다. 또는 shouldComponentUpdate를 구성하는 인수나 데이터 소스를 구성하는 인수를 허용 할 수 있다. HOC는 구성 요소 정의 방법을 완전히 제어 할 수 있기 때문에 이러한 모든 작업이 가능하다.
컴포넌트와 마찬가지로 withSubscription과 래핑 된 컴포넌트 간의 관계는 완전히 props 기반이다. 이렇게 하면 래핑 한 컴포넌트에 동일한 props을 제공하는 한 다른 HOC를 쉽게 교체 할 수 있다. 예를 들어 데이터 가져 오기 라이브러리를 변경하는 경우 유용 할 수 있다.


Don’t Mutate the Original Component. Use Composition.

컴포넌트의 프로토타입을 HOC내에서 modify하려는 유혹을 견뎌라. - 순수 함수니까 당연해야 하는거 아닌가?!

function logProps(InputComponent) {
  InputComponent.prototype.componentWillReceiveProps = function(nextProps) {
    console.log('Current props: ', this.props);
    console.log('Next props: ', nextProps);
  };
  // 원래 입력을 반환한다는 사실은 그것이 변경되었다는 힌트.
  return InputComponent;
}

// EnhancedComponent는 props를 받을 때마다 기록.
const EnhancedComponent = logProps(InputComponent);





(계속)

참조: 리액트 16.3 공식 문서 영문판

댓글

이 블로그의 인기 게시물

서버에 파일 저장하기 - blob

Nuxt를 사용해야하는 10가지 이유 - 번역

후지필름 XC 50-230mm f4.5-6.7 OIS II