저번글에서 React Hooks 중 useMemo를 사용하여 최적화하는 방법에 이어 React.memo를 사용하여 최적화를 하는 방법에 대해 간략히 정리하는 글이다.
useMemo는 컴포넌트 내 특정 state나 props의 값이 바뀔 때만 콜백 함수를 수행하여 return 값을 업데이트하는 방식으로 최적화를 했다. React.memo는 이와는 다른 방식으로 컴포넌트가 동일한 props로 동일한 결과를 렌더링 한다면 컴포넌트를 리렌더링하지 않고 마지막으로 렌더링한 결과를 재사용한다.
한가지 주의점은 React.memo는 props로 전달받은 값의 변화에만 영향을 주고 React.memo로 감싸진 함수형 컴포넌트 내 state의 변화는 React.memo와 상관없이 다시 렌더링 된다.
1. React.memo 사용법
사용법은 매우 간단하다. 함수형 컴포넌트를 React.memo로 감싸주면 된다.
아래의 예시를 확인해보면
import React, { useState, useEffect } from "react";
const Textview = React.memo(({ text }) => { //text props의 값이 바뀔때만 리렌더링
useEffect(() => {
console.log(`Update :: Text : ${text}`);
});
return <div>{text}</div>;
});
const Countview = React.memo(({ count }) => { //count props의 값이 바뀔때만 리렌더링
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;
위 코드는 OptimizeTest 컴포넌트에서 하위 컴포넌트인 Countview와 Textview에게 각각 count와 text라는 props를 넘겨주고 있는 코드이다. 위의 코드에서 React.memo를 사용하지 않았다면 + 버튼을 클릭하여 count의 값을 바뀌었을때 Countview 컴포넌트와 Textview컴포넌트 모두 리렌더링 되어 콘솔에 count가 업데이트 되었다는 문자열과 text가 업데이트 되었다는 문자열이 모두 표출 되었을 것이다. (input 태그에 글자를 입력해도 결과는 동일)
하지만 React.memo를 사용하면 + 버튼을 클릭할 시 count props만 값이 바뀌었기 때문에 Countview 컴포넌트만 리렌더링 되고 Textview컴포넌트는 리렌더링이 되지 않는 것을 확인할 수 있다.
2. props 얕은 비교
React.memo는 하위 컴포넌트가 상위 컴포넌트로부터 받는 이전 props와 다음 props를 비교하여 값이 같으면 렌더링 하지 않고 마지막 렌더링한 결과를 재사용 하고 값이 다르면 리렌더링을 한다. 한가지 문제가 되는 점은 props를 비교하는 방식이 얕은 비교라는 것이다.
아래의 예시를 보면
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 OptimizeTestOther = () => {
const [count, setCount] = useState(1);
const [obj, 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>
<CounterB obj={obj} />
<button onClick={() => setObj({ count: obj.count })}> B button </button>
</div>
</div>
);
};
export default OptimizeTestOther;
위 코드에서 Counter A 컴포넌트와 Counter B 컴포넌트 모두 각각 A버튼, B버튼을 클릭했을때 상위 컴포넌트 state 초기값과 같은 값을 props로 계속 넘겨주기 때문에 리렌더링이 되지 않을 것으로 예상되지만 Counter B는 계속 리렌더링이 되는 것을 확인할 수 있다. Counter B가 리렌더링이 되는 이유는 React.memo의 props 비교방식이 얕은 비교이고 전달되는 props가 object(객체)이기 때문이다. 얕은비교에서의 객체 비교는 실제로 그 객체의 프로퍼티 값을 비교하는 것이 아닌 객체의 주소에 의한 비교이기 때문이다.
위와 같이 얕은비교로 인해 발생하는 현상을 해결하는 방법은 react 공식문서에 기술되어 있다.
공식문서를 보면 첫번째 인자에는 렌더링이 될 함수평 컴포넌트, 두번째 인자에는 nextProps와 prevProps를 비교하여 true or false를 반환하는 areEqual 함수를 인자로 주면 된다고 되어 있다. 바로 적용을 시켜보면 아래와 같다.
import React, { useState, useEffect } from "react";
const CounterA = React.memo(({ count }) => {
useEffect(() => {
console.log(`CounterA Update - count: ${count}`);
});
return <div>{count}</div>;
});
const CounterB = ({ obj }) => {
useEffect(() => {
console.log(`CounterB Update - count: ${obj.count}`);
});
return <div>{obj.count}</div>;
};
//이전 props와 다음 props의 값을 비교하여 true/false 반환
const areEqual = (prevProps, nextProps) => {
return prevProps.obj.count === nextProps.obj.count;
};
const MemoizedCoutnerB = React.memo(CounterB, areEqual);
const OptimizeTestOther = () => {
const [count, setCount] = useState(1);
const [obj, 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>
<MemoizedCoutnerB obj={obj} />
<button onClick={() => setObj({ count: obj.count })}> B button </button>
</div>
</div>
);
};
export default OptimizeTestOther;
위와 같이 areEqual 함수를 작성하여 함수 내에서 이전 props와 다음 props가 어떻게 넘어오는지 debugger를 찍어 확인해보고 점표기법으로 접근하여 true/false를 반환해주면 간단하게 해결된다.