리액트 렌더링 성능 최적화(useMemo, useCallback, React.memo)

Noma·2024년 7월 5일
0
post-custom-banner

리액트는 다음과 같은 경우에 컴포넌트를 리렌더링 한다.

*조건

  • 부모 컴포넌트에서 전달받은 props가 변경될 때
  • 부모 컴포넌트가 리렌더링 될 때
  • 컴포넌트 내부 state가 변경될 때

불필요한 리렌더링은 성능에 문제가 되므로, 상황에 따라
useMemo, useCallback, React.memo를 적절히 사용해 최적화해보자.

1. useMemo

컴포넌트 내부에서 특정 변수가 선언되었고, 그 변수는 어떠한 함수의 리턴값이 담긴다고 하자.

코드 실행 중 *조건에 부합해 해당 컴포넌트가 리렌더링된다고 하면 이 변수 또한 다시 함수를 실행해 리턴되는 값을 할당하는 과정을 거치게 된다.

만약 이 함수가 CPU 소모가 심한 고비용의 함수일 경우 어떻게 될까? 컴포넌트가 리렌더링 될 때마다 함수가 호출되면서 많은 시간을 소요하게 될 것이다.

이럴 때 사용하면 좋은 것이 useMemo다.

🎁 useMemo는 고비용의 함수가 리턴하는 "값"을 캐싱하기 위해 사용하는 리액트 훅이다.

형태

const cachedValue=useMemo(calculateValue, dependencies);

파라미터

  • calculateValue: 캐시하려는 값을 계산하는 함수로, 반드시 어떤 타입의 값이든 반환해야 한다.
  • dependencies:calculateValue 코드 내에서 참조되는 모든 값을 배열 형태로 전달해야 한다.
    해당 값은 props, state, 컴포넌트 본문 내에서 직접 선언한 모든 변수와 함수가 해당된다.

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])
  
  //...
}

2. React.memo

자식 컴포넌트에서는 변한게 없어도 부모 컴포넌트가 리렌더링되면 자식 컴포넌트도 리렌더링된다.

이럴 때는 memo를 사용해주자.

🎁 memoprops가 변경되지 않을 경우 컴포넌트를 리렌더링을 하지 않는 고차 컴포넌트(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에서 복잡한 비교 로직을 사용할 경우 성능에 영향을 줄 수 있으니 주의하는게 좋다.

3. useCallback

만약 부모 컴포넌트에서 정의된 함수를 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가 변경되지 않는 한 자식 컴포넌트는 리렌더링 되지 않는다.

참고자료

profile
오히려 좋아
post-custom-banner

0개의 댓글