본문 바로가기
FE/React

React 렌더링 퍼포먼스 개선

by Jiyoon-park 2021. 12. 29.
 리액트 렌더링 위한 고민, useMemo, useCallback, React.memo

 

 

리액트의 컴포넌트는 state가 변경되었을 때, 그리고 props가 변경되었을 때, 부모 컴포넌트가 렌더링 될 때 렌더링된다. 예를 들어, 부모 컴포넌트의 state가 바뀌면 부모 컴포넌트, 변경된 state를 받는 자식 컴포넌트는 물론, 심지어 변경된 state를 받지 않는 자식 컴포넌트까지 모두 렌더링된다. 쓸데없는 렌더링을 줄일 수 있는 방법이 없을까? 쓸데없는 렌더링을 줄이고 성능을 최적화하기 위한 유용한 훅을 몇가지 소개하고자 한다.

useMemo

useMemo를 사용해 복잡한 계산식의 계산한 값을 재사용할 수 있다. useMemo는 호출 시 메모라이즈된 값을 반환한다. 복잡한 계산식을 수행해야하는 값이라면, 매번 수행하는 것이 아니라 배열 안의 값이 바뀔 때에만 계산식을 수행한다.

const ClASS_CAPACITY = 40;
const countGirls = (girls) => {
  return ClASS_CAPACITY - girls.length();
}
const countGirls = (boys) => {
  return ClASS_CAPACITY - boys.length();
}
​
const Class = (girls: Girl[], boys: Boy[]) => {
    const numOfGirls: number = useMemo(() => countGirls(girls), [girls]);
  const numOfBoys: number = useMemo(() => countBoys(boys), [boys]);
​
  return (
    <div>
        지금 이 교실 안에 여자 학생 수는 {numOfGirls}명이고,
        남자 학생 수는 {numOfBoys}명이다.
    </div>
  )
}

numOfGirlsnumOfBoys 교실 안의 여학생, 남학생의 수를 나타내는 변수이다. useMemo의 배열 안에 넣은 값이 바뀌면, 등록한 countGirls와 countBoys를 호출해 연산한 값을 넣어준다. 만약, 배열 안의 값이 바뀌지 않는다면, 콜백 함수의 계산을 거치지 않고 바로 이전에 계산한 값을 재사용한다.

예시로 작성해놓은 계산 함수들은 예시를 위한 무척이나 간단한 함수들로, useMemo를 사용할 필요가 없다. 성능상 얻는 이점이 매우 미미하기 때문이다. 하지만 계산 함수가 복잡하면 복잡할수록 useMemo를 통해서 얻는 성능상의 이점은 커진다. 결과적으로, 비싼 계산 함수가 아니라면 useMemo의 사용을 권장하진 않는다. (공식 문서에서도 useMemo 안 콜백함수의 이름은 computeExpensiveValue()이다)

결과를 얻기 위한 계산함수가 복잡하지 않다면, 해당 hook을 사용할 필요는 없다. useMemo가 적용된 레퍼런스는 재활용을 위해서 가비지 컬렉션에서 제외되기 때문에 메모리 쌓이는게 더 비효율적이다.

useCallback

useCallback는 호출 시 메모이제이션된 함수를 반환한다. 메모이제이션된 함수는 의존성 값이 담긴 배열이 변경되었을 때에만 변경된다. 콜백 안에서 참조되는 모든 값은 의존성 값의 배열에 나타나야 한다.

const momoizedCallback = useCallback(() => {doSomething(a,b)}, [a,b]);

사실 자바스크립트가 브라우저에서 실행되는 속도를 생각해보면 컴포넌트가 렌더링될 때마다 함수를 새로 선언하는 것이 성능상 큰 문제는 되지 않는다. 따라서 단순히 컴포넌트 내에서 함수를 반복 생성하지 않기 위해 useCallback을 사용하는 것은 큰 의미가 없거나 오히려 손해일 수도 있다.(코드가 복잡해지거나 유지 보수가 어려워짐)

다만, 자바스크립트가 함수를 객체로 취급하는 특성에 따라, 함수 내에서 어떤 함수를 다른 함수의 인자로 넘기거나, 상위 컴포넌트에서 하위 컴포넌트로 callback 함수를 props로 넘길 때 유용하게 사용될 수 있다.

  • useEffect 내에서 의존 인자로 함수를 사용할 때
  • 상위 컴포넌트에서 하위 컴포넌트로 함수를 props를 넘겨주는 경우

React.memo

React.memo는 렌더링 결과를 메모이징 한다. 컴포넌트를 React.memo로 컴포넌트를 감싸면, 컴포넌트로 넘어오는 props가 변경되지 않았을 때에는 메모이제이션된 함수형 컴포넌트를 사용한다 !

React.memo()안에 단순 컴포넌트를 넣으면 해당 컴포넌트는 PureComponent가 되고, 컴포넌트와 함께 두번째 파라미터로 현재 Props와 미래의 Props를 비교하는 areEqual 함수를 넘긴다면shouldComponentUpdate처럼 만들어 렌더링을 제어할 수 있다.

React.memo(Component, [areEqual(prevProps, nextProps)]);

결론

최적화를 이유로 무분별한 최적화 코드 추가는 지양되어야 한다. 미미한 성능 개선을 위한 코드 추가로 코드 복잡도가 올라가고, 그에 따라 유지 보수성이 떨어지게 된다면 소탐대실인 상황일수도 🥲

정리하여, 리액트 컴포넌트 렌더링 최적화를 위해 useMemo, useCallback, React.memo 생각의 결론은 아래와 같다.

  • useMemo
    • 수초 이상 걸리는 복잡한 계산식이 들어가는 경우나, 1초마다 갱신하는 등 지속적 업데이트가 되는 onChange 이벤트가 걸린 등의 특수한 경우에만 사용한다.
  • useCallback
    • 함수는 선언할 때마다 같은 내용이더라도 다른 함수로 판별된다(함수 동등성). 이런 자바스크립트의 특성을 생각해 사용해야한다.
      • useEffect 내에서 의존 인자로 함수를 사용할 때
      • 상위 컴포넌트에서 하위 컴포넌트로 함수를 props를 넘겨주는 경우
  • React.memo
    • props를 받는 하위 컴포넌트는 React.memo로 감싸 생성하면 좋을 것 같다. 다만, 해당 컴포넌트를 PureComponent로 만들 것인지, 두번째 인자를 넘겨주어 특정 값들만 비교하는 컴포넌트로 만들어낼 것인지에 대한 생각이 필요하다.

참고 문서