React 공식문서 스터디

useCallback (1)

려낭 2024. 6. 18. 22:52

useCallback이란?

 

useCallback은 React에서 성능 최적화를 위해 사용하는 훅으로 , 메모이제이션(memoization)된 콜백 함수를 반환하는 역할을 한다.

즉, 함수의 재사용을 도와 불필요한 함수 재생성을 방지한다.


레퍼런스

 

리렌더링 간에 함수 정의를 캐싱하려면 컴포넌트의 최상단에서 useCallback을 호출

import { useCallback } from 'react';

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

 

 

매개변수

 

함수(fn)캐싱 : useCallback을 전달받은 함수를 캐싱하여 메모이제이션 한다. 첫번쨰 렌더링에서 이 함수를 생성하고 반환한다.

이후의 렌더링에서는 의존성 배열이 변경되지 않는 한, 동일한 함수를 반환한다.

 

함수 호출: React는 useCallback에 전달된 함수를 호출하지 않는다. 대신 이 함수를 반환하여 호출 시점과 호출 여부를 개발자가 결정할 수 있도록 한다.

 

의존성 배열(dependencies)

  • 반응형 값 목록: useCallback의 두번째 인자는 의존성 배열이다. 이 배열은 함수 내에서 참조되는 모든 반응형 값(props,state,컴포넌트 내부 변수 및 함수)을 포함한다.
  • 린터 검증: 설정된 린터는 이 배열에 필요한 모든 의존성이 올바르게 명시되었는지 검증한다.
  • 비교 알고리즘 : React는 Object.js 비교 알고리즘을 이용해 현재 의존성과 이전 의존성을 비교한다. 값이 동일하면 캐싱된 함수를 반환하고, 값이 변경되면 새로운 함수를 생성하여 반환한다.

반환값

 

최초 렌더링에서는 useCallback은 전달한 fn함수를 그대로 반환한다.

후속 렌더링에서는 이전 렌더링에서 이미 저장해 두었던 fn 함수를 반환하거나 (의존성이 변하지 않았을 때) 현재 렌더링 중에 전달한 fn 함수를 그대로 반환한다.

 

주의사항

 

  1. 최상위 레벨에서 호출
    • 조건문, 반복문 내에서 호출 금지
    • 새 컴포넌트로 분리 
  2. 캐시된 함수의 유지와 삭제
    • 캐시 유지: 리액트는 특별한 이유가 없는 한 캐시된 함수를 삭제하지 않는다. 즉, 컴포넌트가 리렌더링 되더라도 의존성 배열이 변경되지 않으면 동일한 함수가 계속 유지된다.
    • 캐시 삭제 조건
      • 개발 환경에서 파일 편집 시: 개발 중에 파일을 편집하면 리액트가 캐시를 삭제할 수 있다.
      • 컴포넌트의 초기 마운트 중 일시 중단: 컴포넌트가 마운트 되는 동안 일시 중단되면 캐시가 삭제될 수 있다.
      • 미래의 리액트 기능: 리액트는 향후 업데이트에서 특정 기능에 대해 캐시 삭제를 활용할 수 있다. 예를 들어, 가상화된 목록에서 뷰포트 밖의 항목에 대한 캐시를 삭제할 수 있다.
  3. 성능 최적화와 적절한 사용
    • 성능 최적화 의존성: useCallback을 성능 최적화 방법으로 의존할 때, 리액트의 캐시 삭제 동작이 예상과 일치해야 한다. 예상치 못한 캐시 삭제가 성능에 영향을 미칠 수 있다.
    • 대안 사용: 경우에 따라 useCallback 대신 state 변수나 ref를 사용하는 것이 더 적절할 수 있다. 예를 들어 함수가 의존하는 데이터가 자주 변경된다면, useCallback이 아닌 useRef를 사용하여 최신 값을 참조하는 것이 나을 수 있다.

 

사용법

 

컴포넌트의 리렌더링 건너뛰기

 

렌더링 성능을 최적화할 때 자식 컴포넌트에 넘기는 함수를 캐싱할 필요가 있다. 먼저 이 작업을 수행하는 방법에 대한 예시를 살펴보자.

 

컴포넌트의 리렌더링 간에 함수를 캐싱하려면 함수 정의를 useCallback 훅으로 감싼다.

 

import { useCallback } from 'react';

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

 

useCallback에게 두 가지를 전달해야 한다.

  1. 리렌더링 간에 캐싱할 함수 정의
  2. 함수에서 사용되는 컴포넌트 내부의 모든 값을 포함하고 있는 의존성 목록

최초 렌더링에서 useCallback으로부터 반환되는 함수는 호출시에 전달할 함수이다.

이어지는 렌더링에서 React는 의존성을 이전 렌더링에서 전달한 의존성과 비교한다. 의존성 중 하나라도 변한 값이 없다면 useCallback은 전과 똑같은 함수를 반환한다. 그렇지 않으면 useCallback 은 이번 렌더링에서 전달한 함수를 반환한다.

 

다시 말하면, useCallback은 의존성이 변하기 전까지 리렌더링 간에 함수를 캐싱한다.

 

이 기능이 언제 유용할지 예시를 통해 알아보자.

 

handleSubmit 함수를 ProductPage에서 ShippingForm 컴포넌트로 전달한다고 가정해보자

 

function ProductPage({ productId, referrer, theme }) {
  // ...
  return (
    <div className={theme}>
      <ShippingForm onSubmit={handleSubmit} />
    </div>
  );

 

theme prop을 토글하면 앱이 잠시 멈춘다. JSX에서 <ShippingForm /> 을 제거하면 앱이 빨라진 것처럼 느껴진다.  <ShippingForm /> 컴포넌트의 최적화를 시도해본 결과이다.

리액트에서 컴포넌트가 리렌더링될 때, 기본적으로 모든 자식 컴포넌트도 재귀적으로 리렌더링된다.  예를 들어, 만약 ProductPage 컴포넌트가 다른 테마(theme) 값으로 리렌더링되면, 그 안에 있는 ShippingForm 컴포넌트도 함께 리렌더링된다. 이는 React가 상위 컴포넌트의 상태나 props가 변경될 때 하위 컴포넌트도 새로 그려져야 하는 것을 의미한다.

그러나 때때로 하위 컴포넌트가 불필요하게 리렌더링되는 경우가 있다. 예를 들어, ShippingForm 컴포넌트가 리렌더링하는데 많은 계산이 필요하지 않는 상황에서도 모든 props가 이전과 같다면 리렌더링을 건너뛰는 것이 효율적일 수 있다.

 

import { memo } from 'react';

const ShippingForm = memo(function ShippingForm({ onSubmit }) {
  // ...
});

 

이렇게 변경한 ShippingForm은 모든 props 가 마지막 렌더링과 같다면 리렌더링을 건너뛴다. 여기서 함수 캐싱이 중요하다. useCallback 없이 handleSubmit을 정의했다고 가정하자.

 

function ProductPage({ productId, referrer, theme }) {
  // theme이 바뀔때마다 다른 함수가 될 것입니다...
  function handleSubmit(orderDetails) {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }
  
  return (
    <div className={theme}>
      {/* ... 그래서 ShippingForm의 props는 같은 값이 아니므로 매번 리렌더링 할 것입니다.*/}
      <ShippingForm onSubmit={handleSubmit} />
    </div>
  );
}

 

자바스크립트에서 function () 나 {} () => {} 은 항상 다른 함수를 생성한다. 이것은 {} 객체 리터럴이 항상 새로운 객체를 생성하는 방식과 유사하다. 보통의 경우에는 문제가 되지 않지만, 여기서는 ShippingForm props는 절대 같아질 수 없고 memo 최적화는 동작하지 않을 것이라는 것을 의미한다. 여기서 useCallback이 유용하게 사용된다.

 

function ProductPage({ productId, referrer, theme }) {
  // React에게 리렌더링 간에 함수를 캐싱하도록 요청합니다...
  const handleSubmit = useCallback((orderDetails) => {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }, [productId, referrer]); // ...이 의존성이 변경되지 않는 한...

  return (
    <div className={theme}>
      {/* ...ShippingForm은 같은 props를 받게 되고 리렌더링을 건너뛸 수 있습니다.*/}
      <ShippingForm onSubmit={handleSubmit} />
    </div>
  );
}

 

handleSubmit을 useCallback으로 감쌈으로써 리렌더링 간에 이것이 같은 함수라는 것을 보장한다. 특별한 이유가 없다면 함수를 꼭 useCallback으로 감쌀 필요는 없다. 이 예시에서의 이유는 memo로 감싼 컴포넌트에 전달하기 때문에 해당 함수가 리렌더링을 건너뛸 수 있기 때문이다. 

 

'React 공식문서 스터디' 카테고리의 다른 글

useTransition  (0) 2024.06.24
useMemo, useCallback 발표자료  (0) 2024.06.21
useMemo (2)  (0) 2024.06.18
useMemo (1)  (0) 2024.06.17
useEffect, useLayoutEffect 발표자료  (0) 2024.06.14