6-11. 최적화 2 - 컴포넌트 재 사용

2022. 7. 6. 14:04React/한입 크기로 잘라 먹는 리액트(React.js)

count가 업데이트되면, 부모컴포넌트가 리렌더링 하기 때문에 자식 컴포넌트들도 리렌더링하게 된다.

근데 여기서 TextView 컴포넌트는 리렌더링할 필요가 없음을 알게된다. >> 성능상 문제 및 낭비

그래서 count가 업데이트 될 때, CountView만 리렌더링 되도록 조건을 주는 기능을 찾아보자.

실제 코드상으로 할 수 있는 기능이 있다. 이는 바로

 

React.memo - 함수형 컴포넌트에게 업데이트 조건을 거는 기능

 

자 우선 React 공식문서 사이트로 이동을 해보자.

https://ko.reactjs.org/

 

React – 사용자 인터페이스를 만들기 위한 JavaScript 라이브러리

A JavaScript library for building user interfaces

ko.reactjs.org

문서의 > API 참고서에 접속하면(https://ko.reactjs.org/docs/react-api.html) > React.memo를 찾을 수 있을 것이다.

그럼 다음과 같이 접속하여 설명을 볼 수 있을 것이다. (https://ko.reactjs.org/docs/react-api.html#reactmemo)

React.memo

const MyComponent = React.memo(function MyComponent(props) {
  /* props를 사용하여 렌더링 */
});

React.memo는 고차 컴포넌트(Higher Order Component)입니다.

컴포넌트가 동일한 props로 동일한 결과를 렌더링해낸다면, React.memo를 호출하고 결과를 메모이징(Memoizing)하도록 래핑하여 경우에 따라 성능 향상을 누릴 수 있습니다. 즉, React는 컴포넌트를 렌더링하지 않고 마지막으로 렌더링된 결과를 재사용합니다.

React.memo는 props 변화에만 영향을 줍니다. React.memo로 감싸진 함수 컴포넌트 구현에 useState, useReducer 또는 useContext 훅을 사용한다면, 여전히 state나 context가 변할 때 다시 렌더링됩니다.

props가 갖는 복잡한 객체에 대하여 얕은 비교만을 수행하는 것이 기본 동작입니다. 다른 비교 동작을 원한다면, 두 번째 인자로 별도의 비교 함수를 제공하면 됩니다.

function MyComponent(props) {
  /* props를 사용하여 렌더링 */
}
function areEqual(prevProps, nextProps) {
  /*
  nextProps가 prevProps와 동일한 값을 가지면 true를 반환하고, 그렇지 않다면 false를 반환
  */
}
export default React.memo(MyComponent, areEqual);

이 메서드는 오직 성능 최적화를 위하여 사용됩니다. 렌더링을 “방지”하기 위하여 사용하지 마세요. 버그를 만들 수 있습니다.

주의

class 컴포넌트의 shouldComponentUpdate() 메서드와 달리, areEqual 함수는 props들이 서로 같으면 true를 반환하고, props들이 서로 다르면 false를 반환합니다. 이것은 shouldComponentUpdate와 정반대의 동작입니다.


더보기

고차 컴포넌트

고차 컴포넌트(HOC, Higher Order Component)는 컴포넌트 로직을 재사용하기 위한 React의 고급 기술입니다. 고차 컴포넌트(HOC)는 React API의 일부가 아니며, React의 구성적 특성에서 나오는 패턴입니다.

구체적으로, 고차 컴포넌트는 컴포넌트를 가져와 새 컴포넌트를 반환하는 함수입니다.

const EnhancedComponent = higherOrderComponent(WrappedComponent);

컴포넌트는 props를 UI로 변환하는 반면에, 고차 컴포넌트는 컴포넌트를 새로운 컴포넌트로 변환합니다.

고차 컴포넌트(HOC)는 Redux의 connect와 Relay의 createFragmentContainer와 같은 서드 파티 React 라이브러리에서 흔하게 볼 수 있습니다.

React.memo가 고차 컴포넌트로 함수처럼 호출이 되고, 매개변수들을 컴포넌트로 전달하게 되면 더 강화된 새로운 컴포넌트로 반환하게 되어서 이로 MyComponent로 반환할 수 있게 된다.

 

컴포넌트가 동일한 props로 동일한 결과를 렌더링해낸다면, React.memo를 호출하고 결과를 메모이징(Memoizing)하도록 래핑하여 경우에 따라 성능 향상을 누릴 수 있습니다. 즉, React는 컴포넌트를 렌더링하지 않고 마지막으로 렌더링된 결과를 재사용합니다.

>> 똑같은 prop을 반환하면 똑같은 prop을 반환하겠다는 뜻이다. (rerendering하지 않겠다는 뜻)

>> props가 변경되지 않으면 rerendering되지 않은 강화된 컴포넌트로 반환하겠다는 뜻 (자기자신 state 변하면 rerendering됨)

 

그럼 한번 만들어보자

실습

먼저 OptimizeTest.js파일을 만들어서 실행해보자(이건 실습 후 지워야한다.)

OptimizeTest.js 코드

더보기
import React, { useState, useEffect } from "react";

// useEffect로 state 변화를 console로 확인할 수 있다.
const Textview = ({ text }) => {
  useEffect(() => {
    console.log(`Update :: Text : ${text}`);
  });
  return <div>{text}</div>;
};

const Countview = ({ count }) => {
  useEffect(() => {
    console.log(`Update :: Count : ${count}`);
  });
  return <div>{count}</div>;
};

const OptimizeTest = () => {
  const [count, setCount] = useState(1);
  const [text, setText] = useState("");
  return (
    <div style={{ padding: 50 }}>
      <div>
        <h2>count</h2>
        <Countview count={count} />
        <button onClick={() => setCount(count + 1)}>+</button>
      </div>
      <div>
        <h2>text</h2>
        <Textview text={text} />
        <input value={text} onChange={(e) => setText(e.target.value)} />
      </div>
    </div>
  );
};

export default OptimizeTest;
const Textview = ({ text }) => {
  useEffect(() => {
    console.log(`Update :: Text : ${text}`);
  });
  return <div>{text}</div>;
};

const Countview = ({ count }) => {
  useEffect(() => {
    console.log(`Update :: Count : ${count}`);
  });
  return <div>{count}</div>;
};

이와 같이 useEffect로 state변화를 console로 확인할 수 있는데, count나 text입력시 state가 변화되어 console로 다음과 같이 계속 출력되는 것을 확인할 수 있다.

state가 변화되어 자식 component인 CountView, TextView 모두가 rendering되는 것을 알 수 있는데 이를 낭비상황이 일어났다 할 수 있다.(Count만 변화되었는데, Text까지 렌더링되었기 때문)

 

>> 이 문제를 Component 재사용 기능을 이용하여 해결할 수 있다.

 

아래와 같이 React.memo기능을 통해 각각 컴포넌트의 prop들이 바뀌면 해당 Component만 변경되어 console로 출력되는것을 확인할 수 있다.

OptimizeTest.js 코드

더보기
import React, { useState, useEffect } from "react";

// useEffect로 state 변화를 console로 확인할 수 있다.
// React.memo 사용
// Prop인 text가 변하지 않는이상 다시 렌더링 하지 않는다.
const Textview = React.memo(({ text }) => {
  useEffect(() => {
    console.log(`Update :: Text : ${text}`);
  });
  return <div>{text}</div>;
});

const Countview = React.memo(({ count }) => {
  useEffect(() => {
    console.log(`Update :: Count : ${count}`);
  });
  return <div>{count}</div>;
});

const OptimizeTest = () => {
  const [count, setCount] = useState(1);
  const [text, setText] = useState("");
  return (
    <div style={{ padding: 50 }}>
      <div>
        <h2>count</h2>
        <Countview count={count} />
        <button onClick={() => setCount(count + 1)}>+</button>
      </div>
      <div>
        <h2>text</h2>
        <Textview text={text} />
        <input value={text} onChange={(e) => setText(e.target.value)} />
      </div>
    </div>
  );
};

export default OptimizeTest;
// useEffect로 state 변화를 console로 확인할 수 있다.
// React.memo 사용
// Prop인 text가 변하지 않는이상 다시 렌더링 하지 않는다.
const Textview = React.memo(({ text }) => {
  useEffect(() => {
    console.log(`Update :: Text : ${text}`);
  });
  return <div>{text}</div>;
});

const Countview = React.memo(({ count }) => {
  useEffect(() => {
    console.log(`Update :: Count : ${count}`);
  });
  return <div>{count}</div>;
});

더보기
import React, { useState, useEffect } from "react";

const CounterA = React.memo(({ count }) => {
  useEffect(() => {
    console.log(`CounterA Update - count : ${count}`);
  });
  return <div>{count}</div>;
});

const CounterB = React.memo(({ obj }) => {
  useEffect(() => {
    console.log(`CounterB Update - count : ${obj.count}`);
  });
  return <div>{obj.count}</div>;
});

const OptimizeTest = () => {
  const [count, setCount] = useState(1);
  const [obj, setObj] = useState({
    count: 1,
  });
  return (
    <div style={{ padding: 50 }}>
      <div>
        <h2>Counter A</h2>
        <CounterA count={count} />
        {/* 기본 값이 1이기 때문에 상태변화가 없어서 console이 출력되지 않는다. */}
        <button onClick={() => setCount(count)}>A button</button>
      </div>
      <div>
        <h2>Count B</h2>
        {/* obj는 객체이기 때문에(객체는 얕은 비교를 하기 때문) 상태변화가 일어나서 console로 나타나는 것을 확인할 수 있다. */}
        <CounterB obj={obj} />
        <button
          onClick={() =>
            setObj({
              count: obj.count,
            })
          }
        >
          B button
        </button>
      </div>
    </div>
  );
};

export default OptimizeTest;
      <div>
        <h2>Counter A</h2>
        <CounterA count={count} />
        {/* 기본 값이 1이기 때문에 상태변화가 없어서 console이 출력되지 않는다. */}
        <button onClick={() => setCount(count)}>A button</button>
      </div>
      <div>
        <h2>Count B</h2>
        {/* obj는 객체이기 때문에(객체는 얕은 비교를 하기 때문) 상태변화가 일어나서 console로 나타나는 것을 확인할 수 있다. */}
        <CounterB obj={obj} />
        <button
          onClick={() =>
            setObj({
              count: obj.count,
            })
          }
        >
          B button
        </button>
      </div>

CountA에서 count는 기본 값이 1이기 때문에 상태변화가 없어서 console로 출력되지 않는데,

CountB에서 count는 객체이기때문에 상태변화가 일어나 console로 출력이 된다.

그 이유는 객체는 얕은 비교를 하기 때문인데 객체를 비교하는 방법은 다음과 같다.

function MyComponent(props) {
  /* props를 사용하여 렌더링 */
}
function areEqual(prevProps, nextProps) {
  /*
  nextProps가 prevProps와 동일한 값을 가지면 true를 반환하고, 그렇지 않다면 false를 반환
  */
}
export default React.memo(MyComponent, areEqual);

그러면 obj를 얕은비교를 하지 않도록하여 메모리를 최적화 해보자

더보기
import React, { useState, useEffect } from "react";

const CounterA = React.memo(({ count }) => {
  useEffect(() => {
    console.log(`CounterA Update - count : ${count}`);
  });
  return <div>{count}</div>;
});

// React.memo 해제
const CounterB = ({ obj }) => {
  useEffect(() => {
    console.log(`CounterB Update - count : ${obj.count}`);
  });
  return <div>{obj.count}</div>;
};

const areEqual = (preProps, nextProps) => {
  // if (preProps.obj.count === nextProps.obj.count) {
  //   return true; // 이전 props와 현재 props가 같다. -> rerendering을 일으키지 않게 된다.
  // }
  // return false; // 이전과 현재가 다르다 -> rerendering을 일으켜라
  // 위와 같이 하거나 아니면 아래와 같이하면된다.
  return preProps.obj.count === nextProps.obj.count;
};

const MemoizedCounterB = React.memo(CounterB, areEqual);

const OptimizeTest = () => {
  const [count, setCount] = useState(1);
  const [obj, setObj] = useState({
    count: 1,
  });
  return (
    <div style={{ padding: 50 }}>
      <div>
        <h2>Counter A</h2>
        <CounterA count={count} />
        {/* 기본 값이 1이기 때문에 상태변화가 없어서 console이 출력되지 않는다. */}
        <button onClick={() => setCount(count)}>A button</button>
      </div>
      <div>
        <h2>Count B</h2>
        {/* CounterB일때, obj는 객체이기 때문에(객체는 얕은 비교를 하기 때문) 상태변화가 일어나서 console로 나타나는 것을 확인할 수 있다. */}
        {/* MemoizedCounterB일때 얕은비교를 하지 않는것을 알 수 있다. */}
        <MemoizedCounterB obj={obj} />
        <button
          onClick={() =>
            setObj({
              count: obj.count,
            })
          }
        >
          B button
        </button>
      </div>
    </div>
  );
};

export default OptimizeTest;
const CounterB = ({ obj }) => {
  useEffect(() => {
    console.log(`CounterB Update - count : ${obj.count}`);
  });
  return <div>{obj.count}</div>;
};

먼저 React.memo를 해제하고 아래와 같이 함수들을 만들어준다.

const areEqual = (preProps, nextProps) => {
  // if (preProps.obj.count === nextProps.obj.count) {
  //   return true; // 이전 props와 현재 props가 같다. -> rerendering을 일으키지 않게 된다.
  // }
  // return false; // 이전과 현재가 다르다 -> rerendering을 일으켜라
  // 위와 같이 하거나 아니면 아래와 같이하면된다.
  return preProps.obj.count === nextProps.obj.count;
};

const MemoizedCounterB = React.memo(CounterB, areEqual);

그렇게 하고, return 에서 CountB함수를 아래와 같이 바꾸면 얕은비교를 하지 않아 값이 같다고 인지하여 rerendering하지 않아 console로 출력하지 않는 것을 알 수 있다.

<MemoizedCounterB obj={obj} />

 

이로인해 객체도 얕은 비교를 하지 않아 같은 값임을 인지하게하여

동일한 연산을 다시 rerendering하지 않게하여 연산최적화를 할 수 있게 됨을 알게 되었다.

 

OptimizeTest.js 파일은 최적화 파일이기에 지워주는 것이 좋기에 삭제를 바란다.(테스트용으로만 쓰길)