본문 바로가기

Personal Posting/ReactJS

React Hook 정리 - 컴퍼넌트 최적화 (React.Memo)

 

어떤 한 컴퍼넌트에 다른 하위 컴퍼넌트를 포함하고 있을 때 상위 컴퍼넌트가 값(props)이 변경되어 재 렌더링되면 부득이하게 값의 변경 없는 하위 컴퍼넌트도 불필요한 렌더링을 할 수 있다.
이럴 때 React.memo를 사용한다. React.memo는 React에서 제공하는 HOC(High Order Component) 고차 컴퍼넌트이다.

 

고차컴퍼넌트란 어떤 컴퍼넌트란 어떠한 컴퍼넌트를 인자로 받아 새로운 컴퍼넌트를 반환하는 컴퍼넌트이다.
React.memo라는 HOC에 컴퍼넌트를 넣으면 UI나 기능적으로는 동일하나 최적화된 컴퍼넌트를 반환하는 것이다.

https://ko.legacy.reactjs.org/docs/higher-order-components.html

 

고차 컴포넌트 – React

A JavaScript library for building user interfaces

ko.legacy.reactjs.org

 

다만 불필요한 React.memo 사용은 오히려 역효과가 날 수 있기 때문에 아래와 같은 상황에서 사용하는 것이 좋다.
1. 컴퍼넌트가 같은 props로 렌더링이 자주 발생할 때
2. 컴퍼넌트가 렌더링될 때 복잡한 처리 로직이 존재할 때

 

📌 여기서 잠깐, 지난 번에 정리한 useMemo와 React.memo의 차이점에 대해

useMemo
useMemo는 함수 컴포넌트 내에서 계산 비용이 높은 연산의 결과를 캐싱하고, 의존성 배열에 변화가 없는 한 이전 결과를 재사용하는 데 사용된다. 예를 들어, 계산이 복잡한 객체나 배열을 만들 때, 해당 값이 변경되지 않는 한 useMemo를 사용하여 이전 결과를 다시 사용할 수 있고, 렌더링 성능을 최적화할 수 있다.

const memoizedValue = useMemo(() => expensiveCalculation(a, b), [a, b]);

 

React.memo
React.memo는 컴포넌트의 렌더링 결과를 Memoization하고, props가 변경되지 않았을 때 이전 결과를 재사용하는 데 사용된다.
함수 컴포넌트를 감싸서 memoized 버전을 생성하며, props가 변경되지 않으면 해당 컴포넌트는 다시 렌더링되지 않는다.

const MyComponent = React.memo(({ prop1, prop2 }) => {
  // 컴포넌트 렌더링 로직
});

 

즉, useMemo는 값의 캐싱과 재사용에 사용되고, React.memo는 컴포넌트의 렌더링 결과를 캐싱하여 props가 변경되지 않으면 이전 결과를 재사용한다.

 

다만 여기서 props가 원시데이터가 아닐 경우에는 의도대로 동작하지 않는 경우도 있다. 아래의 코드를 확인해보자.

// App.jsx
import { useState } from 'react';
import Child from './Child';

function App() {
  const [parentAge, setPareneAge] = useState(0);

  const increaseParentAge = () =>{
    setPareneAge(parentAge + 1);
  };

  window.alert('Rendered Parent Component!!');

  const name = {
    lastName: 'Jim',
    firstName: 'Carry',
  }

  return (
    <div style={{border: '2px solid navy', padding: '10px'}}>
      <h1>Parent</h1>
      <p>age: {parentAge}</p>
      <button onClick={increaseParentAge}>Increase Parent's Age</button>
      <Child name={name} />
    </div>
  );
}

export default App;

// Child.jsx
import React, {memo} from "react";

const Child = ({name}) => {
    window.alert('Rendered Child Component!!');
    return(
        <div style={{border: '2px solid powderblue', padding: '10px'}}>
            <h3>Child</h3>
            <p>First name: {name.firstName}</p>
            <p>Last name: {name.lastName}</p>
        </div>
    );
};

export default memo(Child);

 

위 코드에서 부모 컴퍼넌트는 자식 컴퍼넌트에게 props로 name이라는 오브젝트를 전달한다.

이처럼 오브젝트로 전달되는 경우에는 단순값을 비교하는 것이 아닌 오브젝트의 주소값을 체크하기 때문에 React.memo에서는 다른 값으로 판단하여 처리하게 되고, 의도와는 달리 항상 Child컴퍼넌트를 재렌더링하게 된다.

 

이 경우, name오브젝트를 useMemo를 이용하여 Memoization으로 처리하면 문제를 해결할 수 있다.

const name = useMemo(()=>{
    return{
      lastName: 'Jim',
      firstName: 'Carry',
    };
  }, []);

 

다음으로는 useCallback과 React.memo를 사용한 예시를 살펴보기로 한다.

// App.jsx
import { useState } from 'react';
import Child from './Child';

function App() {
  const [parentAge, setPareneAge] = useState(0);

  const increaseParentAge = () =>{
    setPareneAge(parentAge + 1);
  };

  const callMe = () => {
    window.alert('Hey you what\'s up!!');
  }

  window.alert('Rendered Parent Component!!');

  return (
    <div style={{border: '2px solid navy', padding: '10px'}}>
      <h1>Parent</h1>
      <p>age: {parentAge}</p>
      <button onClick={increaseParentAge}>Increase Parent's Age</button>
      <Child name={'My Name'} callMe={callMe} />
    </div>
  );
}

export default App;

// Child.jsx
import React, {memo} from "react";

const Child = ({name, callMe}) => {
    window.alert('Rendered Child Component!!');
    return(
        <div style={{border: '2px solid powderblue', padding: '10px'}}>
            <h3>Child</h3>
            <p>name: {name}</p>
            <button onClick={callMe}>Start Calling</button>
        </div>
    );
};

export default memo(Child);

 

사실 위 코드도 이전과 큰 차이가 없다. 단순히 오브젝트가 함수로 바뀐 것 뿐이다.

함수도 동일하게 함수 객체의 메모리 주소가 props로 전달되기 때문에 React.memo는 이를 다른 값으로 판단하여 Child컴퍼넌트를 재렌더링 하는 것이다. 이를 해결하기 위해서는 useCallback을 사용한다.

 

정리하자면, 컴퍼넌트의 불필요한 재렌더링을 막기위해서는 React.memo를 사용할 수 있으며, React.memo는 props변화를 감지하여 컴퍼넌트 렌더링 여부를 결정한다. 단, props에 메모리 주소값을 참조해야하는 오브젝트가 전달될 경우에는 useMemo 또는 useCallback을 함께 활용하여 컴퍼넌트의 변경 여부를 정확하게 체크할 수 있다.

 

마지막으로 염두해두어야 할 점은 React.memo는 오직 props의 변화를 기준으로 동작하기 때문에 useState, useReducer, useContext 등의 상태관리 Hook을 사용하는 경우에는 props의 변화가 없더라도 컴퍼넌트는 여전히 다시 렌더링 될 수 있다.

 

<출처: 별코딩 https://youtu.be/oqUgcxwrnSY?si=U-auROHcrz1hcrFH>