
이번엔 리액트 훅의 마지막 React.memo, useCallback, useMemo에 관해 알아보자
리액트에서 리렌더링이 자주 일어나는 것은 좋은 것이 아니다. 우리는 비용이 발생하는 것을 최대한 줄여야 하고, 이를 최적화(Optimization)라고 한다!
불필요한 렌더링을 줄이기 위한 최적화의 대표적인 방법이 바로
위에서 살펴본 리렌더링의 발생 조건의 세 번째
이렇게 되면 부모 컴포넌트의 state의 변경으로 인해 props 변경이 일어나지 않는 한 컴포넌트는 리렌더링 되지 않는다!
그렇다면 실제 사용법을 예제를 통해 익혀보자!

App.jsx 안에 세 개의 Box 컴포넌트를 불렀다. 컴포넌트들 안에는 console을 찍는 코드를 작성했다. 이 때, App.jsx에서 카운트를 증가시키면 박스 컴포넌트들이 모두 리렌더링되는 것을 확인할 수 있다.

React.memo를 사용해 이를 해결하는 방법은 매우 간단하다!
export default React.memo(Box1);
export default React.memo(Box2);
export default React.memo(Box3);
Box 컴포넌트들을 내보낼 때 React.memo로 감싸주면 된다.
React.memo는 컴포넌트를 메모이제이션 했다면, useCallback은 인자로 들어오는 함수 자체를 메모이제이션한다. 바로 예제를 통해 알아보자!
위의 UI에서 Box1에 count를 초기화 해주는 버튼을 추가하고, 초기화하는 함수를 App.jsx로 부터 내려준다.
const initCount = () =>{
setCount(0);
};
...
function Box1({ initCount }) {
console.log("Box1이 렌더링되었습니다.");
const onInitButtonClickHandler = () => {
initCount();
};
return (
<div style={boxStyle}>
<button onClick={onInitButtonClickHandler}>초기화</button>
</div>
);
}
...

+버튼과 -버튼 그리고 초기화 버튼을 누를 때 모두 App 컴포넌트와 Box1 컴포넌트가 리렌더링되는 것을 확인할 수 있다.
이는 우리가 함수형 컴포넌트를 사용하고 App.jsx가 리렌더링 되면서 onInitButtonClickHandler 함수도 다시 실행되기 때문이다.
함수 역시 객체이며 모양이 같더라도 다시 만들어질 때 새로운 주솟값이 할당되고, 이를 Box1.jsx는 props가 변경됐다고 인식하는 것이다.
// 변경 전
const initCount = () => {
setCount(0);
};
// 변경 후
const initCount = useCallback(() => {
setCount(0);
}, []);
이렇게 사용해주면 Box1.jsx 컴포넌트는 리렌더링이 되지 않는다👏
카운트가 변경될 때 console로 카운트 값이 몇에서 0으로 변경되었는지 확인해보면

위와 같이 나온다. 이런 현상이 발생한 이유는 useCallback이 count가 0일 떄의 시점을 기준으로 메모리에 함수를 저장했기 때문이다.

const initCount = useCallback(() => {
console.log(`[COUNT 변경] ${count}에서 0으로 변경되었습니다.`);
setCount(0);
}, [count]);
기존 코드의 dependency array에 count를 넣어주면, count가 변경될 때 마다 새롭게 함수를 할당하게 된다!

동일한 값을 반환하는 함수를 계속 호출해야 한다면 필요없는 렌더링을 계속 일으키게 된다.
맨 처음 해당 값을 반환할 때 그 값을 특별한 메모리에 저장한다.
이러면 이미 저장된 값을 꺼내와서 쓸 수 있다. 이러한 기법을 '캐싱을 한다.' 라고 표현하기도 한다.
// as-is
const value = 반환할_함수();
// to-be
const value = useMemo(()=> {
return 반환할_함수()
}, [dependencyArray]);
마찬가지로 인자로 dependency Array를 받는데, dependency Array의 값이 변경 될때만 반환할_함수()를 호출한다고 보면 된다.
...
const me = {
name: "Ted Chang",
age: 21,
isAlive: isAlive ? "생존" : "사망",
};
useEffect(() => {
console.log("생존여부가 바뀔 때만 호출해주세요!");
}, [me]);
return (
<>
<div>
내 이름은 {me.name}이구, 나이는 {me.age}야!
</div>
<br />
<div>
<button
onClick={() => {
setIsAlive(!isAlive);
}}
>
누르면 살았다가 죽었다가 해요
</button>
<br />
생존여부 : {me.isAlive}
</div>
<hr />
필요없는 숫자 영역이에요!
<br />
{uselessCount}
<br />
<button
onClick={() => {
setUselessCount(uselessCount + 1);
}}
>
누르면 숫자가 올라가요
</button>
</>
);
}
...
위 코드에서 useEffect 훅을 사용해 me의 정보가 바뀌었을 때만 발동되게끔 dependency array를 넣어줬다. 하지만, count 증가 버튼을 눌렀을 때도 계속 log가 찍히는 것을 확인할 수 있다.
이는 불변성과 연관이 있는데,
uselessCount state가 바뀌면 리렌더링이 되고
->컴포넌트 함수가 새로 호출된다.
->me객체가 다시 할당되는데, 다른 메모리 주소값을 할당받는다.
->이전과 주소가 달라졌으므로 me가 바뀌었다 인식하고 useEffect 내부 로직이 호출된다.
const me = useMemo(() => {
return {
name: "Ted Chang",
age: 21,
isAlive: isAlive ? "생존" : "사망",
};
}, [isAlive]);
useMemo를 남발하게 되면 별도의 메모리 확보를 너무 많이 하게 되기 때문에 오히려 성능이 악화될 수 있다.
필요할때만 사용하자🤝