[React] useMemo & useCallback
React를 사용하여 프로젝트를 만들 때 성능을 최적화하기 위한 방법으로 많이 거론되는 것이 useMemo, useCallback, React.memo 훅을 사용하는 것이다. React.memo는 비교적 이해하기 쉬워서 실제로 프로젝트에서도 사용했었는데 useMemo와 useCallback은 잘 이해가 가지 않아 아예 도입하지 않았던 기억이 난다. 개념에 대한 이해 보다는 어떻게 사용해야 똑똑하게 사용할 수 있는 건지 잘 와닿지 않았었다.
어떻게 성능을 최적화하는 걸까?
useMemo와 useCallback의 핵심은 메모이제이션이다.
메모이제이션이란?
메모이제이션이란 동일한 계산을 반복해야 할 때, 이전에 계산한 값을 메모리에 저장함으로써 동일한 계산의 반복 수행을 제거하여 프로그램의 실행 속도를 빠르게 하는 것을 말한다.
useMemo와 useCallback의 차이는 무엇을 메모이제이션 하느냐에 있다. useMemo는 값을 메모이제이션하고, useCallback은 함수 자체를 메모이제이션하여 재사용한다.
useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
useCallback
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
언제 사용할까?
React는 컴포넌트의 prop이나 state가 변경되면 새로 반환된 엘리먼트를 이전에 렌더링된 엘리먼트와 비교해서 실제 DOM에 업데이트가 필요한지의 여부를 결정한다. 그리고 이전과 같지 않다면 DOM을 업데이트한다. 이렇게 리렌더링하는 과정에서 불필요한 함수 호출이 발생할 수 있다. React에서 컴포넌트도 하나의 함수이기 때문에 리렌더링 시 실제 DOM을 업데이트하는 데 필요한 함수 뿐만 아니라 해당 컴포넌트에 있는 모든 함수가 재호출된다. 함수가 동일한 값을 반환하더라도 말이다.
위의 useMemo와 useCallback의 구문을 보면 함수의 파라미터로 받는 a와 b가 의존성 배열에 들어가 있는 것을 알 수 있다. 즉, 함수의 파라미터로 받는 인자가 변경되었을 때만 메모이제이션한 값 또는 함수를 재사용하지 않고 함수 자체를 다시 호출하여 재계산하는 것이다.
메모이제이션하는 대상이 값인지 함수인지에 따라 useMemo 또는 useCallback을 함께 사용하면 함수의 파라미터로 받는 인자가 변하지 않는 한 해당 함수를 가지고 있는 컴포넌트가 리렌더링되어도 함수는 호출되지 않는다.
내가 생각했을 때는 아래와 같은 조건일 때 useMemo와 useCallback을 사용하면 좋을 것 같다!
1) 컴포넌트의 리렌더링은 자주 발생하지만
2) 컴포넌트 안에 있는 어떤 함수의 계산이 복잡하면서도 (✨ computeExpensiveValue ✨)
3) 해당 함수의 파라미터로 받는 인자의 상태는 자주 변경되지 않을 때
그리고 React.memo와 마찬가지로 남발해서 사용해서는 절대 안 될 것 같다.
이해를 돕기 위한 예시
// Parent
const foodList = ['떡볶이', '감자전', '만둣국', '돈가스', '없어요']
const Practice = (props) => {
// ******** useMemo & useCallback ******** //
const [nickname, setNickname] = useState('')
const [favoriteFood, setFavoriteFood] = useState('')
const handleChangeNickname = (e) => {
setNickname(e.target.value)
}
const handleChangeFood = (e) => {
setFavoriteFood(e.target.value)
}
return (
<>
<div style={{
margin: '20px auto',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
}}>
<div>
<label htmlFor="nickname">닉네임을 입력해보세요!</label>
<input type="text" name="nickname" value={nickname} onChange={handleChangeNickname}
style={{
display: 'block',
}}/>
</div>
<div>
<p style={{textAlign: 'center', margin: '20px 0 0 '}}>좋아하는 음식을 골라보세요!</p>
{foodList.map((food, i) => (
<label htmlFor="favorite-food">
<input type="radio" name="favorite-food" value={food} onChange={handleChangeFood} key={i} />
{food}
</label>
))}
</div>
<div>
<Example nickname={nickname} favoriteFood={favoriteFood} is_me={true} />
</div>
</div>
</>
)
}
// Child
const zzangzzang = (nickname) => {
console.log("zzangzzang")
return nickname === 'zubetcha' ? '짱짱' + nickname : nickname
}
const handleShowImage = (favoriteFood) => {
console.log('handleShowImage')
switch (favoriteFood) {
case '떡볶이':
return 'https://cdn.kihoilbo.co.kr/news/photo/202008/880134_302005_3430.png'
case '감자전':
return 'http://img2.tmon.kr/cdn3/deals/2021/01/07/5069462478/original_5069462478_front_6c336_1609992555production.jpg'
case '만둣국':
return 'https://recipe1.ezmember.co.kr/cache/recipe/2015/05/22/eb93de35b7d74cc18b23c307eb4760f81.jpg'
case '돈가스':
return 'https://post-phinf.pstatic.net/MjAyMDA0MTZfMjY3/MDAxNTg2OTk5MzYwOTMw.YZk3XJCkJqOrZmSzXTGfnXcfoj5CoLQfY9kEBhBmlyYg.WdEgjt1SmPLlCfi8nmVMB79FymTDi3ApEfQJrGF57Acg.JPEG/1.jpg?type=w1200'
case '없어요':
return 'https://cdnweb01.wikitree.co.kr/webdata/editor/202011/08/img_20201108102736_15e8ae2e.webp'
default:
return ''
}
}
const Example = ({nickname, favoriteFood, is_me}) => {
// const aaa = () => {
// setTimeout(() => console.log(is_me), 1000)
// }
const aaa = useCallback(() => {
setTimeout(() => console.log(is_me), 1000)
}, [is_me])
useEffect(() => {
aaa()
}, [aaa])
// const zzangNickname = zzangzzang(nickname)
// const zzangImage = handleShowImage(favoriteFood)
const zzangNickname = useMemo(() => zzangzzang(nickname), [nickname])
const zzangImage = useMemo(() => handleShowImage(favoriteFood), [favoriteFood])
return (
<>
<div style={{
margin: '40px 0 0',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
}}>
{nickname !== '' && favoriteFood !== '' ?
<>
<h2 style={{
fontWeight: 'bold',
fontSize: '2rem',
}}>
{zzangNickname}님의 저녁 메뉴
</h2>
<img alt="" src={zzangImage} />
</>
: <></>
}
</div>
</>
)
}