React 컴포넌트 최적화 (memo, useCallback)를 위한 노력

Posted by , January 11, 2025
ReactMemoUseCallback
Series ofReact

thumbnail

컴포넌트 최적화?

요새 마와셀 백엔드랑 프론트엔드를 새로 개발하고 있다.
그래서 글도 오랜만에 남겨본다.

프론트의 경우 React 기반의 Next.Js를 쓰고 있는데, 컴포넌트가 업데이트 될 때마다,
화면의 대부분 컴포넌트가 영향을 받는 것을 보고, 이를 최적화 하기 위해 몇 가지를 찾아봤다.

최적화 하는데는 다양한 기법이 있지만,
그 중 제일 많이 사용하고 알려진게 React.memouseMemo, useCallback이다.
마와셀에 적용하고, 좀 더 찾아보며 정리한 내용을 미래의 나를 위해 남겨둔다.


React.memo

일단 React.memoReact의 고차 컴포넌트 ( Higher-Order Component ) 중 하나다.
고차 컴포넌트란 쉽게 이야기해서 컴포넌트를 불러와서 새로운 컴포넌트를 반환하는 함수를 의미한다.

즉 일반 컴포넌트는 전달인자를 받아서 UI로 변환하여 반환하지만,
고차 컴포넌트는 재사용을 위해 몇 가지 공통 로직 또는 작업을 처리 후 컴포넌트를 반환한다.

돌아와서 React.memo는 불필요한 리렌더링을 막기 위해 사용한다.
컴포넌트 자체를 메모제이션한다.

사용하는 이유는 아래와 같다.

  • 컴포넌트를 메모이제이션하여 props가 변경되지 않으면 리렌더링을 방지.
  • 자식 컴포넌트가 상태 변화와 무관한 경우 사용하면 성능 이점이 큼.

그리고 사용 시점은 아래와 같다.

  • Pure Component처럼, 외부에서 받은 props를 렌더링할 때만 사용하는 컴포넌트.
  • 컴포넌트 자체가 변경이 드문 UI라면 React.memo 사용이 적합.

근데 이 React.memo얉은 비교 ( shallow comparison ) 를 사용하기에,
props에 객체나 배열을 전달할 때는 문제가 발생할 수 있다.

const Parent = () => {
  const options = { color: "blue" } // 매 렌더링마다 새로운 객체 생성

  return <Child options={options} />
}

const Child = React.memo(({ options }) => {
  console.log("Child 컴포넌트 렌더링")
  return <div>{options.color}</div>
})

위 코드는 부모가 리렌더링 될 때마다 Child 컴포넌트도 다시 렌더링이 된다.
그래서 이 때는 useMemo나, useCallback을 같이 사용하게 된다.


useMemo

useMemo는 간단하게 설명하면 값을 메모제이션 하는데 사용되는 Hook이다.
말 그대로 값을 메모리에 넣는다는 이야기다.

컴포넌트가 리렌더링 될 때 동일한 계산을 반복하는 대신, 의존성이 변경되지 않을 경우 값을 재사용한다.
보통 아래와 같이 사용한다.

import React, { useMemo } from "react"

const ExpensiveComponent = ({ items }) => {
  const total = useMemo(() => {
    console.log("비용이 큰 연산 수행 중...")
    return items.reduce((sum, item) => sum + item, 0)
  }, [items])

  return <div>Total: {total}</div>
}

useMemo는 아래와 같은 경우에 사용한다.

  • 계산량이 많은 작업
    • 반복되는 연산을 방지하고 값을 캐싱한다.
  • 렌더링 시 고정된 값
    • 리렌더링 시 값을 다시 계산하지 않도록 한다.

위의 React.memo에서 컴포넌트에 전달되는 값에 useMemo를 사용할 수 있다.


useCallback

useMemo는 값을 메모제이션 했다면, useCallback은 함수 자체를 메모제이션한다.
컴포넌트가 리렌더링 될 때 동일 함수 객체가 생성되는 것을 방지한다.

사용하는 이유는 아래와 같다.

  • 부모 컴포넌트에서 함수를 생성할 때마다 참조값이 바뀌므로 자식 컴포넌트가 리렌더링되는 문제를 방지.
  • 특정 함수에서 props(함수 전달인자)로 전달되는 콜백 함수의 참조값을 고정.

사용 시점은 아래와 같다.

  • 함수가 자식 컴포넌트에 함수 전달인자로 전달되는 경우.

아래는 사용 예시다.

import React, { useState, useCallback } from "react"

const Counter = () => {
  const [count, setCount] = useState(0)

  const increment = useCallback(() => {
    setCount(prev => prev + 1)
  }, [])

  return <button onClick={increment}>Count: {count}</button>
}

적용해보기

만약 아래와 같은 컴포넌트가 있다고 가정해보자.
TextInputBox의 경우 그냥 사용자 입력을 받는 컴포넌트라 가정한다.

import React from "react"

//A Component
const TestAComp = () => {
  return (
    <TextInputBox
      title={"Test"}
      defaultValue={"value"}
      changeTextValueFunc={inputText => {}}
      inputPlaceholder={""}
    />
  )
}

export default React.memo(TestAComp)

//B Component
const TestBComp = () => {
  return (
    <TextInputBox
      title={"Test"}
      defaultValue={"value"}
      changeTextValueFunc={inputText => {}}
      inputPlaceholder={""}
    />
  )
}

export default React.memo(TestBComp)

그리고 이것을 받는 컴포넌트는 아래와 같이 구현되어 있다고 가정한다.

import { useCallback } from "react"

const ParentComponent = () => {
  const handleTextChangeA = useCallback(inputText => {
    console.log("Input A changed:", inputText)
  }, [])

  const handleTextChangeB = useCallback(inputText => {
    console.log("Input B changed:", inputText)
  }, [])

  return (
    <div className="rounded-lg border border-gray-300 py-3 shadow-sm p-5">
      <div className="mt-5">
        <TestAComp changeTextValueFunc={handleTextChangeA} />
      </div>

      <div className="mt-5">
        <TestBComp changeTextValueFunc={handleTextChangeB} />
      </div>
    </div>
  )
}

결론

이 최적화도 정말 필요할 때 사용해야 한다.
useMemouseCallback의 경우 오버헤드가 있기에 성능 최적화가 필요한 시점에만 사용해야 한다.

불필요한 경우에도 사용할 경우 오히려 성능이 저하될 수 있다.