useMemo와 useCallback

Seju·2024년 1월 8일
1

React

목록 보기
7/9


🌏 개요


이글은 리액트 공식문서의 useMemo와 useCallback API Reference를 참고한 글이며, 해당 내용들을 참고해 제 입맛대로 정리한 글입니다!


useMemo?? useCallback??


먼저 결론부터 말하자면 useMemouseCallback 둘 다 동일하게 기능을 가지고 있다. 그 기능은 어떤 값을 메모이제이션하는 기능이다.

메모이제이션(Memoization)이란?
메모이제이션이란 컴퓨터 프로그램이 동일한 계산을 반복해야할 시, 이전에 계산한 값을 메모리에 저장함으로써 동일한 계산의 반복 수행을 제거하여 프로그램 실행속도를 빠르게 하는 기술 - wikipedia

따라서 공통적으로 useMemouseCallback hook 모두 최종적으로 값을 캐시하여 성능최적화를 위해 사용한다는 이야기이다.

  • 그리고 useMemouseCallback등을 통해 성능최적화하기 앞서, React에서 리렌더링이 발생하는 경우에 대해서도 알고있어야 한다.

React에서 리렌더링이 발생하는 경우

이글의 주된 목적은 useMemouseCallback이니 핵심만 요약하겠습니다.
자세한 레퍼런스는 React는 언제 컴포넌트를 다시 렌더링하나요?를 참고했습니다.

리렌더링 트리거의 핵심은?
1. state의 변경 될 때
2. 부모 컴포넌트의 state가 변경되면 자식 컴포넌트 또한 리렌더링이 발생하게 된다
3. props가 변경될 때

여기서 예를 들어서, 만약 2번과 같이 상태를 가지고 있는 상위컴포넌트들의 자식컴포넌트들은 상태를 가지고있지 않음에도 불구하고 최상위 컴포넌트의 상태가 변경될 때 자식 컴포넌트들또한 불필요하게 리렌더링 될것이다.

  • 이런 경우가 useMemouseCallback이 필요한 이유라고 생각할 수 있다.

useMemo와 useCallback의 차이점

앞서 말했듯이 useMemouseCallback은 값을 메모이제이션한다는 기능에선 동일하다고 볼 수 있다.

그러나 차이점은 존재하는데 그 차이점은?

useMemo는 메모이제이션 된 "값"을 반환하고
useCallback은 메모이제이션 된 "함수"를 반환한다.

useMemo는 인자로 받는 첫번째 콜백함수의 반환값을 메모이제이션하고 반환한다.

useMemo(() => fn, deps);

useCallback은 인자로 받는 첫번째 함수 자체를 메모이제이션하고 반환한다.

useCallback(fn, deps);

따라서 두 훅은 useCallback(fn, deps)은 useMemo(() => fn, deps)와 동일하다고 생각할 수 있다.

그러면 두 훅은 왜 따로 생겨났고 어떤 경우에 쓰이는걸까?

useMemo

useMemo 훅은 연산 비용이 큰(=계산하는데 비용이 많이드는)함수의 "반환값"을 재사용하기 위해 사용된다.

  • 이를 통해, 불필요한 연산을 줄이고 성능을 최적화하는데 초점을 둔 hook인것이다.
  • 함수의 실행결과를 메모리에 저장하게 되고, 의존성배열(deps)에 있는 값이 변경될 경우에만 첫번째 콜백함수가 다시 실행하게 되는거다.

useCallback

반면 useCallback"함수 자체"를 메모리에 저장하는데 사용되는 훅이다.

  • 함수의 재생성을 방지하기 위해 사용하는 훅인거다.
  • useMemo와 마찬가지로 의존성 배열인 deps가 변경될때만 새로운 함수를 생성하게 된다.

다시 정리하자면
useMemo : 연산 비용이 큰 계산의 결과를 저장하고 재사용하는 경우, 이전 연산 결과를 재활용하고 싶을 때 사용
useCallback: 이벤트 핸들러와 같이 컴포넌트가 리렌더링될 때마다 재생성되는 함수를 메모리에 저장하고 싶을 때 사용


실제로직에서 useCallback 적용해보기

사실 아래 예제들은 굳이 useCallback을 적용할 필요가 없는 예제긴하다. 하위 컴포넌트가 하나밖에 없고 이는 성능적으로 이슈를 일으킬 확률이 거의 없다고 생각되기 때문이다.

import {useState } from 'react';
import ChildComponent from './ChildComponent'

function App() {
  const [counter, setCounter] = useState(0);

  return(
    <div>
      <h1>{counter}</h1>
      <ChildComponent />
      <button onClick={() => setCounter((prev) => prev + 1)}> Re-Render Trigger! </button>
    </div>
  );
}


// 🪄 ./ChildComponent.jsx
function ChildComponent() {
  console.log("child component rendered")
  
  return(
    <div>
      <h1>Hello World</h1>
    </div>
  );
}

export default ChildComponent;


상위 컴포넌트인 App의 상태가 변경되고 있음에도 불구하고 그와 상관없는 ChildComponent가 리렌더링 되어 콘솔로그가 찍히는것을 확인할 수 있습니다

여기서 이벤트 핸들러를 useCallback으로 래핑하면?

import {useState} from "react";
import ChildComponent from "./components/ChildComponent";
import {useCallback} from "react";

function App() {
  const [counter, setCounter] = useState(0);
  const handleCountUp = useCallback(() => {
    setCounter(counter + 1);
  }, [counter]);

  return (
    <>
      {counter}
      <ChildComponent />
      <button onClick={handleCountUp}>Render Trigger Button!</button>
    </>
  );
}

export default App;

// 🪄 ./ChildComponent.jsx
function ChildComponent() {
  console.log("child component rendered")
  
  return(
    <div>
      <h1>Hello World</h1>
    </div>
  );
}

export default ChildComponent;

오잉? 그래도 똑같이 하위 컴포넌트인 ChildComponent가 리렌더링이 발생하고 있다

이유가 뭘까?

핸들러 함수인 handleCountupuseCallback hook으로 래핑했음에도 불구하고 ChildComponent는 리렌더링을 일으키고 있다.
위의 코드에서 handleCountup 함수는 counter의 상태가 변경될 때마다 새로이 생성된다.
따라서, 여기서 useCallback을 사용하는것은 좋지 않은 선택인것같다.
왜냐하면 counter 값이 변경될 때마다 새로운 함수 handleCoutup이 재생성되므로, 이 경우 useCallback의 메모이제이션 효과를 기대하기 어렵기 때문.

ChildComponent가 리렌더링이 여전히 발생하고 있는 이유

ChildComponent가 App 컴포넌트의 state에 의존하고 있지 않음에도 불구하고, App 컴포넌트가 리렌더링될 때마다 ChildComponent도 함께 리렌더링되고 있다.
이는 ChildComponent가 App 컴포넌트의 자식 컴포넌트이기 때문이다.

이문제를 해결하려면?

  • React에서 제공하는 memo를 사용해 ChildComponent의 불필요한 리렌더링을 방지할 수 있다.
    • memo는 컴포넌트의 props가 변경되지 않았다면, 리렌더링을 방지하고, 마지막으로 렌더링 결과를 재사용한다.
const ChildComponent = memo(function ChildComponent {
  console.log("child component rendered")
  
  return(
    <div>
      <h1>Hello World</h1>
    </div>
  );
});

export default ChildComponent;

마무리


useCallback과 useMemo는 성능 최적화를 위한 도구이지만, 무조건적으로 사용해야 하는 것은 아니다.
때로는 불필요한 메모이제이으로 인해 성능이 오히려 저하될 수 있다.
따라서, 실제 성능 문제가 발생했을 때 적절하게 사용하는 것이 중요하다!

profile
Talk is cheap. Show me the code.

0개의 댓글