useMemo는 렌더링 사이에 계산 결과를 캐시합니다. React는 첫 렌더링에서 함수를 실행하고 결과를 보관한 뒤, 의존성 배열의 값 중 하나가 바뀔 때까지 이후 렌더링에서 같은 결과를 돌려줍니다.
Console을 열고 테마를 몇 번 토글해 보세요. 아무것도 기록되지 않고 토글은 즉시 반응합니다. 탭을 바꾸면 tab이 의존성이므로 잠깐 멈춘 뒤 filtering for active가 나타납니다. 이제 useMemo 줄을 const visible = filterTodos(todos, tab);로 바꾸고 다시 테마를 토글해 보세요. 클릭할 때마다 로그가 남고 100ms씩 멈춥니다.
문법
const cachedValue = useMemo(calculateValue, dependencies);
const visible = useMemo(() => filterTodos(todos, tab), [todos, tab]);
calculateValue는 캐시할 값을 반환하는 인자 없는 함수입니다. React는 렌더링 중에 이것을 호출합니다.dependencies는 함수가 읽는 컴포넌트의 모든 값 목록입니다. props, state, 컴포넌트 본문에서 선언한 변수가 들어갑니다.- React는 렌더링마다
Object.is로 각 의존성을 이전 렌더링의 값과 비교합니다. 모두 같으면 함수를 호출하지 않고 캐시된 값을 반환합니다. 하나라도 다르면 함수를 호출하고 새 결과를 캐시합니다.
useMemo는 훅이므로 훅의 규칙을 따릅니다. 반복문이나 조건문 안이 아니라 컴포넌트의 최상위에서 호출하세요.
의존성은 빠짐없이 넣어야 합니다
배열에서 값을 빼면 캐시가 그 값의 이전 값으로 계산한 결과를 계속 내놓습니다. 여기서 total은 qty를 읽지만 price만 나열합니다.
Quantity를 클릭하면 2가 되지만, total은 10에 머물고 아무것도 기록되지 않습니다. Price를 클릭하면 그제야 계산이 다시 실행되어 현재의 qty를 읽으므로 total이 22로 뜁니다. 배열에 qty를 추가하면 두 버튼 모두 total을 업데이트합니다. 실제 프로젝트에서는 react-hooks/exhaustive-deps 린트 규칙이 이 실수를 잡아 줍니다.
반대 실수는 컴포넌트 본문에서 만든 객체처럼 렌더링마다 새로운 의존성입니다. 이전 값과 절대 일치하지 않으므로 캐시가 쓰이지 않습니다.
const filters = { tab, query }; // a new object every render
const visible = useMemo(() => applyFilters(todos, filters), [todos, filters]); // recomputes every time
// List the primitive values instead
const visible = useMemo(() => applyFilters(todos, { tab, query }), [todos, tab, query]);
memo 자식을 위해 객체를 안정적으로 유지하기
useMemo를 꺼내는 두 번째 이유는 느린 계산과 아무 관련이 없습니다. memo로 감싼 자식은 모든 prop이 지난번과 같을 때만 렌더링을 건너뜁니다. 컴포넌트 본문에 쓴 객체 리터럴은 렌더링마다 새 객체이므로, 내용이 바뀌지 않았어도 memo를 무력화합니다. useMemo는 입력이 바뀔 때까지 같은 객체를 유지합니다.
note 필드에 입력해도 Chart는 조용하고, Switch unit은 렌더링 한 번을 기록합니다. useMemo 호출을 일반 const options = { unit, max: unit === 'km' ? 100 : 62 };로 바꾸면 note에 입력할 때마다 render Chart가 기록됩니다.
객체에 의존하는 이펙트에도 같은 원리가 적용됩니다. 이펙트가 의존성에 options를 나열하고 options가 렌더링마다 다시 만들어진다면, 이펙트는 렌더링마다 실행됩니다. useMemo로 해결할 수 있지만, 더 간단한 해결책은 대개 이펙트 안에서 객체를 만들고 원시 값에 의존하는 것입니다.
useEffect(() => {
const options = { serverUrl, roomId };
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [serverUrl, roomId]);
useMemo를 쓰지 말아야 할 때
컴포넌트 안의 계산은 대부분 가볍습니다. 숫자 더하기, 문자열 두 개 합치기, 수십 개 항목 필터링 같은 것들입니다. 이런 것을 캐시해도 얻는 것은 거의 없습니다. React는 여전히 렌더링마다 값을 저장하고 의존성을 비교하며, 코드는 읽기 어려워집니다.
// No useMemo needed
const fullName = first + ' ' + last;
const doneCount = todos.filter((t) => t.done).length;
무언가가 느린지 알아보려면 시간을 재세요. 계산을 console.time('filter')와 console.timeEnd('filter')로 감싸고, 상호작용을 실행한 뒤 숫자를 보세요. 대략적인 기준으로, 내 기기에서 1밀리초 이상 걸리는 작업은 후보가 되며, 사용자의 기기는 더 느릴 수 있다는 점을 염두에 두세요. useMemo를 꺼내기 전에 그 작업이 렌더링에 속하는지도 확인하세요. 절대 바뀌지 않는 데이터는 컴포넌트 바깥에서 한 번만 계산할 수 있고, 페이지의 작은 부분 하나만 쓰는 state는 더 작은 컴포넌트로 옮길 수 있습니다(렌더링 참고).
파생 값에는 useEffect보다 useMemo
흔한 패턴은 이펙트에서 값을 계산해서 state에 저장하는 것입니다.
// Two renders per change, and the first one has stale data
const [visible, setVisible] = useState([]);
useEffect(() => {
setVisible(filterTodos(todos, tab));
}, [todos, tab]);
// One render, always in sync
const visible = useMemo(() => filterTodos(todos, tab), [todos, tab]);
이펙트 버전은 이전 visible로 한 번 렌더링하고, 이펙트를 실행하고, state를 설정하고, 다시 렌더링합니다. 렌더링이 한 번 더 들고, 첫 번째 렌더링에서는 목록이 탭과 맞지 않습니다. useMemo로(또는 작업이 가볍다면 훅 없이) 렌더링 중에 계산하면 같은 렌더링에서 올바른 값을 얻습니다. 이펙트는 React 바깥의 것과 동기화하기 위한 것이지, 한 state에서 다른 state를 파생시키기 위한 것이 아닙니다.
보장이 아니라 성능 힌트입니다
useMemo는 React에 결과를 재사용해도 된다고 알려 줄 뿐, 재사용하겠다고 약속하지는 않습니다. 평소에는 React가 캐시된 값을 유지하지만, 개발 중에 파일을 수정할 때나 컴포넌트가 첫 마운트 중에 일시 중단될 때처럼 버릴 수도 있습니다. 계산이 다시 실행되어도 올바르도록 코드를 작성하세요. 함수는 순수해야 하며, 요청, 구독, 바깥 변수 변경 같은 부수 효과가 없어야 합니다. 부수 효과는 이벤트 핸들러나 이펙트에 속합니다.
개발 환경의 StrictMode에서 React는 순수하지 않은 코드를 찾도록 돕기 위해 계산 함수를 두 번 호출합니다. 여기 미리보기는 프로덕션 빌드처럼 실행되므로 각 filtering for 로그는 한 번씩만 보입니다.
useMemo와 React Compiler
React Compiler 1.0은 컴포넌트를 분석해서 메모이제이션을 자동으로 추가하는 빌드 도구로, useMemo, useCallback, memo가 도움이 될 곳에서 계산과 JSX를 캐시합니다. 이것을 쓰는 프로젝트에서는 직접 작성할 일이 줄어듭니다. 직접 쓴 useMemo는 컴파일러와 함께 여전히 동작하고, 컴파일러를 설정하지 않았을 때는 여전히 쓰는 도구이며, 기존 React 코드 대부분에서 보게 됩니다.
자주 묻는 질문
React에서 useMemo는 무엇을 하나요?
첫 렌더링에서 계산 함수를 호출하고 결과를 저장한 뒤, 이후 렌더링에서는 의존성 배열의 모든 값이 지난번과 같은 한(Object.is로 비교) 저장된 결과를 반환합니다.
useMemo는 언제 써야 하나요?
계산이 눈에 띄게 느리고 입력이 거의 바뀌지 않을 때, 또는 memo 자식이나 이펙트가 의존하기 때문에 객체나 배열이 렌더링 사이에 같은 참조를 유지해야 할 때입니다. 가벼운 작업에는 쓰지 마세요.
useMemo와 useCallback의 차이는 무엇인가요?
useMemo는 함수가 반환하는 값을 캐시합니다. useCallback은 함수 자체를 캐시합니다. useCallback(fn, deps)는 useMemo(() => fn, deps)와 같습니다.
useMemo는 값이 절대 다시 계산되지 않는다고 보장하나요?
아니요. 성능을 위한 힌트일 뿐입니다. React는 개발 중에 파일을 수정할 때처럼 어떤 경우에는 캐시된 값을 버릴 수 있으므로, 계산이 다시 실행되어도 코드가 올바르게 동작해야 합니다.
React Compiler가 있어도 useMemo가 필요한가요?
빌드에서 React Compiler를 쓰면 메모이제이션을 자동으로 추가할 수 있습니다. 컴파일러가 없거나 컴파일러가 건너뛰는 경우에는 직접 쓴 useMemo가 여전히 똑같이 동작하며, 기존 코드에서도 여전히 흔합니다.