리액트는 다음과 같은 경우에 컴포넌트를 리렌더링 한다.
*조건
props
가 변경될 때불필요한 리렌더링은 성능에 문제가 되므로, 상황에 따라
useMemo
, useCallback
, React.memo
를 적절히 사용해 최적화해보자.
컴포넌트 내부에서 특정 변수가 선언되었고, 그 변수는 어떠한 함수의 리턴값이 담긴다고 하자.
코드 실행 중 *조건에 부합해 해당 컴포넌트가 리렌더링된다고 하면 이 변수 또한 다시 함수를 실행해 리턴되는 값을 할당하는 과정을 거치게 된다.
만약 이 함수가 CPU 소모가 심한 고비용의 함수일 경우 어떻게 될까? 컴포넌트가 리렌더링 될 때마다 함수가 호출되면서 많은 시간을 소요하게 될 것이다.
이럴 때 사용하면 좋은 것이 useMemo
다.
🎁
useMemo
는 고비용의 함수가 리턴하는 "값"을 캐싱하기 위해 사용하는 리액트 훅이다.
형태
const cachedValue=useMemo(calculateValue, dependencies);
파라미터
calculateValue
: 캐시하려는 값을 계산하는 함수로, 반드시 어떤 타입의 값이든 반환해야 한다.dependencies
:calculateValue
코드 내에서 참조되는 모든 값을 배열 형태로 전달해야 한다.useMemo
는 초기 렌더링에서 calculateValue
를 호출한 결과를 반환하고, 이후에 의존성이 변경되지 않는 한 재호출 되지 않고 이전 렌더링에 저장된 값을 반환한다.
예시 코드
(최적화 전)
function Example(){
const [numArr,setNumArr]=useState([1,2,3,4]);
//...
const computeExpensiveValue=((a,b)=>{
//오래 걸리는 작업 수행...
return value;
})();
//...
}
(후)
import {useMemo} from 'react';
function Example(){
//...
const computeExpensiveValue=useMemo((a,b)=>{
//오래 걸리는 작업 수행...
return value;
},[a,b])
//...
}
자식 컴포넌트에서는 변한게 없어도 부모 컴포넌트가 리렌더링되면 자식 컴포넌트도 리렌더링된다.
이럴 때는 memo
를 사용해주자.
🎁
memo
는props
가 변경되지 않을 경우 컴포넌트를 리렌더링을 하지 않는 고차 컴포넌트(HOC)이다.
형태
const MemoizedComponent=memo(SomeComponent, arePropsEqual?)
파라미터
component
: memoize할 컴포넌트(Optional)arePropsEqual
: 이 함수는 컴포넌트의 이전 props
와 다음 props
를 비교하여 컴포넌트를 다시 렌더링할지 여부를 결정하는 함수이다.arePropsEqual
함수가 true
를 반환하면 리렌더링 하지 않고, false
를 반환하면 리렌더링한다.import React from 'react';
const MyComponent = (props) => {
// 컴포넌트 구현
};
위와 같이 첫번째 인자만 전달하면, 얕은 비교(Shallow Comparision)를 통해 props 변경 여부를 판단한다.
하지만 두번째 인자를 전달하게 되면, 해당 함수의 반환 값에 따라 컴포넌트를 리렌더링하게 된다.
import React from 'react';
const MyComponent = (props) => {
// 컴포넌트 구현
};
const arePropsEqual = (prevProps, nextProps) => {
// 두 개의 props를 비교하여 true 또는 false를 반환
return prevProps.someValue === nextProps.someValue;
};
export default React.memo(MyComponent, arePropsEqual);
arePropsEqual
에서 복잡한 비교 로직을 사용할 경우 성능에 영향을 줄 수 있으니 주의하는게 좋다.
만약 부모 컴포넌트에서 정의된 함수를 props
로 받아 렌더링하는 자식 컴포넌트가 있다고 하자.
예를 들면, 다음과 같다.
import Button from './Button';
function Form(){
const onClick=()=>{
console.log('Clicked!');
}
return (
<form>
//...
<Button onClick={onClick}/>
</form>
)
}
import {memo} from 'react';
function Button({onClick}){
console.log("The button has been rendered.");
return (
<button onClick={onClick}>버튼</button>
);
}
export default memo(Button);
자식 컴포넌트를 memo
했지만, 여전히 부모 컴포넌트가 리렌더링되면 같이 리렌더링이 된다.
왜일까?
위와 같이 memo(Button)
하면, Button
컴포넌트의 props
를 얕은 비교를 통해 리렌더링을 하게 된다.
onClick
은 함수고 함수는 객체이기 때문에, 부모에서 리렌더링 되면 onClick
도 새로 생성되어 새로운 참조 값을 가지게 된다.
따라서 Button
입장에서는 props
가 변했다고 판단하고 리렌더링을 실행한다.
이럴때 useCallback
을 써주면 좋다.
🎁
useCallback
은 "함수의 정의"를 캐싱해주는 리액트 훅이다.
형태
const cachedFn = useCallback(fn, dependencies)
파라미터
fn
: 캐싱할 함수값dependencies
: fn
에서 참조하는 모든 값 배열을 전달한다.props
, state
, 그리고 컴포넌트 내부에서 직접 선언된 모든 변수와 함수를 포함한다.초기 렌더링에서는 fn
을 반환하고,
다음 렌더링부터는 dependencies
가 변경되었다면 새로 만든 함수를, 변경되지 않았다면 이전 함수를 반환한다.
function UserForm(){
const onClick=useCallback(()=>{
console.log('Clicked!');
},[]);
return (
<form>
//...
<Button onClick={onClick}/>
</form>
)
}
위와 같이 자식 컴포넌트의 props
로 전달되는 함수를 useCallback
으로 감싸주면, dependencies
가 변경되지 않는 한 자식 컴포넌트는 리렌더링 되지 않는다.