[React Hooks] useMemo 사용하기
먼저 간단하게 `메모제이션`에 대해 알고 넘어가자.
메모제이션(Memoization)
→ 기존에 수행한 연산의 결과값을 어딘가에 저장해두고 동일한 입력이 들어오면 재활용하는 프로그래밍 기법.
→ 적절히 적용하면 중복 연산을 피할 수 있기 때문에 메모리를 더 사용하는 경향이 있어도 성능 측면에서 큰 이점이 있어서 알고리즘 성능 최적화에 많이 사용된다.
컴퓨터프로그래밍용어로, 동일한 계산을 반복해야 할 경우 한 번 계산한 결과를 메모리에 저장해 두었다가 꺼내 씀으로써 중복 계산을 방지할 수 있게 하는 기법이다.동적 계획법의 핵심이 되는 기술로써 결국 메모리라는 공간 비용을 투입해 계산에 소요되는 시간 비용을 줄이는 방식이다. - 나무위키 -
→ 시간복잡도를 O(N)으로 줄인다.
useMemo
일반적으로 React의 함수형 컴포넌트는 다음과 같이 작성된다.
function Jacob({x, y}) {
const z = lazyLogic(x, y);
return <div>{z}</div>
}
이러한 컴포넌트는 앱이 랜더링 될때마다 호출된다.
(기본로직) 컴포넌트 함수가 호출 → 자바스크립트 로직 수행 → HTML 마크업된 UI 리턴
하지만 랜더링이 한번 되고 끝나는게 아니라, 컴포넌트 자신의 상태 변경 또는 부모 컴포넌트의 상태 변경이 일어나 덩달아 함께 렌더링되야 하는 경우도 있다.
위 함수에서 lazyLogic은 세상에세 제일 느린 함수라고 가정해 보자. 아주 복잡하고 복잡한 로직이다. 그리고 값으로 받는 x, y 값이 항상 바뀌는게 아니라면 lazyLogic() 함수를 계속 실행해야할 필요가 있을까?
바로 이러한 경우를 메모제이션이라는 개념을 통해 개선할 수 있으며, React hook에서는 v16.8 버전에서부터 기본적으로 내장돼있는 useMemo로 해결할 수 있다.
useMemo()는 2개의 인자를 받는데, 첫번째는 결과 값을 생성해주는 팩토리 함수이고, 두번째는 결과값을 재활용할 때 기준이 되는 입력값 배열이다.
그럼 위의 함수를 개선해 보자.
import React, { useMemo } from 'react';
function Jacob({x, y}) {
const z = useMemo(() => lazyLogic(x, y), [x, y]);
return <div>{z}</div>
}
x, y 값이 이전에 렌더링했을 때와 동일한 경우, 랜더링 때 구했던 결과값을 재활용 한다.
하지만, 이전과 값이 달라졌다면, ()=>lazyLogic(x, y)함수를 호출하여 z를 할당해 준다.
(2020-02-02 예제 추가) 기존 설명으로는 부족한 감이 있어, 실제로 useMemo()를 적용한 예제를 추가한다.
import React, { useState, useMemo } from "react";
function App() {
const [count, setCount] = useState(0);
const [wordIndex, setWordIndex] = useState(0);
const words = ["H", "E", "L", "L", "O"];
const word = words[wordIndex];
const expensiveCompute = word => {
console.log("expensiveComput() 호출!");
let i = 0;
while (i < 1000000000) i++;
return word.length;
};
const wordsLength = useMemo(() => expensiveCompute(word), [word]);
return (
<div style={{ padding: "15px", border: "solid 1px" }}>
<h2>매우 비싼 계산</h2>
<p>
"{word}"의 길이: {wordsLength}
</p>
<button
onClick={() => {
const next = wordIndex + 1 === words.length ? 0 : wordIndex + 1;
setWordIndex(next);
setCount(count + 1);
}}
>
버튼 클릭({count})
</button>
</div>
);
}
export default App;
위 예제는 단어의 길이를 구하는 엄청 복잡한 계산을 한다(사실 1만 반환되긴 하지만...).
아래 동작하는 모습을 확인해 보자.
계속 word의 값이 바뀌다가 ["H", "E", "L", "L", "O"]에서 인덱스 2와 3의 값이 같기 때문에
expensiveCompute() 함수를 다시 호출하여 계산을 하지 않고 캐싱된 값을 그대로 제공된다.
이 글을 마치며,
소프트웨어 최적화에는 항상 그에 상응하는 대가가 따르기 마련이다.
성능 최적화를 함에 따라 얻는 이점이 더 많은지 꼭 따져보고 사용해야 한다.
예를 들면, useMemo hook함수를 남용하면, 컴포넌트의 복잡도가 올라가기 때문에 코드를 읽기도 어렵고 유지보수성도 떨어진다. 또한 gc에서 제외되기 때문에 메모리를 더 쓰게된다. (← 추가설명: useMemo가 적용된 레퍼런스를 재활용하기 위해 가비지 컬렉션에서 제외된다.)
잘 생각해보면 프론트에서 수 초이상 소요되는 로직을 구현하는 일 자체가 흔치 않다. 사용할 일이 많지 않는게 정상이다. 설사 있다고 해도 useEffect 함수 등을 이용해서 비동기로 처리하는 방안이 우선 고려된다.
결론: 무분별한 useMemo를 사용하지 말고, 필요한 경우만 사용하자!