본문 바로가기
JavaScript/React

[React] 속도 향상

by 혀나Lee 2017. 7. 25.

Component vs. PureComponent vs. Functional Component

React에서 컴포넌트를 만드는 방법에는 크게 클래스 기반(React.Component, React.PureComponent)과 함수 기반(Functional Stateless Component)로 나뉜다. 

클래스 기반 컴포넌트

React.Component와 React.PureComponent는 shouldComponentUpdate 라이프 사이클 메소드를 다루는 방식을 제외하곤 동일하다.

PureComponent ≈ Component + shouldComponentUpdate 

shouldComponentUpdate

shouldComponentUpdate(nextProps, nestState)

React.Component를 extends해서 컴포넌트를 만들 때, shouldComponentUpdate 메소드를 별도로 선언하지 않았다면, 그 컴포넌트는 props, state 값이 변경될 때마다 항상 re-render 한다.

따라서 props, state 값이 변경되지 않았을 때 re-render를 막기 위해서는 shouldComponentUpdate를 override해야한다.

class CounterButton extends React.Component {
  constructor(props) {
    super(props);
    this.state = {count: 1};
  }
 
  shouldComponentUpdate(nextProps, nextState) {
    if (this.props.color !== nextProps.color) {
        return true;
    }
    if (this.state.count !== nextState.count) {
        return true;
    }
    return false;
  }
 
  render() {
    return (
      <button
        color={this.props.color}
        onClick={() => this.setState(state => ({count: state.count + 1}))}>
        Count: {this.state.count}
      </button>
    );
  }
}

props.color 또는 state.count가 바뀔 때 빼고는 CounterButton이 re-render되지 않는다.


shouldComponentUpdate(nextProps, nextState) {
  return JSON.stringify(this.props) !== JSON.stringify(nextProps) || JSON.stringify(this.state) !== JSON.stringify(state);
}


React.PureComponent

PureComponent는 shouldComponentUpdate에 모든 props, state를 비교한 것과 동일하다.

class CounterButton extends React.PureComponent {
  constructor(props) {
    super(props);
    this.state = {count: 1};
  }
 
  render() {
    return (
      <button
        color={this.props.color}
        onClick={() => this.setState(state => ({count: state.count + 1}))}>
        Count: {this.state.count}
      </button>
    );
  }
}

함수형 컴포넌트

함수형 컴포넌트는 클래스 기반의 컴포넌트와 달리, state, 라이프 사이클 메소드(componentDidMount, shouldComponentUpdate 등등...)와 ref 콜백을 사용할 수 없다. 만약 컴포넌트에 state가 없다면 함수형을 사용하는 것이 좋다.

속도 비교

Functional Component ≈ React.Component > React.PureComponent

기본적인 컴포넌트로만 속도 비교를 할 때, PureComponent가 속도가 가장 빠르며 Functional Component와 React.Component는 속도가 비슷하다.

그 이유는 PureComponent는 re-render를 막는 구문이 있기 때문이고 Functional Component는 클래스 기반 컴포넌트로 래핑(wrapping)되기 때문에 속도가 비슷하다고 한다. (2017. 5월 기준)

PureComponent의 shallow level 비교

class ListOfWords extends React.PureComponent {
  render() {
    return <div>{this.props.words.join(',')}</div>;
  }
}

class WordAdder extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      words: ['marklar']
    };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    // This section is bad style and causes a bug
    const words = this.state.words;
    words.push('marklar');
    this.setState({words: words});
  }

  render() {
    return (
      <div>
        <button onClick={this.handleClick} />
        <ListOfWords words={this.state.words} />
      </div>
    );
  }
}

PureComponent는 props, state의 현재값과 새로운 값을 비교만 하기 때문에 완벽한 비교는 불가능하다.

위의 예제 코드에서 WordAdder의 handleClick 메서드에서 words 어레이 값을 변경하지만 this.props.words의 현재값과 새로운값이 동일하므로 re-render하지 않는다. 

class Child extends React.PureComponent {
  render() {
    const {isOpen} = this.props;
 
    return (
      <div style={isOpen? null: {display: 'none'}}>텍스트</div>
	)
  }
}
 
class Parent extends React.Component {
  constructor(props) {
    this.state = {isOpen: [false, false]};
    this.touchButtonZero = this.touchButton.bind(this, 0);
    this.touchButtonOne = this.touchButton.bind(this, 1);
  }
 
  touchButton = (idx) => {
    let {isOpen} = this.state;
    isOpen[idx] = !isOpen[idx];
    this.setState({isOpen: isOpen});
  };
 
  render() {
    const {isOpen} = this.state;
 
    return (
      <div>
        <button onTouchTap={this.touchButtonZero}>
        <Child isOpen={isOpen[0]} />
        <button onTouchTap={this.touchButtonOne}>
        <Child isOpen={isOpen[1]} />
      </div>
    )
  }
}

이러한 경우에도 isOpen의 인덱스 1의 값을 true로 바꿀 경우에도 this.state.isOpen, nextState.isOpen 의 값이 동일하여 re-render되지 않는다.


render 안은 최대한 간단하게

this.setState()를 할 때 render안의 요소들은 전부 re-render된다. 따라서 render안에 요소가 많다면 re-render될 때마다 그리는 데 시간이 오래 걸리기 때문에 성능에 좋지 않다.

creating new arrays, objects, functions or any other new identities ...

공통된 값은 하나의 변수로 사용

class Table extends PureComponent {
  render() {
    return (
      <div>
        {this.props.items.map(i =>
          <Cell data={i} options={this.props.options || []} />
         )}
       </div>
     );
  }
}

위의 컴포넌트는 re-render될 때 마다 new Array() 를 생성한다. 

In Javascript different instances have different identities and thus the shallow equality check always produces false and tells React to re-render the components.

const default = [];
class Table extends PureComponent {
  render() {
    return (
      <div>
        {this.props.items.map(i =>
          <Cell data={i} options={this.props.options || default} />
         )}
       </div>
     );
  }
}

Functions create identities too

 render안에 함수 생성문을 넣는 것도 같은 이슈이다.

class App extends PureComponent {
  render() {
    return <MyInput
      onChange={e => this.props.update(e.target.value)} />;
  }
}


class App extends PureComponent {
  update(e) {
    this.props.update(e.target.value);
  }
  render() {
    return <MyInput onChange={this.update.bind(this)} />;
 }
}

위의 예제1, 예제2 모두 render할 때 마다 함수를 새로 생성한다. (new function is created with a new identity)

class App extends PureComponent {
  constructor(props) {
    super(props);
    this.update = this.update.bind(this);
  }
  update(e) {
    this.props.update(e.target.value);
  }
  render() {
    return <MyInput onChange={this.update} />;
  }
}

따라서 render안에서는 arraow 함수나 함수 bind를 하지 말고 constructor 에서 bind하여 사용해야 한다.

최대한 컴포넌트를 쪼개라

컴포넌트의 적절한 분리가 이루어지지 않는다면 가독성, 유지보수 등 뿐만 아니라 성능적으로도 매우 좋지 않다.

다음과 같은 state가 있다고 생각해 보자.

state = {
  listItmes: [],
  title: 'Test app'
};

다음은 List를 하나의 컴포넌트로 분리하지 않은 코드이다. (테스트에서 Item 컴포넌트는 PureComponent를 상속 받았다.)

// 아래는 간단히 나타낸 App 컴포넌트의 render 메서드이다.
render() {
  <div className="app">
    ...
    <div className="app-intro">
      {this.state.title}
    </div>
    <div className="list-container">
      <ul>
      {
        this.state.items.map((item) => {
          return <Item key={item.id} {...item} />
        })
      }
      </ul>
    </div>
  </div>
}

위와 같은 구조일 경우, this.setState({title: ''}); 를 할 때 마다 re-render가 되고 render가 불필요한 <ul> 태그 안의 map이 돌게된다. 속도에 상당히 안 좋다.

// 아래는 간단히 나타낸 App 컴포넌트의 render 메서드이다.
render() {
  <div className="app">
    ...
    <div className="app-intro">
      {this.state.title}
    </div>
    <List items={this.state.items} />
  </div>
}

<ul> 태그를 <List />라는 PureComponent로 분리하면 title이 변경되도 <List />의 this.props.items는 변경되지 않았기 때문에 re-render되지 않는다.

성능 비교

  • 분리 전(bad)

  • 분리 후(good)

주의 (warning)

분리된 컴포넌트에 잘못된 props를 전달할 경우 성능이 더 악화될 수 있음.

// 아래는 간단히 나타낸 App 컴포넌트의 render 메서드이다.
render() {
  return (
    <div className="app">
      ...
      <div className="app-intro">
        {this.state.title}
      </div>
      <List items={this.state.items} deleteItem={id => this.deleteItem(id)}/>
    </div>
  );
}

title을 변경할 때 deleteItem={id => this.deleteItem(id)} deleteItem 함수가 새로 생성되기 때문에, 속도가 저하된다.


추가

map 돌면서 bind(this, i)하는 구문 분리 방법

현재 list를 보여주는 화면의 코드를 보면 이벤트 처리를 위해 map을 돌면서 event={this.funcName.bind(this, i)}로 작성돼 있다. 이 부분이 안 좋은 점을 다 갖다 넣은 것이다.

class List extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      items: []
    };
  }
 
 ...
 
  touchItem = (key) => {
    console.log(key);
  };
 
  render() {
    const {items} = this.state;
    
    const cloneItems = items.map((item, i) => {
      return <li onTouchTap={this.touchItem.bind(this, i)}>item.text</li>
    });
 
    return (
	  <ul>
        {cloneItems}
      </ul>
    )
  }
}


class List extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      items: []
    };
  }
 
 ...
 
  touchItem = (key) => {
    console.log(key);
  };
 
  render() {
    const {items} = this.state;
    
    const cloneItems = items.map((item, i) => {
      return <ListItem touchItem={this.touchItem} idx={i} item={item}/>
    });
 
    return (
	  <ul>
        {cloneItems}
      </ul>
    )
  }
}
 
class ListItem extends React.PureComponent {
  constructor(props) {
    super(props);
    this.state = {};
  }
 
  touchItem = () => {
    this.props.touchItem(this.props.idx);  
  };
 
  render() {
    const {item} = this.props; 
   
    return (
      <li onTouchTap={this.touchItem}>item.text</li>
    )
  }
}

map 안에서 return 되어 지는 태그를 컴포넌트로 분리하고 props 로 idx값을 전달해 줘서 함수를 호출할 때 this.props.touchItem(this.props.idx) 로 호출한다.

  • props 함수에 두 개 이상의 파라미터를 던져야 할 경우 pass 순서대로 넣어주면 된다.
// parent
touchItem = (key, value) => {
  console.log(key, value);
}
 
// child
class ListItem extends React.PureComponent {
  constructor(props) {
    super(props);
    this.state = {};
 
    this.touchItemFrom = this.touchItem.bind(this, 'from');
    this.touchItemTo = this.touchItem.bind(this, 'to');
  }
 
  touchItem = (value) => {
    this.props.touchItem(this.props.idx, value);  
  };
 
  render() {
    const {item} = this.props; 
   
    return (
      <li onTouchTap={this.touchItem}>item.text</li>
    )
  }
}
 
touchItem = (value) => {
  this.props.touchItem(this.props.idx, value);
}
 
render() {
  const {item} = this.props; 
   
  return (
    <li>
      <p>item.text</p>
      <button onTouchTap={this.touchItemFrom}>from button</button>
      <button onTouchTap={this.touchItemTo}>to button</button>
    </li>
  )
}


'JavaScript > React' 카테고리의 다른 글

React 로 TDD 쵸큼 맛보기  (0) 2018.07.20
[ReactJS] scroll to top  (0) 2017.07.31
[ReactJS] add component in list  (0) 2016.12.29
[ECMAScript6] get max number in list  (0) 2016.12.28
[React] window.print  (0) 2016.11.30

댓글