useMemo, useCallback 는 언제 사용해야 할까? (feat. React.memo)

2021. 11. 26. 20:00Frontend/React

** 초보 개발자로 글에 수정해야 할 부분이 있을 수 있습니다. 정정해야 할 부분은 댓글로 소통 부탁드립니다!

 

리액트의 다양한 훅 중 useMemo, useCallback 를 언제 사용하면 좋을지에 대한 고민을 나눠보려 합니다.

 

1. useMemo()와 useCallback()은 무엇을 위한 Hook 인가?

useMemo 와 useCallback 을 사용하기 전에 리액트의 특징인 "리렌더링"에 대한 이해가 필요합니다.

리액트는 SPA(Single Page Application) 로 리렌더링 최적화를 위해 가상돔을 사용합니다.

그렇다면, 언제 렌더링이 발생할까요?

 

- state 변경이 있을 때
- props 변경이 있을 때
- 부모 컴포넌트가 업데이트 될 때
- shouldComponentUpdate에서 true가 반환될 때
- forceUpdate가 실행될 때

 

위와 같이 렌더링이 발생할 때 "불필요한 렌더링을 막기 위해" 리액트에서는 useMemo, useCallback 훅을 제공합니다.

사실 최적화의 관점에서는...!

오히려 useMemo, useCallback 을 위한 메모리가 사용되며, 코드가 늘어난다는 비용적인 측면에서 추천하지 않습니다.

 

 

2. 언제 useMemo(), useCallback() 훅을 사용해야 하는가?

해당 글에서는 다음의 경우라고 설명하고 있습니다.

 

1. 참조 동일성의 이점을 고려하라
2. 과도한 계산이 포함된 함수인 경우 사용하라

 

1) 참조 동일성의 이점을 고려하라

먼저, 다음에 대한 이해가 필요합니다.

  • javascript 의 타입의 특징인 primitive type 과 non-primitive type
    • string, number 의 경우에는 값 자체를 저장 (primitive type)
    • object, array 의 경우에는 주소값을 저장하여 reference (non-primitive type)
  • react 리렌더링 일어나는 과정
    • prev-state(이전 상태)와 next-state(변경될 상태)를 shallow 비교

따라서 primitive type의 경우에는 렌더링이 발생할 때 == 값이 변경될 때 임이 어느정도 보장 되지만,

non-primitive type 의 경우에는 그렇지 않습니다

(값이 변경되든, 그렇지 않든 값 비교를 했을 때 다른 값이므로 무조건 렌더링 되어야 한다고 하기 때문). 

 

예제 코드로 이해해 봅시다.

const Child = ({ bar, baz }) => {
  const options = { bar, baz };

  useEffect(() => {
    handleOptions(options);
  }, [options]);

  return <Box>foo</Box>;
}

 

useEffect는 options에 대해서 equality 체크를 매 렌더마다 하게 되고,

자바스크립트의 오브젝트 비교 방식(comparison of non-primitive type) 때문에,

options는 매 시간마다 매번 새롭게 만들어집니다.

그래서 리액트는 options가 매 렌더마다 변화 했는지 체크를 하는 테스트를 할때마다 항상 true를 반환하게 됩니다.

이를 해결 하기 위한 방법은 아래와 같습니다.

 

const Child = ({ bar, baz }) => {

  useEffect(() => {
    const options = { bar, baz };
    handleOptions(options);
  }, [bar, baz]); // 여기만 달라졌죠!

  return <Box>foo</Box>;
}

 

아주 간단한 방법이고, 실제로 해결 될 수만 있다면 더할나위 없다.

하지만 문제는 barbaz가 객체일 경우! 아래를 살펴 보자.

 

 

const Parent = () => {
  const bar = () => {};
  const baz = { a: 123 };

  return <Foo bar={bar} baz={baz} />;
}

 

barbaz는 이제 오브젝트 입니다!

Foo컴포넌트의 useEffect는 equality 체크에서 매번 true를 반환하게 되고 매번 렌더링을 다시 하게 됩니다.

문제의 코드를 다음과 같이 수정해 보았습니다.

 

const Parent = () => {
  const bar = useCallback(() => {}, [])
  const baz = useMemo(() => { a: 123 }, [])
  
  return <Child bar={bar} baz={baz} />;
}

 

이처럼 참조 동일성이 유지 되지 않는 object 의 경우 useMemo(), useCallback()을 사용하면

불필요한 렌더링을 막을 수 있습니다.

 

 

2) 과도한 계산이 포함된 함수인 경우 사용하라

 

예제의 calculatePrimes() 와 같이 복잡한 계산이 포함된 함수인 경우

매번 새로 값을 계산하게 되면 불필요한 연산이 수행됩니다.

function RenderPrimes({iterations, multiplier}) {
  const primes = calculatePrimes(iterations, multiplier)
  return <div>Primes! {primes}</div>
}

 

따라서 다음과 같이 useMemo()를 사용하게 되면, 

값이 변경되지 않는 이상 반복되는 렌더링시에 다시 계산할 필요 없이 값을 사용할 수 있습니다.

function RenderPrimes({iterations, multiplier}) {
  const primes = React.useMemo(
    () => calculatePrimes(iterations, multiplier),
    [iterations, multiplier],
  )
  return <div>Primes! {primes}</div>
}

 

 

3. (번외) React.memo(), HOC 은 무엇일까?

일종의 HOC인데, shouldComponentUpdate를 내장하고 있어 shallow copy를 실행하여 리렌더링을 방지합니다.

방법적으론 예시코드 처럼 모듈화 시키는 컴포넌트를 export할때 React.memo로 감싸주면 됩니다.

export default React.memo(componentNm)

이 방식은 '같은 props로 렌더링이 자주일어나는 컴포넌트','렌더링에 리소스 소모가 큰 컴포넌트'에 사용됩니다.

그러나 무분별하게 사용되는 것은 좋지 않습니다. 

어떠한 경우에는 그저 불필요한 비교 연산만 추가되는 꼴이 되기 때문이죠!

 

 

추가적으로...

얕은 비교를 원하지 않는 경우 아래처럼 customizing 이 가능합니다.

const MyComponent = (props) => { 
	/* props 를 사용하여 렌더링 */
};
const isEqual = (prevProps, nextProps) => { 
	/* 
    nextProps 가 prevProps 와 동일한 값을 가진다면, return true;
    그렇지 않다면, return false; (리렌더링) 
    */
};

export default React.memo(MyComponent, isEqual);

React.memo는 false일 경우에만 리렌더링이 됩니다.

(shouldComponentUpdate는 true일때 렌더링을 허용한다는 점이 다름)

 

 

 

[References]

 

[react] 리렌더링이 되는 조건들 살펴보기

state 변경이 있을 때 - react 에서 유동적인 데이터를 저장하기 위해서 state 라는 것을 이용한다. 이때 state 값을 바꿔주기 위해서는 state 를 직접 조작해서는 안되고 setState() 메서드를 이용해 주어

seungddak.tistory.com

 

[개념정리] useMemo, useCallback, React.memo의 사용

- CASE: 특정 상황에서만 동작되어야 하는 함수가, Component의 렌더링 조건에 따라 지속적으로 함수가 실행되는 경우 예시 이런 경우. countActiveUser는 users가 변화가 있을때만 다시 실행되어야 하는데

crmrelease.tistory.com

 

When to useMemo and useCallback

Performance optimizations ALWAYS come with a cost but do NOT always come with a benefit. Let's talk about the costs and benefits of useMemo and useCallback.

kentcdodds.com