[React] 성능 최적화

Yuno·2021년 8월 7일
2

React

목록 보기
4/5
post-thumbnail

React에서 성능 최적화는 효율이 높지 않습니다. 더 중요한 것은 가독성과 생산성입니다.
하지만, 알고 있는데 안하는 것과, 모르는 것은 다릅니다.

프론트엔드에서 성능은 DOM 조작이 가장 큰 부분을 차지합니다.
리액트에서는 DOM을 조작하는 대신, 데이터를 변경하면 다시 렌더링합니다.

이러한 렌더링을 줄이는게 성능 최적화의 가장 간단한 방법입니다.

✨불필요한 렌더링을 막는 법

기본적으로 propsstate가 변경되어, 새로운 엘리먼트가 반환되면
React는 이전 엘리먼트와 비교하여 다른 부분만 실제 DOM에 업데이트 합니다.

변경된 부분만 실제 DOM에 업데이트하더라도, 리렌더링은 여전히 시간이 걸립니다.

불필요한 리렌더링은 다음과 같습니다.

  • props값이 같아도 부모 컴포넌트가 리렌더링 되면, 자식 컴포넌트도 리렌더링됩니다.
  • state값이 같아도, setState를 호출하면 항상 리렌더링 됩니다.

이 때문에, 속도 저하가 눈에 띌 수 있습니다.

다시 렌더링 되기 전에 props, state값이 같다면, 리렌더링을 막아 속도를 높힐 수 있습니다.

메모이제이션 : 동일한 계산을 반복할 때, 이전 계산값을 저장하여 연산의 반복 수행을 제거합니다.

✔ ShouldComponentUpdate

리렌더링 이전에 실행되는 생명주기 메서드 ShouldComponentUpdate를 사용해 리렌더링을 조절합니다.

컴포넌트가 props.colorstate.count를 통해 변경된다고 할 때,
다음처럼, 다른 값일 때만 true를 반환하여 리렌더링 하도록 합니다.

class CounterButton extends React.Component {
  constructor(props) { 
    //...
  }
  
  ShouldComponentUpdate(nextProps,nextState) {
    if (this.props.color !== nextProps.color) {
     return true;
    }
    if (this.state.count !== nextState.count) {
      return true;
    }
    
    return false;
  }
  
  render() {
    return <button style={{ background : this.props.color }}> </button>
  }
}

✔ PureComponent

ShouldComponentUpdate는 리렌더링을 일으키는 요소가 많다면, 조건이 매우 길어질 수 있습니다.
위와 같은 패턴은, React.PureComponent를 상속받는 것으로 대체하여 간단히 할 수 있습니다.

class CounterButton extends React.PureComponent {
  render() {
    return <button style={{ background : this.props.color }}> </button>
  }
}

다만, React.PureComponent얕은 비교로 리렌더링을 막기 때문에 참조형 데이터에 주의해야 합니다.

// parent
function App() {
  return (
    <div className="App">
      <CounterButton color={'red'} />
    </div>
  );
}
// =====

// child
class CounterButton extends React.PureComponent {
  render() {
    const {color} = this.props;

    return (
      <button style={{ background:color }}>
        button
      </button>
    )
  }
}
// =====

위의 예제는 PureComponent가 잘 동작합니다.
color가 바뀌지 않아, 부모가 렌더링 되어도, 다시 렌더링되지 않습니다.


참조형 데이터 주의

자식 컴포넌트의 프로퍼티를 참조형 데이터로 바꾸면 어떻게 될까요?

color 프로퍼티를 colors로 바꾸고, 배열을 할당했습니다.

// parent
function App() {
  return (
    <div className="App">
      <CounterButton colors={['red']} /> // (*)
    </div>
  );
}
// =====

// child
class CounterButton extends React.PureComponent {
  render() {
    const {colors} = this.props;

    return (
      <button style={{ background:colors[0] }}>
        button
      </button>
    )
  }
}
// =====

이번에는, colors가 바뀌지 않음에도 부모가 렌더링되면 같이 렌더링됩니다.

클래스형 컴포넌트에서, 렌더링 될 때마다 render 메서드가 호출됩니다.
['red']는 리터럴로 render의 호출마다 (값은 같지만)새로운 배열을 만들어 할당합니다.

얕은 비교를 하기 때문에 배열의 값이 같아도 다른 참조를 가지는 배열입니다.

미리 선언하여, 해결합니다.

function App() {
  colors = ['red']; // (*)
   
  return (
    <div className="App">
      <CounterButton colors={this.colors} />
    </div>
  );
}

클래스형 컴포넌트는, 인스턴스화 되고 라이프 사이클에 맞게 메서드가 호출됩니다.
클래스 필드로 선언하면, 다시 실행되지 않아 렌더링되어도 같은 참조의 배열이 할당됩니다.

✔ memo

함수형 컴포넌트에서는, React.memo()를 export하여 값이 같다면 리렌더링을 막을 수 있습니다.

function CounterButton(props) {
  return(
    <button style={{background:props.color}}>
        button
    </button>
  )
}

export default React.memo(CounterButton);

함수형 컴포넌트도 마찬가지로 참조형 데이터를 주의해야합니다.
추가로, 함수형 컴포넌트는 리렌더링 될 때, 함수가 다시 실행됩니다.

배열이나 객체같은 참조형 데이터를 할당할 때,
함수 내에서 선언하면 렌더링 마다 매번 선언됩니다.

아래 코드는 불필요한 렌더링을 막지 않습니다.

// parent
function App() {
  // 렌더링 될 때마다, 함수가 실행
  // 매번 할당되는 color는 다른 참조를 가진다.
  const color = {main:'red'};
  
  return(
    <CounterButton color={color}> // 매번 다른 프로퍼티를 할당한다.
        button
    </CounterButton>
  )
}
// =====


// child
function CounterButton(props) {
  return(
    <button style={{background:props.color}}> // props가 달라 렌더링된다.
        button
    </button>
  )
}

export default React.memo(CounterButton);
// =====

함수형 컴포넌트에서 콜백을 할당할 때

종종 부모 컴포넌트에서 만든 함수를 자식 컴포넌트에 할당하게 됩니다.

function app() {
  const [count,setCount] = useState(0);
  
  const increaseCount = () => {
    setCount(count=> count+1);
  }
  
  const decreaseCount = () => {
    setCount(count=> count-1);
  }
  
  return (
    <CounterButton decrease={decreaseCount}/>
  )
}

함수도 참조형 데이터입니다.

위 처럼 콜백 함수를 할당하면, 부모 컴포넌트가 렌더링 될 때마다 decreseCount가 다시 선언됩니다.
그리고 매번 다른 콜백함수가 할당됩니다.

때문에, decreseCounte의 내용은 그대로지만 CounterButton은 리렌더링 됩니다.

useCallback

useCallback은 메모이제이션 된 콜백을 반환합니다.
콜백 함수의 내용과, 의존성 값을 배열에 전달하여 만듭니다.
메모이제이션된 콜백은 의존성이 변경되었을 때만 변경됩니다.

Hooks Reference : 불필요한 렌더링을 방지하기 위해,
자식컴포넌트에 콜백을 전달할 때 유용하다고 되어 있습니다.

함수형 컴포넌트는 렌더링 마다 함수를 실행하기 때문에, 이를 방지하는 여러 Hooks가 존재합니다.

function app() {
  const [count,setCount] = useState(0);
  
  const decreaseCount = useCallback(() => { // 메모이제이션된 콜백을 반환
    setCount(count=> count-1);
  },[])
  
  return (
    <CounterButton decrease={decreaseCount}/> // 같은 참조를 가진 콜백을 할당
  )
}

React의 성능을 최적화 하기 위해, 불필요한 렌더링을 막아야합니다.
렌더링시키는 props, state의 값이 같을 때는 렌더링 하지 않으면 됩니다.
다만, 참조형 데이터에 주의하여 할당합니다.

profile
web frontend developer

0개의 댓글