useCallback과 useMemo를 잘 use하려면?

Haizel·2023년 5월 30일
1
post-thumbnail

기술 면접을 대비해 개념을 🍰 한입 크기로 잘라 정리합니다.
깃허브가 궁금하다면 놀러오세요!
👉 깃허브 보러가기 (Since 2023.05.10 ~ )

💭 useCallback과 useMemo의 차이점에 대해 알아보기에 앞서, Memoization과 Re-rendering 두 개념에 대해 간략히 살펴본 후 본격적으로 두 차이점에 대해 알아보도록 하자!


💡 사전 지식


Memoization

컴퓨터 프로그램이 동일한 계산을 반복해야 할 때, 이전에 계산한 값을 메모리에 저장함으로써 동일 계산의 반복 수행을 제거하여 프로그램 실행 속도를 빠르게 하는 기술을 말합니다.

useCallback과 useMemo는 Memoization 기능을 제공하는 React의 내장 Hook으로, 퍼포먼스 최적화를 위해 사용합니다.

Re-rendering

이전에 생성한 컴포넌트 정보와 다시 렌더링한 정보를 비교해 최소한의 연산으로 DOM 트리를 업데이트하는 것을 말합니다. 즉 기존의 Virtual DOM과 현재의 Virtural DOM을 비교해 변경된 값만 DOM트리에 업데이트해줍니다.

useMemo와 USeCallback은 리렌더링 최적화를 돕은 Hook입니다. 주어진 렌더에서 수행해야 하는 작업 양을 줄이고, 컴포넌트가 다시 렌더링해야하는 횟수를 줄여 리렌더링을 최적화할 수 있도록 돕습니다.

👉 React의 Re-Rendering 조건

  • 자신의 state가 변경될 때
  • 부모 컴포넌트로부터 전달받은 props가 변경될 때
  • 부모 컴포넌트가 리렌더링될 때
  • forceUpdate 함수가 실행될 때

📝 useMemo


useMemo는 Memoization된 을 반환하는 함수입니다.

두번째 인자로 받는 deps 배열에 변화가 생기면, 내부에 정의된 콜백 함수가 실행되고, 그 함수의 반환 값을 반환합니다.

useMemo(() => fn, [])

예제코드

import React, { useState, useMemo } from "react";

export default function App() {
  const [buttonX, setButtonX] = useState(0);
  const [buttonY, setButtonY] = useState(0);
  
  const handleButtonX = () => {
    setButtonX((prev) => prev + 1)
  };
  
  const handleButtonY = () => {
    setButtonY((prev) => prev + 1)
  };
  
  useMemo(() => {console.log(buttonX)}, [buttonX]);

  return (
    <>
      <button onClick={handleButtonX}>X</button>
      <button onClick={handleButtonY}>Y</button>
    </>
  );
}

useMemoe의 deps 배열로 buttonX값이 주어졌기 때문에 X 버튼을 누르면, hook안에 정의된 console.log(buttonX)가 실행됩니다. Y 버튼을 놀러도 함수 컴포넌트는 리렌더링되지만, buttonX의 값은 변하지 않기 때문에 console.log(buttonX) 는 실행되지 않습니다.


📞 USeCallback


useCallback은 Memoization된 함수를 반환하는 함수입니다. 즉 useMemo는 함수를 실행해 그 실행 값을 반환한다면, useCallback은 함수 자체를 반환합니다.

따라서 deps의 변화가 생기면 새로운 함수를 반환하게 됩니다.

useCallback(fn, [])

예제코드

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

function App() {
  const [buttonX, setButtonX] = useState(0);
  const [buttonY, setButtonY] = useState(0);
  
  const handleButtonX = () => {
    setButtonX((prev) => prev + 1)
  };
  
  const handleButtonY = () => {
    setButtonY((prev) => prev + 1)
  };
  
  const returnUseCallback = useCallback(() => {console.log(buttonY)}, [buttonX]);
  
  returnUseCallback();
    
  return (
    <>
      <button onClick={handleButtonX}>X</button>
      <button onClick={handleButtonY}>Y</button>
    </>
  );
}

export default App;

UseCallback은 deps으로 주어진 buttonX값이 변화가 생길 때, () => {console.log(buttonY)}함수를 반환합니다. 즉 Y버튼을 계속해 눌러도 X버튼을 누르지 않으면 () => {console.log(0)}함수가 반환되고, X버튼을 누르면 비로소 () => {console.log(buttonY의 새로운값)} 함수가 반환됩니다.

즉 useCallback은 함수와 상관없는 상태 값이 변할 때, 함수 컴포넌트에서 불필요한 업데이트가 일어나는 것을 방지합니다. 이때 알아두어야할 useCallback의 특징 2가지가 있습니다.

1.  useCallback은 새로운 함수를 반환한다.

useCallback의 deps의 변경으로 반환되는 함수는 이전 함수와 형태는 같지만 새로운 함수입니다. 즉 새로운 무기명 함수를 반환한 것이며, 이전 함수와 값이 같을 뿐, 다른 메모리 주소를 가집니다.

2. 하위 컴포넌트가 PureComponent여야만 UseCallback이 유효하다.

단순히 useCallback의 사용만으로는 하위 컴포넌트의 리렌더링을 방지할 수 없습니다. 하위 컴포넌트가 PureComponent 여야만 비로소 불필요한 리렌더링을 막을 수 있습니다. 이를 위해서 컴포넌트를 React.memo()로 래핑하면 해당 컴포넌트는 PureComponent가 됩니다.

👉 React.PureComponent

React.PureComponent에 shouldComponentUpdate()가 적용된 버전을 말합니다.


⚛︎ React.memo


컴포넌트가 React.memo()로 래핑될 때, React는 컴포넌트를 렌더링하고 결과를 Memoizing합니다. 그 후, 다음 렌더링이 일어날 때 props가 같다면, Memoizing된 내용을 재사용합니다.

예제코드

export function Movie({ title, releaseDate }) {
  return (
    <div>
      <div>Movie title: {title}</div>
      <div>Release date: {releaseDate}</div>
    </div>
  );
}

export const MemoizedMovie = React.memo(Movie);

props으로 주어진 title, releaseDate가 변경되지 않으면, 다음 렌더링 때 Memoizing된 내용을 그대로 사용하게 됩니다.

💡 React.Memo는 HOC이다.

React.memo는 HOC(High-Orger-Components)로 컴포넌트를 인자로 받아서 새로운 컴포넌트를 반환하는 구조의 함수입니다.

공통점

useMemo, useCallback과 마찬가지로 성능 최적화를 위해 불필요한 렌더링과 연산을 제어하는 용도로 사용됩니다.

차이점

  1. React.memo는 HOC이고,useMemo, useCallback 는 Hook입니다.
  2. React.memo는 클래스형 컴포넌트, 함수형 컴포넌트 모두 사용 가능하지만, useMemo, useCallback 는 Hook이기 때문에 함수형 컴포넌트 안에서만 사용 가능합니다.
  3. React.memo는 props이 같을 시 컴포넌트 자체를 기억해 새로 그리지 않지만, useMemo, useCallback과 같은 Hook은 컴포넌트 자체가 아닌 렌더링 시 계산되는 함수 또는 값을 기억해 컴포넌트 내부 조직에서 실행됩니다.

useMemo와 useCallback, 언제 사용할까?


🙆🏻‍♀️ GOOD

  1. 계산 비용이 많이 들고, 사용자의 입력 값이 map과 filter를 사용했을 때와 같이, 렌더링 이후로도 참조적으로 동일할 가능성이 높은 경우, UseMemo를 사용하는 것이 좋습니다.

  2. ref 함수를 부수작용과 함께 전달하거나, mergeRef-style과 같이 wrapper 함수 ref를 만들때 useMemo를 활용할 수 있습니다.

    ref 함수가 변경이 있을 때마다, 리액트는 과거 값을 null로 호출하고 새로운 함수를 호출한다.
    이 경우, ref 함수의 이벤트 리스너를 붙이거나 제거하는 등의 불필요한 작업이 일어날 수 있습니다. 예로 useIntersectionObserver가 반환되는 ref의 경우 ref 콜백 내부에서 observer의 연결이 끊기거나 연결되는 등의 동작이 일어날 수 있습니다.

  3. 자식 컴포넌트에서 useEffect가 반복적으로 트리거되는 것을 막고싶을 때 사용합니다.

  4. 크기가 큰 리액트 트리 구조 내에서 부모가 리렌더링 되었을 때, 다른 렌더링 전파를 막고싶을 때 사용합니다.

    자식 컴포넌트가 React.Memo, React.PureComponent일 경우, 메모이제이션된 props를 사용하면 딱 필요한 부분에서만 리렌더링됩니다.

🙅🏻‍♀️ BAD

  1. host 컴포넌트(div, span, a, img…)에 전달하는 모든 항목에는 쓰지 않는 것이 좋습니다.

    React는 해당 컴포넌트에 함수 참조가 변경되었는지 신경쓰지 않기 때문입니다. 단 이때 ref, mergeRefs는 제외입니다.

  2. useCallback과 useMemo의 의존성 배열로 완전히 새로운 객체와 배열을 전달해서는 안됩니다.

    항상 의존성이 같지 않은 결과를 의미하기 때문에, 메모이제이션을 사용하는 이유가 없습니다.
    useEffect, useCallback, useMemo의 모든 종속성 배열은 참조 동일성을 확인힙니다.

  3. 전달하려는 항목이 새로운 참조여도 상관 없다면, 사용하지 않는 것이 좋습니다.

    마찬가지로, 새로운 참조는 메모이제이션을 하는 의미가 없기 때문입니다.


✍️ Summary


React의 렌더링 시점

  • 자신의 state가 변경될 때
  • 부모 컴포넌트로부터 전달받은 props가 변경될 때
  • 부모 컴포넌트가 리렌더링될 때
  • forceUpdate 함수가 실행될 때

UseEffect : Side Effect을 처리할 때

모든 컴포넌트가 렌더링된 후 상태변화, 구독, 타이머, 로깅 및 기타 side Effect을 처리한다.

useCallback : 함수 재생성을 방지

Memoization된 함수 를 반환하는 함수로, 특정 함수를 새로 만들지 않고 재사용하고 싶을 때 사용한다.

useMemo : 함수 연산량이 많을 때 이전 결과값을 재사용

useMemo는 Memoization된 을 반환하는 함수이다.

React.Memo : 같은 props로 렌더링이 자주 일어날 때 이전 값을 재사용

props가 같다면, Memoizing된 내용을 재사용하는 함수로, 성능 최적화를 위해 불필요한 렌더링 또는 연산을 제어하는 용도로 사용된다.



📎 참고문서

profile
한입 크기로 베어먹는 개발지식 🍰

0개의 댓글