[React] React.memo로 Rendering 최적화2 (feat.예제1,2)

Hyun·2022년 1월 8일
1

React

목록 보기
16/22
post-thumbnail

💡React.memo란?

[React공식문서]
React.memo는 고차 컴포넌트(Higher Order Component)이다
(=컴포넌트를 가져와 새 컴포넌트를 반환하는 함수)

=rendering결과를 Memoizing함으로써, 불필요한 re-rendering을 건너뛴다.

사용방법(얕은비교)

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

혹은 export할때 감싸서 내보내면 된다!!

export default React.memo(OptimizeTest);

➡️ React.memo는 props에 변화가 일어날때만 영향을 준다
➡️ React.memo(re-rendering이 일어나지않았으면 하는 컴포넌트)를 입력하면 props가 변화하지않으면 반환되지않는 고차 컴포넌트(강화된 컴포넌트)를 돌려준다. 물론 자기자신의 state가 바뀌면 re-rendering이 일어난다.
➡️ props 혹은 props의 객체를 비교할 때 얕은(shallow) 비교를 한다.

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

사용방법(깊은비교)

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

[React공식문서]
areEqual 함수는 props들이 서로 같으면 true를 반환하고, props들이 서로 다르면 false를 반환합니다. 이것은 shouldComponentUpdate와 정반대의 동작입니다.


💡React.memo이해하기


부모인 App.js에는 state로 count,text가 있고
자식컴포넌트인 CountView.js에는 count를 또다른 자식컴포넌트인 TextView.js에는 text를 각각 prop으로 전달해주었다

👍🏻처음으로 setCount에 10이 실행 ➡️ App컴포넌트의 count state를 변화 ➡️ (state가 업데이트되었기때문에)App컴포넌트 re-rendering ➡️ CountView컴포넌트 re-rendering(count prop업데이트), TextView는 불필요한 re-rendering
✌🏻두번째로 setText가 "HI"로 실행 ➡️ App컴포넌트의 text state를 변화 ➡️ (state가 업데이트되었기때문에)App컴포넌트 re-rendering ➡️ TextView도 re-rendering(prop업데이트), CountView컴포넌트는 불필요한 re-rendering

여기서 불필요한 re-rendering들이 있다.
React.memo기능을 사용하여 불필요한 re-rendering들을 방지할 수 있다.

앞서 re-rendering이 되는경우를 다뤘을때,
부모컴포넌트가 re-rendering되면 자식컴포넌트들 또한 re-rendering이 된다는것을 다뤘다


React.memo로 함수형 자식 컴포넌트들에게 업데이트 조건을 걸어두면

업데이트 조건에 일치한 컴포넌트만 업데이트가 실행이되고, 조건에 부합하지 않으면 실행이안되는것을 볼 수 있다.


💡React.memo이해하기(얕은비교)


a,b 변수에 각각 count:1이라는 property를 갖고있는 객체를 할당함
('Property'는 '속성'으로, JS에서는 객체 내부의 속성을 의미함)

그 후 a와b가 같은지를 물어본 결과,

값도 같고 형태도 같은데 다르다고 출력이 된것을 확인할 수 있다.

JS가 객체,함수,배열같은 비원시타입의 자료를 비교할때는 값에 의한 비교가아닌 (주소에 의한 비교)얕은비교를 하기때문이다!

변수에 객체를 할당하게되면, 객체들은 생성되자마자 고유한 메모리주소를 갖게됨


사람에 비유하면 이런느낌이다.
가지고있는 특성,속성이 같다고해서 두사람은 사람이 아닌것과같이 JS의 비원시타입자료들도 그러하다.

하지만, b의 값으로 a를 대입시키게되면 다르다.
이렇게되면 b변수는 메모리상으로 a변수와 같은 것을 가르키게되어 같은값으로 인식한다.


📖예제1 (얕은비교)

OptimizeTest.js 테스트용 컴포넌트를 생성하여 count와 text를 자식컴포넌트로 두어 re-rendering이 어떻게 일어나는지 consle.log로 확인하고 불필요한 re-rendering을 React.memo를 사용하여 최적화한다.

OptimizeTest.js코드 (적용 전)

import { useEffect, useState } from "react";

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>

      <h2>text</h2>
      <TextView text={text} />
      <input value={text} onChange={(e) => setText(e.target.value)} />
    </div>
  );
};
//나는 onChange={(e)=>setText{text}}으로 했었다.
//이게아니라 onChange={(e)=>setText(e.target.value)} 이벤트가 일어나는것의 타켓의 값을 바꾸는것이기에
export default OptimizeTest;

<코드해석>

1) TextView와CountView는 prop으로 text.count를 받고 렌더링만 해줌
그 후 OptimizeTest(부모)에 넣어줌 (넣어줄떄도 prop으로 전달해야한다)
re-renderimg이 일어났을떄 props가 어떻게 변화하는지 확인하기위해서 useEffect를 넣어줬다
➡️자식컴포넌트1_TextView

const TextView = ({ text }) => {
useEffect(() => {
console.log(Update :: Text : ${text});
});
return <div>{text} </div>;
};

➡️자식컴포넌트2_CountView
const CountView = ({ count }) => {
useEffect(() => {
console.log(Update :: Count : ${count});
});
return <div>{count}</div>;
};

2) OptimizeTest에 useState를 사용하여 count와text를 정의했고,
렌더링스타일을 padding50으로 css설정을 했다.

그 후, 현재상태를 렌더링해주는 CountView와 TextView컴포넌트를 자식으로 넣어 버튼에 onClick효과,text에 onChange효과를 넣어 변화가 일어나면 setState를 실행시켰다.

export하여 App.js 상단에 넣어주었다.
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>

<h2>text</h2>
<TextView text={text} />
<input value={text} onChange={(e) => setText(e.target.value)} />
</div>
);
};

export default OptimizeTest;
(*나의 잘못
나는 onChange={(e)=>setText{text}}으로 했었다.
이벤트가 일어나는것의 타켓의 값을 바꾸는것이기에 onChange={(e)=>setText(e.target.value)}이 맞는것이다 주의하자😭)

🖥결과화면

(이제 useEffect로 콘솔이 어떻게 변화하는지 확인해보자)

처음 실행을하면 두번 rendering되는것을 console창을 통해 확인할 수 있다.

count에 버튼을 클릭하여 변화를 주었을때 text는 불필요한 re-rendering을 계속하고, text또한 마찬가지인것을 확인할 수 있다.
(부모컴포넌트인 OptimizeTest에 count,text의 state가 바뀌어서 자식컴포넌트인 CountView,TextView가 rendering이 일어나기 떄문에 두개의 콘솔이 출력되는 상황)

이 상황을 React.memo(컴포넌트 재사용)로 불필요한 re-rendering을 최적화해보자🔥

OptimizeTest.js코드 (적용 후)

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

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>

      <h2>text</h2>
      <TextView text={text} />
      <input value={text} onChange={(e) => setText(e.target.value)} />
    </div>
  );
};

export default OptimizeTest;

<코드해석>

1) React기능이기에 사용한다고 상단에 import해주기
import React, { useEffect, useState } from "react";

2) TextView,CountView컴포넌트를 React.memo로 전체 감싸주었다. 그렇게되면 전달받은 prop인 text,count가 바뀌지않으면 절대로 렌더링이 일어나지 않게된다.
➡️자식컴포넌트1_TextView
const TextView = React.memo(({ text }) => {
useEffect(() => {
console.log(Update :: Text : ${text});
});
return

{text}
;
});
➡️자식컴포넌트2_CountView
const CountView = React.memo(({ count }) => {
useEffect(() => {
console.log(Update :: Count : ${count});
});
return
{count}
;
});

🖥결과화면


오..처음 실행한거에 한번만 rendering이 된다

불필요한 re-rendering이 사라졌다!!


📖예제2 (깊은비교)

OptimizeTest.js 테스트용 컴포넌트를 생성하여 count와 obj(객체)를 자식컴포넌트로 두어 re-rendering이 어떻게 일어나는지 consle.log로 확인하고 areEqual 함수를 써서 깊은비교하는 코드로 바꿔보자.

OptimizeTest.js코드 (적용 전)

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

const CounterA = React.memo(({ count }) => {
  useEffect(() => {
    console.log(`CounterA Update - count: ${count}`);
  });
  return <div>{count}</div>;
});
const CounterB = React.memo(({ odj }) => {
  useEffect(() => {
    console.log(`CounterB Update - odj.count: ${odj.count}`);
  });
  return <div>{odj.count}</div>;
});
//odj useState안에 객체로 count:1이라는 프로퍼티를 넣어둠
const OptimizeTest = () => {
  const [count, setCount] = useState(1);
  const [odj, setObj] = useState({
    count: 1,
  });

  //Counter A는 count를 쓸 곳, setCount에 count를 전달함 그러면 원래 count초기값이 1인데 setCount에 count를 하게되면 setCount로 상태변화를 일으켰지만 바뀌는값은 원래값임..버그버그
  //Counter B는 setObj를 상태변화를 시킬건데 count: odj.count를 넣으면 값이 똑같은 count: 1 값을 할당하게 됨

  return (
    <div style={{ padding: 50 }}>
      <div>
        <h2>Counter A</h2>
        <CounterA count={count} />
        <button onClick={() => setCount(count)}>A Button</button>
      </div>
      <div>
        <h2>Counter B</h2>
        <CounterB odj={odj} />
        <button
          onClick={() =>
            setObj({
              count: odj.count,
            })
          }
        >
          B Button
        </button>
      </div>
    </div>
  );
};

export default OptimizeTest;

<코드해석>

1) count와 odj상태를 받아서 prop으로 받아서 활용할 두개의 자식컴포넌트를 만듦
odj는 객체이기떄문에 점표기법으로 count를 꺼내서 씀
const CounterA = React.memo(({ count }) => {
useEffect(() => {
console.log(CounterA Update - count: ${count});
});
return <div>{count}</div>;
});

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

2) 차이를 비교하기위해 odj useState안에 객체로 count:1이라는 프로퍼티를 넣어둠
const OptimizeTest = () => {
const [count, setCount] = useState(1);
const [odj, setObj] = useState({
count: 1,
});

3) Counter A는 setCount에 count를 전달함 그러면 원래 count초기값이 1인데 setCount에 count를 하게되면 setCount로 상태변화를 일으켰지만 바뀌는값은 원래값임..버그버그!!
<div>
<h2>Counter A</h2>
<CounterA count={count} />
<button onClick={() => setCount(count)}>A Button</button>
</div>

4) Counter B는 setObj를 상태변화를 시킬건데 count: odj.count를 넣으면 값이 똑같은 count: 1 값을 할당하게 됨(객체객체라는 차이~)
<div>
<h2>Counter B</h2>
<CounterB odj={odj} />
<button onClick={() => setObj({ count: odj.count })}>B Button</button>
</div>

🖥결과화면


처음 화면

CounterA버튼을 여러~~번 클릭해도 콘솔창이 변하지않는것을 확인할 수 있다.


반면, CounterB버튼을 10번 클릭한 결과 콘솔창에 실행된것을 볼 수 있다.
이유는 React.memo는 객체에 대해 얕은비교를하기때문에 같은 값을 줘도 다른주소로 인식하여 rendering이 된다.

이제 areEqual함수를 적용하여 CounterB컴퍼런트에서 {obj} prop을 얕은비교를 하지않도록하여 rendering최적화를 하도록 하겠다!!😆

OptimizeTest.js코드 (적용 후)

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

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

const areEqual = (prevProps, nextProps) => {
  if (prevProps.odj.count === nextProps.odj.count) {
    return true;
  }
  return false;
};

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

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

  return (
    <div style={{ padding: 50 }}>
      <div>
        <h2>Counter A</h2>
        <CounterA count={count} />
        <button onClick={() => setCount(count)}>A Button</button>
      </div>
      <div>
        <h2>Counter B</h2>
        <MemoizedCounterB odj={odj} />
        <button
          onClick={() =>
            setObj({
              count: odj.count,
            })
          }
        >
          B Button
        </button>
      </div>
    </div>
  );
};

export default OptimizeTest;

<코드해석>

1) 우선, CounterB에 React.memo를 없애준다.
const CounterB = ({ odj }) => {
useEffect(() => {
console.log(CounterB Update - odj.count: ${odj.count});
});
return <div>{odj.count}</div>;
};

2) areEqual함수를 반들어서
true반환 -> prevProps=nextProps 이렇게되면 re-rendering을 일으키지않음
false반환 -> prevProps!=nextProps 이러면 re-rendering을 일으키게됨
const areEqual = (prevProps, nextProps) => {
if (prevProps.odj.count === nextProps.odj.count) {
return true;
}
return false;
};

3) MemoizedCounterB컴퍼런트를 새로 만들어서 React.memo에 첫번째인자로 CounterB를 넣고 두번쨰인자로는 areEqual함수를 넣어줌
areEqual가 마치 배열내장함수에 sort에 compare비교함수전달하듯이 React.memo에 비교함수로써 작용을 하게되고 결론적으로 CounterB는 areEqual의 판단에 따라서 re-rendering의 여부를 결정하게되는 메모화된 컴퍼런트가 되는것
const MemoizedCounterB = React.memo(CounterB, areEqual);

4) 그렇게되면 React.memo는 컴퍼런트를 반환하는 고차 컴퍼런트이기때문에
CounterB를 메모이제이션한 상태로 사용하기위해서는 MemoizedCounterB로 밑에도 바꿔줘야한다.
<div>
<h2>Counter B</h2>
<MemoizedCounterB odj={odj} />
<button onClick={() => setObj({ count: odj.count })}>B Button</button>
</div>

🖥결과화면


CounterA, CounterB를 아무리눌러도 re-rendering이 안되는것을 확인할 수 있다


🚀참고자료

React.memo()
React공식문서-React.memo()
React강의-이정환강사

profile
FrontEnd Developer (with 구글신)

0개의 댓글