React 렌더링 최적화
React 렌더링 최적화 (React.memo, useMemo, useCallback)
한 눈에 보는 결론
- useMemo는 결과 값을 캐싱한다.
- useCallback은 함수를 캐싱한다.
- React.memo는 고차 컴포넌트로, 컴포넌트의 리렌더링을 최소화한다.
성능 최적화 필수성
React로 개발할 때, 누구나 한 번씩 고민하게 되는 주제. 그것은 바로 성능 최적화다.
사용자 경험을 향상시키고, 불필요한 렌더링을 줄여 앱의 효율을 높이는 것.
성능 저하의 주요 원인
성능 저하를 일으키는 원인은 다양하지만 그 중 주요 원인을 알아보자.
- 불필요한 렌더링. state가 변경될 때, 변화가 없는 컴포넌트까지 불필요하게 렌더링 되는 것.
- 복잡한 계산식. 복잡한 계산과 데이터 처리는 앱의 자원을 많이 소모하여, 사용자 경험을 저해하는 것. (ex. 입력 폼)
이 외에도 다양한 원인이 있지만, 주제를 벗어나지 않도록 마무리 짓고, 주요 원인을 해결할 수 있는 방법을 설명해보겠다.
React.memo
React의 고차 컴포넌트이다.
컴포넌트가 리렌더링될 때, 해당 컴포넌트의 props
나 state
가 변경되면 React는 기본적으로 컴포넌트를 리렌더링한다.
이러한 이유 때문에 부모 컴포넌트가 리렌더링할 때, 리렌더링이 필요없는 자식 컴포넌트도 함께 리렌더링되며 자원 낭비를 하는 경우가 생긴다.
이럴 때, React.memo를 사용하면 불필요한 렌더링을 방지할 수 있다.
React.memo
로 래핑된 컴포넌트는 이전에 렌더링된 결과를 기억하고, 이전 props
와 현재 props
를 비교하여 변경 여부를 판단한다.
만약 props
가 동일하다면 이전에 렌더링된 결과를 재사용하여 불필요한 리렌더링을 방지한다.
useMemo
useMemo
훅은 무거운 계산 작업의 결과를 메모리에 저장해 애플리케이션의 효율성을 높이는 데 도움을 준다.
첫 번째 인자로 콜백 함수를, 두 번째 인자로 의존성 배열을 받는다. 의존성 배열 내의 값이 변경되면 콜백 함수가 다시 실행되어 계산이 이루어지고, 그 결과가 메모리에 저장된다. 변경이 없다면 이전에 메모리에 저장된 값을 재사용한다.
import React, { useMemo } from 'react';
const getCalculatePrice = (max) => {
// 가격 계산 로직
};
const App = ({ max }) => {
// 의존성 배열 내의 값이 변경되면 getCalculatePrice 함수가 재실행된다.
const price = useMemo(() => getCalculatePrice(max), [max]);
return (
<div>{max}</div>
);
};
// App 컴포넌트는 동일
useCallback
useCallback
훅은 함수를 메모이제이션하여 컴포넌트의 불필요한 재렌더링을 방지한다. 특히 함수를 하위 컴포넌트에 props
로 전달할 때 유용하게 사용된다.
함수를 메모리에 저장하고, 의존성 배열 내의 값이 변경될 때만 함수를 새로 생성한다. 이를 통해 컴포넌트가 재렌더링되어도 동일한 함수 참조를 유지할 수 있어서 성능을 개선할 수 있다.
- 이벤트 핸들러 함수를 자식 컴포넌트에 props로 전달할 때
- 렌더링 최적화가 필요한 대규모 리스트나 테이블에서 항목의 이벤트 처리
- 업데이트가 잦은
state
나props
에 의존하는 함수
import React, { useState, useCallback } from 'react';
const ChildComponent = React.memo(({ onIncrement }) => {
console.log('ChildComponent 렌더링...');
return <button onClick={onIncrement}>증가</button>;
});
const ParentComponent = () => {
const [count, setCount] = useState(0);
const handleIncrement = useCallback(() => {
setCount((prevCount) => prevCount + 1);
}, []);
return (
<div>
<p>카운터: {count}</p>
<ChildComponent onIncrement={handleIncrement} />
</div>
);
};
export default ParentComponent;
훅을 적용하기 전에는 부모가 재렌더링될 때마다 핸들러 함수가 새로 생성되어 자식도 불필요하게 재렌더링되었다. 훅을 적용한 후에는 핸들러 함수의 참조가 변경되지 않기에 자식의 재렌더링이 방지되어 전체적인 성능이 향상된다.
useCallback
의 의존성 배열을 로 뒀기에, 컴포넌트가 처음 렌더링될 때 한 번만 생성되고, 절대 재생성되지 않는다.