[React/Js] React UseMemo 살펴보기
시작하며
React에서는 함수형 컴포넌트를 사용하기 때문에, hooks를 사용하여 상태를 관리하게 됩니다. 함수이기 때문에 상태는 저장될 수 없습니다.
이때, 함수가 재호출되어도 값을 유지해야하는 경우가 발생을 합니다.
UseState도 비슷한 역할을 하지만, UseState는 값이 변할 때 마다, 리랜더링이 발생을 합니다. 리랜더링이 필요가 없을때도 값을 저장하는 로직이 필요하다면, UseMemo 또는 UseRef hook을 사용할 수 있습니다.
오늘은 UseMemo에 대해서 다뤄봅니다.
UseMemo
UseMemo 훅은 복잡한 로직이 담긴 계산의 결과값을 저장하는 용도로 많이 사용됩니다. 또는, 캐싱되어야 하는 값을 저장하는데 많이 사용합니다.
UseRef도 값을 저장하는데 사용하지만, 이 둘의 차이점은 UseRef는 직접 DOM요소에 접근을 한다는 점이 다릅니다.
또한, UseMemo는 Dependency Array를 인자로 받을 수 있어, 특정값이 변할 때, 로직을 다시 수행하여 값을 캐싱할 수 있고, UseRef는 그렇게 할 순 없습니다.
UseMemo는 Memoization 을 위한 용도로 사용하기 때문에, 성능 최적화가 일어날 수 있으나, 무분별하게 사용하면, 메모리에 불필요한 데이터들이 많이 쌓이기 때문에, 신중하게 사용해야 합니다.
예제
복잡한 함수 최적화
값을 계산하는 함수 2개를 만든다고 가정합니다. 첫번쨰 함수는 엄청 무거운 작업을 하는 함수로 가정하고, 두번째 함수는 가벼운 작업을 하는 함수로 가정합니다. 그 값을 컴포넌트에서 hardSum 과 easySum으로 값을 받는다고 생각해봅니다.
하지만 문제가 있습니다. useState를 사용한 hardNumber와 easyNumber의 값이 바뀔 때 마다, 컴포넌트가 리랜더링 되어서, hardSum과 easySum이 동시에 계산을 다시 수행하게 됩니다.
즉, 쉬운 계산을 하는데도 복잡한 계산식이 불필요하게 동작하는 것입니다. 시간을 재보면 다음과 같습니다.

복잡한 계산식은 쉬운계산식 보다 약 20만배 정도 더 오래걸리는 작업입니다. 이것을 계속 수행한다는것은 너무나 비효율적이죠.
import { useState, useMemo } from "react";
const hardCalculate = (number) => {
console.time("hard");
for (let i = 0; i < 1000000000; i++) {}
console.timeEnd("hard");
return number + 10000;
};
const easyCalculate = (number) => {
console.time("easy");
console.timeEnd("easy");
return number + 1;
};
function UseMemo1() {
const [hardNumber, setHardNumber] = useState(1);
const [easyNumber, setEasyNumber] = useState(1);
const hardSum = hardCalculate(hardNumber);
const easySum = easyCalculate(easyNumber);
return (
<div>
<h3>어려운 계산기</h3>
<input
type="number"
value={hardNumber}
onChange={(e) => setHardNumber(parseInt(e.target.value))}
/>
<span>+ 10000 = {hardSum}</span>
<h3>쉬운 계산기</h3>
<input
type="number"
value={easyNumber}
onChange={(e) => setEasyNumber(parseInt(e.target.value))}
/>
<span>+ 1 = {easySum}</span>
</div>
);
}
export default UseMemo1;

이러한 비효율을 없애기 위해 UseMemo를 사용합니다. useMemo는 콜백함수와 Dependency Array를 인자로 받게 되는데, Dependency Array에 있는 값이 변경될 때만, 콜백함수에 있는 함수를 실행하여 계산한 결과를 변수에 저장하게 됩니다.
import { useState, useMemo } from "react";
const hardCalculate = (number) => {
console.time("hard");
for (let i = 0; i < 1000000000; i++) {}
console.timeEnd("hard");
return number + 10000;
};
const easyCalculate = (number) => {
console.time("easy");
console.timeEnd("easy");
return number + 1;
};
function UseMemo1() {
const [hardNumber, setHardNumber] = useState(1);
const [easyNumber, setEasyNumber] = useState(1);
const hardSum = useMemo(() => {
return hardCalculate(hardNumber);
}, [hardNumber]);
const easySum = useMemo(() => {
return easyCalculate(easyNumber);
}, [easyNumber]);
return (
<div>
<h3>어려운 계산기</h3>
<input
type="number"
value={hardNumber}
onChange={(e) => setHardNumber(parseInt(e.target.value))}
/>
<span>+ 10000 = {hardSum}</span>
<h3>쉬운 계산기</h3>
<input
type="number"
value={easyNumber}
onChange={(e) => setEasyNumber(parseInt(e.target.value))}
/>
<span>+ 1 = {easySum}</span>
</div>
);
}
export default UseMemo1;

이렇게 하면, 각자의 값이 바뀔때만, 함수를 사용하여 값을 계산할 수 있어 성능적으로 최적화가 됩니다.
참조타입일때
사실, 리액트를 하다보면 이런 복잡한 계산식이 들어갈 일이 많이 없긴 합니다. 그렇다면 useMemo는 사실 쓸 필요가 없는것일까요?
아닙니다. 객체를 사용할 때를 예로 들어보겠습니다.
이번 예제에서는 값이 value type 즉, 값타입 이 아닌 reference type을 생성하여, useEffect훅으로 관리해보겠습니다.
import { useEffect, useMemo, useState } from "react";
function UseMemo2() {
const [number, setNumber] = useState(0);
const [isKorea, setIsKorea] = useState(true);
const location = useMemo(() => {
return { country: isKorea ? "한국" : "외국" };
}, [isKorea]);
useEffect(() => {
console.log("useEffect 호출");
}, [location]);
return (
<div>
<h2>하루에 몇끼 먹어요?</h2>
<input
type="number"
value={number}
onChange={(e) => setNumber(e.target.value)}
/>
<hr />
<h2>어느 나라에 있어요?</h2>
<p>나라 : {location.country}</p>
<button onClick={() => setIsKorea(!isKorea)}>비행기 타자</button>
</div>
);
}
export default UseMemo2;
위 코드를 짜서 돌리면, location 변수를 변경하지 않으면, useEffect안에 있는 콜백함수가 실행되지 않을것 처럼 보입니다. 하지만 실제 결과를 돌려보면, 예상과 다르게 결과가 출력됩니다.

제가 숫자만 올렸는데, useEffect의 콜백함수가 실행되는것을 볼 수 있습니다. 이 역시 함수형 컴포넌트 이기 때문에 발생하는 문제 입니다.
리렌더링 콜이 나오고, 함수가 다시 호출되면서, 지역변수로 있던, location이 사라지고, 새로운 location 변수를 만들게 됩니다. 그와 동시에 Heap 메모리에 객체가 새로 생성되며, location 변수에는 새로 생성된 변수의 주솟값이 할당이 됩니다.
그래서 useEffect는 변수가 가지고 있던 참조값이 바뀌었으므로, 해당 값이 변경되었다고 판단하여 콜백함수를 호출하게 됩니다.
이런 상황을 막기 위해 useMemo를 많이 사용합니다. 객체의 참조값을 캐싱하여 리랜더링시에 객체 참조가 변하지 않게 하는 역할을 수행하는 것이죠.
import { useEffect, useMemo, useState } from "react";
function UseMemo2() {
const [number, setNumber] = useState(0);
const [isKorea, setIsKorea] = useState(true);
const location = useMemo(() => {
return { country: isKorea ? "한국" : "외국" };
}, [isKorea]);
useEffect(() => {
console.log("useEffect 호출");
}, [location]);
return (
<div>
<h2>하루에 몇끼 먹어요?</h2>
<input
type="number"
value={number}
onChange={(e) => setNumber(e.target.value)}
/>
<hr />
<h2>어느 나라에 있어요?</h2>
<p>나라 : {location.country}</p>
<button onClick={() => setIsKorea(!isKorea)}>비행기 타자</button>
</div>
);
}
export default UseMemo2;
객체 참조가 변하지 않도록 useMemo를 사용하여 캐싱한 후에 다시 실행해보면, 저희가 원하는 결과가 나옵니다.

