[React] Memoization

Joowon Jang·2024년 10월 11일
1

React

목록 보기
5/19

React App의 성능 향상

App을 만들 때, 성능 향상은 매우 중요한 부분이다.
사소한 부분들이 쌓여서 App의 전체적인 속도가 느려지고, 그렇게 느려진 서비스는 사용자들에게 불편함과 불만을 야기하고, 서비스를 사용하지 않게 되는 큰 이유가 될 수 있다.

그렇다면 React를 사용해 App을 만들 때, 성능 향상을 위해 어떤 부분을 신경써야 할까?
나는 가장 먼저 불필요한 리렌더링 방지라고 대답할 것이다.

리액트는 컴포넌트의 state 혹은 prop이 변경되면 리렌더링이 발생한다.
이 과정에서, 아래의 그림처럼 렌더 트리(Render Tree)를 생성하고, 이 트리에서 어떤 노드(컴포넌트)가 리렌더링될 때, 하위 노드(컴포넌트)도 함께 리렌더링된다.

렌더 트리

리렌더링하는 컴포넌트의 개수와 크기가 커질수록 비교 연산과 DOM 업데이트 등에 더 많은 시간이 들 수 밖에 없다.

그래서, 리액트에서는 memo, useMemo, useCallback과 같은 Memoization 기능을 제공해 이런 리렌더링을 최소화할 수 있도록 한다.

React 19 버전에서는 이러한 Memoization을 개발자가 아닌 React가 내부적으로 처리하도록 하여 개발자 경험을 향상하겠다고 했다.
하지만, React 팀이 실패할 수도 있고, 어떻게 동작하는지 확실하게 알고 있어야 좋은 코드를 작성할 수 있을 것이라 생각한다.

참고: https://ko.react.dev/learn/understanding-your-ui-as-a-tree

Memoization API

memo

const MemoizedComponent = memo(SomeComponent, arePropsEqual?)
  • SomeComponent: 캐싱하고 싶은 React 컴포넌트
  • arePropsEqual: 컴포넌트의 이전 props와 새로운 props의 두 가지 인수를 받는 함수. 컴포넌트가 이전 props와 동일한 결과를 렌더링하고 새로운 props에서도 이전 props와 동일한 방식으로 동작하는 경우 true를, 그렇지 않으면 false를 반환해야 한다. 이 함수를 지정하지 않으면, React는 기본적으로 Object.is로 각 props를 비교한다. (일반적으로, 지정하지 않음)

memo 메서드는 기본적으로 매개변수로 컴포넌트를 받고 함수와 forwardRef 컴포넌트를 포함한 모든 유효한 React 컴포넌트가 허용된다.
반환값은 새로운 React 컴포넌트인데, memo에 제공한 컴포넌트와 동일하게 동작하지만, 부모가 리렌더링되더라도 props가 변경되지 않는 한 React는 이를 리렌더링하지 않는다.

const Greeting = memo(function Greeting({ name }) {
  return <h1>Hello, {name}!</h1>;
});

export default Greeting;

위의 코드처럼 컴포넌트를 memo 메서드로 감싸주기만 하면 끝!
부모 컴포넌트의 리렌더링이 잦은 경우에 성능 향상을 기대할 수 있다.

참고: https://ko.react.dev/reference/react/memo

useMemo

const cachedValue = useMemo(calculateValue, dependencies)
  • calculateValue: 캐싱하려는 값을 계산하는 함수
  • dependencies: calculateValue 코드 내에서 참조된 모든 반응형 값들의 목록. 반응형 값에는 props, state와 컴포넌트 바디에 직접 선언된 모든 변수와 함수가 포함된다. ReactObject.is 비교를 통해 각 의존성 들을 이전 값과 비교한다.
    (쉽게말해, dependencies가 변경되지 않는 한, 캐싱된 값이 변경되지 않는다.)

기본적으로, React가 컴포넌트를 리렌더링할 때, 원시형 데이터는 이전 값을 그대로 사용하지만, 객체형 데이터는 변경된 내용이 없더라도 새롭게 생성한다. 객체형 데이터가 propsstate 혹은 다른 훅의 dependencies에 사용된다면, 변경된 내용이 없더라도 불필요하게 리렌더링되는 것이다. 그래서 useMemouseCallback을 사용해 함수, 배열, 객체 등을 캐싱하여 리렌더링때마다 새롭게 생성되는 것을 방지한다.

import { useMemo } from 'react';

function TodoList({ todos, tab, theme }) {
  const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
  // ...
}

위의 예시처럼 filterTodos라는 함수를 사용해 계산된 값을 캐싱해두고 사용할 수 있다!

참고: https://ko.react.dev/reference/react/useMemo

useCallback

const cachedFn = useCallback(fn, dependencies)
  • fn: 캐싱할 함수
  • dependencies: useMemodependencies와 같음.
import { useCallback } from 'react';

export default function ProductPage({ productId, referrer, theme }) {
  const handleSubmit = useCallback((orderDetails) => {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }, [productId, referrer]);

위의 예시처럼 useCallback으로 함수 본문을 감싸면, 다음 렌더링에서 해당 함수를 새롭게 생성하지 않는다.

useMemo에서 설명한 것처럼 함수도 컴포넌트가 리렌더링될 때마다 새롭게 생성되기 때문에, useCallback은 함수를 캐싱하기 위해 사용한다.
사실, useMemo의 첫 번째 매개변수인 calculateValue에 함수를 리턴하는 함수를 넣어도 된다고 생각할 수 있는데, 맞는 말이다! 공식 문서에서도 useCallback은 내부적으로 useMemo를 사용하고 있다고 생각하는 것이 도움이 된다고 말하고 있다.
(참고: https://ko.react.dev/reference/react/useCallback#how-is-usecallback-related-to-usememo)

참고: https://ko.react.dev/reference/react/useCallback

profile
깊이 공부하는 웹개발자

0개의 댓글