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>

문제

개발자 도구 콘솔 창에서 0.1 + 0.1 + 0.1을 실행해보자.

0.1 + 0.1 + 0.1
//0.30000000000000004

우리가 원하는 값은 0.3인데, 정확한 0.3이 나오지 않고 약간의 오차가 생겼다.

원인

Javascript는 수를 표현할 때 IEEE 754방식을 사용하는데, 0.5, 0.25 등을 제외한 대부분의 소수는 이 방식으로 표현이 불가능하기 때문에 표현하기 위해 반올림을 사용한다.
이 문제는 해당 반올림 과정에서 미세한 오차가 생기기 때문에 일어난다.

IEEE 754 표현법

IEEE 754 표현은 다음 그림과 같이 부호, 지수부, 가수부로 크게 세 부분으로 나누어져 있다.
IEEE754 표현

일반적으로 많이 쓰이는 형식은 32bit 단정도와 비트 수를 두배로 한 64bit 배정도이다.

예시

-118.625를 32bit 단정도를 IEEE 754로 표현해보자.
32bit 단정도에서는 지수부를 8bit, 가수부를 23bit로 잡고 있다.

  1. 음수이므로 부호는 1이다.
  2. 118.625를 이진수로 변환하면 1110110.101이 된다. [이진수 변환 참조]
  3. 앞자리에 1만 남을 때 까지 소수점을 앞으로 옮긴다. (이 과정을 정규화라고 한다)
    • 1110110.101 -> 1.110110101 * 26
  4. 가수부에는 110110101이 남아있으며, 이를 23bit로 만들기 위해 0을 채워넣는다.
    • 1.110110101 * 26 -> 1.11011010100000000000000 * 26
  5. 지수부에는 2의 지수에 127(32bit의 bias)를 더한 수를 넣는다. 즉 6 + 127 = 133이므로, 10000101이다.
    • 지수부는 8비트이므로 -127부터 127을 0부터 255까지 저장하게 된다.
      따라서 127이 실질적인 0이 되므로 127만큼을 더해주어야 한다.

결과는 다음과 같이 나타난다.
11000010111011010100000000000000

반올림 규칙

앞서 서술하듯이 이 표현법으로는 대부분의 소수를 표현하지 못한다.
예를 들어 0.3은 이진수로 변환하면 0.01001100110011001...로 순환한다. 때문에 IEEE 754에서는 이를 처리하기 위한 5가지 반올림 규칙을 정해두었다.

  • round to nearest, ties to even (짝수로 반올림)
    • 가장 많이 사용하는 방식으로, 반올림을 할 때 가까운 숫자가 두 개이면 가수부의 마지막 자리가 짝수인 값으로 반올림 한다.
  • round to nearest, ties away from zero (0에서 먼 방향으로 반올림)
    • 반올림을 할 때 가까운 숫자가 두 개이면 절대값이 큰 값으로 반올림한다.
  • 올림
  • 버림
  • 절삭
    • 원래 값보다 절대값이 작거나 같은 값을 선택한다.

즉, 0.3은 1.001100110011001... * 2-2로 변환 할 수 있는데, 가수부는 23bit를 차지하므로 소수점 아래 24번째 자리에서 반올림을 하면 가수부의 값은 00110011001100110011010이 된다.

결론적으로 0.3을 정규화하면 1.00110011001100110011010 * 2-2이고, 이는 사실상 0.30000001192092895508이라는 0.3과 약간의 오차가 있는 소수로 나타.

덧셈

IEEE 754 형식의 사칙연산을 할 때, 지수부가 같은 경우에는 일반적인 이진수 계산을 하면 되지만 지수부가 다른 경우에는 지수가 작은 쪽을 따른다.

예시

0.1 + 0.1 + 0.1을 계산해보자. 0.1을 정규화하면 1.10011001100110011001101 * 2-4이다.
이 경우는 지수부가 같으므로, 지수 조정 없이 이진수 덧셈을 하면 된다.
해당 연산의 결과는 11.00110011001100110011010 * 2-4이 되는데, 이 값을 정규화 및 반올림하면 1.10011001100110011001101 * 2-3이라는 값이 나온다.

여기에 다시 1.10011001100110011001101 * 2-4을 더하면 100.11001100110011001100111 * 2-4이 되며,
최종적으로는 1.00110011001100110011010 * 2-2으로 나타낼 수 있다. 이는 앞서 반올림 규칙에서 정규화했던 0.3과 동일한 값을 나타낸다.

여기에서는 32bit 단정도를 사용하여 비교적 큰 단위에서 반올림을 해 0.3이 나타나게 되나, 64bit 배정도를 사용해서 계산하면 0.30000000000000004이 나온다.

결론

Javascript를 포함한 많은 프로그래밍 언어는 수를 표현할 때 IEEE 754를 사용하고 있으며, 이 언어들 역시 연산을 실행하면 반올림 방법에 따라 차이가 있을 순 있으나 약간의 오차를 가지고 있다.
따라서 이는 프로그래밍 언어의 설계나 연산능력의 문제가 아닌, 약속된 숫자 표현 자체의 한계점이라고 볼 수 있다.
미래에 소수점 아래 값들을 표기하는 획기적인 방법이 나오거나 컴퓨터 구조 자체가 뒤집히지 않는 이상, 우리가 사용할 단위 아래 값들은 반올림을 하거나 버리는 등의 정제를 하여 사용하는 수 밖에 없겠다.

+ Recent posts