동일한 값을 반환하는 함수가 반복적으로 호출될 때 Memoization을 이용해 메모리상에 값을 저장해 함수를 호출하지 않고 이전에 저장된 값을 불러오도록 하는 것
간단하게 말하면 한 번 계산된 값을 캐싱해두어 이후에 같은 값이 필요할 때 메모리에서 불러와 사용하는 것이다.
딱봐도 적절하게만 활용한다면 성능상 큰 이점을 가져다줄 수 있다.
첫 번째 인자인 콜백함수는 Memoize할 값을 계산해 리턴해주는 역할을 담당한다.
두 번째 인자인 dependency 배열은 배열 안의 값이 변경되었을 때 Memoization된 값을 업데이터해 다시 Memoization을 실행한다.
const value = useMemo(()=>{
return calculate()
}, [item])
빈 배열을 넘겨주었을 경우에는 컴포넌트가 렌더링될 때 한 번만 Memoization을 실행하고 이후에는 해당 값을 그대로 불러오는 작업만 한다.
const value = useMemo(()=>{
return calculate()
}, [])
Memozation 자체가 메모리에 값을 따로 할당해 놓는 것이기 때문에 무분별하게 사용했을 시에는 성능에 오히려 악영향을 줄 수 있다.
뭐든 알맞은 상황에 적절하게 사용하는 것이 중요하다.
다음의 예시에서 동작 원리를 한번 살펴보자.
뱅기
버튼을 누를 때마다 useState으로 선언된 isKorea 값이 변경된다.만약 내가 count 값을 변경한다고 해도 useEffect의 의존성 배열에는 location이 들어가 있으니,
변화된 count에는 영향을 받지 않아 콜백함수가 실행되지 않는다.
그런데 location의 값이 Primitive Type(원시타입)이 아니라 Reference Type(참조타입)이라면 상황이 달라진다.
import React, { useState, useEffect } from 'react'
function App() {
const [count, setCount] = useState(0)
const [isKorea, setIsKorea] = useState(true)
// Primitive Type
const location = isKorea ? 'Korea' : 'Taiwan'
useEffect(() => {
console.log('useEffect')
}, [location])
return (
<div>
<div>
<h3>Counting</h3>
<input
type="number"
value={count}
onChange={(e) => setCount(e.target.value)}
/>
<hr />
<h3>Country</h3>
<p>나라 : {location}</p>
<button onClick={() => setIsKorea(!isKorea)}>뱅기</button>
</div>
</div>
)
}
export default App
location의 값을 Reference Type인 객체로 바꿔주었다.
참조타입은 메모리의 주소를 참조하고 있기 때문에, 재렌더링이 되었을 경우 참조하고 있는 메모리의 주소값이 달라진다. 이런 경우에는 count의 값만 바뀌었는데도 useEffect가 계속 실행되는 문제가 있다.
이 코드에서 동작원리는 다음과 같다.
import React, { useState, useEffect } from 'react'
function App() {
const [count, setCount] = useState(0)
const [isKorea, setIsKorea] = useState(true)
// Reference Type
const location = { country: isKorea ? 'Korea' : 'Taiwan' }
useEffect(() => {
console.log('USEEFFECT')
}, [location])
return (
<div>
<div>
<h3>COUNTING</h3>
<input
type="number"
value={count}
onChange={(e) => setCount(e.target.value)}
/>
<hr />
<h3>Country</h3>
<p>나라 : {location.country}</p>
<button onClick={() => setIsKorea(!isKorea)}>뱅기</button>
</div>
</div>
)
}
export default App
이런 경우 사용할 수 있는 것이 useMemo다.
저렇게 코드를 작성했을 때 VSCode IDE에서 useMemo를 사용할 것을 권장한다.
아래와 같이 useMemo를 사용했을 경우 count 값이 달라져도 메모리에 저장된 값을 가져오기 때문에 useEffect의 콜백 함수가 호출되지 않는다!
import React, { useState, useEffect, useMemo } from 'react'
function App() {
const [count, setCount] = useState(0)
const [isKorea, setIsKorea] = useState(true)
// Memoization
const location = useMemo(() => {
return { country: isKorea ? 'Korea' : 'Taiwan' }
}, [isKorea])
useEffect(() => {
console.log('USEEFFECT')
}, [location])
return (
<div>
<div>
<h3>COUNTING</h3>
<input
type="number"
value={count}
onChange={(e) => setCount(e.target.value)}
/>
<hr />
<h3>Country</h3>
<p>나라 : {location.country}</p>
<button onClick={() => setIsKorea(!isKorea)}>뱅기</button>
</div>
</div>
)
}
export default App
useCallback도 React의 Memoization 기법 중 하나다.
useMemo는 값을 Memoization 하지만 useCallback은 콜백 함수 자체를 Memoization 한다.
어떤 함수를 Memoization하려면 해당 함수를 useCallback으로 감싸면 된다.
함수가 필요할 때마다 새로 생성하는 것이 아니라 메모리에서 가져와 재사용한다.
const calculate = useCallback((num)=>{
return num + 1
},[item])
여기서 중요한 것은 방금 선언한 calculate는 calculate란 변수에 함수 객체(Reference Type)이 할당된 형태다.
따라서 렌더링이 다시 발생했을 경우 새로운 주소값을 참조하는 함수 객체를 매번 계속 할당받는 비효율이 발생한다.
첫 번째 코드는 다음과 같이 동작한다.
문제는, Box 스타일과 무관한 Theme이 변경되었을 때 changeStyle 함수가 새로 할당된다는 것이다.
// Box.js
import React, { useEffect, useState } from 'react'
export default function Box({ changeStyle }) {
const [style, setStyle] = useState({})
// changeStyle이 바뀔 때마다 실행
useEffect(() => {
console.log('size changed')
setStyle(changeStyle)
}, [changeStyle])
return <div style={style}></div>
}
// App.jsimport React, { useState } from 'react'
import Box from './Box'
function App() {
const [size, setSize] = useState(100)
const [isDark, setIsDark] = useState(false)
const changeStyle = () => {
return {
backgroundColor: 'salmon',
height: `${size}px`,
width: `${size}px`,
}
}
return (
<div
style={{
backgroundColor: isDark ? 'black' : 'white',
}}
>
<input
type="number"
value={size}
onChange={(e) => setSize(e.target.value)}
/>
<button onClick={() => setIsDark(!isDark)}>Chagne Theme</button>
<Box changeStyle={changeStyle} />
</div>
)
}
export default App
이를 해결하기 위해 changeStyle을 useCallback으로 감싸줘 Memoization할 수 있다.
이렇게 되면 isDark 값이 변경되었을 땐 Memoization된 changeStyle 함수를 그대로 가져오기 때문에 Box.js useEffect의 콜백함수가 호출되지 않는다.
const changeStyle = useCallback(() => {
return {
backgroundColor: 'salmon',
height: `${size}px`,
width: `${size}px`,
}
}, [size])