
메모이제이션(Memoization)이란?
JS 메모이제이션에서도 공부했지만 리마인드 차원에서 다시 살펴 보자면, 프로그래밍에서 반복되는 결과를 메모리에 저장 해놓고 다음에 같은 결과가 나올 때 다시 계산할 필요없이 빨리 실행 하는 코딩 기법을 말하는 것이다.
먼저 React에서 컴포넌트가 렌더링하는 규칙에는 대표적으로 크게 3가지가 존재한다.
이 중 세번째 Re-Rendering 된 부모컴포넌트의 모든 자식컴포넌트들은 불필요한 렌더링이 일어난다.
이러한 불필요한 렌더링은 애플리케이션의 규모가 커질수록 더욱 성능저하가 올 수 있다.
따라서 성능최적화를 위해 메모이제이션을 하는데 대표적으로 React.memo, useMemo, useCallback가 있다.
공식문서를 확인해보면 첫 문장에 다음과 같이 적혀있다.
"memo lets you skip re-rendering a component when its props are unchanged."
props가 변하지 않을 경우 re-rendering을 건너 뛴다.
또한 공식문서에 따르면 React.memo는 고차 컴포넌트(Higher Order Component, HOC)이며, React.memo를 이용해서 감싸는 방식으로 자식 컴포넌트가 받는 props에 변화가 있다면 리렌더링을 하고 변화가 없다면 기존에 저장되어 있던 내용을 재사용한다.
고차 컴포넌트 공식문서 (HOC, Higher Order Component)란?
고차 컴포넌트는 컴포넌트 로직을 재사용하기 위해 사용되고 컴포넌트를 매개변수로 받아 새로운 컴포넌트르 반환하는 함수를 의미합니다.
테스트 환경 - codepen
import React from 'https://esm.sh/react@18.2.0'
import ReactDOM from 'https://esm.sh/react-dom@18.2.0'
// 기본 컴포넌트
const MyComponent = ({ name }) => {
console.log('MyComponent 리렌더리이이잉'); // 렌더링 로그 출력
return <div>Hello, {name}!</div>;
};
// React.memo로 래핑된 컴포넌트
const MemoizedComponent = React.memo(MyComponent);
// 사용 예시
const App = () => {
const [count, setCount] = React.useState(0);
console.log('리렌더리이이잉');
// count 상태가 변경될 때마다 App 컴포넌트가 재렌더링됨
// 그러나 MemoizedComponent는 동일한 name prop으로 재렌더링되어도 이전에 계산한 결과를 재사용함
return (
<div>
<button onClick={() => setCount(count + 1)}>Increment</button>
<MyComponent name="SeungWon" />
</div>
);
};
ReactDOM.render(<App/>,document.getElementById("root"));
출처: https://openai.com/blog/chatgpt
위 예시 코드에서는
React.memo()는 얕은 비교만 수행한다. 컴포넌트의 props를 얕은 비교(Shallow comparison)로 변경되었을 때에만 리렌더링을 방지한다. 즉, 객체나 함수를 props로 받을 때 값이 같더라도 이전 참조값과 다르기 때문에 리렌더링이 발생 합니다. 해당 부분을 방지하려면 useMemo, useCallback 등의 hook이 있고, React.memo(Component, areEqual)에 두번째 인자값으로 이전 props와 새로운 props를 비교하여 true/false를 반환하고 리렌더링을 결정하는 함수이다.
React.memo(Component, areEqual) 예시코드
const customEqual = (prevProps, nextProps) => { // 깊은 비교 로직 구현 return deepEqual(prevProps, nextProps); // 예시로 deepEqual 함수 사용 }; const MyComponent = React.memo(Component, customEqual);
useMemo도 공식문서를 확인해 보자.
"useMemo is a React Hook that lets you cache the result of a calculation between re-renders."
useMemo는 리렌더링 사이에 계산 결과를 캐시할 수 있는 React Hook입니다.
리액트는 상태가 업데이트 될 때마다 리렌더링이 되기 때문에 이전에 쓰이던 값과 똑같은 결과를 내는 복잡한 연산이 들어있는 함수(useCallback), 그 결과값(useMemo)들 까지도 새롭게 불러오는 것은 엄청난 낭비가 될 수 있다.
useMemo는 React의 훅(Hook) 중 하나로, 계산 비용이 많은 함수의 결과 값을 기억하고 재사용하는 데 사용된다. 이를 통해 성능을 최적화할 수 있다.
테스트 환경 - codepen
import React from 'https://esm.sh/react@18.2.0'
import ReactDOM from 'https://esm.sh/react-dom@18.2.0'
// 기본 컴포넌트
const MyComponent = () => {
const [count, setCount] = React.useState(0);
const [inputValue, setInputValue] = React.useState('');
const calculateMemoizedCount = (count) => {
// 복잡한 계산 로직 (예시로 for문 사용)
let result = count;
for (let i = 0; i < 3000; i++) {
console.log('테스트 :: ',i);
result += i;
}
console.log('끝');
return result;
};
// count 값이 변경될 때만 함수를 실행하고 이전에 계산된 값을 재사용
const memoizedCount = React.useMemo(() => calculateMemoizedCount(count), [count]);
return (
<div>
<p>Count: {count}</p>
<p>Memo Count: {memoizedCount}</p>
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="Enter a value"
/>
<button onClick={() => setCount(count + 1)}>count</button>
</div>
);
};
ReactDOM.render(<MyComponent/>,document.getElementById("root"));
위의 예시코드 에서는
테스트 플로우
카운트 버튼 1번 클릭 -> input창에 두번 값(예: 1,2)을 입력.


위 예시코드를 개발자도구 Performance를 사용하여 테스트결과
Scripting, Rendering에서 1초정도 차이를 보이고 있다.
const memoizedValue = useMemo(() => {
// 과도하고 거대한 로직...
}, []);useCallback 공식문서를 확인해 보자.
"useCallback is a React Hook that lets you cache a function definition between re-renders."
useCallback은 리렌더링 사이에 함수 정의를 캐시할 수 있게 해주는 React Hook입니다.
useMemo에서도 말 했듯이, 리액트는 상태가 업데이트 될 때마다 리렌더링이 되기 때문에 이전에 쓰이던 값과 똑같은 결과를 내는 복잡한 연산이 들어있는 함수(useCallback), 그 결과값(useMemo)들 까지도 새롭게 불러오는 것은 엄청난 낭비가 될 수 있다고 설명 하였다. useCallback은 메모이제이션을 통해서 특정함수를 재사용한다는 것이다.
useCallback과 useMemo의 차이점은 함수를 재사용하느냐, 값을 재사용하느냐의 차이다.
useCallback 예시를 보기전에 함수동등성을 짧게 보고가자.
함수동등성이란?
자바스크립트에서 함수는 객체로 취급이 되기때문에, 함수를 동일하게 만들어도 메모리 주소가 다르면 다른 함수로 간주한다.const add1 = () => x + y; const add2 = () => x + y; add1 === add2 false
이러한 자바스크립트의 특성은 React 컴포넌트 내에서 어떤 함수를 자식 컴포넌트의 props로 넘길 때 예상치 못한 성능 문제(불필요한 렌더링)로 이어질 수 있다.
아래와 같이 데이터를 가져오는 fetchData 함수를 만들고, useEffect에 의존성 배열로 fetchData를 추가해보자.
import React, { useState, useEffect } from 'react'
function Profile({ id }) {
const [data, setData] = useState(null)
const fetchData = () =>
fetch(`https://test-api.com/data/${id}`)
.then(response => response.json())
.then(({ data }) => data)
useEffect(() => {
fetchData().then(data => setData(data))
}, [fetchData])
// ...
}
아래는 위 코드에 useCallback을 활용해 보겠다.
import React, { useState, useEffect } from 'react'
function Profile({ id }) {
const [data, setData] = useState(null)
const fetchData = useCallback(
() =>
fetch(`https://test-api.com/data/${id}`)
.then(response => response.json())
.then(({ data }) => data),
[id],
)
useEffect(() => {
fetchData().then(data => setData(data))
}, [fetchData])
// ...
}
공식 문서를 참고해보면 useMemo의 설명 중에 굵은 글씨로 "useMemo는 성능 최적화를 위해서 사용될 수 있지만 의미상으로 보장이 있다고 생각하지는 마라." 라는 말이 있다. useMemo는 분명 성능 최적화를 해주고, 좀 더 웹, 앱을 빠르게 만들어 줄 수 있지만, 무분별하게 useMemo로 감싸게 되면 이 또한 리소스 낭비가 될 수 있으므로 퍼포먼스 최적화가 필요한 연상량이 많은 곳에 사용하는 것이 좋다.
useCallback도 마찬가지로 꼭 필요한 상황에 사용하라고 적혀있다.
useMemo에 언제 사용해야 성능상 이점을 가져올 수 있는지 테스트한 글이다.
https://github.com/yeonjuan/dev-blog/blob/master/JavaScript/should-you-really-use-usememo.md
이번 글을 작성하면서 조금이라도 React Memoization에 대해 조금은 알게 된 것 같다.
성능최적화를 위해 메모이제이션을 하는데 대표적으로 React.memo, useMemo, useCallback가 있고,
모두 성능최적화를 하기한 훌륭한 도구이지만 무작정 써야되는게 아니라 언제 어떤 상황에서 알맞게 사용해야 될 지가 정말 중요하다는걸 알 수 있었고 무분별하게 사용하게 된다면 오히려 성능저하가 올 수 있다는 심각성도 알 수 있었다.
참고