들어가기전 최적화에 관하여
- 리액트에는 여러 최적화 기법들이 있습니다.
- React.memo, useMemo, useCallback
- 이런 기법들은 처음 보았을 때 마치 개발의 은탄환처럼 사용하면 리렌더링을 줄여 막연히 최적화가 잘 될것 이라는 착각에 빠지게 하고
- 그래서 여러 함수들, 컴포넌트에 무턱대고 남발하기 쉽습니다.
- 하지만 특정한 상황이 아니면 오히려 이런 기법들을 사용하는 게 성능 최적화에는 독이 될 수 있습니다.
- 그 이유는 리액트에서 함수나 컴포넌트를 렌더링할때 일반 요소들에 비해 이런 최적화 기법을 확인하는 비용이 들기 때문입니다.
- 즉, 이 기법들이 올바르게 사용되어지지 않는다면 괜히 최적화는 되지도 않고 거기에 더해 최적화 확인 비용이 추가되는 경우가 생길 수 있습니다.
- 따라서 이번 기회에 올바른 최적화 방법을 정리해보고자 합니다.
Memoization이란?
메모이제이션은 비용이 많이 드는 함수 호출의 결과를 저장하고 동일한 입력이 다시 발생할 때 캐시된 결과를 반환하여 컴퓨터 프로그램의 속도를 높이는데 주로 사용되는 최적화 기술입니다.
React.memo란?
- React.memo는 구성 요소의 렌더링 결과를 메모하여 성능 최적화에 사용되는 React JavaScript 라이브러리의 고차 구성 요소(HOC)입니다.
- React 컴포넌트의 메모화된 버전을 만들 수 있습니다.
- 컴포넌트가 props가 변경된 경우에만 다시 렌더링됩니다.
다른예시
- react에서는 먼저 컴포넌트를 랜더링 한 뒤, 이전 랜더링 된 결과와 비교하여 랜더링 결과가 이전과 다르다면,
DOM
을 업데이트 한다.
React.memo()
로 컴포넌트를 래핑하게 되면, React
는 컴포넌트를 랜더링 하고,그 결과를 메모이징(Memoizing) 한다.
- 그 뒤, 다음 랜더링이 일어났을 때 해당 컴포넌트의 props가 같다면,React는 메모이징 된 내용을 재사용한다.
React.memo는 왜 사용하는가?
- react에서는 부모 컴포넌트가 리렌더링되면 자식 또한 리렌더링됩니다.
- 이때 부모 컴포넌트 A에 자식 컴포넌트 B, C, D가 있고
- 이 중 D에 CARD들이 무수히 많다면 그 CARD들 또한 전부 리렌더링되게 됩니다.
- 이 경우 D컴포넌트를 React.memo로 묶게 되면 A가 리렌더링되어도 D는 리렌더링하지 않게되고 CARD들 또한 불필요한 리렌더링을 하지 않게 됩니다.
- A에서 D로 props를 전달한다면 이 props를 비교하여 다를 경우에만 D를 리렌더링합니다.
- 단! 이때 A에서 D로 전달하는 props가 복잡한 객체인 경우 React.memo는 props를 얕은 비교하여 다른 경우에만 D를 리렌더링합니다.
- 얕은비교란 객체의 주소값을 비교한다는 의미입니다.
- 만약 A컴포넌트에서 함수나 객체를 그대로 props로 전달한다면 D컴포넌트는 리렌더링 됩니다.
- 왜냐면 A컴포넌트에서의 함수와 객체는 리렌더링되면 새로운 주소값을 가지게 되기 때문입니다.
- 과거의 A컴포넌트에서의 객체 ≠ 리렌더링 이후 A컴포넌트에서의 객체
- 겉보기에 완전히 그 값들이 같다고 하여도!!!
- 따라서 props로 객체를 전달하기보다 string으로 그 값을 전달하는 것이 좋습니다.
- 그렇다면 함수는? 아래 useCallback에서 다루겠습니다.
React.memo는 어떻게 사용하는가?
- 일반함수일때와 화살표 함수일때가 다릅니다.
- 일반함수의 경우
import React from 'react';
const MyComponent = ({ prop1, prop2 }) => {
};
const MemoizedComponent = React.memo(MyComponent);
export default MemoizedComponent;
- 화살표 함수의 경우
import React from 'react';
const MyComponent = React.memo(({ prop1, prop2 }) => {
});
export default MyComponent;
useMemo란?
- 리액트가 동일한 계산을 반복해야 할 때, 이전에 계산한 값을 메모리에 저장함으로써 동일한 계산의 반복 수행을 제거하여 프로그램 실행 속도를 빠르게 하는 기술입니다.
- 동일한 값을 반환하는 함수를 반복적으로 호출해야한다면 처음 값을 계산할 때 해당 값을 메모리에 저장해 필요할 때마다 다시 계산하지 않고 메모리에서 꺼내서 재사용하는 것입니다.
useMemo()
를 사용하면 연산된 결과 값을 캐싱해주기 때문에, 매렌더링마다 고비용 연산이 일어나는 것을 방지할 수 있습니다.
useMemo()
는 연산된 결과값이 바뀌지 않는다면 재연산이 일어나지 않기 때문입니다.
useMemo는 왜 사용하는가?
- 고비용 연산을 방지하기 위해 사용합니다.
- 하지만 여기에는 큰 함정이 있는데 바로 메모할 만큼 어려운 연산이 아닌경우 오히려 제거함으로써 파일에서 약간의 공간을 절약할 수 있다는 점입니다.
- 그럼 올바르게 사용하는 경우는 언제일까요?
- React의 공식문서를 확장하면, 병목현상을 만드는 고비용 연산이란 하위 렌더트리를 렌더링하는 것을 말하며 React가 의도한 useMemo는 render tree 내부의 특정 부분을 메모할 때 사용하는 것입니다.
- (물론 2^n, n!과 같은 계산을 메모할때 사용하는 것도 맞습니다.)
useMemo는 어떻게 사용하는가?
일반적인 예시
import React, { useMemo } from 'react';
const MyComponent = ({ data }) => {
const result = useMemo(() => {
return computedResult;
}, [data]);
return (
<div>
<p>Result: {result}</p>
{}
</div>
);
};
export default MyComponent;
- 의존성 배열 data가 바뀔때만 함수가 실행됩니다.
- 즉, 이 MyComponent가 다른 이유로 리렌더링되어도 data가 바뀌지 않으면 result함수는 새로 실행되지 않습니다.
하위 렌더트리를 렌더링하는 예시
function List({ countries }) {
const sortedCountries = countries.sort()
return (
<>
{sortedCountries.map((country) => (
<Item country={country} key={country.id} />))}
</>);
}
- List라는 컴포넌트는 props로 countries를 받고 여기에는 250개의 나라가 담겨있습니다.
- React에서 무거운 계산은 컴포넌트를 리렌더링하고 업데이트하는 계산입니다.
- 즉 위에서 표시한대로 주목해야할 부분은 컴포넌트를 다시 그리는 부분입니다.
function List({ countries }) {
const content = useMemo(() => {
const sortedCountries = countries.sort()
return sortedCountries.map((country) => <Item country={country} key={country.id} />);
}, [countries])
return content
}
useCallback이란?
useCallback
은 불필요하게 다시 렌더링하지 않고 자식 구성 요소에 소품으로 전달할 수 있도록 함수를 메모화하는 데 사용됩니다.
- 즉, useMemo()와 그 메커니즘은 같지만
useMemo()
가 연산된 “값” 을 캐싱했다면, useCallback()
은 값이 아닌 “함수 그 자체”를 캐싱합니다.
useCallback은 왜 사용하는가?
- 위의 React.memo에서 봤듯이 props로 함수를 전달한다면 부모 컴포넌트의 리렌더링시 자식 또한 리렌더링 되고 이때 React.memo를 쓰면 객체나 함수를 제외하고는 props가 같은 경우 리렌더링을 안하게 할 수 있습니다.
- 객체의 경우는 위에서 가능한 string값만 전달해서 그 값이 필요한 당사자인 컴포넌트에서 직접 값을 가공하는 것이 좋다고 했습니다.
- 그리고 함수의 경우 useCallback을 사용합니다.
- useCallback으로 함수를 묶으면 이 함수는 의존성배열이 변하지 않는 한 리렌더링시 주소가 변하지 않게 됩니다.
- 따라서 자식 컴포넌트로 해당 함수를 props로 전달해주어도 자식에서는 부모의 리렌더링시 같이 리렌더링 하지 않습니다.
useCallback은 어떻게 사용하는가?
const Page = () => <Item />;
const PageMemoized = React.memo(Page);
const App = () => {
const [state, setState] = useState(1);
return (
...
<PageMemoized />);
};
- useCallback을 안쓰면 계속 리렌더링 되지만
const PageMemoized = React.memo(Page);
const App = () => {
const [state, setState] = useState(1);
const onClick = () => {
console.log('Do something on click');
};
return (
...
<PageMemoized onClick={onClick} />);
};
- useCallback을 쓰면 리렌더링을 막습니다.
const PageMemoized = React.memo(Page);
const App = () => {
const [state, setState] = useState(1);
const onClick = useCallback(() => {
console.log('Do something on click');
}, []);
return (
<PageMemoized onClick={onClick} />
);
};
끝으로
(출처: https://velog.io/@hyunjine/React-Rendering-Optimization)
- 대부분의 경우에 useMemo와 useCallback을 제거해야합니다.
- 수백개의 컴포넌트가 존재하는 앱이 있다고 해보겠습니다. 글에서 설명한바와 같이 useMemo와 useCallback은 첫 렌더링 때 React가 그 값을 캐시해두어야합니다. 이것은 시간이드는 작업이죠. 100개의 컴포넌트를 성능개선하겠다고 useMemo, useCallback으로 도배를 해놓았다면 어떨까요? 1ms, 2ms,... 100ms 점점 늘어납니다.
- 반면에 리렌더링은 어떨까요? 애플리케이션을 잘 설계하면 리렌더링은 특정 부분에서만 일어납니다. 특정 부분에서만 일어나는 리렌더링에서 만들어야할 함수와 값은 개수가 적습니다. 개수가 적을수록 함수를 만드는 연산과 참조 동일성을 체크하는 연산 자체를 비교하는 것 자체가 무의미해지게 됩니다.
- 이렇기 때문에 초기렌더링에 유리하도록 필요없는 useMemo와 useCallback을 없애고 리렌더링은 변경되어야하는 부분만 일어나게 만들어서 애플리케이션을 최적화할 수 있습니다.
- 사람들은 성능 개선에는 trade-off가 있다고 많이 말합니다. React에서 성능을 개선하는 것은 useMemo와 useCallback으로 애플리케이션 내부에 모든 함수와 값들을 감싸는 것이 아닙니다. 이렇게 하면 컴퓨터 자원만 의미 없이 사용하게되고 심지어 애플리케이션이 더 느려질 수 있습니다.(역효과)
- React가 의도한 성능 최적화는 memo, useMemo, useCallback을 사용해서 컴포넌트와 그 하위 컴포넌트들이 리렌더링되는 것을 막거나(memo), 컴포넌트 하위 트리를 메모이제이션해서 사용하거나(useMemo), memo로 감싸진 컴포넌트에 props를 전달할 때 값이 변하지 않도록 해주는 것(useCallback)을 말합니다.
- 단순히 값이나 함수를 메모이제이션하는 것은 성능에 미미한 영향을 끼치거나 오히려 애플리케이션이 더 느려질 수 있습니다. React에서 최적화란, 이런 Pure한 JavaScript를 최적화하는 것이아닌 보다 훨씬 오래걸리는 하위 트리의 렌더링을 막는 것입니다.
- 역설적으로, 성능에 대해 고민하는 것보다 애플리케이션이 렌더링되고 렌더링되지 않아야 할 부분을 잘 설계하는데 집중하는게 성능을 더 좋게 만들 수 있는 길입니다.
출처:
https://velog.io/@hyunjine/React-Rendering-Optimization
https://velog.io/@jinyoung985/React-useMemo란
https://ssdragon.tistory.com/106
https://leetrue-log.vercel.app/react-rendering
https://ryulog.tistory.com/164
https://velog.io/@integer/React.memo와-useMemo