useMemo와 useCallback
useMemo
- 메모이제이션된 값을 반환하는 훅
- 리렌더링 사이에 계산 결과를 캐싱해주는 훅
useMemo(calculateValue, dependencies)
- calculateValue : 캐싱하려는 값을 계산하는 함수. 순수해야 하며 인자를 받지 않고, 모든 타입의 값을 반환할 수 있어야 함
- dependencies : calculateValue 코드 내에서 참조되는 모든 의존성 값들의 목록. props, state, 컴포넌트 내의 변수 및 함수들
- 비용이 높은 로직의 재계산을 생략 가능
- 큰 배열을 필터링 혹은 변환하거나, 비용이 많이 드는 계산을 수행하는 경우, 데이터가 변경되지 않았다면 계산을 생략하는 것이 좋음
💡 비싼 연산인지 어떻게 알 수 있나요?
- 정확하게 확인하고 싶다면 콘솔 로그를 추가하여 코드에 소요된 시간을 측정할 수 있다.
- 전체적으로 기록된 시간이 클 때(예시:
1ms
이상) 해당 계산을 메모해 두는 것이 좋다.
- 컴포넌트 리렌더링 건너뛰기 ⇒ 경우에 따라 하위 컴포넌트의 리렌더링 성능 최적화
- 기본적으로, 컴포넌트가 리렌더링할 때 React는 해당 컴포넌트의 모든 자식을 재귀적으로 재렌더링한다. List: 자식 컴포넌트 / TodoList : 부모 컴포넌트
- 이 변경으로
하위 컴포넌트
는 모든 props가 마지막 렌더링 때와 동일한 경우 다시 렌더링하지 않는다. List: 자식 컴포넌트 / TodoList : 부모 컴포넌트- 위의 예시에서 filtersTodo 함수는 항상 다른 배열을 생성한다.
- 이는 객체 리터럴이 항상 새 객체를 생성하는 것과 유사 ⇒ 최적화 X
visibleTodos
연산을useMemo
로 감싸면 다시 렌더링 될 때마다 같은 값을 갖게 할 수 있다 (종속성이 변경되기 전까지)
- 다른 hook의 종속성 메모화
사용 시기 및 주의사항
사용 추천 환경
- useMemo에 입력하는 계산이 눈에 띄게 느리고 종속성이 거의 변경되지 않는 경우
- memo로 감싸진 컴포넌트에 prop으로 전달할 경우
- 값이 변경되지 않았다면 렌더링을 건너뛰고 싶었을 것.
- 메모이제이션을 사용하면 의존성이 동일하지 않은 경우에만 컴포넌트 리렌더링 가능
- 전달한 값을 나중에 일부 hook의 종속성으로 이용할 경우
- 다른 useMemo의 계산 값이 useEffect의 값에 종속되어 있을 수도 있음
주의 사항
- Strict Mode에서는 React가 의도적으로 계산 함수를 두 번 호출한다. (실수로 발생한 오류를 찾기 위함)
- React는 캐싱된 값을 버려야 할 특별한 이유가 없는 한 버리지 않는다.
- useMemo는 성능 최적화를 위한 용도로만 사용해야 한다.
- useMemo는 첫 렌더링 을 더 빠르게 만들지는 않는다. 이는 업데이트 시 불필요한 작업을 건너뛰는 데 도움이 될 뿐이다.
useCallback
- 리렌더링 간에 함수 정의를 캐싱해주는 훅
useCallback(fn, dependencies)
- fn : 캐싱할 함수 값으로, 첫 렌더링에서 해당 함수를 반환함
- dependencies : fn 내에서 참조되는 모든 의존성 값들의 목록. props, state, 컴포넌트 내의 변수 및 함수들
- 최초 렌더링에서 fn 함수를 그대로 반환함
- React는 Object.is 비교 알고리즘을 통해 각 의존성을 이전 값과 비교
- 변화 X : 이미 저장해 두었던 fn 함수 반환
- 변화 O : 현재 렌더링 중에 전달한 fn 함수를 그대로 반환
⇒
useCallback
은 의존성이 변하기 전까지 리렌더링 간에 함수를 캐싱한다.
사용 시기 및 주의사항
- 기본적으로, 컴포넌트가 리렌더링할 때 React는 해당 컴포넌트의 모든 자식을 재귀적으로 재렌더링한다.
- 자바스크립트에서 함수(
function () {}
나() => {}
)는 항상 다른 함수를 생성한다. - useCallback으로 감싼 함수는 의존성이 변경되지 전까지는 같은 함수로 존재한다.
- React는 Object.is 비교 알고리즘을 통해 각 의존성을 이전 값과 비교함
- 특별한 이유가 없다면 useCallback을 사용하는 것이 오히려 비효율적
- 개발자의 예상과 일치하지 않다면, state 변수나 ref가 더 적절할 수 있음
- useCallback은 성능 최적화를 위한 용도로만 사용해야 한다.
- useCallback은 컴포넌트 최상위 레벨 또는 커스텀 훅에서만 호출 가능하다.
항상 useCallback
을 사용해야 할까요?
실제로 몇 가지 원칙을 따르면 많은 memoization을 불필요하게 만들 수 있다.
- 가능한 로컬 상태를 선호하고, 컴포넌트 간 상태 공유를 필요 이상으로 하지 않기
- form이나 hover 여부와 같은 일시적 상태를 트리의 상단이나 전역 상태 라이브러리에 유지하지 말기
- 렌더링 로직을 순수하게 유지하기
- 리렌더링 시 문제가 일어나거나 눈에 띄는 시각적인 형체를 생성한다면, 컴포넌트의 버그!!
- 상태를 업데이트하는 불필요한 Effects를 피하기
- 대부분의 성능 문제는 effects로부터 발생한 연속된 업데이트가 리렌더링을 반복해서 생김
- Effects에서 불필요한 의존성을 제거하기
useCallback과 useMemo
useMemo
와useCallback
은 어떤 연관이 있나요?
- 공통점 : 자식 컴포넌트를 최적화할 때 유용. 무언가를 전달할 때 메모이제이션(=캐싱) 할 수 있도록 함
- 차이점 : 무엇을 캐싱하는지
- useMemo : 호출한 함수의 결과 값을 캐싱
- useCallback : 함수 자체를 캐싱
import { useMemo, useCallback } from "react";
function ProductPage({ productId, referrer }) {
const product = useData("/product/" + productId);
const requirements = useMemo(() => {
// 함수를 호출하고 그 결과를 캐싱합니다.
return computeRequirements(product);
}, [product]);
const handleSubmit = useCallback(
(orderDetails) => {
// 함수 자체를 캐싱합니다.
post("/product/" + productId + "/buy", {
referrer,
orderDetails,
});
},
[productId, referrer]
);
return (
<div className={theme}>
<ShippingForm requirements={requirements} onSubmit={handleSubmit} />
</div>
);
}