
useMemo와 useCallback에는 메모이제이션이라는 개념이 적용된다. 여기서 메모이제이션은 무엇일까?
메모이제이션은 거창한 개념이 아니고 단순하다.
내가 계산한 값을 재사용할 수 있도록 기억해두는 것이다.
useMemo, useCallback과 직접적인 관련이 있는 것은 아니지만, 간단하게 피보나치 수열을 예시로 살펴보겠다.
function fibo(n) {
if(n < 2) {
return n;
}
return fibo(n - 1) + fibo(n - 2);
}
console.log(fibo(5));
위 코드와 같이 재귀 함수로 피보나치 수열을 구현하면 5번째 값을 구하기 위해 fibo 함수를 15회 호출해야 한다.
여기에 메모이제이션 개념을 적용하면 아래 코드가 된다.
const memo = [0, 1];
for(int i = 2; i <= 5; i++) {
memo[i] = memo[i - 1] + memo[i - 2];
}
console.log(memo[5]);
위 코드에서는 6번의 연산(초기화 과정을 2회로 가정)만에 피보나치 수열의 5번째 값을 구할 수 있다.
두 코드의 차이점은, 계산했던 값을 기억하고 있느냐이다.
첫 번째 코드가 fibo를 호출하는 과정을 살펴보자
fibo(1)을 5회 호출했다. 계산했던 값이지만 그 값을 기억하고 있지 못하기 때문에 동일한 결과를 내놓는 작업을 반복했다.
비효율적이다.
하지만 두 번째 코드는 memo[0], memo[1], memo[2], memo[3], memo[4], memo[5] 각각에 대한 연산을 1번씩만 했다. 이전에 계산했던 값을 memo라는 배열에 저장해 놓았기 때문이다.
이렇게 동일한 결과를 반환하는 작업을 줄이기 위해 각각에 대한 결과를 메모해놓는 것을 메모이제이션(memoization)이라고 한다.
리액트의 함수형 컴포넌트는 리랜더링 될 때마다 컴포넌트 함수가 다시 실행된다. 따라서 컴포넌트가 리랜더링 될 때마다 컴포넌트가 갖고 있는 모든 함수나 계산들도 다시 실행된다. 값이 바뀌지 않더라도 말이다. 이러한 점은 불필요한 계산을 포함할 수 있다.
컴포넌트의 props에 종속되지 않는, 계산이 필요한 값이 존재한다고 하자.
const value = calculate(noPropsValue);
이 때 props가 바뀌거나 컴포넌트의 state가 변경되어 리랜더링이 발생한다면 calculate 함수가 다시 실행 되어 계산된 결과값이 result에 저장될 것이다. calculate 함수의 실행시간이 크다면 이러한 점은 문제가 된다. 불필요한 계산을 반복하기 때문에 성능에 문제가 발생할 수 있기 때문이다.
이럴 때 필요한 것이 앞서 살펴본 메모이제이션 개념이다.
useMemo는 리랜더링 사이에 계산 결과를 캐싱(메모이제이션)해주는 React Hook이다.
const memoizedValue = useMemo(calculateValue, dependencies);
calculateValue는 캐싱하려는 값을 계산하는 함수이다. 인자로 아무것도 넘길 수 없다.
calculateValue로부터 반환되는 값이 memoizedValue에 저장된다. dependencies는 의존성 배열로, 콜백 함수를 실행할지 결정한다. dependencies의 값이 변경되면 콜백 함수를 실행하여 캐시된 값을 최신화 한다. dependencies 값의 변경 여부는 Object.is로 확인한다.
const memoizedValue = useMemo(
() => calculate(a, b),
[a, b]
);
이와 같이 코드를 작성하면 첫 랜더링에 calculate 함수가 실행되며 memoizedValue가 결정된다. 이후에 발생하는 리랜더링에서는 a나 b의 값이 바뀌지 않는다면 calculate 함수가 실행되지 않고 이미 계산된 memoizedValue가 사용되며, a나 b의 값 둘 중 하나라도 바뀐다면 calculate 함수가 다시 실행되어 memoizedValue가 업데이트 된다.
불필요한 계산이 많이 발생한다면 useMemo를 적용하는 것이 성능 측면에서 유리할 수 있다. 불필요한 계산이 많지 않더라도 딱히 문제가 생기는 점은 없기 때문에 항상 useMemo를 적용해도 괜찮다.
하지만 useMemo를 아무 때나 모든 코드에 사용한다면 코드의 가독성을 해칠 수 있기 때문에, 필요할 때만 사용하는 것이 권장된다.
컴포넌트의 리랜더링 자체를 막는 방법도 있다. React.memo는 props의 변경에 따라 컴포넌트의 리랜더링을 결정한다.
const MemoizedComponent = memo(Component, arePropsEqual?);
현재 시리즈에서는 React Hook에 대해 알아보고 있으니 리액트 공식 문서를 확인해보자.
https://ko.react.dev/reference/react/memo
useCallback은 useMemo와 거의 동일하다. 유일한 차이점은, useMemo에서는 캐시된 값을 반환하지만, useCallback은 캐시된 함수를 반환한다는 점이다.
const memoizedFn = useCallback(fn, dependencies);
fn은 캐싱할 함수이다. 어떤 인자나 반환값도 가질 수 있다. 모든 랜더링에서 첫 번째 랜더링과 dependencies의 값이 바뀔 때마다 fn의 함수가 memoizedFn으로 반환된다.
https://ko.react.dev/reference/react/useMemo
https://ko.react.dev/reference/react/useCallback
https://ko.react.dev/reference/react/memo
제 블로그 글 하진(복사 + 붙여넣기)하시면 어떡해요.......
근처 경찰서에 신고하겠습니다.