memo, useMemo, useCallback

HANITZ·2024년 3월 14일
0

React

목록 보기
8/8
post-thumbnail

React에서 최적화를 위한 캐싱 장치들을 공부해보겠다.

대표적으로 memo, useMemo, useCallback가 있는데 이들의 목적은 불필요한 연산 반복을 방지하기 위해 메모이제이션을 통해 리렌더링을 막아주는 역할을 한다.

작동원리

useMemo

컴포넌트 내에서 메모이제이션을 하는 훅이다.

마운트할 때 deps의 여부와 상관없이 바로 nextCreate를 실행해 초기값으로 잡아준다.

function useMemo(create, deps) {
  var dispatcher = resolveDispatcher();
  return dispatcher.useMemo(create, deps);
}
 .
 .

 {

    useMemo: function (create, deps) {
      currentHookNameInDev = 'useMemo';
      mountHookTypesDev();
      checkDepsAreArrayDev(deps);
      var prevDispatcher = ReactCurrentDispatcher$1.current;
      ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnMountInDEV;

      try {
        return mountMemo(create, deps);
      } finally {
        ReactCurrentDispatcher$1.current = prevDispatcher;
      }
    },
 }

function mountMemo(nextCreate, deps) {
  var hook = mountWorkInProgressHook();
  var nextDeps = deps === undefined ? null : deps;
  var nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

업데이트의 경우 마운트와 달리 deps내부 변수들의 변화여부를 확인하고 변경되었을 때만 nextCreate 함수를 실행시켜준다.

그리고 이전 deps값과 현재값을 비교하고 업데이트를 시켜준다.

    useMemo: function (create, deps) {
      currentHookNameInDev = 'useMemo';
      updateHookTypesDev();
      var prevDispatcher = ReactCurrentDispatcher$1.current;
      ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV;

      try {
        return updateMemo(create, deps);
      } finally {
        ReactCurrentDispatcher$1.current = prevDispatcher;
      }
    },

function updateMemo(nextCreate, deps) {
  var hook = updateWorkInProgressHook();
  var nextDeps = deps === undefined ? null : deps;
  var prevState = hook.memoizedState;

  if (prevState !== null) {
    if (nextDeps !== null) {
      var prevDeps = prevState[1];

      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }

  var nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}
      

React.memo

React.memo는 컴포넌트 자체를 메모이제이션 해 부모 컴포넌트가 리렌더링하더라도 자식은 하지않도록 막아준다.

function memo(type, compare) {
  {
    if (!isValidElementType(type)) {
      error('memo: The first argument must be a component. Instead ' + 'received: %s', type === null ? 'null' : typeof type);
    }
  }

  var elementType = {
    $$typeof: REACT_MEMO_TYPE,
    type: type,
    compare: compare === undefined ? null : compare
  };

  {
    var ownName;
    Object.defineProperty(elementType, 'displayName', {
      enumerable: false,
      configurable: true,
      get: function () {
        return ownName;
      },
      set: function (name) {
        ownName = name; 
        if (!type.name && !type.displayName) {
          type.displayName = name;
        }
      }
    });
  }

  return elementType;
}

memo함수에 두번째 인자로 compare를 받는데 이는 props를 비교하는 함수를 넣어주는 곳이다. 인자를 따로 넣지않으면 단순비교로 넘어간다.

이전 props와 현재 props를 비교하고 context가 바뀌었는지 확인하고 모두 동일하면 bailoutOnAlreadyFinishedWork 함수를 실행한다. 하나라도 다르면 updateFunctionComponent 함수를 실행한다.
이때 props 비교함수로 shallowEqual를 사용해 다른 캐싱 hook과는 다르게 얕은 비교를 해주고있다.

function updateMemoComponent(current, workInProgress, Component, nextProps, renderLanes) {
  if (current === null) {
    var type = Component.type;

    if (isSimpleFunctionComponent(type) && Component.compare === null && 
    Component.defaultProps === undefined) {
      var resolvedType = type;

      {
        resolvedType = resolveFunctionForHotReloading(type);
      } 
      workInProgress.tag = SimpleMemoComponent;
      workInProgress.type = resolvedType;

      return updateSimpleMemoComponent(current, workInProgress, resolvedType, nextProps, renderLanes);
    }

    var child = createFiberFromTypeAndProps(Component.type, null, nextProps, workInProgress, workInProgress.mode, renderLanes);
    child.ref = workInProgress.ref;
    child.return = workInProgress;
    workInProgress.child = child;
    return child;
  }

  {
    var _type = Component.type;
    var _innerPropTypes = _type.propTypes;

    if (_innerPropTypes) {
      checkPropTypes(_innerPropTypes, nextProps, // 
      'prop', getComponentNameFromType(_type));
    }
  }

  var currentChild = current.child; 
  var hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(current, renderLanes);

  if (!hasScheduledUpdateOrContext) {
    var prevProps = currentChild.memoizedProps; 

    var compare = Component.compare;
    compare = compare !== null ? compare : shallowEqual;

    if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
      return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
    }
  } 


  workInProgress.flags |= PerformedWork;
  var newChild = createWorkInProgress(currentChild, nextProps);
  newChild.ref = workInProgress.ref;
  newChild.return = workInProgress;
  workInProgress.child = newChild;
  return newChild;
}

function updateSimpleMemoComponent(current, workInProgress, Component, nextProps, renderLanes) {

  if (current !== null) {
    var prevProps = current.memoizedProps;

    if (shallowEqual(prevProps, nextProps) && current.ref === workInProgress.ref && ( 
     workInProgress.type === current.type )) {
      didReceiveUpdate = false;

      workInProgress.pendingProps = nextProps = prevProps;

      if (!checkScheduledUpdateOrContext(current, renderLanes)) {
        workInProgress.lanes = current.lanes;
        return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
      } else if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
        didReceiveUpdate = true;
      }
    }
  }

  return updateFunctionComponent(current, workInProgress, Component, nextProps, renderLanes);
}

useCallback

useCallback은 함수를 캐싱하기 위한 훅이다.

useCallback의 두번째 인자인 deps가 이전과 다르다면 새롭게 함수를 정의해주고 같다면 캐싱된 함수를 그대로 사용한다.

보통 리렌더링 방지를 위해 컴포넌트에 memo를 사용하는 경우 함수를 인자로 넘겨주기위해 useCallback을 함께 사용해 함수를 재정의하는 것을 방지시켜준다.

mountCallback 에서 할당된 memoizedStateupdateCallback 에서 areHookInputsEqual 함수로 이전 deps와 현재 deps를 비교해준다. 이 둘이 같다면 이전에 할당됐었던 prevState[0] 을 그대로 반환시켜주고 다르면 새로운 callback 을 재할당해준다.

function mountCallback(callback, deps) {
  var hook = mountWorkInProgressHook();
  var nextDeps = deps === undefined ? null : deps;
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

function updateCallback(callback, deps) {
  var hook = updateWorkInProgressHook();
  var nextDeps = deps === undefined ? null : deps;
  var prevState = hook.memoizedState;

  if (prevState !== null) {
    if (nextDeps !== null) {
      var prevDeps = prevState[1];

      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }

  hook.memoizedState = [callback, nextDeps];
  return callback;
}

0개의 댓글

관련 채용 정보