useMemo
는 리렌더링 사이에 계산 결과를 캐싱해주는 리액트 훅이에요.
const cachedValue = useMemo(calculateValue, dependencies)
Reference | 레퍼런스
useMemo(calculateValue, dependencies)
리렌더링 사이에 계산을 캐싱하고 싶다면 최상위 컴포넌트에서 useMemo
를 호출하세요.
import { useMemo } from 'react';
function TodoList({ todos, tab }) {
const visibleTodos = useMemo(
() => filterTodos(todos, tab),
[todos, tab]
);
// ...
}
Parameters | 파라미터(매개변수)
calculateValue
: 캐싱하고 싶은 값을 계산하는 함수. 순수함수여야하고, 인자는 없어야하며, 값을 반환해야한다. 리액트는 초기 렌더링 동안 함수를 호출할 거예요. 이후 렌더링에서, 리액트는dependencies
가 가장 마지막 렌더링 이후에 변경되지 않았다면 같은 값을 다시 반환해요. 만약dependencies
가 변경되었다면 리액트는calculateValue
를 호출하고 결과를 반환하고 이후에도 재사용될 수 있도록 그 값을 저장할 거예요.dependencies
:calculateValue
의 내부에서 참조하는 반응값의 목록. 반응값은 props, 상태, 그리고 컴포넌트 바디 내부에서 선언된 모든 변수와 함수를 포함해요. 만약 린터가 리액트에 맞게 구성되어 있다면, 모든 반응 값이 올바르게 의존성으로 지정되어 있는지를 확인해요. 의존성 목록은 일정한 숫자의 아이템을 가지고 있어야하고[dep1, dep2, dep3]
과 같이 인라인으로 작성해야해요. 리액트는 각 의존성을Object.js
의 비교 알고리즘을 사용하여 이전의 값과 비교해요.
Returns | 반환값
최초 렌더링에서 useMemo
는 호출한 인자가 없는 calculateValue
의 결과를 반환해요.
이후 렌더링 동안, useMemo
는 (의존성 배열에 변화가 없다면) 마지막 렌더링에서 이미 저장된 값을 반환하거나, calculateValue
를 다시 호출하고 calculateValue
가 반환한 값을 반환할 거예요.
Caveats | 주의사항
useMemo
는 훅이기 때문에 최상위 컴포넌트 또는 직접 만든 훅에서만 호출할 수 있어요. 반복문이나 조건문 안에서는 호출할 수 없어요. 만약 그럴 필요가 있다면, 컴포넌트를 추출하고 그 안으로 이펙트를 옮기세요.- 엄격한 모드(Strict Mode)에서 리액트는 의도하지 않은 불순물을 찾는 것을 돕기 위해 인자로 받은 계산함수를 두번 호출해요. 개발모드에서만 이렇게 동작하고 실제 프로덕션 환경에는 영향을 미치지 않아요. 만약 계산 함수가 순수함수라면 이 동작이 로직에 영향을 미치지 않을 거예요. 둘 중 하나의 결과값는 무시해요.
- 리액트는 캐싱된 값을 버려야하는 특별한 이유가 없는 한 캐싱된 값을 버리지 않아요. 예를 들어, 개발 과정에서 리액트는 컴포넌트의 파일을 편집할 때 캐시를 버려요. 개발 과정과 프로덕션 환경 모두에서 리액트는 최초로 마운트를 하는 동안 컴포넌트가 중단된다면 캐시를 버려요. 미래에 리액트는 캐시를 버려서 얻는 이점을 살리는 더 많은 기능을 추가할 거예요. 예를 들면, 만약 리액트가 언젠가 가상 리스트에 대한 내장 지원을 추가한다면, 가상 테이블의 뷰포트 밖으로 스크롤된 항목에 대해 캐시를 삭제하는 것이 합리적일 거예요. 만약 성능최적화 수단으로만
useMemo
에 의존한다면 괜찮을 거예요. 그렇지 않다면 상태 변수 또는 ref가 적절해요.
노트
이와 같이 반환 값을 캐싱하는 것은 메모이제이션이라고 알려져 있고 이 훅이 useMemo라고 불리는 이유에요.
Usage | 용법
Skipping expensive recalculations | 비용이 많이 드는 재연산 건너뛰기
리렌더링 사이에 계산을 캐싱하기 위해서는 최상위 컴포넌트에서 해당 연산을 useMemo
로 감싸세요.
import { useMemo } from 'react';
function TodoList({ todos, tab }) {
const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
// ...
}
useMemo
에는 두 개의 인자를 넘겨줘야해요.
() =>
와 같이 인자가 없고 계산하고 싶은 것을 반환하는 계산 함수- 계산 함수 내부에서 사용하는 컴포넌트의 모든 변수를 포함하는 의존성 배열
최초 렌더링에서 useMemo
에서 얻은 값은 계산 함수를 호출한 결과값이에요.
그 이후의 모든 렌더링에서 리액트는 의존성 배열을 마지막 렌더링을 할 때 전달한 의존성 배열과 비교해요. 만약 (Object.js
로 비교해서) 의존성 중 어느 것도 바뀌지 않았다면 useMemo
는 이전에 이미 계산된 값을 반환해요. 그렇지 않다면 리액트는 계산함수를 재실행하고 새로운 값을 반환해요.
다른 말로, useMemo
는 의존성이 변화할 때까지 리렌더링 동안 계산 결과는 캐싱해요.
언제 유용한지 예시를 통해 확인해보아요.
기본적으로 리액트는 리렌더링이 될 때마다 컴포넌트 바디의 전체를 재실행해요. 예를 들어 아래의 TodoList
가 상태를 업데이트하거나 부모로부터 새로운 props를 받는다면 filterTodos
함수를 재실행해요.
function TodoList({ todos, tab, theme }) {
const visibleTodos = filterTodos(todos, tab);
}
일반적으로 대부분의 계산은 굉장히 빠르기 때문에 이는 문제가 되지 않아요. 그러나 만약 거대 배열을 필터링하거나 변환한다면, 혹은 비용이 많이 드는 연산을 실행한다면 데이터가 변하지 않았을 때 함수를 다시 실행하는 행위를 건너뛰고 싶을 거예요. todos
와 tab
이 마지막 렌더링동안 이전과 동일하다면 앞의 예시처럼 useMemo
로 연산함수를 감싸는 것은 이전에 이미 계산한 visibleTodos
를 재사용하게 해줘요.
이러한 형식의 캐싱을 메모이제이션이라고 불러요.
노트
성능최적화 수단으로만useMemo
에 의존하세요. 만약 코드가useMemo
없이 작동하지 않는다면 근본적인 문제를 찾고 그 문제점을 먼저 해결하세요. 그리고 나서 성능을 향상시키기 위해useMemo
를 추가하세요.
계산에 비용이 많이 드는지 어떻게 확인해요?
일반적으로 수천개가 넘는 객체를 만들거나 반복하지 않는 한 아마 고비용의 연산은 아니에요. 만약 조금 더 확신이 필요하다면 코드가 실행되는 시간을 측정하는 콘솔창의 로그를 추가해보세요.
console.time('filter array');
const visibleTodos = filterTodos(todos, tab);
console.timeEnd('filter array');
(인풋창에 입력하는 것과 같이) 측정할 상호작용을 수행하세요. filter array: 0.15ms
와 같은 로그를 콘솔창에서 볼 수 있을 거예요. 전체적으로 로그에 적힌 시간이 눈에 띄는 양(1ms
또는 그 이상)보다 크다면 연산을 메모이제이션하는 게 말이 돼요. 이제 상호작용의 로그로 찍힌 총 시간이 줄어들었는지 혹은 그렇지 않은지를 확인하기 위해 아래 실험처럼useMemo
안에 계산함수를 감싸보세요.
console.time('filter array');
const visibleTodos = useMemo(() => {
return filterTodos(todos, tab); // todos와 tab이 변경되지 않았다면 건너뛰어요.
}, [todos, tab]);
console.timeEnd('filter array');
useMemo
는 첫 번째 렌더링을 더 빠르게 만들어주진 않아요. 업데이트 과정에서 불필요한 작업을 건너 뛰게 도와줄 뿐이에요.
사용자의 기기보다 당신의 기기가 더 빠를 수 있다는 사실을 명심하세요. 그렇기 때문에 인위적으로 속도를 낮춘 환경에서 성능을 테스트 해보는 것이 좋아요. 이를 위해 크롬은 CPU 쓰로틀링 옵션을 제공하고 있어요.
또한 개발 환경에서 성능을 측정하는 것은 가장 정확한 결과를 제공하지 않는다는 것을 명심하세요. (에를 들어, 엄격한 모드가 켜져있을 때, 각 컴포넌트가 한번이 아니라 두번 렌더링 되는 것을 볼 수 있어요.) 가장 정확한 시간을 얻기 위해서는 프로덕션 환경을 위해 어플리케이션을 빌드하고 사용자가 사용하는 것과 같은 기기에서 테스트해보세요.
모든 곳에 useMemo를 사용하나요?
만약 당신의 앱이 이 사이트와 같은 앱이고 대부분의 상호작용이 (페이지나 섹션 전체를 대체하는 것과 같이) 간단하다면 메모이제이션은 보통 필요 없어요. 반면, 당신의 앱이 그림판과 같은 앱이고 대부분의 상호작용이 (도형을 움직이는 것과 같이) 세밀하다면 메모이제이션이 굉장히 유용하다는 것을 알게 될 거예요.
useMemo
로 최적화하는 것은 몇가지 상황에서만 가치가 있어요.
useMemo
내부에 넣은 계산이 눈에 띄게 느리고 의존성이 거의 변경되지 않을 때.- 해당 값을
memo
로 감싼 컴포넌트에 props로 내릴 때. 만약 값이 변하지 않았다면 리렌더링을 건너뛰고 싶어요. 메모이제이션은 의존성이 같지 않을 때만 컴포넌트를 리렌더링 시켜요. - 전달한 값이 나중에 훅의 의존성으로 사용될 때. 예를 들어, 다른
useMemo
계산 값이 이 값에 의존하고 있을 수도 있어요. 또는useEffect
에서 이 값이 의존할 수도 있고요.
다른 경우에는 useMemo
로 계산 함수를 감싸서 크게 이득이 되지 않아요. 물론 감싼다고해서 눈에 띄는 불이익이 있는 것은 아니기 때문에 어떤 팀은 각각의 경우에 대해 생각하지 않는 것을 택하고 가능한 한 많이 메모이제이션을 할 수도 있어요. 이 방법의 단점은 코드 가독성이 낮아진다는 점이에요. 또한 모든 메모이제이션이 효율적이진 않아요. "항상 새로운" 어떤 변수는 모든 컴포넌트의 메모이제이션을 깰 충분한 위력을 갖고 있어요.
실제로는 몇몇 원칙을 따르면 메모이제이션을 불필요하게 만들 수 있어요.
- 컴포넌트가 다른 컴포넌트를 시각적으로 감쌀 때, JSX를 자식으로 허용하세요. 이 방법에서 만약 감싸는 컴포넌트가 내부 상태를 업데이트할 때, 자식 컴포넌트는 리렌더링 할 필요가 없다고 리액트는 인지해요.
- 로컬 상태를 사용하고 필요한 것 이상으로 상태를 끌어올리지 마세요. 예를 들어, 폼이나 어떤 아이템이 트리의 최상단에서 호버되는지 또는 전역상태 라이브러리에서 호버되는지의 여부와 같이 일시적으로 사용하는 상태를 갖고있지 마세요.
- 리렌더링 로직을 순수한 상태로 유지하세요. 만약 컴포넌트를 리렌더링하는 것이 문제를 야기하거나 눈에 띄는 시각적 산출물을 만든다면, 컴포넌트에 버그가 있다는 의미에요! 메모이제이션을 추가하지 말고 버그를 수정하세요.
- 상태를 업데이트하는 불필요한 이펙트를 피하세요. 리액트 앱에서 일어나는 대부분의 성능문제는 컴포넌트의 무한 리렌더링을 유발하는 이펙트에서 생성된 업데이트 체인으로 인해 발생해요.
- 이펙트에서 불필요한 의존성을 제거하세요. 이를 테면, 메모이제이션 대신, 객체나 함수를 이팩트 안에 넣거나 컴포넌트 외부로 빼는 것이 더 간단해요.
만약 특정한 상호작용의 반응이 느리다고 느낀다면, 어떤 컴포넌트가 메모이제이션으로 가장 이익을 얻는지, 어디가 메모이제이션을 추가할 필요가 있는지를 확인하기 위해 리액트 개발자 도구 프로파일러를 사용하세요. 이러한 원칙은 컴포넌트를 더 쉽게 디버깅하고 이해할 수 있게 만들어주기 때문에 어떤 경우에서든 이 원칙을 따르는 게 좋아요. 장기적으로, 우리는 이 문제를 한 번에 해결하기 위해 자동적으로 세밀한 메모이제이션을 시행하는 것을 연구하고 있어요.
useMemo와 직접적으로 값을 계산하는 것의 차이 알아보기
1. useMemo
와 memo
로 재연산 넘어가기
이번 예시에서 filterTodos
의 구현은 인위적으로 속도를 늦춰서 렌더링 동안 호출한 JavaScript 함수가 굉장히 느려졌을 때 어떤 일이 일어나는지 볼 수 있어요. 탭을 바꾸고 테마를 토글링해보세요.
filterTodos
가 재실행되는 속도를 강제로 낮췄기 때문에 탭을 변경하는 것은 느리다고 느껴져요. tab
이 변경변경되면 전체 계산이 재실행될 필요가 있기 때문에 이는 예상된 결과에요. (만약 왜 두번 실행되는지 궁금하다면, 이곳에 설명이 되어있어요.)
테마를 토글링해보세요. useMemo
덕분에 인위적으로 속도를 늦췄음에도 불구하고 속도가 빨라요! (useMemo
에 의존성으로 전달한) todos
와 tab
가 모두 이전 렌더링 상태에서 변경되지 않았기 때문에 느린 filterTodos
호출을 건너뛰었어요.
2. 항상 값을 재연산하기
이번 예시에서 filterTodos
의 구현은 이전과 같이 인위적으로 속도를 늦춰서 렌더링 동안 호출한 JavaScript 함수가 굉장히 느려졌을 때 어떤 일이 일어나는지 볼 수 있어요. 탭을 바꾸고 테마를 토글링해보세요.
이전의 예시와는 달리, 이번엔 테마를 바꾸는 것도 느려요! 그 이유는, 이번 버전에는 useMemo
호출이 없어서 인위적으로 느려진 filterTodos
를 모든 리렌더링마다 호출하기 때문이에요. 심지어는 theme
만 변경되었을 때도 호출돼요.
하지만 이번엔 인위적으로 속도를 늦추는 코드가 제거된 같은 코드가 있어요. useMemo
가 없는 것이 직접적으로 느껴지나요?
메모이제이션이 없는 코드는 생각보다 자주 잘 작동해요. 만약 상호작용의 속도가 충분히 빠르다면 메모이제이션이 필요하지 않을 거예요.
utils.js
에서 할일(todos)의 아이템을 더 늘릴수 있고 작동이 어떻게 바뀌는지 확인할 수 있어요. 이는 특정 계산이 시작할 때는 비용이 많이 들지 않았지만, 할일(todos)의 수가 굉장히 많아지면 대부분의 오버헤드는 필터링이 아니라 리렌더링에서 발생할 거예요. useMemo
를 사용하여 리렌더링을 최적화는 방법을 아래를 계속 읽으면서 확인하세요.
Skipping re-rendering of components | 컴포넌트 리렌더링 건너뛰기
어떤 경우에는 useMemo
가 자식 컴포넌트의 리렌더링 성능 최적화를 도와줄 수 있어요. 이를 설명하기 위해 이 TodoList
컴포넌트가 visibleTodos
를 props로 자식인 List
컴포넌트에 전달한다고 생각해보세요.
export default function TodoList({ todos, tab, theme }) {
// ...
return (
<div className={theme}>
<List items={visibleTodos} />
</div>
);
}
theme
prop을 토글링하면 잠시동안 앱이 멈추지만 <List />
를 JSX에서 지우면 빨라진다는 것을 알 수 있어요. 이는 즉, List
컴포넌트를 최적화할 가치가 있다는 것을 말해줘요.
기본적으로, 컴포넌트가 리렌더링되면 리액트는 재귀적으로 모든 자식 컴포넌트를 리렌더링해요. 왜냐하면 TodoList
가 다른 theme
으로 리렌더링될 때, List
컴포넌트도 리렌더링되기 때문이에요. 리렌더링할 때 많은 연산을 필요로하지 않는 컴포넌트에서는 별 문제가 되지 않아요. 하지만 리렌더링이 느려진다는 것을 확인했다면 memo
로 그것을 감싸서 마지막 렌더링과 props가 똑같다면 List
가 리렌더링을 건너뛰게 만드세요.
import { memo } from 'react';
const List = memo(function List({ items }) {
// ...
});
이렇게 바꾸면 List
는 이전 렌더링과 props가 동일할 때는 리렌더링을 건너뛰어요. 여기서 계산을 캐싱하는 것이 중요해요! useMemo
없이visibleTodos
를 계산한다고 생각해보세요.
export default function TodoList({ todos, tab, theme }) {
// 테마가 바뀔 때마다 다른 배열이 될 거예요
const visibleTodos = filterTodos(todos, tab);
return (
<div className={theme}>
{/* 따라서 List의 props는 절대 같을 수 없고 계속 리렌더링 될 거예요. */}
<List items={visibleTodos} />
</div>
);
}
{}
객체 리터럴이 항상 새로운 객체를 생성하는 것과 비슷하게 위의 예시에서 filterTodos
함수는 항상 다른 배열을 만들어요. 보통은 문제를 일으키진 않지만 List
props가 절대 같을 수 없고 memo
최적화가 동작하지 않는다는 것을 의미해요. 이는 useMemo
가 들어오면 간편해져요.
export default function TodoList({ todos, tab, theme }) {
// 리액트가 리렌더링 중에 계산을 캐싱하도록 말해주세요
const visibleTodos = useMemo(
() => filterTodos(todos, tab),
[todos, tab] // 그러면 이 의존성 배열이 변경되지 않는 한 ...
);
return (
<div className={theme}>
{/* ... List는 같은 props를 받고 리렌더링을 건너뛸 수 있어요 */}
<List items={visibleTodos} />
</div>
);
}
visibleTodos
계산을 useMemo
에서 래핑하면 리렌더링 사이에 같은 값을 가진다는 것을 보장해요. 특별한 이유가 있어서 필요치 않는 한, useMemo
안에 계산 함수를 넣을 필요가 없어요. 이 예시에서는 해당 값이 memo
안에 감싸진 컴포넌트로 전달하기 때문에 리렌더링을 건너뛰게 만들어줘요. 이 페이지에서 앞으로 더 설명할 useMemo
를 추가하는 다른 몇가지 이유가 있어요.
개별 JSX 노드를 메모이제이션하기
List
를 memo
로 감싸는 대신 <List />
라는 JSX 노드 자체를 useMemo
안에 넣을 수 있어요.
export default function TodoList({ todos, tab, theme }) {
const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
const children = useMemo(() => <List items={visibleTodos} />, [visibleTodos]);
return (
<div className={theme}>
{children}
</div>
);
}
동작은 동일해요. visibleTodos
가 바뀌지 않았다면, List
는 리렌더링 되지 않아요.
<List items={visibleTodos} />
와 같은 JSX 노드는 { type: List, props: { items: visibleTodos } }
와 같은 객체에요. 이 객체를 생성하는 것은 굉장히 저렴하지만 리액트는 이 콘텐츠가 이전의 것과 동일한지 알지 못해요. 기본적으로 리액트는 List
컴포넌트를 리렌더링하기 때문이죠.
하지만 만약 리액트가 이전 렌더링과 정확이 동일한 JSX를 본다면 컴포넌트를 리렌더링하지 않을 거예요. 왜냐하면 JSX 노드는 불변값이기 때문이에요. JSX 노드 객체는 시간이 지나도 바뀌지 않아요. 그래서 리액트는 리렌더링을 건너뛰어도 안전하다는 것을 알 수 있어요. 그러나 이렇게 동작하기 위해서는 노드가 그저 코드에서만 똑같이 보이는 것이 아니라 _실제로 같은 객체_여야만해요. 이 예시에서는 useMemo
가 하는 일이에요.
JSX 노드를 useMemo
로 직접 감싸는 것은 편리하지 않아요. 예를 들면 조건적으로 이 작업을 할 수는 없어요. 그렇기 때문에 보통은 JSX 노드 대신 컴포넌트를 memo
로 감싸요.
리렌더링을 건너뛰는 것과 항상 리렌더링 하는 것의 차이
1. useMemo
와 memo
로 리렌더링 건너뛰기
이번 예시에서 List
컴포넌트는 인위적으로 속도를 늦춰서 렌더링하는 리액트 컴포넌트가 실제로 굉장히 느려졌을 때 무슨 일이 일어나는지 볼 수 있어요. 탭을 바꿔보고 테마를 토글링해보세요.
탭을 바꾸는 것은 List
를 리렌더링 하는데 일부러 속도를 늦췄기 때문에 느리게 느껴져요. tab
을 변경하면 유저의 새로운 선택을 스크린에 반영할 필요가 있기 때문에 이는 예상이 가능해요.
그 다음, 테마를 바꿔보세요. useMemo
와 memo
덕분에, 인위적으로 속도를 늦췄음에도 빨라요! visibleTodos
배열이 이전 렌더링에서 변경되지 않았기 때문에 List
는 리렌더링을 건너뛰어요. (useMemo
에 의존성으로 전달된) todos
와 tab
이 모두 이전 렌더링에서 변경되지 않았기 때문에 visibleTodos
배열은 변하지 않아요.
2. 항상 리렌더링 하기
이번 예시에서 List
의 구현은 이전과 같이 인위적으로 속도를 늦춰서 렌더링하는 리액트 컴포넌트가 실제로 굉장히 느려졌을 때 무슨 일이 일어나는지 볼 수 있어요. 탭을 바꿔보고 테마를 토글링해보세요.
이전 예시와는 달리 이번엔 테마를 바꾸는 것도 느려요! 왜냐하면 useMemo
호출이 이번에는 없기 때문이에요. 그래서 visibleTodos
는 항상 다른 배열이고 느려진 List
컴포넌트는 리렌더링을 건너뛸 수 없어요.
하지만 이번엔 인위적으로 속도를 늦추는 코드가 제거된 같은 코드가 있어요. useMemo
가 없는 것이 직접적으로 느껴지나요?
메모이제이션이 없는 코드는 생각보다 자주 잘 작동해요. 만약 상호작용의 속도가 충분히 빠르다면 메모이제이션이 필요하지 않을 거예요.
리액트 개발자 도구를 사용할 수 없는 프로덕션 모드에서 리액트를 동작할 필요가 있고 정말 앱을 느리게 만드는 것이 무엇인지를 실질적으로 알 수 있도록 사용자가 가지고 있는 기기와 동일한 기기를 사용해야한다는 사실을 명심하세요.
Memoizing a dependency of another Hook | 다른 훅의 의존성 메모이제이션 하기
컴포넌트 바디에서 직접적으로 생성된 객체가 의존하는 연산이 있다고 가정할게요.
function Dropdown({ allItems, text }) {
const searchOptions = { matchMode: 'whole-word', text };
const visibleItems = useMemo(() => {
return searchItems(allItems, searchOptions);
}, [allItems, searchOptions]); // 🚩 경고: 컴포넌트 바디에서 생성된 객체에 대한 종속성
// ...
이와 같은 객제에 의존하는 것은 메모이제이션을 굴복시켜요. 컴포넌트가 리렌더링될 때, 컴포넌트 바디 내부의 모든 코드는 다시 실행돼요. searchOptions
객체를 생성한 코드도 모든 리렌더링마다 실행돼요. searchOptions
가 useMemo
호출의 의존성이고 매번 달라지기 때문에 리액트는 의존성이 달라졌다고 생각하여 리렌더링마다 searchItems
를 다시 계산해요.
이를 해결하기 위해 의존성으로 넘기기 전에 searchOptions
객체 자체를 메모이제이션할 수 있어요.
function Dropdown({ allItems, text }) {
const searchOptions = useMemo(() => {
return { matchMode: 'whole-word', text };
}, [text]); // ✅ text가 변경될 때만 바뀌어요.
const visibleItems = useMemo(() => {
return searchItems(allItems, searchOptions);
}, [allItems, searchOptions]); // ✅ allItems나 searchOptions가 변경될 때만 바뀌어요.
// ...
위의 예시에서 text
가 바뀌지 않는다면 searchOptions
객체도 변하지 않아요. 그러나 searchOptions
객체 선언을 useMemo
의 계산 함수 내부에서하는 것이 더 나은 수정방법이에요.
function Dropdown({ allItems, text }) {
const visibleItems = useMemo(() => {
const searchOptions = { matchMode: 'whole-word', text };
return searchItems(allItems, searchOptions);
}, [allItems, text]); // ✅ allItems나 text가 변경될 때만 바뀌어요.
// ...
이제 계산함수가 직접적으로 의존하는 의존성은 (문자열 타입이고 "실수로" 달라질 수 없는) text
뿐이에요.
Memoizing a function | 함수를 메모이제이션하기
Form
컴포넌트가 memo
로 감싸진다고 가정할게요. 여기에 prop으로 함수를 넘기고 싶어요.
export default function ProductPage({ productId, referrer }) {
function handleSubmit(orderDetails) {
post('/product/' + productId + '/buy', {
referrer,
orderDetails
});
}
return <Form onSubmit={handleSubmit} />;
}
{}
가 다른 객체를 생성하는 것처럼 function() {}
와 같은 함수 선언식과 () => {}
과 같은 함수 표현식은 다른 함수를 매 리렌더링마다 생산해요. 새로운 함수를 생성하는 것자체로는 문제가 되지 않아요. 이건 피해야할 것이 아니에요! 하지만 만약 Form
컴포넌트가 메모이제이션 된다면 아마도 props가 바뀌지 않았다면 이 컴포넌트를 리렌더링하는 것을 건너뛰고 싶을 거예요. 항상 변하는 prop은 메모이제이션의 포인트를 파괴해요.
useMemo
로 함수를 메모이제이션하기 위해서는 계산함수는 다른 함수를 반환해야해요.
export default function Page({ productId, referrer }) {
const handleSubmit = useMemo(() => {
return (orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails
});
};
}, [productId, referrer]);
return <Form onSubmit={handleSubmit} />;
}
이건 조금 어색해요! 함수를 메모이제이션하는 것은 너무 흔해서 리액트가 특별한 내장 훅을 가지고 있어요. useMemo
대신 useCallback
으로 함수를 감싸세요. 추가적인 중첩함수가 작성되는 것을 피할 수 있어요.
export default function Page({ productId, referrer }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails
});
}, [productId, referrer]);
return <Form onSubmit={handleSubmit} />;
}
위의 두 예시는 완전히 같아요. useCallback
의 유일한 장점은 내부에 불필요한 중첩함수를 작성하는 것을 막아준다는 것이에요. 다른 것을 하진 않아요. useCallback
에 대해 더 알아보세요.
Troubleshooing | 트러블슈팅
My calculation runs twice on every re-render | 내 계산함수는 리렌더링마다 두번씩 실행돼요.
엄격한 모드에서 리액트는 어떤 함수를 두번씩 실행해요.
function TodoList({ todos, tab }) {
// 이 컴포넌트는 렌더링될 때마다 두 번 실행돼요.
const visibleTodos = useMemo(() => {
// 이 계산은 의존성이 바뀔 때마다 두 번 실행돼요.
return filterTodos(todos, tab);
}, [todos, tab]);
// ...
이는 코드가 깨진 것이 아니라 예상가능한 상황이에요.
이 개발 모드에서만 실행되는 동작은 컴포넌트를 순수하게 만들어줘요. 리액트는 이 호출 중 하나의 결과만을 사용하고 다른 호출의 결과는 무시해요. 컴포넌트와 계산 함수가 순수한 한, 이 실행이 로직에 영향을 미치지 않아요. 하지만 의도치 않게 이들이 순수하지 않다면 이 실행이 실수를 알아차리고 고칠 수 있게 도와줘요.
예를 들어, 이 순수하지 않은 계산함수는 prop으로 받은 배열을 변경해요.
const visibleTodos = useMemo(() => {
// 🚩 실수 : prop 변경하기
todos.push({ id: 'last', text: 'Go for a walk!' });
const filtered = filterTodos(todos, tab);
return filtered;
}, [todos, tab]);
리액트는 이 함수를 두번 실행하여 todo가 두 번 추가된다는 사실을 알아차릴 수 있어요. 이 계산함수는 어떤 존재하는 객체도 바꾸면 안되지만 계산 중에 생성한 새로운 객체를 변경하는 것은 괜찮아요. 예를 들어 만약 filterTodos
함수가 항상 다른 배열을 반환한다면, 그 배열을 대신 변경할 수 있어요.
const visibleTodos = useMemo(() => {
const filtered = filterTodos(todos, tab);
// ✅ 이상 없음 : 계산 중에 새로 생성한 객체를 변경하기
filtered.push({ id: 'last', text: 'Go for a walk!' });
return filtered;
}, [todos, tab]);
순수성에 대해 더 알고 싶다면 컴포넌트를 순수하게 유지하기를 읽어보세요.
또한 변경 없이 객체 업데이트하기와 배열 업데이트하기에 대한 가이드를 확인하세요.
My useMemo
call is supposed to return an object, but returns undefined | useMemo
호출이 객체를 반환할 줄 알았는데 undefined를 반환해요.
이 코드는 동작하지 않아요.
// 🔴 () => { 로 같은 화살표함수에서 객체를 반환할 수 없어요.
const searchOptions = useMemo(() => {
matchMode: 'whole-word',
text: text
}, [text]);
자바 스크립트에서 () => {
는 화살표 함수의 바디를 시작하기 때문에 {
중괄호는 객체의 일부가 아니에요. 따라서 객체를 반환하지 않고 실수를 만들죠. ({
와 })
같은 괄호를 추가하여 이 문제를 해결하세요.
// 동작은 하지만 누군가 다시 이 패턴을 깨기가 쉬워요.
const searchOptions = useMemo(() => ({
matchMode: 'whole-word',
text: text
}), [text]);
하지만 이는 여전히 혼란스럽고 누군가 괄호를 지워서 패턴을 깨버리는 것이 쉬워요.
실수를 방지하기 위해 return
구문을 명시적으로 작성하세요.
// ✅ This works and is explicit
const searchOptions = useMemo(() => {
return {
matchMode: 'whole-word',
text: text
};
}, [text]);
Every time my component renders, the calculation in useMemo re-runs | 컴포넌트가 렌더링될 때마다, useMemo 내부의 계산함수를 재실행해요.
의존성 배열을 두 번째 인자로 지정하는 것을 잊지 마세요!
만약 의존성 배열을 잊어버렸다면 useMemo
는 계산함수를 렌더링 할 때마다 실행할 거예요.
function TodoList({ todos, tab }) {
// 🔴 의존성 배열이 없기 때문에 렌더링 때마다 재계산해요
const visibleTodos = useMemo(() => filterTodos(todos, tab));
// ...
아래 예시는 두 번째 인자로 의존성 배열을 전달한 올바른 코드에요.
function TodoList({ todos, tab }) {
// ✅ 불필요한 재실행을 하지 않아요.
const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
// ...
만약 이 해결책이 도움이 되지 않았다면 적어도 하나의 의존성이 이전 렌더링과 달라진 것이 문제예요. 콘솔에 의존성 로그를 수동으로 찍어보면 이 문제를 디버깅할 수 있어요.
const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
console.log([todos, tab]);
그 다음에 콘솔에서 다른 리렌더링에서 나온 서로 다른 배열을 오른쪽 마우스 클릭을 하고 " 전역 변수로 저장"을 두 배열 모두에서 선택하세요. 첫 번째는 temp1로 저장되고 두번째는 temp2로 저장된다고 가정하면 브라우저 콘솔에서 각 배열에 안에 들어간 의존성이 동일한지를 확인할 수 있어요.
Object.is(temp1[0], temp2[0]); // 첫 번째 배열이 배열 간에 동일한가요?
Object.is(temp1[1], temp2[1]); // 두 번째 배열이 배열 간에 동일한가요?
Object.is(temp1[2], temp2[2]); // ... 그리고 모든 의존성에서도 동일한지 확인하기 ...
어떤 의존성이 메모이제이션을 깨뜨리는지 찾았다면 그 종속성을 지우거나 메모이제이션할 방법을 찾으세요.
I need to call useMemo for each list item in a loop, but it’s not allowed | 반복문에서 각 아이템에 대한 useMemo를 호출해야하는데 불가능해요.
Chart
컴포넌트를 memo
로 감싸고 있어요. ReportList
컴포넌트가 리렌더링하면 목록 안에 있는 모든 Chart
의 리렌더링은 건너뛰고 싶어요. 그러나 반복문에서는 useMemo
를 호출할 수 없어요.
function ReportList({ items }) {
return (
<article>
{items.map(item => {
// 🔴 아래와 같은 반복문 안에서 useMemo를 호출할 수 없어요:
const data = useMemo(() => calculateReport(item), [item]);
return (
<figure key={item.id}>
<Chart data={data} />
</figure>
);
})}
</article>
);
}
대신, 각 아이템을 컴포넌트로 추출하고 개별 아이템의 데이터를 메모이제이션 하세요.
function ReportList({ items }) {
return (
<article>
{items.map(item =>
<Report key={item.id} item={item} />
)}
</article>
);
}
function Report({ item }) {
// ✅ 최상위에서 useMemo를 호출하세요:
const data = useMemo(() => calculateReport(item), [item]);
return (
<figure>
<Chart data={data} />
</figure>
);
}
다른 방법으로는 useMemo
를 지우고 memo
로 Report
자체를 감싸세요. 만약 item
prop이 변하지 않았다면, Report
는 리렌더링을 건너뛸 것이고 Chart
도 리렌더링을 건너뛸 거예요.
function ReportList({ items }) {
// ...
}
const Report = memo(function Report({ item }) {
const data = calculateReport(item);
return (
<figure>
<Chart data={data} />
</figure>
);
});
'리액트 공식문서 | React Docs > Reference > react@18.2.0' 카테고리의 다른 글
[Hooks] useReducer | useReducer 훅 (1) | 2024.02.04 |
---|---|
[Hooks] useOptimistic | useOptimistic 훅 (0) | 2024.02.03 |
[Hooks] useLayoutEffect | useLayoutEffect 훅 (1) | 2024.01.25 |
[Hooks] useInsertionEffect | useInsertionEffect 훅 (0) | 2024.01.25 |
[Hooks] useImperativeHandle | useImperativeHandle훅 (1) | 2024.01.21 |