🥯 애피타이저
안녕하세요. 이번 글에서는 useMemo를 예제와 함께 소개해보겠습니다.
끝까지 읽어주실것이라 믿고 미리 감사드립니다.
🍖 메인
useMemo가 뭐예요?
useMemo는 React에서 컴포넌트의 성능을 최적화하는 데 사용되는 Hook입니다.
함수의 결과를 메모이제이션하여 비용이 많이 드는 계산을 최적화하는데 사용됩니다.
함수를 메모하는 useCallback과는 달리, useMemo는 함수의 결과 값을 메모합니다.
const memo = useMemo(calculateValue, dependencies);
calculateValue
useMemo의 첫 번째 인수로 들어가는 calculateValue는 캐시 하려는 값을 계산하는 함수입니다.
이 함수는 순수 함수여야 하며, 인수를 받지 않아야 하고, 모든 유형의 값을 반환할 수 있어야 합니다.
dependencies
calculateValue에서 참조된 모든 값의 목록입니다.
이 값은 property, state, 컴포넌트 내부에서 직접 선언된 모든 변수와 함수가 포함됩니다.
React는 여기에 담겨 있는 종속성들을 이전 값과 비교하여 변화가 있는지 판단합니다.
Returns
첫 렌더링이라면 인자 없이 함수에서 계산된 값을 반환합니다.
그다음 렌더링부터는 저장되어 있던 값을 반환하거나 함수를 호출하여 새로 계산된 값을 반환합니다.
useMemo은 어떻게 동작하나요?
useMemo를 사용하면 React는 먼저 현재 종속성과 이전 종속성을 비교하여 종속성이 변경되었는지 확인합니다.
종속성이 변경되지 않았다면, React는 함수를 다시 실행하지 않고 이전에 메모한 값을 반환합니다.
변경되었다면, 함수를 실행하고 값을 새로 계산하고 메모합니다. 그다음, 이 메모된 값을 반환합니다.
React는 내부적으로 각 useMemo 훅에 대해 메모된 값을 저장하기 위해 메모이제이션 캐시를 사용합니다.
캐시는 메모된 값을 나타내는 키와 종속성을 나타내는 값이 있는 JavaScript 개체로 구현됩니다.
구성 요소가 렌더링 될 때 React는 캐시를 확인하여 메모된 값이 주어진 종속성에 대해 이미 존재하는지 확인합니다.
메모된 값이 있으면 React는 함수를 다시 계산하지 않고 반환합니다.
useMemo의 참고 사항
일반적으로 대부분의 계산은 매우 빠르기 때문에 useMemo를 사용하지 않아도 괜찮습니다.
하지만 내용이 많은 배열을 필터링하거나, 수정하거나, 비용이 많이 드는 계산을 수행하는 경우에 데이터가 변경되지 않았다면, 계산을 다시 수행하지 않는 것이 좋습니다.
따라서 useMemo를 사용하면 데이터가 변경되지 않았을 때 업데이트를 하지 않기 때문에, 리렌더링을 막아서 쓸데없는 성능 저하를 막을 수 있습니다.
useMemo를 사용한다고 해서 첫 번째 렌더링을 더 빠르게 만들지 않습니다.
업데이트 시 불필요한 작업을 건너뛰는 데만 도움이 됩니다.
useMemo를 사용하면 유용한 경우
- useMemo에 넣는 계산이 눈에 띄게 느리고 종속성이 거의 변하지 않는 경우
- useMemo로 감싸진 컴포넌트에 props를 전달하는 경우
function MyComponent({ propA, propB }) {
const expensiveValue = useMemo(() => {
return propA + propB;
}, [propA, propB]);
return (
<div>{expensiveValue}</div>
);
}
컴포넌트가 리렌더링되면 props가 변경되지 않아도 컴포넌트의 모든 prop이 다시 전달됩니다.
렌더링이 자주 되는 컴포넌트인데다가 props의 계산도 오래 걸린다면 매우 비효율적일 수 있습니다.
useMemo를 사용한다면 props의 결과를 메모할 수 있기 때문에 props가 변경되었을 때에만 렌더링이 되기 때문에
불필요한 계산을 줄여 성능을 향상시킬 수 있습니다.
- useMemo를 사용한 값이 다른 Hook의 종속성으로 사용될 수 있는 경우
function Example() {
const [count, setCount] = useState(0);
const factorial = useMemo(() => {
let fact = 1;
for (let i = 1; i <= count; i++) {
fact = fact * i;
}
return fact;
}, [count]);
useEffect(() => {
console.log(`Factorial of ${count} is ${factorial}`);
}, [factorial]);
return (
<div>
<p>Count: {count}</p>
<p>Factorial: {factorial}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
버튼을 누르면 count의 숫자가 1 증가합니다.
factorial 함수는 useMemo로 감싸져 있고, 종속성 배열에 count가 들어있습니다.
버튼을 눌러서 count의 상태가 변했으므로 factorial 함수가 작동해서 factorial 값을 반환합니다.
useEffect의 종속성 배열에는 factorial 값이 있습니다.
따라서 factorial의 값이 변했으므로 리렌더링을 하고 console.log에 적힌 문자열을 출력합니다.
이렇게 useEffect의 종속성으로 factorial 값을 사용해서 factorial의 값이 변할 때만 실행되도록 사용할 수 있습니다.
그 외의 경우, useMemo를 사용한다고 해서 크게 이득이 없습니다.
근데 사용한다고해서 손해는 아니기 때문에 그냥 사용하는 경우도 많다고 합니다.
하지만 아래와 같은 단점들이 있기 때문에 무작정 useMemo를 사용하는 것을 고려해봐야 합니다.
- useMemo를 과하게 사용하면 코드의 복잡성이 증가해 가독성이 떨어질 수 있습니다.
- 지나치게 많은 값을 메모하게 되면 메모리 사용량이 증가해서 성능이 오히려 저하될 수 있습니다.
- 컴포넌트가 리렌더링될 때마다 실행되는 로직이나 사이드 이펙트에 사용되어서는 안됩니다.
외부 상태에 의존하지 않는 순수한 계산, 순수 함수에만 사용해야합니다.
예제와 함께 이해해 보기
useMemo가 없는 경우
// TodoList.js
const TodoList = ({ todos, onRemove }) => {
const tempTodos = () => {
let startTime = performance.now();
while (performance.now() - startTime < 300) {}
console.log("delay");
return todos;
};
const notMemoTodos = tempTodos();
if (todos.length === 0) {
return <div>Loading...</div>;
}
return (
<ol className="todolist">
{notMemoTodos.map((todo) => (
<TodoListItem todo={todo} key={todo.id} onRemove={onRemove} />
))}
</ol>
);
};
React에서는 기본적으로 컴포넌트가 다시 렌더링 되면 모든 자식을 재귀적으로 리렌더링 합니다.
여기서 등록할 todo의 제목을 쓰면 onChange 이벤트가 발생하기 때문에 input의 값이 변경될 때마다 App이 리렌더링 됩니다.
따라서 App의 하위 컴포넌트인 TodoList 컴포넌트도 계속 리렌더링 됩니다.
예제에서는 확인을 위하여 일부로 딜레이를 걸었기 때문에 차이를 확인 가능합니다.
useMemo를 사용한 경우
// TodoList.js
const TodoList = ({ todos, onRemove }) => {
const tempTodos = () => {
let startTime = performance.now();
while (performance.now() - startTime < 300) {} // 인위적으로 딜레이
console.log("delay");
return todos;
};
const memoTodos = useMemo(tempTodos, [todos]);
if (todos.length === 0) {
return <div>Loading...</div>;
}
return (
<ol className="todolist">
{memoTodos.map((todo) => (
<TodoListItem todo={todo} key={todo.id} onRemove={onRemove} />
))}
</ol>
);
};
useMemo의 첫 번째 인수로 todos를 반환하는 함수를 넣어주고, 두 번째 인수인 종속성 배열엔 todos를 넣었습니다.
이렇게 하면 todos에 변경사항이 있을 때만 tempTodos 함수가 호출이 되고, todos가 이전과 동일하다면 호출이 되지 않습니다.
input에 값을 입력하면 onChange 이벤트가 계속 발생하지만, todos는 변하지 않기 때문에 딜레이가 걸리지 않는 모습을 확인할 수 있습니다. 버튼을 눌러서 todo를 등록하면, todos 배열에 todo가 추가되어 변화가 생기기 때문에 딜레이가 걸리는 모습을 확인할 수 있습니다.
참고
https://react.dev/reference/react/useMemo
useMemo – React
The library for web and native user interfaces
react.dev
또한 간단한 예제는 ChatGPT에게 도움을 받았습니다.
🍨 디저트
부족하고 또 부족한 글 읽어주셔서 감사합니다. 피드백이 있으시다면 댓글로 알려주세요!
다음에 또 오세요!
'React > Hooks' 카테고리의 다른 글
useId를 간단하게 알아보자 (0) | 2023.08.15 |
---|---|
useReducer로 초초초간단 TodoList 만들기 (6) | 2023.03.23 |
useReducer를 알아보자 (0) | 2023.03.23 |