실제로 useCallback과 useMemo의 차이점은 무엇입니까?


85

내가 뭔가를 오해했을 수도 있지만 useCallback Hook은 다시 렌더링이 발생할 때마다 실행됩니다.

입력 값을 useCallback에 대한 두 번째 인수로 전달했습니다. 항상 변경할 수없는 상수입니다.하지만 반환 된 메모 화 된 콜백은 렌더링 할 때마다 여전히 내 값 비싼 계산을 실행합니다 (아래 스 니펫에서 직접 확인할 수 있음).

useCallback을 useMemo로 변경했으며 useMemo는 예상대로 작동합니다. 입력이 변경되면 실행됩니다. 그리고 값 비싼 계산을 정말 기억합니다.

라이브 예 :

'use strict';

const { useState, useCallback, useMemo } = React;

const neverChange = 'I never change';
const oneSecond = 1000;

function App() {
  const [second, setSecond] = useState(0);
  
  // This 👇 expensive function executes everytime when render happens:
  const calcCallback = useCallback(() => expensiveCalc('useCallback'), [neverChange]);
  const computedCallback = calcCallback();
  
  // This 👇 executes once
  const computedMemo = useMemo(() => expensiveCalc('useMemo'), [neverChange]);
  
  setTimeout(() => setSecond(second + 1), oneSecond);
  
  return `
    useCallback: ${computedCallback} times |
    useMemo: ${computedMemo} |
    App lifetime: ${second}sec.
  `;
}

const tenThousand = 10 * 1000;
let expensiveCalcExecutedTimes = { 'useCallback': 0, 'useMemo': 0 };

function expensiveCalc(hook) {
  let i = 0;
  while (i < tenThousand) i++;
  
  return ++expensiveCalcExecutedTimes[hook];
}


ReactDOM.render(
  React.createElement(App),
  document.querySelector('#app')
);
<h1>useCallback vs useMemo:</h1>
<div id="app">Loading...</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>


1
전화 할 필요가 없다고 생각합니다 computedCallback = calcCallback();. computedCallback= calcCallback , it will update the callback once neverChange` 변경 사항 이어야합니다 .
Noitidart

1
useCallback (fn, deps)는 useMemo (() => fn, deps)와 같습니다.
Henry Liu

답변:


148

TL; DR;

  • useMemo 함수 호출 사이와 렌더링 사이에 계산 결과를 메모하는 것입니다.
  • useCallback 렌더링 사이에 콜백 자체 (참조 동등성)를 메모하는 것입니다.
  • useRef 렌더링 사이에 데이터를 유지하는 것입니다 (업데이트해도 다시 렌더링이 실행되지 않음).
  • useState 렌더링 사이에 데이터를 유지하는 것입니다 (업데이트하면 다시 렌더링이 시작됨).

긴 버전 :

useMemo 무거운 계산을 피하는 데 중점을 둡니다.

useCallback다른 것에 초점을 맞추고 있습니다. 인라인 이벤트 핸들러 onClick={() => { doSomething(...); }PureComponent자식 다시 렌더링을 유발할 때 성능 문제를 수정합니다 (함수 표현식이 매번 참조 적으로 다르기 때문).

이것은 계산 결과를 메모하는 방법이 아니라에 useCallback더 가깝습니다 useRef.

문서를 살펴보면 혼란스러워 보입니다.

useCallback입력 중 하나가 변경된 경우에만 변경되는 메모 화 된 콜백 버전을 반환합니다. 이는 불필요한 렌더링을 방지하기 위해 참조 동등성에 의존하는 최적화 된 하위 구성 요소에 콜백을 전달할 때 유용 합니다 (예 : shouldComponentUpdate).

변경된 후에 만 다시 렌더링 되는 PureComponent기반 자식 이 있다고 가정 합니다.<Pure />props

이 코드는 부모가 다시 렌더링 될 때마다 자식을 다시 렌더링합니다. 인라인 함수는 매번 참조 적으로 다르기 때문입니다.

function Parent({ ... }) {
  const [a, setA] = useState(0);
  ... 
  return (
    ...
    <Pure onChange={() => { doSomething(a); }} />
  );
}

다음의 도움으로 처리 할 수 ​​있습니다 useCallback.

function Parent({ ... }) {
  const [a, setA] = useState(0);
  const onPureChange = useCallback(() => {doSomething(a);}, []);
  ... 
  return (
    ...
    <Pure onChange={onPureChange} />
  );
}

그러나 일단 a변경 되면 onPureChange우리가 생성 한 핸들러 함수 (그리고 React가 기억 한)는 여전히 이전 a값을 가리 킵니다 . 성능 문제 대신 버그가 있습니다! 이는 선언 될 때 캡처 된 변수 onPureChange에 액세스 하기 위해 클로저를 사용 하기 때문 입니다. 이 문제를 해결하려면 React 에 올바른 데이터를 가리키는 새 버전 을 삭제 하고 다시 생성 / 기억 (기억) 할 위치를 알려야합니다. 우리는 추가하여이를 A와 종속 useCallback '의 두 번째 인수 :aonPureChangeonPureChangea

const [a, setA] = useState(0);
const onPureChange = useCallback(() => {doSomething(a);}, [a]);

이제 a가 변경되면 React는 구성 요소를 다시 렌더링합니다. 그리고 다시 렌더링하는 동안에 대한 종속성 onPureChange이 다르며 새 버전의 콜백을 다시 생성 / 기억해야합니다. 마침내 모든 것이 작동합니다!


3
매우 자세하고 <순수한> 답변 감사합니다. ;)
RegarBoy

17

다음을 수행 할 때마다 메모 된 콜백을 호출합니다.

const calcCallback = useCallback(() => expensiveCalc('useCallback'), [neverChange]);
const computedCallback = calcCallback();

이것이 수가 증가하는 이유 useCallback입니다. 그러나 함수는 절대 변경되지 않으며 ***** 생성 **** 새 콜백을 생성하지 않으며 항상 동일합니다. 의미useCallback 는 제대로하는 일입니다.

이것이 사실인지 확인하기 위해 코드를 약간 변경해 보겠습니다. lastComputedCallback새로운 (다른) 함수가 반환되는지 추적 할 전역 변수를 만들어 보겠습니다 . 새 함수가 반환되면 useCallback"다시 실행 됨" 을 의미 합니다. 그래서 그것이 다시 실행될 때 우리는를 호출 할 것입니다. expensiveCalc('useCallback')이것이 useCallback작동 했는지를 계산하는 방법이기 때문 입니다. 아래 코드에서이 작업을 수행 useCallback하고 예상대로 메모 하는 것이 분명해졌습니다.

당신이보고 싶은 경우에 useCallback전달하는 배열에, 다음 줄의 주석을 함수 매번 다시 작성 second. 함수가 다시 생성되는 것을 볼 수 있습니다.

'use strict';

const { useState, useCallback, useMemo } = React;

const neverChange = 'I never change';
const oneSecond = 1000;

let lastComputedCallback;
function App() {
  const [second, setSecond] = useState(0);
  
  // This 👇 is not expensive, and it will execute every render, this is fine, creating a function every render is about as cheap as setting a variable to true every render.
  const computedCallback = useCallback(() => expensiveCalc('useCallback'), [
    neverChange,
    // second // uncomment this to make it return a new callback every second
  ]);
  
  
  if (computedCallback !== lastComputedCallback) {
    lastComputedCallback = computedCallback
    // This 👇 executes everytime computedCallback is changed. Running this callback is expensive, that is true.
    computedCallback();
  }
  // This 👇 executes once
  const computedMemo = useMemo(() => expensiveCalc('useMemo'), [neverChange]);
  
  setTimeout(() => setSecond(second + 1), oneSecond);
  return `
    useCallback: ${expensiveCalcExecutedTimes.useCallback} times |
    useMemo: ${computedMemo} |
    App lifetime: ${second}sec.
  `;
}

const tenThousand = 10 * 1000;
let expensiveCalcExecutedTimes = { 'useCallback': 0, 'useMemo': 0 };

function expensiveCalc(hook) {
  let i = 0;
  while (i < 10000) i++;
  
  return ++expensiveCalcExecutedTimes[hook];
}


ReactDOM.render(
  React.createElement(App),
  document.querySelector('#app')
);
<h1>useCallback vs useMemo:</h1>
<div id="app">Loading...</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>

의 이점은 useCallback반환 된 함수가 동일하므로 변경 사항 이없는 한 반응이 매번 요소에 removeEventListener'ing and addEventListenering ' 하지 않는다는 것 computedCallback입니다. 그리고 computedCallback변수가 변경 될 때만 변경됩니다. 따라서 addEventListener한 번만 반응 합니다.

좋은 질문입니다. 대답하면서 많은 것을 배웠습니다.


2
좋은 답변에 대한 작은 의견 : 주요 목표는 아닙니다 addEventListener/removeEventListener(DOM 리플 로우 / 다시 그리기로 이어지지 않기 때문에이 작업 자체는 무겁지 않습니다). 그러나이 콜백을 사용하는 자식을 다시 렌더링 PureComponent(또는 사용자 지정 사용 shouldComponentUpdate()) 하는 것을 피하는 것입니다
skyboyer

@skyboyer에게 감사드립니다. *EventListener저렴한 것에 대해 전혀 몰랐 습니다. 리플 로우 / 페인트를 일으키지 않는 좋은 점입니다! 항상 비싸다고 생각해서 피하려고 했어요. 그래서 내가 전달하지 않는 경우에 PureComponent, useCallback반응하고 DOM이 추가 복잡성을 수행하는 것의 절충 가치 에 의해 추가되는 복잡성이 remove/addEventListener있습니까?
Noitidart

1
사용하지 않는 경우 PureComponent사용자 정의 또는 shouldComponentUpdate다음 중첩 된 구성 요소 useCallback(오버 헤드 초 동안 추가로 확인하여 모든 값을 추가하지 않습니다 useCallback추가 건너 뛰는 무효화됩니다 argumgent removeEventListener/addEventListener이동)
skyboyer

와우 매우 흥미 롭습니다. 이것을 공유해 주셔서 감사합니다. 이것은 *EventListener나에게 비싼 작업이 아닌 방법에 대한 완전히 새로운 모습입니다 .
Noitidart

15

에 대한 하나의 라이너 useCallbackuseMemo:

useCallback(fn, deps)상응 하는 useMemo(() => fn, deps).


으로 useCallback당신이 기능을 memoize, useMemo어떤 계산 된 값을 memoizes :

const fn = () => 42 // assuming expensive calculation here
const memoFn = useCallback(fn, [dep]) // (1)
const memoFnReturn = useMemo(fn, [dep]) // (2)

(1)fn동일한 경우 여러 렌더링에 걸쳐 동일한 참조- 의 메모 버전을 반환합니다 dep. 그러나 호출때마다 복잡한 계산이 다시 시작됩니다. memoFn

(2)변경 될 fn때마다 호출 dep하고 반환 된 값 ( 42여기)을 기억 한 다음 memoFnReturn.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.