React에서 성능최적화를 위해 '메모이제이션'을 활용하는 것인데, 대표적으로 useMemo와 useCallback 두 가지 훅이 있다. 메모이제이션을 담당하는 useMemo와 useCallback 이라는 각각의 장점과 단점이 뭐가 있는지 알아보자.
메모이제이션(memoization)은 값비싼 함수 호출의 결과를 캐싱하고 동일한 입력이 다시 발생할 때 불필요하게 다시 계산하는 대신 캐싱된 결과를 반환하는 프로그래밍 기술로, 동일한 입력으로 여러 번 호출되는 함수 또는 컴포넌트가 있을 때 유용하다. React에서는 useCallback, useMemo와 같은 메모이제이션 훅을 통해 성능을 향상시키고 코드의 복잡성을 줄일 수 있다. 하지만 메모이제이션은 메모리에 특정한 값을 저장하는 것이기 때문에, 정말 필요하지 않은 경우에도 남용하면 오히려 성능을 저하시킬 수 있다.
useMemo 는 리렌더링 사이에 계산 결과를 캐싱할 수 있게 해주는 Rect Hook 이며, Memoization된 '값'을 반환한다.
const cachedValue = useMemo(calculateValue, dependencies);
기본형태 : const cachedValue = useMemo(()=>{값을 연산하여 결과값을 반환하는 로직}, [])
calculateValue : 캐시하려는 값을 계산하는 함수입니다. ()=>{} 형태
순수해야 하고 인수를 사용하지 않아야 하며 모든 유형의 값을 반환해야 합니다. React는 초기 렌더링 중에 함수를 호출합니다.
dependencies : calculateValue 코드 내에서 참조된 모든 반응형 값들의 목록이다. 반응형 값에는 props, state와 컴포넌트 바디에 직접 선언된 모든 변수와 함수가 포함된다. 어떤 값이 변경되었을 때 다시 연산해야 할지 알려주기 위한 배열을 넣어줍니다. 이를 dependencies array, 줄여서 deps라고 합니다.
useMemo는 의존성이 변경되었을 때에만 메모이제이션된 값을 다시 계산한다. 따라서, 기존에 렌더링마다 실행되었던 복잡한 계산을 방지해준다. (만약 배열이 없는 경우 매 렌더링 마다 새로운 값을 계산하게 된다.)
특히, 복잡한 계산이나 외부 데이터가 필요한 작업에 특히 유용하다.
useCallback은 리렌더링 간에 함수 정의를 캐싱해 주는 React Hook이다.
useCallback은 Memoization된 '값'이 아닌 '함수'를 반환한다. useMemo는 함수를 실행해서 그 실행 값을 반환하지만, useCallback은 함수 자체를 반환하는 것이다.
const cachedFn = useCallback(fn, deps)
fn: 캐싱할 함숫값이다. 이 함수는 어떤 인자나 반환값도 가질 수 있다. React는 첫 렌더링에서 이 함수를 반환한다. (호출하는 것이 아니다!) 다음 렌더링에서 dependencies 값이 이전과 같다면 React는 같은 함수를 다시 반환한다. 반대로 dependencies 값이 변경되었다면 이번 렌더링에서 전달한 함수를 반환하고 나중에 재사용할 수 있도록 이를 저장한다. React는 함수를 호출하지 않는다. 이 함수는 호출 여부와 호출 시점을 개발자가 결정할 수 있도록 반환된다.
dependencies: fn 내에서 참조되는 모든 반응형 값의 목록이다. 반응형 값은 props와 state, 그리고 컴포넌트 안에서 직접 선언된 모든 변수와 함수를 포함한다.
useCallback(fn, deps)은 useMemo(() => fn, deps)와 같다. useCallback 의존성 배열에 있는 상태나 props가 변경되지 않는다면, 해당 함수는 다시 생성되지 않는다.
상당한 시간이나 리소스가 소요되는 함수나 계산이 있는 경우 메모이제이션을 사용하여 결과를 캐싱하여 불필요하게 다시 계산하지 않도록 할 수 있다.
연산 혹은 처리량이 매우 많아서 렌더링의 문제가 되는 경우 메모이제이션으로 재렌더링 시 비용을 절감할 수 있다.
사용자의 입력 값이 map과 filter을 사용했을 때와 같이 이후 렌더링 이후로도 참조적으로 동일할 가능성이 높은 경우 useMemo를 사용하는 것이 좋다.
부모로부터 동일한 props를 수신하고 동일한 입력에 대해 동일한 출력을 생성하는 컴포넌트의 경우, props가 변경되지 않을 때 재렌더링되는 것을 방지할 수 있다. 즉, 부모가 재렌더링될 때 자식 컴포넌트까지 렌더링 전파를 막을 수 있다.
자식 컴포넌트로 전달되거나 이벤트 처리기로 사용되는 콜백 함수가 있는 경우, 콜백 함수를 메모이제이션해 해당 자식 컴포넌트의 불필요한 재렌더링을 방지할 수 있다. 이렇게 하면 콜백에 의존하는 자식 컴포넌트의 불필요한 재렌더링을 방지하면서 렌더링 간에 동일한 함수 참조가 사용된다.
useEffect의 의존성 목록에 비용이 많이 드는 계산이나 렌더링 간에 변경할 필요가 없는 개체가 있는 경우 메모이제이션을 사용할 수 있다. 특히 콜백 함수 등을 의존성에 포함해야 하는 경우 메모이제이션을 사용하면 콜백 함수 참조가 안정적으로 유지되고, 의존성이 변경될 때 불필요한 Effect가 트리거되지 않는다.
빠르게 렌더링되는 작고 간단한 컴포넌트의 경우 메모이제이션이 상당한 성능 향상을 제공하지 않을 수 있다. 일반적으로 메모이제이션은 더 큰 컴포넌트나 계산 비용이 많이 드는 컴포넌트에 더 유용하다.
기본 산술, 문자열 연결 또는 배열 조작과 같은 간단한 작업은 일반적으로 빠르고 저렴하기 때문에 메모이제이션이 필요하지 않다. 연산이 복잡하지 않은 함수에 메모이제이션을 사용하는 것은 메모리 낭비이므로, 간단한 함수에는 메모이제이션을 사용하지 않는 것이 좋다.
메모이제이션은 동일한 입력 값이 정기적으로 표시될 가능성이 높은 함수에 적합하다. 동일한 인수로 함수를 거의 호출하지 않으면 캐시 적중이 드물고 인수 직렬화 및 비교로 인해 오히려 성능이 저하될 수 있다. 또한 캐시를 유지하면 이전의 모든 입력과 출력을 유지해야 하므로 메모리 사용량도 증가한다. useMemo, UseCallback 모두 성능최적화를 하기엔 훌륭한 도구이지만, 리렌더링이 발생할 수 있는 부분을 미리 생각하고 특정 부분에서만 사용하는게 도구의 역할 최대치로 끌어올려서 사용하는 방법이라는 것을 깨달았다. 무분별한 사용은 오히려, 도구의 목적에 반하는 사용법이라는 것도 알 수 있었다.
참고 자료 출처:
https://d2.naver.com/helloworld/9223303
https://ko.react.dev/reference/react/useMemo#memoizing-a-dependency-of-another-hook