큰 꿈은 파편이 크다!!⚡️

리액트 성능 향상 첫걸음: useMemo, useCallback, React.memo 본문

Web FE

리액트 성능 향상 첫걸음: useMemo, useCallback, React.memo

wood.forest 2023. 3. 12. 12:21

들어가며: 나의 기술부채 💰

리액트를 배울 때 정말 기본으로 포함되어있는 useMemo, useCallback 훅과 React.memo재사용리렌더링 방지를 통한 성능 향상을 야기한다고 알려진 함수들이다. 이 주제에 대해 처음 학습할 때 성능 향상에 대한 효과까지는 이해를 했었으나 항상 성능을 향상시키는 것이 아니라는 글을 보고서는 이해가 안되어서 그냥 사용을 안해버렸는데.. 이번 기회를 통해 간단하게나마 전체적인 정리를 해보려 한다.

 

 

useMemo: 연산 값 재사용

리액트에서 제공하는 useMemo 훅은 메모이제이션Memoization 이라는 기법을 통해 기존에 계산했던 결과를 저장하여 필요 시 재사용하므로써 불필요한 연산을 줄여 성능을 향상시킨다.

반대로, 항상 바뀌는 값이라면 결과를 재사용할 일이 없으니 useMemo를 사용할 필요가 없다.

1. 문제 상황 예시: 팩토리얼

 

· 재귀 팩토리얼 연산 factorialOf을 수행할 때마다 함수가 호출되었음을 콘솔에 출력한다.

· Re-render 버튼을 누르면 실제로 사용되지 않는 inc값을 증가시켜 <CalculateFactorial /> 컴포넌트를 리렌더링하는데, 콘솔에 출력되는 결과를 통해 input 값을 변경하지 않았음에도 factorialOf연산을 한다는 것을 알 수 있다.

· 즉, 이미 계산했기 때문에 계산할 필요가 없는 상황에서 계산을 하고 있다. 이전에 계산한 값을 재사용하고 싶지 않은가? 😎

2. 이때 useMemo를!

const cachedValue = useMemo(calculateValue, dependencies)

useMemo는 두 개의 인자를 받는다.

· calculateValue: 인자가 없는 연산 함수 (ex. () =>)

· dependencies: 연산 함수에 쓰이는 값들 (다시 연산해야 할 때의 기준이 되는 값)

· cachedValue: useMemo의 첫번째 인자인 함수를 호출했을 때의 결과

 

이 모양을 팩토리얼 예시에 적용해보면 아래와 같다.

const factorial = useMemo(() => factorialOf(number), [number]);

3. 문제 해결

 

숫자를 바꿔가며 팩토리얼 계산을 하다가 Re-render 버튼을 눌러도 더이상 로그가 찍히지 않는다. 계산을 다시 하지 않는다는 뜻이다!

 

 

 

useCallback: 함수 재사용

useCallback은 함수를 재사용하는데 도움을 주는 훅이다.

함수도 결국 값이다. useMemo로도 함수를 재사용할 수 있다. 따라서 useCallbackuseMemo에서도 사용할 수 있는 방법을 리팩토링한 정도로 이해할 수 있다.

//✅ useMemo
const handleSubmit = useMemo(() => {
    return (orderDetails) => {                  //👈 함수 한번 더 감싸는 것 뿐..!
      post('/product/' + product.id + '/buy', {
        referrer,
        orderDetails
      });
    };
  }, [productId, referrer]);

//✅ useCallback
const handleSubmit = useCallback((orderDetails) => {
    post('/product/' + product.id + '/buy', {
      referrer,
      orderDetails
    });
  }, [productId, referrer]);

이러한 특성 때문에 공식 문서에서는 useCallback을 아래와 같이 표현한다.

The only benefit to useCallback is that it lets you avoid writing an extra nested function inside. It doesn’t do anything else.

 

따라서 useCallback의 모양은 이렇다.

const cachedFn = useCallback(fn, dependencies)

· cachedFn: 캐싱된 fn 함수

· fn: 캐싱할 함수 (호출이 아님!)

· dependencies: fn 함수 안에서 참조되는 값의 배열

 

 

 

React.memo

memo는 컴포넌트의 props를 비교하여, 변경된 경우에만 리렌더링을 하도록 한다.

const MemoizedComponent = memo(SomeComponent, arePropsEqual?)

· SomeComponent: 메모할 컴포넌트

· arePropsEqual?: 이전 props와 새로운 props를 인자로 받아, 같으면 true, 아니면 false를 반환하는 함수

 

메모할 컴포넌트를 감싸는 방식으로 코드를 작성한다.

import { memo } from 'react';

const SomeComponent = memo(function SomeComponent(props) {
  // ...
});

 

 

 

단, 항상 성능을 향상시키는 것은 아니다 👀

Every line of code which is executed comes with a cost.

 

성능을 향상시키기 위한 코드에도 비용cost이 있다. 개선 효과가 미미하다면 개선을 고민하고 적용하는 시간은 낭비가 된다.

따라서 이 세 가지 방법은 컴포넌트의 성능을 실제로 개선시킬 수 있는 경우에만 사용하자

두 가지 중 성능이 더 좋은 코드는 무엇일까? 그 이유는?

//✅ original
const dispense = (candy) => {
  setCandies((allCandies) => allCandies.filter((c) => c !== candy));
}
//✅ useCallback
const dispense = React.useCallback((candy) => {
  setCandies((allCandies) => allCandies.filter((c) => c !== candy));
}, [])

위 코드를 약간 리팩토링해보자.

//✅ original
const dispense = candy => {
  setCandies(allCandies => allCandies.filter(c => c !== candy))
}

//✅ useCallback
const dispense = candy => {
  setCandies(allCandies => allCandies.filter(c => c !== candy))
}
const dispenseCallback = React.useCallback(dispense, [])

정답은 original 코드를 사용하는 것이다!

코드를 보면 useCallback 을 사용하는 경우가 React.useCallback를 호출하고, []를 정의하므로서 조금 더 많은 일을 하는 것을 알 수 있다. 두가지 경우 모두 해당 컴포넌트를 렌더링 할 때마다 메모리에 함수를 할당하지만, useCallback이 구현된 방식에 따라 더 많은 메모리 할당이 일어날 수 있다.

덧붙여 위 경우에서는, 컴포넌트가 두 번째 렌더링 될 때 original 버전에서는 기존 dispense 함수가 가비지 콜렉트 되며 메모리 공간을 해제하고, 새로운 dispense 함수를 생성한다. useCallback 버전에서는 가비지 콜렉션 없이 새로운 dispense 함수를 생성하게 되어 (props가 바뀌기 전까지는 값을 캐싱해야 하는 useCallback의 특성 때문이다) 비효율적이다.

 

useCallback, useMemo

· (앞서 말했듯이,) deps 배열에 항상 바뀌는 값(새로운 객체, 배열 등..)을 넣는 것은 값을 재사용할 일이 없다는 뜻이므로 메모이제이션을 하는 의미가 없다.

· 코드를 복잡하게 만들 수 있다

React.memo

· 항상 props를 비교하게 되는 비용이 있다는걸 생각하자

    · 실제로 렌더링을 줄일 수 있는 상황에서만 사용하자

 

 

 

맺으며:

자료조사를 하며 리액트를 많이 사용하고 있지만 모르는 부분이 훨씬 더 많다는 것을 깨달았다. 한편으로는 성능 개선이라는 작업은 이해도가 꽤 있어야 할 수 있는 것이라는 생각이 들었다.

아주 오랜 시간동안의 숙원이었던 기술부채를 조금은 정리한 기분이 든다. 한번쯤은 실전에서 실제로 성능을 개선하기 위해 사용해보고 싶다.

Reference

반응형