Post

프로젝트 최적화(3) - 메모이제이션

📌시작하며

지금까지 정적 파일들에 대한 최적화에 대해 알아봤다면, 이번에는 코드 작업에서 불필요한 리렌더링을 줄이는 방법 ‘메모이제이션’에 대해 알아보자.🥰

✅React Developer Tools

먼저 만든 리액트 컴포넌트들이 어떻게 작동하는지 확인하기 위해서 React Developer Tools를 사용할 수 있다. Chrome, Firefox, Edge 버전이 있는데 Chrome을 주로 사용하기 때문에 크롬버전으로 다운로드 하자.

이제 리렌더링 되는 상황을 발생시켜 검사 > Profile 탭에서 컴포넌트들의 렌더링을 녹화해서 확인하면 된다.

이때, 어떻게 리렌더링 되는지 쉽게 확인하기 위해서는 최상위 컴포넌트에서 state를 변경시켜주면 된다.

예를 들어 버튼을 눌러 count를 증가시키는 등의 방법으로 강제 리렌더링을 발생시킬 수 있다.

그러면 이러한 값들 중 동적인 값이 아님에도 불구하고 자주 리렌더링 되는 요소를 발견할 수 있다! 굳이 렌더링 될 필요가 없는 것을 위해 연산을 하고 있으니 성능에 좋지 않은 영향을 미칠거라는걸 쉽게 짐작할 수 있다.🤔

그러면 이러한 것들은 어떻게 처리할 수 있을까?

✅memo

먼저 컴포넌트를 메모해보자. memo를 사용하면 컴포넌트의 props가 변경되지 않은 경우 리렌더링을 건너뛸 수 있다.

1
2
3
4
5
import { memo } from "react"

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

➡️memo가 필요한 상황

memo를 사용할 때는, 컴포넌트가 정확히 동일한 props로 자주 리렌더링 되고, 리렌더링 로직이 비용이 많이 드는 경우에만 유용하다. 만약 컴포넌트가 리렌더링 될 때 인지할 수 있을 만큼의 지연이 발생하지 않는다면 memo를 사용할 필요가 없다.

✅useCallback

1
const cachedFn = useCallback(fn, dependencies)

useCallback은 리렌더링 간에 함수 정의를 캐싱해 주는 React Hook이다.

라고 공식문서에서 설명하곤 있는데.. 정확히 무슨 말인지 이해하기 어려우니 다시 한 번 살펴보자.😅

먼저 아래와 같은 Example 컴포넌트를 생성했다고 생각해보자. 컴포넌트 밖에서 funcOut 이란 함수를 선언하고, 컴포넌트 안에서 funcIn 함수를 작성했다.

1
2
3
4
5
6
7
8
9
10
11
// Example.tsx

// 컴포넌트 밖에서 정의된 전역 함수
const funcOut = () => {}

// Example 컴포넌트 정의
const Example = () => {
  // 컴포넌트 내부에서 정의된 함수
  const funcIn = () => {}
  return <div></div>
}

여기서 funcOut은 컴포넌트 외부에서 정의된 전역 함수이므로, Example 컴포넌트가 다시 렌더링될 때마다 새로 생성되지 않고, 언제나 동일한 함수를 사용하게 된다.

하지만 funcIn은 Example 컴포넌트 내부에서 정의된 함수로, 컴포넌트가 다시 렌더링될 때마다 새로 생성된다.

이 차이에서 힌트를 얻어 이제 useCallback의 소개를 다시 읽어보자.

useCallback은 리렌더링 간에 함수 정의를 캐싱해 주는 React Hook이다.

함수를 캐싱한다 라는 뜻은 것은 함수를 기억해두고, 이전과 동일한 함수를 다시 사용한다는 뜻으로, useCallback을 사용하면 리렌더링 간에 함수를 새로 생성하지 않고, 이전에 만든 함수를 재사용할 수 있다는 뜻이다.

➡️useCallback이 필요한 상황

보통 부모 컴포넌트가 리렌더링될 때마다 해당 콜백 함수가 새로 생성되는데, useCallback을 사용하면 이를 방지할 수 있어, 해당 함수가 변경될 때만 자식 컴포넌트가 리렌더링된다.

➡️useCallback 예제 코드

이제, 공식문서에서 제공하는 예제코드를 분석해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
import { useCallback } from 'react';

export default function ProductPage({ productId, referrer, theme }) {
  // handleSubmit 함수 useCallback을 이용해 메모이제이션
  const handleSubmit = useCallback((orderDetails) => {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  // 의존성 배열에 productId와 referrer를 넣어 해당 값이 변경될 때만 handleSubmit 함수가 다시 생성됨
  }, [productId, referrer]);


✅useMemo

useMemo 는 재렌더링 사이에 계산 결과를 캐싱할 수 있게 해주는 Rect Hook 입니다.

이 것도 말이 조금 어려운데, 풀어서 이해해보자면 , useMemo를 사용하면 함수나 연산의 결과를 기억해두었다, 같은 입력 값이 들어올 경우 다시 계산하지 않고 이전에 계산한 값을 재사용한다는 뜻이다.

➡️useMemo 예제 코드

앞서 살펴본 것 처럼, React는 컴포넌트를 다시 렌더링할 때마다 컴포넌트의 전체 본문을 다시 실행한다.

예를 들어, TodoList가 상태를 업데이트하거나 부모로부터 새로운 props를 받으면 filterTodos 함수가 다시 실행된다.

1
2
3
4
function TodoList({ todos, tab, theme }) {
  const visibleTodos = filterTodos(todos, tab)
  // ...
}

일반적으로 대부분의 계산은 매우 빠르게 처리되므로 문제가 되지 않지만, 큰 배열을 필터링 하거나 변환하는 등 비용이 많이 드는 계산 을 수행하는 경우에는 데이터가 변경되지 않는 경우에는, 재계산 하지 않는 것이 계산을 생략하는 것이 좋다.

위의 예제코드에서, 만약 todos과 tab이 마지막 렌더링 때와 동일한 경우, useMemo로 계산을 감싸면 이전에 계산된 visibleTodos를 재사용할 수 있다.

➡️ 비용이 많이 드는 계산

그런데 이런 설명을 읽다보면 궁금한게 생긴다.🤔 여기서 말하는 비용이 많이 드는 계산 은 무엇인가?

친절한 공식문서의 설명으로 보다 명확한 기준을 확인할 수 있었다.🥳

일반적으로 수천 개의 개체를 만들거나 반복하는 경우가 아니라면 비용이 많이 들지 않는다. 만약 해당 코드가 비용이 많이 드는지 조금 더 명확하게 확인하고 싶다면, 콘솔 로그를 이용해 코드에 소요된 시간을 측정하면 된다.

1
2
3
console.time("filter array")
const visibleTodos = filterTodos(todos, tab)
console.timeEnd("filter array")

측정하려는 상호작용(예시: Input에 입력)을 수행하면, filter array: 0.15ms와 같은 로그가 콘솔에 표시되며, 전체적으로 기록된 시간이 클 때(예시: 1ms 이상) 해당 계산을 메모하는 것이 좋다. useMemo를 사용하고, 총 시간이 얼마나 감소했는지 확인할 수 있다.

✅useCallback vs useMemo

1. useCallback: useCallback은 함수 자체를 캐싱한다. 일반적으로 콜백 함수를 자식 컴포넌트에 props로 전달할 때 사용되어, 부모 컴포넌트가 리렌더링될 때마다 새로운 함수가 생성되는 것을 방지하여 자식 컴포넌트의 불필요한 리렌더링을 줄인다. 2. useMemo: useMemo는 호출한 함수의 결과값을 캐싱한다. 주로 계산 비용이 많이 드는 연산의 결과를 캐싱하고 싶을 때 사용된다.

즉, 함수의 재사용에 초점이 맞춰진 경우에는 useCallback을 사용하고, 값의 재사용에 초점이 맞춰진 경우에는 useMemo를 사용하면 된다.

📩마무리

이렇게 리액트에서 메모이제이션 하는 방식을 알아보았다. 하지만 여기서 꼭 주의해야할 점은, 메모한다고 무조건 성능을 향상시킬 수 있는 건 아니라는 것이다.

자주 변동 되는 값은 메모를 해도 아무런 효과를 발휘하지 못한다. 이런 경우 굳이 메모이제이션 기능을 사용할 필요가 없을 뿐 더러, 오히려 불필요한 메모 연산이 추가되는 것이다.😅

즉 꼭 필요한 상황에서 적절히 사용할 것! 에 주의하자

🗂️참고 사이트

  • https://ko.react.dev/learn/react-developer-tools
  • https://ko.react.dev/reference/react/memo
  • https://ko.react.dev/reference/react/useCallback
  • https://ko.react.dev/reference/react/useMemo
  • https://ko.react.dev/reference/react/useCallback#how-is-usecallback-related-to-usememo
This post is licensed under CC BY 4.0 by the author.