리액트의 메모이 제이션은 함수 컴포넌트 내에서 불필요한 재계산을 방지하고 컴포넌트의 성능을 최적화 하기 위한 기술이다.
다음과 같은 함수를 활용하여 메모이제이션을 구현할 수 있다.
먼저 메모이제이션이 왜 필요한지 알아보자
React의 랜더링 과정은 위 그림과 같다.
React
는 Virtual DOM
을 생성한다.Virtual DOM
을 생성한다.변경된 부분
을 찾아낸다확정(Commit)
이라고 한다.최소한의 작업으로 업데이트
한다.확정(Commit)
단계가 끝나면 브라우저는 실제 DOM을 업데이트하고 변경된 내용이 화면에 반영
된다.이처럼 virtual DOM을 사용하면 실제 DOM을 조작하는것 보다 효율적으로 화면을 랜더링 할 수 있다.
여기서 문제는 사소한 변경(state, props)이 생기기라도 하면 virtual DOM
을 처음부터 다시 그린다는 것이다. 모든 컴포넌트가 변경될 일이 없기 때문에 , React에서는 컴포넌트를 메모이제이션 하여 불필요한 리랜더링을 방지 할 수 있다.
예를들어 부모컴포넌트의 사소한 변경에 자식컴포넌트들이 전부다 재 랜더링되는건 상당한 낭비라고 할 수 있다.
const MyComponent = React.memo((props) => {
// props를 이용한 컴포넌트 렌더링
});
// 사용법 예시
<MyComponent prop1={value1} prop2={value2} />
react.memo의 사용은 컴포넌트 전체를 감싸면서 시작된다. React.memo로 감싼 컴포넌트는 props가 변경되지 않는다면 해당컴포넌트는 다시 virtual DOM을 그릴 때 다시 만들지 않고 메모이제이션한 컴포넌트를 재 사용하게 된다.
const memoizedValue = useMemo(() => {
// 계산할 값 또는 함수
}, [dependency1, dependency2]);
useMemo는 특정 값의 계산을 메모이제이션하여 의존성이 변경되지 않는 한 이전에 계산한 결과를 재사용한다.
useMemo는 주로 계산이 오래걸리는 작업의 결과를 보존
할 때 사용한다. 그렇지 않으면 컴포넌트를 재 랜더링할 때마다 계산을 다시 해야하기 때문이다.
import React, { useState, useMemo } from 'react';
const SumCalculator = ({ n }) => {
// 1부터 n까지의 합을 계산하는 함수
const calculateSum = (n) => {
console.log('Calculating sum...');
let sum = 0;
for (let i = 1; i <= n; i++) {
sum += i;
}
return sum;
};
// useMemo를 사용하여 결과를 메모이제이션
const sum = useMemo(() => calculateSum(n), [n]);
return (
<div>
<p>1부터 {n}까지의 합: {sum}</p>
</div>
);
};
export default SumCalculator;
다음 코드는 1~n까지의 합을 계산하는 함수의 값을 메모이제이션을 사용하여 재사용하고 있다. 만약 useMemo를 사용하지 않는다면 해당 컴포넌트를 사용할 때마다 SumCalculator
함수를 내부 로직이 다시 수행될 것이다.
위의 예시에서는 n의 값이 변경되지 않는이상 sum은 메모이제이션된 값을 사용하게 된다.
const memoizedCallback = useCallback(() => {
// 콜백 함수 로직
}, [dependency1, dependency2]);
React에서 컴포넌트가 리랜더링 될 때 , 함수 컴포넌트는 내부에 선언된 모든 함수를 새로 생성
하게 된다.
부모 컴포넌트가 리랜더링되면 자식 컴포넌트도 함께 리랜더링 된다. 이 때 자식컴포넌트의 함수가 불필요하게 리랜더링 되는것을 방지하기 위해 사용한다.
컴포넌트는 state
나 props
가 변경되면 리랜더링하게 되어있다.
그럴 때마다 컴포넌트에 모든 함수를 재생성하는데 만약 그 재생성 함수가 자식컴포넌트의 props
로 전달하는 함수라면 이전 함수와 형태는 동일하지만 다른 객체로 판단
하기 때문에 자식 컴포넌트는 불필요하게 리랜더링이 되고 만다.
다음 예시는 useCallback
을 사용하는 방법을 보여주고 있다.
자식 컴포넌트
import React from 'react';
// React.memo를 통해 컴포넌트 전체를 메모이제이션 했다.
const ChildComponent = React.memo(({ onClick }) => {
console.log('ChildComponent rendered');
return (
<button onClick={onClick}>Click me</button>
);
});
export default ChildComponent;
자식 컴포넌트는 리랜더링하지 않기 위해 React.memo까지 사용했다.
useCallback을 사용하지 않은 부모 컴포넌트
import React, { useState, useCallback } from 'react';
import ChildComponent from './ChildComponent';
const ParentComponent = () => {
const [count, setCount] = useState(0);
// ParentComponent가 리랜더링 될때마다 handleClick은 재생성된다.
const handleClick = function(){
setCount((prevCount) => prevCount + 1);
}
return (
<div>
<ChildComponent onClick={handleClick} />
<p>Count: {count}</p>
</div>
);
};
export default ParentComponent;
다음은 useCallback
을 사용하지 않은 부모 컴포넌트의 예시이다. 자식 컴포넌트에 있는 button을 누를 때마다 count의 값은 변경되고 ParentComponent
는 리랜더링
된다. 이때 handleClick
도 재생성하게 된다. 새로 생성된 함수는 이전함수와 형태는 같지만 다른함수로 취급하기 때문에 해당 handleClick
함수를 props
로 전달 받은 ChildComponent
는 똑같 형태의 함수를 내려받았음에도 재랜더링되는 참사가 발생한다.
useCallback을 사용한 부모 컴포넌트
import React, { useState, useCallback } from 'react';
import ChildComponent from './ChildComponent';
const ParentComponent = () => {
const [count, setCount] = useState(0);
// 자식에게 넘겨줄 콜백함수를 useCallback을 사용해 메모이제이션 했다
const handleClick = useCallback(() => {
setCount((prevCount) => prevCount + 1);
}, []);
return (
<div>
<ChildComponent onClick={handleClick} />
<p>Count: {count}</p>
</div>
);
};
export default ParentComponent;
useCallback
을 사용했기 때문에 부모컴포넌트가 재랜더링 되더라도 ChildComponent
는 항상 같은 handleClick
함수를 받게 된다 때문에 ParentComponent
가 리랜더링 되더라도 React.memo
를 사용한 ChildComponent
는 리랜더링 되지 않고 메모이제이션된 값을 사용할 수 있다.
이런 메모이제이션도 주의할 점이 있다. 자주 변경되는 컴포넌트나 함수에 메모이제이션을 사용하게 되면 계속해서 메모이제이션(캐싱)하는 비용이 발생하기 때문에 주의해야한다.
유익한 글이었습니다.