[유데미x스나이퍼팩토리] 프로젝트 캠프 : Next.js 2기 - 사전직무교육 2주차 - Memoization
학습 목표
memoization을 이해하고 렌더링 최적화 방법을 익힙니다.
memoization이란?
💡 컴퓨터 프로그램이 동일한 계산을 반복해야 할 때, 이전에 계산한 값을 메모리에 저장하여 사용함으로써 동일한 계산의 반복 수행을 제거하여 성능 향상을 도모하는 기술
useCallback
함수형 컴포넌트에서 함수를 메모이제이션하는 데 사용한다.
사용 방법
useCallback(fn, dependencies)
예시
카운트를 증가시키는 함수를 생성하고 이를 집합에 넣어보자.
const functionSet = new Set();
const App = () => {
const [count, setCount] = useState(0);
const increament = setCount((prev) => prev + 1);
functionSet.add(increament);
console.log(functionSet);
return (
<>
<h1>Count: {count}</h1>
<button onClick={increament}>increase</button>
</>
);
};
집합을 출력해보면 카운트를 증가시킬 때마다 increment 함수가 추가되는 것을 확인할 수 있다.
왜 추가될까?
카운트가 증가될 때마다 App컴포넌트가 rerendering 되는데, 이때 함수의 참조값인 increment가 갱싱되기 때문에 js는 이를 다른 객체로 인식해서 집합에 계속해서 쌓이는 것이다.
어떻게 해결할 수 있을까?
useCallback 사용!
const functionSet = new Set();
const App = () => {
const [count, setCount] = useState(0);
const increament = useCallback(() => {
setCount((prev) => prev + 1);
}, []);
functionSet.add(increament);
console.log(functionSet);
return (
<>
<h1>Count: {count}</h1>
<button onClick={increament}>increase</button>
</>
);
};
위와 같이 작성하면 useCallback이 함수를 저장하고 있기 때문에 카운트를 증가시키더라도 집합의 개체가 증가하지 않는 것을 확인할 수 있다.
주의사항
setState를 할 때 콜백함수를 사용하자
만약 setState를 사용해 상태를 업데이트할 때
const increament = useCallback(() => {
setCount(count + 1);
}, []);
이런 방식으로 작성한다면 카운트는 1에서 더이상 증가하지 않을 것이다. 왜냐하면 useCallback은 스냅샷을 찍듯 함수를 저장한다. 따라서 위의 코드에서는 메모이제이션됐을 때 count가 0이었기 때문에 1에서 더이상 증가하지 않게 된다.
dependencies를 확인하자
useCallback에서는 dependencies에 등록된 값의 변화를 인지하고 변화가 일어났을 때 useCallback에 정의된 함수를 실행한다. 따라서 다음과 같이 작성했을 때
const increament = useCallback(() => {
setCount((prev) => prev + 1);
}, [count]);
카운트를 증가시킬 때마다 카운트를 증가시키는 함수가 실행되기 때문에 카운트가 무한으로 증가되는 문제가 발생하게 된다.
useMemo
값을 메모이제이션하는 훅이다.
사용 방법
const cachedValue = useMemo(calculateValue, dependencies)
예시
길이가 매우 긴 배열에서 특정 값을 찾아 렌더링하는 코드가 있다고 하자.
import { useState } from "react";
const initialItems = new Array(29_999_999).fill(0).map((_, i) => {
return { id: i, selected: i === 29_999_998 };
});
const App = () => {
const [count, setCount] = useState(0);
const [items, _] = useState(initialItems);
const selectedItem = items.find((item) => item.selected);
return (
<>
<h1>Count: {count}</h1>
<button onClick={() => setCount((prev) => prev + 1)}>increase</button>
<h2>{selectedItem?.id}</h2>
</>
);
};
export default App;
위와 같이 작성하면 카운트가 변경될 때마다 selectedItem은 값이 변하지 않음에도 find함수가 새롭게 실행되어 성능적인 문제가 발생하게 된다.
어떻게 해결할까?
import { useMemo, useState } from "react";
const initialItems = new Array(29_999_999).fill(0).map((_, i) => {
return { id: i, selected: i === 29_999_998 };
});
const App = () => {
const [count, setCount] = useState(0);
const [items, _] = useState(initialItems);
const selectedItem = useMemo(() => items.find((item) => item.selected), []);
return (
<>
<h1>Count: {count}</h1>
<button onClick={() => setCount((prev) => prev + 1)}>increase</button>
<h2>{selectedItem?.id}</h2>
</>
);
};
export default App;
useMemo를 사용해 값을 지정해주면 그 값이 메모이제이션되어 값을 찾는 코드를 다시 실행하지 않는다.
주의사항
dependencies를 확인하자
useCallback과 동일하게 useMemo 역시 dependencies에 등록된 값의 변화를 인지하고 변화가 일어났을 때 useCallback에 정의된 함수를 실행한다. 따라서 다음과 같이 작성했을 때
const selectedItem = useMemo(
() => items.find((item) => item.selected),
[count]
);
카운트를 증가시킬 때마다 selectedItem을 찾는 함수가 실행되기 때문에 useMemo를 사용하는 의미가 없어진다.
React.memo
컴포넌트를 메모이제이션할 때 사용하는 함수이다.
사용 방법
const MemoizedComponent = React.memo(SomeComponent, arePropsEqual?)
예시
상위 컴포넌트에서 상태변화가 일어난다면 하위 컴포넌트는 모두 재렌더링된다. 이때 React.memo를 사용하면 재렌더링을 막을 수 있다.
import { useState } from "react";
import A from "./components/learn/A";
import B from "./components/learn/B";
const App = () => {
const [count, setCount] = useState(0);
return (
<>
<h1>App Component: {count}</h1>
<button onClick={() => setCount((prev) => prev + 1)}>increase</button>
<A />
<B />
</>
);
};
export default App;
export default React.memo(A);
export default React.memo(B);
주의사항
만약 B 컴포넌트에 count를 props로 넘겨주면 React.memo를 지정해줬어도 App에서 count를 증가시킬 때마다 컴포넌트가 재렌더링된다.
그 값을 사용하지 않더라도 props로 전달되는 순간부터 영향을 끼친다.
만약 함수를 넘겨준다면 useCallback으로 함수를 메모이제이션해주면 해결할 수 있다.
useCallback vs useMemo vs React.memo
useCallback | useMemo | React.memo | |
참조 대상 | 함수 | 값 | 컴포넌트 |
훅 / 함수 | 훅 | 훅 | 함수 (고차 컴포넌트) |
*고차 컴포넌트: 컴포넌트를 반환하는 함수