React

[React] useMemo를 이용한 성능 최적화

킹우현 2023. 7. 18. 15:45

1) 메모이제이션(memoization)

useMemo 함수에 대해서 알아보기 전에 알고리즘 시간에 자주 나오는 메모이제이션(memoization) 개념에 대해서 잠깐 짚고 넘어가겠습니다. 

 

memoization이란 기존에 수행한 연산의 결과값을 자료구조에 저장해두고 동일한 입력이 들어오면 재활용하는 프로그래밍 기법을 말합니다. 

 

memoization을 적절히 적용하면 중복 연산을 피할 수 있기 때문에 메모리를 조금 더 쓰더라도 애플리케이션의 성능을 최적화할 수 있습니다.

 

2) 렌더링마다 호출되는 컴포넌트 함수

function MyComponent(props) {
  // 어떤 로직 (JavaScript)
  return; /* 어떤 화면 (JSX) */
}

일반적으로 React의 함수형 컴포넌트는 다음과 같은 구조로 작성이 됩니다. 이렇게 작성된 컴포넌트 함수는 React 앱에서 렌더링(rendering)이 일어날 때마다 호출이 됩니다. 

 

컴포넌트 함수가 호출이 되면 그 안에 자바스크립트 로직들이 수행되고, 이를 기반으로 JSX로 마크업된 UI가 리턴되는 기본 구조를 가지고 있죠. ⭐️

React에서 컴포넌트의 랜더링은 한 번 일어나고 끝이 아니라 수시로 계속 일어날 수 있습니다. 대표적인 예로 컴포넌트의 자신의 상태 변경(state update)이 일어날 수 있고, 아니면 부모 컴포넌트의 상태 변경이 일어나 덩달아 함께 렌더링되야 하는 경우도 있습니다. 

 

React에는 수동으로 다시 레더링을 해주는 API도 있고, 사용자가 브라우저에서 새로고침을 할 때도 컴포넌트의 리랜더링은 불가피 합니다.

 

3) 함수형 컴포넌트의 실행이 느리다면

아래 컴포넌트는 prop으로 넘어온 x와 y 값을 compute 함수에 인자로 넘겨서 z 값을 구한 후, 그 결과값을 div 엘리먼트로 감싸 출력해줍니다.

function MyComponent({ x, y }) {
  const z = compute(x, y);
  return <div>{z}</div>;
}

만약에, compute 함수가 내부적으로 매우 복잡한 연산을 수행하여 결과값을 리턴하는데 시간이 몇초 이상 오래 걸린다면 어떻게 될까요? 

 

컴포넌트의 리렌더링이 필요할 때 마다 이 함수가 호출이 되므로 사용자는 지속적으로 UI에서 지연이 발생하는 경험을 하게 될 것입니다.

 

4) 함수형 컴포넌트에 Memoization 적용

렌더링이 일어날 때 마다, compute 함수의 인자로 넘어오는 x와 y 값이 항상 바뀌는 게 아니라면 굳이 compute 함수를 계속 호출할 필요가 있을까요?

 

위와 같이 불편한 사용자 경험은 위에서 간단히 설명드린 Memoization 기법을 적용하면 개선할 수 있습니다.

 

랜더링이 발생했을 때, 이전 랜더링과 현재 랜더링 간에 x와 y 값이 동일한 경우, 다시 함수를 호출을 하여 z 값을 구하는 대신, 기존에 메모리의 어딘가에 저장해두었던 z 값을 그대로 사용하는 것입니다.

이러한 상황에서 memoization 로직을 직접 구현할 수도 있겠지만, 대신에 간편하게 사용할 수 있는 것이 바로 React의 useMemo hook 함수입니다. 

 

compute 함수에 넘겨주는 x, y의 값이 이전과 동일하다면 컴포넌트가 리렌더링 되더라도 연산을 다시 하지 않고 이전 렌더링 때 저장해 두었던 값을 재활용하게 됩니다.

 

useMemo 함수는 2개의 인자를 받는데, 첫번째는 결과값을 생성해주는 팩토리 함수이고, 두번째는 기존 결과값 재활용 여부의 기준이 되는 입력값 배열입니다. 예를 들어, 다음과 같이 위에서 작성한 컴포넌트를 재작성하면,

 

function MyComponent({ x, y }) {
  const z = useMemo(() => compute(x, y), [x, y]);
  return <div>{z}</div>;
}

 

x와 y 값이 이 전에 랜더링했을 때와 동일할 경우, 이전 랜더링 때 저장해두었던 결과값을 재활용합니다.

 

하지만, x와 y 값이 이 전에 랜더링했을 때와 달라졌을 경우, () => compute(x, y) 함수를 호출하여 결과값을 새롭게 구해 z에 할당해줍니다.

 

5) useCallback과 useMemo의 차이

useCallback 메모이제이션된 함수를 반환하며. useMemo 메모이제이션된 값를 반환합니다.