useMemo는 리액트에서 컴포넌트의 성능을 최적화 하는 데 사용되는 훅이다.
useMemo에서 memo는 memoization을 뜻하는데
이를 직역하면 '메모리에 넣기'이다.
memoization은 컴퓨터 프로그램이 동일한 계산을 반복해야 할 때,
이전에 계산한 값을 메모리에 저장함으로써 동일한 계산의 반복 수행을 제거하여
프로그램 실행 속도를 빠르게 하는 기술이다.
대표적인 예로 피보나치 수열을 구하는 상황을 들 수 있다.
재귀를 이용해서 피보나치 수열을 구하는 코드는 다음과 같다.
function fib(n) {
if (n < 2) {
return n;
}
return fib(n - 1) + fib(n - 2);
}
console.log(fib(5));
fib(5)
을 구하기 위해 fib(4)
와 fib(3)
을 호출하고,
fib(4)
을 구하기 위해 fib(3)
와 fib(2)
을 호출하는 등 재귀 호출이 반복된다.
이때 상위 값을 구하기 위해 이전에 계산했던 값들을 반복해서 호출하면서
fib
함수를 총 15번 실행하게 된다.
이처럼 불필요한 함수 호출을 막기 위해 메모이제이션을 활용할 수 있다.
이미 함수가 실행된 적이 있어 연산된 값이 존재한다면,
함수를 호출하지 않고 기존에 연산된 값을 재활용한다!
메모이제이션을 활용하여 피보나치 수열을 구하는 코드는 다음과 같다.
const memo = [0, 1];
function fib(n) {
if (memo[n] || n < 2) return memo[n];
const result = fib(n - 1) + fib(n - 2);
memo[n] = result;
return result;
}
이미 연산된 값은 다시 계산하지 않도록 memo
라는 배열에 피보나치 수열을 저장하였다.
이처럼 useMemo도 메모이제이션을 활용하여 기존에 연산된 값을 재활용한다.
따라서 적절한 시점에 useMemo를 활용한다면 컴포넌트의 성능을 최적화할 수 있다!
const memoizedValue = useMemo(() => calculate(a, b), [a, b]);
useMemo에는 두 가지 인자를 전달할 수 있다.
첫 번째 인자에는 값을 연산하고 반환하는 콜백 함수를 전달한다.
이때 이 함수는 순수 함수여야 하며 인수를 받지 않아야 하고, 모든 타입의 값을 반환해야 한다.
React는 초기 렌더링 중에 콜백 함수(calculate
)를 호출하고
다음 렌더링에서 의존성이 변경되지 않은 경우에 동일한 값을 다시 반환한다.
만약 의존성이 변경되었다면 콜백 함수(calculate
)를 호출하고
반환된 결과 값을 나중에 재사용할 수 있도록 저장한다!
두 번째 인자에는 콜백 함수 내부에서 참조되는 의존성 배열을 전달한다.
위 예시 코드에서 의존성은 a
와 b
이다.
만약 a
혹은 b
가 변경되었다면 콜백 함수를 다시 호출하여 새로 계산된 값을 반환한다.
메모이제이션이라는 개념만 보았을 때는
굉장히 효율적이고 사용하기만 하면 최적화가 이루어질 것 같은 느낌이 든다.
그러나 useMemo는 값을 재활용하기 위해 따로 메모리를 사용하기 때문에
불필요한 값까지 메모이제이션한다면 오히려 메모리를 낭비할 수 있다.
또한 useMemo을 사용하면서 코드의 복잡도가 올라가 개발 비용도 커질 수 있다.
따라서 명확한 목적 없이 useMemo를 남발하는 것은 오히려 비효율적이다.
구체적으로 언제 useMemo의 사용을 피해야 할까? 🤔
최적화하려는 계산의 비용이 크지 않은 경우
일반적으로 React에서 많은 작업을 효율적으로 처리하도록 설계되어 있다.
대부분의 계산은 매우 빠르기 때문에 굳이 useMemo을 사용하여 최적화할 필요가 없다.
메모이제이션이 필요한지 확실하지 않은 경우
일단 useMemo 없이 코드를 작성한 다음에 성능 문제가 발생할 경우에
점진적으로 최적화를 하는 것이 좋다!
의존성 배열이 너무 자주 변경되는 경우
useMemo는 의존성 배열의 값이 변경될 때만 메모이제이션된 값을 재계산한다.
그러나 의존성 배열에 있는 값들이 너무 자주 변경된다면,
useMemo가 거의 매 렌더링마다 호출될 것이기 때문에 성능적인 이점을 보기 어렵다.
그렇다면 언제 useMemo을 사용해야 할까? 🤔
console.time('filter array');
const visibleTodos = filterTodos(todos, tab);
console.timeEnd('filter array');
위 코드처럼 콘솔 로그를 추가하여 코드에서 소요된 시간을 측정할 수 있다.
전체 기록된 시간이 1ms 혹은 그 이상이라면 해당 계산은 메모하는 것이 합리적일 수 있다!
console.time('filter array');
const visibleTodos = useMemo(() => {
return filterTodos(todos, tab);
}, [todos, tab]);
console.timeEnd('filter array');
참고 📎 리액트 공식 문서
https://react.dev/reference/react/useMemo
useCallback도 useMemo와 마찬가지로 성능을 최적화할 때 사용한다.
둘의 차이점은,
useMemo는 함수 호출 결과 값을 메모이제이션하고
useCallback은 함수 자체를 메모이제이션한다.
즉, useCallback은 함수를 새롭게 생성하는 대신 메모이제이션된 함수를 반환한다.
자바스크립트에서 함수는 객체로 취급이 되기 때문에
함수를 동일하게 만들어도 메모리 주소가 다르면 다른 함수로 간주한다.
왜냐하면 메모리 주소에 의한 참조 비교를 하기 때문이다.
const add1 = () => x + y;
const add2 = () => x + y;
console.log(add1 === add2); // false
위의 예시에서 동일한 내용의 함수를 정의하고
두 함수를 ===
연산자로 비교해보면 false가 반환된다.
useCallback은 이러한 함수 동등성 문제를 해결하기 위해 사용된다.
컴포넌트가 렌더링 될 때 새로 함수를 생성하는 대신,
동일한 참조를 가진 함수를 반환함으로써 불필요한 리렌더링을 방지한다.
const memoizedValue = useCallback(() => calculate(a, b), [a, b]);
useCallback에는 두 가지 인자를 전달할 수 있다.
첫 번째 인자에는 캐시하려는 함수를 전달한다.
이때 이 함수는 인수를 취하고 값을 반환할 수 있다.
React는 초기 렌더링 중에 함수(calculate
)를 반환한다.
(함수를 호출하지 않음!)
다음 렌더링에서 의존성이 변경되지 않은 경우에 동일한 함수를 다시 반환한다.
만약 의존성이 변경되었다면 현재 렌더링 중에 전달된 새로운 함수를 저장하고 반환한다.
주의할 점은 useCallback이 반환하는 것은 함수의 참조이며
함수를 실제로 호출하는 것은 사용자의 코드에서 결정된다!
두 번째 인자에는 콜백 함수 내부에서 참조되는 의존성 배열을 전달한다.
위 예시 코드에서 의존성은 a
와 b
이다.
만약 a
혹은 b
가 변경되었다면 현재 렌더링 중에 전달된 새로운 함수를 저장하고 반환한다.
아래는 useCallback이 필요한 이유를 잘 보여주는 예시이다.
function Profile({ id }) {
const [data, setData] = useState(null);
const fetchData = () =>
fetch(`https://test-api.com/data/${id}`)
.then((response) => response.json())
.then(({ data }) => data);
useEffect(() => {
fetchData().then((data) => setData(data));
}, [fetchData]);
}
위 코드에서 함수의 동등성 문제 때문에 예상치 못한 무한 루프에 빠진다.
그 이유는 fetchData
는 컴포넌트가 렌더링될 때마다 새로 생성되는 함수이므로
함수의 메모리 주소가 계속 변경되기 때문이다.
fetchData
함수에 의존하는 useEffect가 매번 실행되기 때문에 이는 무한 루프를 초래한다.
따라서 아래 코드와 같이 useCallback
을 사용하여 함수의 동등성을 유지해야 한다.
function Profile({ id }) {
const [data, setData] = useState(null);
const fetchData = useCallback(() =>
fetch(`https://test-api.com/data/${id}`)
.then((response) => response.json())
.then(({ data }) => data), [id]);
useEffect(() => {
fetchData().then((data) => setData(data));
}, [fetchData]);
}
위와 같이 useCallback을 사용하면 함수를 메모이제이션하여,
id
가 변경되지 않는 한 동일한 함수의 참조를 반환한다.
이를 통해 fetchData
가 이전과 동일한 참조를 가지게 되어
useEffect가 불필요하게 재실행되지 않도록 방지한다.
위의 예시 외에 아래의 경우에도 함수를 메모이제이션하기 위해 useCallback을 사용할 수 있다.
이벤트 핸들러 함수가 자주 재생성되는 경우
이벤트 핸들러 함수는 매번 새로운 인스턴스가 생성된다.
그러나 useCallback을 사용하면 함수가 처음 한 번만 생성되며,
나중에는 동일한 함수 객체를 재사용하게 된다.
하위 컴포넌트에 props로 전달되는 함수가 자주 재생성되는 경우
React에서 props로 함수를 전달하는 경우에
해당 함수가 변경되면 하위 컴포넌트가 재렌더링된다.
따라서 useCallback을 통해 함수를 재사용하면 하위 컴포넌트의 재렌더링을 방지할 수 있다.
잘못된 정보나 궁금하신 사항이 있다면 언제든 편하게 댓글로 말씀해주세요!
읽어주셔서 감사합니다 ☺️
일단 불펌 신고하고 글 읽어볼게요~~