React의 성능 최적화 1편 (useCallback, useMemo, React.memo)

2021. 8. 1. 20:03개발/React

 

React 컴포넌트가 한 화면에 렌더링 되는게 많아지면 많아질수록 최종 렌더링 시간이 오래 걸리는건 팩트다.
보여주고 싶은 정보가 많을수록, 구조가 복잡할수록 렌더링할 컴포넌트는 많아진다.

 

더 많이 보여주고싶은 요구를 무시할수는 없으니, 더 효과적으로 렌더링 할 수 있는 방법을 찾아야한다.

그 첫번째 편으로 성능 최적화에 대한 Hooks, Api 를 알아보자!

 

useCallback, useMemo, React.memo

위 3개 항목은 react에서 성능 최적화를 논할때 빠지지 않고 등장하는 react API 이다.

 

useCallback

오늘 코드리뷰를 하다가 짧은 토글 코드에 useCallback 이 사용된 경우를 봤다.
사용되면 안되는 곳에 사용된 것 같은 느낌이 계속 들었는데, 동료가 관련 문서를 찾아주었다.

I'd like to mention also that on the second render of the component, the original dispense function gets garbage collected (freeing up memory space) and then a new one is created. However with useCallback the original dispense function wont get garbage collected and a new one is created, so you're worse-off from a memory perspective as well.

dispense 함수가 useCallback을 사용한 경우에는, 함수 호출시 garbage collected를 거치지않고 새로운걸 생성한다

 

이런 내용인데, 다른 문서를 더 찾아보자. 나는 useCallback을 언제 효율적으로 잘 쓰는지를 알고싶다.

관련 문서2 를 살펴보자

  1. A functional component wrapped inside React.memo() accepts a function object prop
  2. When the function object is a dependency to other hooks, e.g. useEffect(..., [callback])
  3. When the function has some internal state, e.g. when the function is debounced or throttled.

 

위와 같은 경우가 useCallback을 사용해야 하는 경우라고 하는데 하나씩 살펴보자.

 

  1. Functional component가 React.memo로 감싸져 있는 경우
    • memo로 감싸게 되면 props가 변하기 전에는 항상 같은 렌더링 결과를 보여준다. props가 변하면 컴포넌트는 re-rendering이 되므로 재생성이 필요없는 함수는 useCallback으로 처리해주라는 뜻 같다.
  2. useEffect(..., [callback])
    • 이런 문제를 방지하기위해 useCallback 과 함께 사용하라는 뜻이다. 요약하자면, 컴포넌트가 렌더링 될 때 마다 function은 재할당이 되기 때문에 useEffect(..., [callback]) 에서 사용한 callback은 항상 변경되고, useEffect는 callback이 변경되었기 때문에 다시 실행된다. 이런 경우를 막기위해 useCallback을 사용한다.
  3. 함수가 internal state를 가진 경우
    • internal state가 정확히 어떤건지 감이 잘 안온다.
      추측키로는, "객체를 함수 내부에서 새로 생성하는 상태" 라고 생각한다.
    • 위 링크의 예시에서 debounce에 대한 예시를 들었는데, FileList라는 컴포넌트를 여러곳에서 사용한다고 했을 때, 컴포넌트가 렌더링 될 때 마다 debounce 객체는 새로 생성된다.
      같은 동작을 하는 함수가 여러 곳에서 쓸모없이 메모리만 잡아먹고 있으니까 useCallback을 사용해서 메모리사용을 최적화 하는 시나리오 이다.

 

이제 useCallback에 대해서는 어느정도 이해했다.


정리하자면,

  1. useCallback은 function의 메모리 재할당을 막기위한 수단이고
  2. 여러곳에서 사용되는 컴포넌트가 불필요하게 같은 function을 메모리에 여러번 할당한다면, useCallback을 사용한 최적화가 필요하다.
  3. useCallback함수의 결과를 메모리에 저장하는게 아니다.
    메모리에 저장된 함수를 같은 컴포넌트들에서 공유하는 느낌이다.
    • Ex) AddModal.jsx 라는 컴포넌트가 있다고 했을 때, 해당 컴포넌트 내부에서 useCallback으로 선언한 함수들은 AddModal 컴포넌트가 어디에서 사용되던 같은 메모리값을 참조한다.

그럼 결과를 메모리에 저장하고 불러오려면 어떻게 해야할까?

 

useMemo

여기서 Memo는 "memorized"를 의미한다.
memo는 이전에 사용했던 결과값을 저장했다가 다시 연산하지않고 memorized된 결과값만 다시 사용할 수 있게 해준다.

함수형 컴포넌트는 기본적으로 rendering 될 때 jsx를 반환한다. 모든 함수가 그렇듯 return 문을 만나기 이전의 작업들을 항상 수행하고 return문을 만나서 결과값을 반환한다.

import React from 'react';

const AddModal = () => {

    // Do Somthing 1 ...

    // Do Somthing 2 ...

    // Do Somthing 3 ...

    // Do Somthing 4 ...

    return (
        <div> ...Modal </div>
    )
}

export default AddModal;

하지만 컴포넌트는 다양한 props를 받고
이 prop가 달라지게 될 때 마다 react는 다시 렌더링을 시킨다.

렌더링 할 때 마다 재연산이 필요하지 않은 값이 있다고 할 때, 이걸 그냥 놔두는건 성능적인 측면에서 +요소가 아닌건 확실하다.

이 때 재 연산이 필요하지 않은 값을 useMemo를 이용해서 성능 최적화를 진행한다.

import React, {useMemo} from 'react';

const AddModal = ({name, price, discountedPrice, quantity}) => {

    const discountRate = useMemo(()=> (1 - discountedPrice / price) * 100, [price, discountedPrice])

    return (
        <div>
            name : {name}
            price : {price}
            discountedPrice : {discountedPrice}
            quantity : {quantity}
            discountedRate : {discountRate}
        </div>
    )
}

export default AddModal;

price, discountedPrice가 바뀔 때 마다 다시 연산을 해주고 할인율을 구한다.

주의해야할 점은 useMemo도 결과값을 어딘가에 memorized 하고 있기 때문에 useMemo를 남발하는건 오히려 독이 될 수 있다.
따라서 간단한 연산이라면 CPU 자원을 사용하는걸 허락해야 하되,
복잡한 연산이라면 memorized된 결과를 가져오도록 해야한다.

추가로 useMemo에서 function을 memorized 하는건 useCallback과 똑같다.

// 같의 의미
useCallback(callback, [prop]);
useMemo(() => callback, [prop]);

 

React.memo & React.PureComponent

의미적으로는 useMemo 처럼 "Memoizing 한다" 는 의미를 가지고 있다.

memoFunctional Component에서 사용되고 PureComponentClass Component에서 사용된다.

 

React.memo

hoc(High Order Component)로써 렌더링한 결과를 Memoizing한다.
Component의 props를 얕은 비교를 통해 비교해서 re-rendering을 해야하는가 를 판단한다.
더 복잡한 자료구조를 가지고 있는 props라면 React.memo(Component, areEaual)의 두번째 인자인 areEqual함수를 사용한다.

import React, {useMemo, memo} from 'react';

const AddModal = ({name, price, discountedPrice, quantity}) => {

    const discountRate = useMemo(()=> (1 - discountedPrice / price) * 100, [price, discountedPrice])

    return (
        <div>
            name : {name}
            price : {price}
            discountedPrice : {discountedPrice}
            quantity : {quantity}
            discountedRate : {discountRate}
        </div>
    )
}

export default memo(AddModal);

Document에서는 React.memo는 "성능 최적화"를 위해 사용되어야 하지 렌더링을 "방지"하기 위해선 사용하지 말라고 명시되어 있다. 즉, re-rendering을 막기 위해서 React.memo가 사용되어야 하고 Rendering 행동 자체를 block 하기 위한 용도로 사용되면 안된다.

 

React.PureComponent

하는 역할은 React.memo와 같다.

기본적으로 원시적인 데이터(String, Number, Boolean)는 값(value)을 비교하고

Array, Object, Function같은 고차원적인 데이터들은 참조(reference)로 비교된다.

 

PureComponent는 shouldComponentUpdate(nextProps, nextState)를 사용해서
얕은 비교 이후 결과값에 따라 re-rendering할지 안할지 정한다.


단, PureComonent에서 shouldComponentUpdate함수를 오버라이딩해서 사용하면 안된다. (관련문서)
PureComponent에서는 shouldComponentUpdate함수가 모든 props, state에 대해 얕은 비교를 진행하는데,

오버라이딩하게 되면 기존 의도와는 다르게 동작할 수 있기 때문이다.


따라서 shouldComponentUpdate함수를 사용해야 하는 경우에는

PureComponent대신 React.Component를 사용하자.

import React from 'react';

class AddModal extends React.PureComponent{

    render(){
        const {name, price, discountedPrice, quantity} = this.props;
        const discountRate = (1 - discountedPrice / price) * 100;

        return (
            <div>
                name : {name}
                price : {price}
                discountedPrice : {discountedPrice}
                quantity : {quantity}
                discountedRate : {discountRate}
            </div>
        )
    }
}

export default AddModal;

참고 문서

'개발 > React' 카테고리의 다른 글

[React]-React로 사고하기  (0) 2020.08.18
[React]-리스트와 Key  (0) 2020.08.18
[React]-조건부 렌더링  (0) 2020.08.18
[React]-이벤트 처리하기  (0) 2020.08.18
[React]-Component와 Props  (0) 2020.08.18