React.memo / useCallback / useMemo
컴포넌트에서 state가 바뀌었을 때
컴포넌트가 내려받은 props가 변경되었을 때
부모 컴포넌트가 리-렌더링 된 경우 자식 컴포넌트는 모두
리액트에서 리렌더링이 빈번하게, 자주 일어난다는 것은 좋지않다.
비용이 발생하는 것은 최대한 줄여야하기 때문.
리액트에서 불필요한 렌더링이 발생하지 않도록 최적화하는 대표적인 방법이 아래이다.
여기서 캐싱이라는 것은 메모리에 저장해두고 필요할 때 쓰겠다는 뜻.
아래에 리턴문을 가지고 있는 카운트 컴포넌트를 예시로 들어보자
return (
<>
<h3>카운트 예제입니다!</h3>
<p>현재 카운트 : {count}</p>
<button onClick={onPlusButtonClickHandler}>+</button>
<button onClick={onMinusButtonClickHandler}>-</button>
<div style={boxesStyle}>
<Box1 />
<Box2 />
<Box3 />
</div>
</>
);
현재 컴포넌트에서 useState
를 사용하여 {count}
값을 변경시키면 하위에 있는 Box1, Box2, Box3 모두 부모컴포넌트의 상태변경으로 인해 리렌더링 된다.
이처럼 리렌더링할 이유가 없기때문에 예외처리를 하기 위해 React.memo
를 사용한다.
export default React.memo(Box1);
export default React.memo(Box2);
export default React.memo(Box3);
우리가 export할 때 React.memo로 컴포넌트를 감싸주면 부모컴포넌트의
state가 변경되더라도 자식 컴포넌트들은 리렌더링이 되지 않는다.
Box1이 count를 초기화 해 주는 코드라고 가정해보자.
// count를 초기화해주는 함수
const initCount = () => {
setCount(0);
};
return (
<>
<h3>카운트 예제입니다!</h3>
<p>현재 카운트 : {count}</p>
<button onClick={onPlusButtonClickHandler}>+</button>
<button onClick={onMinusButtonClickHandler}>-</button>
<div style={boxesStyle}>
<Box1 initCount={initCount} />
<Box2 />
<Box3 />
</div>
</>
);
}
현재 컴포넌트에서 useState
를 사용하여 {count}
값을 변경시키면 하위에 있는 Box1만 리렌더링이 된다. (현재 각 box를 React.memo로 감싼상황)
그 이유는 현재 컴포턴트가 리렌더링 되면서 함수를 다시 선언하기 때문이다.
자바스크립트에서는 함수도 객체의 한 종류.
따라서 모양은 같더라도 다시 만들어지면 그 주솟값이 달라지고 이에 따라 하위 컴포넌트인 Box1.jsx
는 props가 변경됐다
고 인식한다.
// 변경 전
const initCount = () => {
setCount(0);
};
// 변경 후
const initCount = useCallback(() => {
setCount(0);
}, []);
위와 같이 useCallback을 사용해서 함수를 감싸주면 뒤에오는 dependency array
여부에 따라 마운트될 때만 혹은 특정값이 업데이트 될 때 새롭게 함수를 할당한다.
여기서 짚고 넘어갈 부분이 있다. 아래코드를 보게되면,
const initCount = useCallback(() => {
console.log(`[COUNT 변경] ${count}에서 0으로 변경되었습니다.`);
setCount(0);
}, []);
위와 같이 코드를 변경했을 때 참조되는 count값은 초기 마운팅 되었을때 값이다. 따라서 직전 값이 뭐였는지 추적이 안된다.
const initCount = useCallback(() => {
console.log(`[COUNT 변경] ${count}에서 0으로 변경되었습니다.`);
setCount(0);
}, count]);
따라서 dependency array
에 count를 넣어주어 상태변경 시 마다 새롭게 함수를 할당해주어야 한다.
동일한 값을 반환하는 함수를 계속 호출해야 하면 필요없는 렌더링을 하기때문에 이부분을 보완하기 위해 useMemo
를 사용한다.
예를 들어 아래와 같이 시간이 오래걸리는 작업을 수행하는 함수가 있다고 가정해보자.
여기서 반환되는 값의 변화가 없을 시에는 함수를 수행하지 않고 값만 저장하여 반환하는 것이다.
const heavyWork = () => {
for (let i = 0; i < 1000000000; i++) {}
return 100;
};
// CASE 1 : useMemo를 사용하지 않았을 때
const value = heavyWork();
// CASE 2 : useMemo를 사용했을 때
// const value = useMemo(() => heavyWork(), []);
위와 같은 구문이 리렌더링될 때마다 heavyWork
라는 시간이 오래걸리는 함수이가 매번 작동한다. 따라서 CASE 2
처럼 useMemo
를 사용하여 값을 캐싱한다.
또 다른 사용 예시를 살펴보자.
이번에는 객체를 dependency array
로 참조하는 useEffect
훅을 사용한다고 가정했을때,
const [isAlive, setIsAlive] = useState(true);
const [uselessCount, setUselessCount] = useState(0);
const me = {
name: "Ted Chang",
age: 21,
isAlive: isAlive ? "생존" : "사망",
};
useEffect(() => { // 객체를 참조
console.log("생존여부가 바뀔 때만 호출해주세요!");
}, [me]);
return (
<>
<div>
<button
onClick={() => {
setIsAlive(!isAlive);
}}
/>
<br />
생존여부 : {me.isAlive}
</div>
<hr />
필요없는 숫자 영역이에요!
<br />
{uselessCount}
<br />
<button
onClick={() => {
setUselessCount(uselessCount + 1);
}}
>
+1 증가버튼
</button>
</>
);
위와 같은 컴포넌트에서는 객체와 관련없는 카운트를 1씩 증가시켜도 useEffect가 동작한다. 그 이유는 컴포넌트가 리렌더링 되면서 객체의 주소값이 새롭게 할당되기 때문이다.
따라서 useEffect는 변화를 감지하고 동작하게 된다.
아래와 같이 메모를 사용해야 우리가 원하는대로 작동하게 된다.
const me = useMemo(() => {
return {
name: "Ted Chang",
age: 21,
isAlive: isAlive ? "생존" : "사망",
};
}, [isAlive]);
여기까지 React.memo / useCallback / useMemo 를 사용하여 컴포넌트에 불필요한 리렌더링을 줄여 최적화하는 방법에 대해 살펴보았다.