HOC(Higher Order Component)

개요

HOC란 React에서 순수 함수를 작성하기 위해 사용되는 HOF(Higher Order Function)에서 파생된 개념으로, 컴포넌트를 인자로 받아 컴포넌트를 반환하는 패턴이다.

HOC 패턴을 사용하면 공통된 로직이나 데이터를 사용하는 여러개의 컴포넌트를 쉽게 만들 수 있으며, 사용하기에 따라 UI를 Component 단위로 분리할 수 있다.

Higher Order Function

  • 1개 이상의 함수를 인수로 받아 함수를 반환하는 함수
  • 예) Array.prototype.map, Array.prototype.filter 등여기서 map의 인수는 x => {return x * 2} 라는 함수이고,
  • 반환값은 array의 각 요소에 2를 곱하는 함수이다.
    let array = [1, 2, 3, 4, 5];
    let double = array.map(x => {return x * 2});
    console.log(double); // [2, 4, 6, 8, 10]

예시

아래는 주어진 숫자만큼 증가하는 카운터를 만드는 예시이다.

// 주어진 숫자만큼 증가하는 로직을 가진 HOC
const withAdder = (WrappedComponent, offset) => {
  class withAdder extends React.Component {
    constructor(props) {
      super(props);

      this.state = {
        count: 0
      };
    }

    componentDidUpdate() {
      console.log(this.state.count);
    }

    addCount = () => {
      this.setState({
        count: this.state.count + offset
      });
    };

    render() {
      return (
        <WrappedComponent
          {...this.props}
          count={this.state.count}
          addCount={this.addCount}
        />
      );
    }
  }

  return withAdder;
};

// 버튼을 누르면 숫자가 증가하는 컴포넌트
class Counter extends React.Component {
  render() {
    return (
      <>
        <div>{this.props.count}</div>
        <button onClick={this.props.addCount}>add</button>
      </>
    );
  }
}

//1만큼 증가하는 카운터를 가지는 컴포넌트
const CounterWithAdder1 = withAdder(Counter, 1);
//2만큼 증가하는 카운터를 가지는 컴포넌트
const CounterWithAdder2 = withAdder(Counter, 2);

위 예시에서 withAdder HOC를 사용하여 증가하는 수가 서로 다른 Counter를 만들었다.

만약 증가하는 카운터가 필요한 다른 컴포넌트가 있다면 내부에 카운트 로직을 구현하지 않고도 withAdder를 붙여주는 것으로 로직을 재사용 할 수 있다.

또한, 해당 예시에서 실제로 카운팅 로직을 실행하는 것은 withAdder이며 Counter에서는 렌더링만 하고 있는 것을 볼 수 있다.

 

 

HOC-example - CodeSandbox

HOC-example by kameru using css-loader, react, react-dom, react-scripts

codesandbox.io

HOC 패턴을 사용하는 유명한 라이브러리로는 react-redux가 있다.

//Component에 store 데이터를 주입한 ConnectedComponent 생성
const ConnectedComponent = connect(
  mapStateToProps,
  mapDispatchToProps
)(Component) 

단점

  • 중첩하여 사용할 경우 어떤 props가 어떤 HOC에서 왔는지 알기 어려워진다.
  • 같은 이름의 props가 있으면 충돌이 일어난다.
  • JSX 안에서 동적으로 사용할 수 없다.

render props

개요

render props는 render 함수를 prop으로 받아서 실행하는 패턴이며. HOC의 단점을 대부분 극복하고 있다.

예시

class Adder extends React.Component {
  constructor(props) {
    // {offset}
    super(props);

    this.state = {
      count: 0
    };
  }

  componentDidUpdate() {
    console.log(this.state.count);
  }

  addCount = () => {
    this.setState({
      count: this.state.count + this.props.offset
    });
  };

  render() {
    return <div>{this.props.render(this.state.count, this.addCount)}</div>;
  }
}

class Counter extends React.Component {
  render() {
    const { count, addCount } = this.props;
    return (
      <div>
        <div>{count}</div>
        <button onClick={addCount}>add</button>
      </div>
    );
  }
}

class CounterWithAdder extends React.Component {
  renderCounter = (count, addCount) => (
    <Counter count={count} addCount={addCount} />
  )

  render() {
    return (
      <Adder
        offset={2}
        render={this.renderCounter}
      />
    );
  }
}

위 예시는 앞서 소개한 HOC와 동일하게 동작하는 Component이다. 각 render 함수(renderCounter)에서 어떤 props를 받아서 컴포넌트가 그려지는지 명시되어 있다.

 

 

render-props-example - CodeSandbox

render-props-example by kameru using react, react-dom, react-scripts

codesandbox.io

render 함수를 props로 전달하는 대신 기본적으로 주어지는 props.children 속성을 사용할 수도 있다.

class Adder extends React.Component {
  constructor(props) {
    // {offset}
    super(props);

    this.state = {
      count: 0
    };
  }

  componentDidUpdate() {
    console.log(this.state.count);
  }

  addCount = () => {
    this.setState({
      count: this.state.count + this.props.offset
    });
  };

  render() {
    return <div>{this.props.children(this.state.count, this.addCount)}</div>;
  }
}

class Counter extends React.Component {
  render() {
    const { count, addCount } = this.props;
    return (
      <div>
        <div>{count}</div>
        <button onClick={addCount}>add</button>
      </div>
    );
  }
}

class CounterWithAdder extends React.Component {
  render() {
    return (
      <Adder offset={2}>
                {(count, addCount) => <Counter count={count} addCount={addCount}/>}            
            </Adder>
    );
  }
}

react-router 라이브러리는 render props 패턴을 사용하고 있다.

// Router 컴포넌트 아래 children에 routing 관련 props가 주입됨
<Router>
  <div>
    <Header />
    <Route exact path="/" component={Home} />
    <Route path="/about" component={About} />
  </div>
</Router>

+ Recent posts