최적화2 - React.memo

조뮁·2022년 11월 6일

React

목록 보기
17/34

  • count와 text 두 개의 state를 가지고 있는 앱 컴포넌트가 있을 때, count state만 업데이트 하여도 자식 컴포넌트인 CountView 컴포넌트와 TextView 컴포넌트 둘 다 리랜더링 된다.

  • 컴포넌트 리랜더링의 낭비를 방지하기 위해 자식 컴포넌트(CountView, TextView)에 각각 업데이트 될 조건을 걸어줄 수 있음

React.memo

리액트 공식 문서
https://ko.reactjs.org/

const MyComponent = React.memo(function MyComponent(props) {
  /* props를 사용하여 렌더링 */
});
React.memo는 고차 컴포넌트(Higher Order Component)입니다.

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

  • React.memo는 고차 컴포넌트로서 함수처럼 호출로서 호출 되고, 매개변수로는 컴포넌트를 받는다.
  • React.memo의 매개변수로 들어온 컴포넌트에 동일한 state가 들어올 경우, memoizing을 통해 똑같은 결과를 내놓음 (리랜더 되지 않음)
  • 자기 자신의 state가 바뀌면 리랜더링됨

실습

import { useState, useEffect } from "react";

const TextView = ({ text }) => {
  return <div>{text}</div>;
};
const CountView = ({ count }) => {
  return <div>{count}</div>;
};

const OptimizeTest = () => {
  const [count, setCount] = useState(1);
  const [text, setText] = useState("");

  return (
    <div style={{ padding: 30, border: `1px solid gray` }}>
      <div>
        <h2>count</h2>
        {/* 자식 컴포넌트에 각각 count와 text를 prop으로 전달 */}
        <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 추가
import { useState, useEffect } from "react";

const TextView = ({ text }) => {
  // useEffect에 두 번째 인자로 배열을 받지 않았으니 모든 state가 변할때마다 실행됨
  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: 30, border: `1px solid gray` }}>
      <div>
        <h2>count</h2>
        {/* 자식 컴포넌트에 각각 count와 text를 prop으로 전달 */}
        <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;


--> count, text state가 변화할 때 마다 두 컴포넌트 모두 리랜더됨 (낭비 발생)

React.memo 사용 (1)

const TextView = React.memo(({ text }) => {
  // 각 컴포넌트의 업데이트 상태 알아보기 위해 useEffect 추가
  // useEffect에 두 번째 인자로 배열을 받지 않았으니 모든 state가 변할때마다 실행됨
  useEffect(() => {
    console.log(`update === Text : ${text}`);
  });
  return <div>{text}</div>;
});
  • TextView 컴포넌트를 React.memo() 적용
    - TextView 컴포넌트는 prop인 text state가 변경되지 않는다면 리랜더되지 않음
    - text state가 변경될 시, CountView와 TextView 컴포넌트가 모두 리랜더 되는 것을 볼 수 있음 (countView 컴포넌트는 React.memo 되지 않았기 때문)


React.memo 사용 (2)

객체 prop사용

import React, { useState, useEffect } from "react";

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

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


const OptimizeTest = () => {
  
  const [count, setCount] = useState(1);
  const [obj, setObj] = useState({
    count: 1,
  });

  return (
    <div style={{ padding: 30, border: `1px solid gray` }}>
      <div>
        <h2>count A</h2>
        <CounterA count={count}></CounterA>
        <button
          onClick={() => {
            setCount(count);
          }}
        >
          A Button
        </button>
      </div>
      <div>
        <h2>count B</h2>
        <CounterB obj={obj}></CounterB>
        <button
          onClick={() => {
            setObj({
              count: obj.count,
            });
          }}
        >
          B Button
        </button>
      </div>
    </div>
  );
};

export default OptimizeTest;

  • A버튼을 누르면 count state가 1 -> 1로 동일하기 때문에 리랜더 되지 않음
  • B버튼을 눌러도 obj.count는 1 -> 1로 동일해서 아무 일도 일어나지 않을 것 같지만 B컴포넌트는 리랜더됨

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

function MyComponent(props) {
  /* props를 사용하여 렌더링 */
}
function areEqual(prevProps, nextProps) {
  /*
  nextProps가 prevProps와 동일한 값을 가지면 true를 반환하고, 그렇지 않다면 false를 반환
  */
}
export default React.memo(MyComponent, areEqual);
  • 얕은비교 : 객체의 값이 아니라 객체를 가르키고 있는 주소값을 비교하는 것.
    얕은비교 예시
    얕은비교 예시

객체 props 비교

import React, { useState, useEffect } from "react";

/* CounterB 컴포넌트에서 얕은 비교를 하지 않게 하여 최적화하기 */
const CounterB = ({ obj }) => {
  useEffect(() => {
    console.log(`BBBBB update === obj: ${obj.count}`);
  });
  return <div>{obj.count}</div>;
};

// 객체 동일 여부 판단하는 함수
const areEqual = (prevProps, nextProps) => {
  // return true // prevProps === nextProps => 리랜더링 하지 않음
  // return false // prevProps !== nextProps => 리랜더링 실행
  if (prevProps.obj.count === nextProps.obj.count) {
    return true;
  }
  return false;
};

// counterB에 React.memo() 적용한 컴포넌트 (state동일 여부에 따라 memoized 되는 컴포넌트)
// React.memo는 컴포넌트를 반환하는 고차 컴포넌트이기 때문에, MemoizedCounterB를 컴포넌트처럼 사용 가능
const MemoizedCounterB = React.memo(counterB, areEqual);

const OptimizeTest = () => {
  const [count, setCount] = useState(1);
  const [obj, setObj] = useState({
    count: 1,
  });

  return (
    <div style={{ padding: 30, border: `1px solid gray` }}>
      <div>
        <h2>count B</h2>
        <MemoizedCounterB obj={obj} />
        <button
          onClick={() =>
            setObj({
              count: obj.count,
            })
          }
        >
          B Button
        </button>
      </div>
    </div>
  );
};


  • 주의 : 다음과 같이 컴포넌트 내에 state를 생성했을 때, obj를 변경할 때 마다 A와 B 모두 리랜더 되었음.
const OptimizeTest = () => {

  const CounterA = React.memo(({ count }) => {
    useEffect(() => {
      console.log(`AAAAA update === count: ${count}`);
    });
    return <div>{count}</div>;
  });
  
  const CounterB = React.memo(({ obj }) => {
    useEffect(() => {
      console.log(`BBBBB update === obj: ${obj.count}`);
    });
    return <div>{obj.count}</div>;
  });
  
  const [count, setCount] = useState(1);
  const [obj, setObj] = useState({
    count: 1,
  });

  return (
    ...
    )

  • 버튼A, 버튼B 를 눌러도 두 컴포넌트 모두 리랜더 되지 않음

0개의 댓글