ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 11/17 TIL | React 성능 최적화 1탄. React.memo & React.PureComponent
    📝 기록/매일의 기록 2022. 11. 17. 16:30

    오늘은 어제 수요 지식회에서 언급됐었던 React 성능 최적화에 대해 한번 정리해보려고 한다. 시작하기에 앞서, 사실 성능 최적화는 엄청나게 중요한 요소는 아니다. 물론 미리 해놓으면 언젠가는 좋겠지만! 그건 내가 만드는 서비스가 성능 최적화에 대해서 고민해야 할 정도로 잘 나갈 때 고려해야 하는 문제이다..!

    최적화하기 위한 코드는 가독성이 좋지 않고 프로그램 복잡도가 높아져 유지 보수가 힘들어지는데, 일어나지도 않을 미래의 일을 생각하면서 코드의 본질을 놓치면 절대 안 된다는 것이다(열심히 차려놓은 잔칫집에 손님 한 명도 안 오면 어떡하냐며..😅). 더군다나 지금은 램이 128GB까지도 가능한 2022년! 성능 최적화를 고려할 정도로 컴퓨터 사양이 좋지 못한 시절이 결코 아니라는 거~!

    React에서의 성능 최적화도 역시 마찬가지이다. 코드 1000줄 정도 리팩토링하면 0.001초 개선되는 수준이기에 주니어가 신경 써야 할 정도로 중요한 사안은 아니다. 또한 대부분의 비지니스들이 성능 최적화와는 좀 유리되어 있기 때문에 처음부터 그렇게까지 고려하지 않아도 된다!(하지만 Three.js, D3.js와 같이 몇 천 개, 몇 만개의 배열과 객체 정보를 가지고 DOM을 조작하는 작업에는 성능 최적화가 중요하다.) 그러니 성능 이슈가 크게 문제 될 만한 곳에만 최적화를 하도록 하자! 

    다만 중요하지 않다고 성능 최적화에 대해 몰라도 된다는 소리는 아니다. 그러니 오늘 한번 깔끔하게 정리해보자!


    프론트엔드 개발자에게 대부분의 작업은 DOM 조작이다. DOM 조작은 CPU 리소스를 많이 사용하고 브라우저에 부하를 주기 때문에 React의 성능 최적화라고 하면 리렌더링이 되지 않게 하는 팁이 대부분이다.

    👾 React.memo와 React.PureComponent

    React.PureComponent는 클래스 컴포넌트, React.memo는 함수 컴포넌트에서 사용하는데, 이걸 사용하게 되면 props 값이 변하지 않는 경우 리렌더링하지 않는다. 뭔가 이렇게만 봤을 때는 당연한 거 아닌가? 싶을 수도 있는데, 다시 생각해보면 부모 컴포넌트가 리렌더링되면 자식 컴포넌트도 모두 리렌더링된다. 즉, props 값이 변하든 안 변하든 부모가 리렌더링하면 자식 컴포넌트도 리렌더링된다는 것이다. 그렇기 때문에 props는 변함이 없고 부모 컴포넌트에서 렌더링이 많이 일어날 경우 사용하면 좋다. 코드로도 한번 살펴 보자.

    const Modal = ({ title }) => { // 1️⃣
      const [buttonColor, setButtonColor] = useState('yellow'); // 2️⃣
    
      return (
        <div>
          <p>{title}</>
          <button onClick={() => setButtonColor('black')}>버튼 색상 변경!</button> // 3️⃣
          <CounterButton color={buttonColor} /> // 4️⃣
        </div>
      );
    }
     

    1️⃣ 만약 이런 Modal이라는 부모 컴포넌트가 있다고 가정해보자.

    2️⃣ 초기값으로 노란색을 가지고 있는 buttonColor라는 상태 값과 그의 세터 함수를 선언한다.

    3️⃣ '버튼 색상 변경!' 버튼을 클릭하면 검은색으로 buttonColor의 상태 값이 바뀐다.

    4️⃣ CounterButton이라는 자식 컴포넌트에 color라는 props로 buttonColor state를 넘겨주고 있다.

    // 함수 컴포넌트.ver
    const CounterButton = ({ color }) => {
      const [count, setCount] = useState(1);  
    
      return (
        <button
          color={color}
          onClick={() => setCount(prevCount => prevCount + 1)}>
          Count: {count}
        </button>
      );
    }
    
    export default React.memo(CounterButton); // 1️⃣
    // 클래스 컴포넌트.ver
    class CounterButton extends React.PureComponent { // 2️⃣
      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>
        );
      }
    }

    1️⃣ & 2️⃣ 자식 컴포넌트인 CounterButton에서 React.memo(함수 컴포넌트), React.PureComponent(클래스 컴포넌트)로 묶어주면 부모 컴포넌트인 Modal 리렌더링된다고 해도 CounterButton은 리렌더링되지 않는다.

    React.memo와 React.PureComponent는 props 간의 비교를 통해 변화가 일어났을 때만 렌더링을 한다. 이런 걸 '메모이제이션(Memoization)'이라고 하는데, 프로그래밍을 할 때 반복되는 결과를 메모리에 저장해서 다음에 같은 결과가 나올 때 빨리 실행하는 코딩 기법을 말한다. 실제로 React.memo의 memo는 메모이제이션을 의미하는 것!

    다만 얕은 비교를 통해 최적화가 이루어지기 때문에 기본형(primitive type)의 값 비교는 잘하지만, 참조형 데이터 타입의 경우 깊은 비교가 되지 않기 때문에 원하는 대로 동작이 안 된다. 이 내용은 내일 TIL로 정리해보려고 한다!(Stay tuned✌🏻)

    😇 객체의 불변성 유지하기

    ⚠️ In order to allow React to quickly discern new changes in your state object, you should avoid object mutations and instead create a new object.

    → React가 state 객체의 새로운 변경 사항을 빠르게 식별할 수 있도록 객체 자체를 접근해서 수정하지 말고, 새로운 객체를 만들어야 한다.


    불변성을 유지하기 위해서는 state의 값 자체를 변경하지 말고, spread 문법(...), Object.assign 등을 사용하면 된다. 코드를 함께 살펴보자!

    👎 Bad

    // ❌ state가 객체나 배열일 때 값 자체 변경은 하면 안됨
    handleClick() {
      this.setState(state => ({
        words: state.words.concat(['foo'])
      }));
    }
    
    // ❌ 역시나 이렇게 값 자체 변경은 절대 NOPE!
    function updateColorMap(colormap) {
      colormap.right = 'blue';
    }

    👍 Good

    // ⭕️ ES6 Spread Syntax
    handleClick() {
      this.setState(state => ({
        words: [...state.words, 'foo'],
      }));
    };
    
    // ⭕️ ES6 Object.assign()
    function updateColorMap(colormap) {
      return Object.assign({}, colormap, {right: 'blue'});
    }
    
    // ⭕️ ES8 Object Spread Properties
    function updateColorMap(colormap) {
      return {...colormap, right: 'blue'};
    }

    정리하다 보니 생각보다 양이 많아져 시리즈를 2개로 쪼개기로 결심하였다. 내일은 참조형 타입, 얕은 비교에 대해서 정리해보려고 한다! (to be continued..)