이글은 리액트 공식문서의 useMemo와 useCallback API Reference를 참고한 글이며, 해당 내용들을 참고해 제 입맛대로 정리한 글입니다!
먼저 결론부터 말하자면 useMemo
와 useCallback
둘 다 동일하게 기능을 가지고 있다. 그 기능은 어떤 값을 메모이제이션하는 기능이다.
메모이제이션(Memoization)이란?
메모이제이션이란 컴퓨터 프로그램이 동일한 계산을 반복해야할 시, 이전에 계산한 값을 메모리에 저장함으로써 동일한 계산의 반복 수행을 제거하여 프로그램 실행속도를 빠르게 하는 기술 - wikipedia
따라서 공통적으로 useMemo
와 useCallback
hook 모두 최종적으로 값을 캐시하여 성능최적화를 위해 사용한다는 이야기이다.
useMemo
나 useCallback
등을 통해 성능최적화하기 앞서, React
에서 리렌더링이 발생하는 경우에 대해서도 알고있어야 한다.이글의 주된 목적은 useMemo
와 useCallback
이니 핵심만 요약하겠습니다.
자세한 레퍼런스는 React는 언제 컴포넌트를 다시 렌더링하나요?를 참고했습니다.
리렌더링 트리거의 핵심은?
1.state
의 변경 될 때
2. 부모 컴포넌트의state
가 변경되면 자식 컴포넌트 또한 리렌더링이 발생하게 된다
3.props
가 변경될 때
여기서 예를 들어서, 만약 2번과 같이 상태를 가지고 있는 상위컴포넌트들의 자식컴포넌트들은 상태를 가지고있지 않음에도 불구하고 최상위 컴포넌트의 상태가 변경될 때 자식 컴포넌트들또한 불필요하게 리렌더링 될것이다.
useMemo
나 useCallback
이 필요한 이유라고 생각할 수 있다.앞서 말했듯이 useMemo
와 useCallback
은 값을 메모이제이션한다는 기능에선 동일하다고 볼 수 있다.
그러나 차이점은 존재하는데 그 차이점은?
useMemo
는 메모이제이션 된 "값"을 반환하고
useCallback
은 메모이제이션 된 "함수"를 반환한다.
useMemo
는 인자로 받는 첫번째 콜백함수의 반환값을 메모이제이션하고 반환한다.
useMemo(() => fn, deps);
useCallback
은 인자로 받는 첫번째 함수 자체를 메모이제이션하고 반환한다.
useCallback(fn, deps);
따라서 두 훅은 useCallback(fn, deps)은 useMemo(() => fn, deps)
와 동일하다고 생각할 수 있다.
그러면 두 훅은 왜 따로 생겨났고 어떤 경우에 쓰이는걸까?
useMemo
훅은 연산 비용이 큰(=계산하는데 비용이 많이드는)함수의 "반환값"을 재사용하기 위해 사용된다.
hook
인것이다.반면 useCallback
은 "함수 자체"를 메모리에 저장하는데 사용되는 훅이다.
useMemo
와 마찬가지로 의존성 배열인 deps
가 변경될때만 새로운 함수를 생성하게 된다.다시 정리하자면
useMemo
: 연산 비용이 큰 계산의 결과를 저장하고 재사용하는 경우, 이전 연산 결과를 재활용하고 싶을 때 사용
useCallback
: 이벤트 핸들러와 같이 컴포넌트가 리렌더링될 때마다 재생성되는 함수를 메모리에 저장하고 싶을 때 사용
사실 아래 예제들은 굳이 useCallback
을 적용할 필요가 없는 예제긴하다. 하위 컴포넌트가 하나밖에 없고 이는 성능적으로 이슈를 일으킬 확률이 거의 없다고 생각되기 때문이다.
import {useState } from 'react';
import ChildComponent from './ChildComponent'
function App() {
const [counter, setCounter] = useState(0);
return(
<div>
<h1>{counter}</h1>
<ChildComponent />
<button onClick={() => setCounter((prev) => prev + 1)}> Re-Render Trigger! </button>
</div>
);
}
// 🪄 ./ChildComponent.jsx
function ChildComponent() {
console.log("child component rendered")
return(
<div>
<h1>Hello World</h1>
</div>
);
}
export default ChildComponent;
상위 컴포넌트인 App의 상태가 변경되고 있음에도 불구하고 그와 상관없는 ChildComponent가 리렌더링 되어 콘솔로그가 찍히는것을 확인할 수 있습니다
import {useState} from "react";
import ChildComponent from "./components/ChildComponent";
import {useCallback} from "react";
function App() {
const [counter, setCounter] = useState(0);
const handleCountUp = useCallback(() => {
setCounter(counter + 1);
}, [counter]);
return (
<>
{counter}
<ChildComponent />
<button onClick={handleCountUp}>Render Trigger Button!</button>
</>
);
}
export default App;
// 🪄 ./ChildComponent.jsx
function ChildComponent() {
console.log("child component rendered")
return(
<div>
<h1>Hello World</h1>
</div>
);
}
export default ChildComponent;
오잉? 그래도 똑같이 하위 컴포넌트인 ChildComponent가 리렌더링이 발생하고 있다
핸들러 함수인 handleCountup
을 useCallback
hook으로 래핑했음에도 불구하고 ChildComponent는 리렌더링을 일으키고 있다.
위의 코드에서 handleCountup
함수는 counter
의 상태가 변경될 때마다 새로이 생성된다.
따라서, 여기서 useCallback
을 사용하는것은 좋지 않은 선택인것같다.
왜냐하면 counter 값이 변경될 때마다 새로운 함수 handleCoutup
이 재생성되므로, 이 경우 useCallback
의 메모이제이션 효과를 기대하기 어렵기 때문.
ChildComponent가 App 컴포넌트의 state
에 의존하고 있지 않음에도 불구하고, App 컴포넌트가 리렌더링될 때마다 ChildComponent도 함께 리렌더링되고 있다.
이는 ChildComponent가 App 컴포넌트의 자식 컴포넌트이기 때문이다.
이문제를 해결하려면?
React
에서 제공하는 memo
를 사용해 ChildComponent의 불필요한 리렌더링을 방지할 수 있다.memo
는 컴포넌트의 props
가 변경되지 않았다면, 리렌더링을 방지하고, 마지막으로 렌더링 결과를 재사용한다.const ChildComponent = memo(function ChildComponent {
console.log("child component rendered")
return(
<div>
<h1>Hello World</h1>
</div>
);
});
export default ChildComponent;
useCallback과 useMemo는 성능 최적화를 위한 도구이지만, 무조건적으로 사용해야 하는 것은 아니다.
때로는 불필요한 메모이제이으로 인해 성능이 오히려 저하될 수 있다.
따라서, 실제 성능 문제가 발생했을 때 적절하게 사용하는 것이 중요하다!