React.memo를 이용해 B 컴포넌트의 성능 최적화하기
B 컴포넌트는 B, List, ListItem, message 컴포넌트로 나눠져 있음
input에 글을 타이핑 할 때 원래는 message 컴포넌트와 그 state를 갖고 있는 app 컴포넌트만 렌더링이 돼야 하는데 현재는 상관 없는 다른 부분들까지 모두 렌더링 되고 있음
React.memo 적용은 간단하게 원하는 컴포넌트를 React.memo로 감싸주면 됨
-> 렌더링이 필요치 않은 List, ListItem 컴포넌트는 렌더링이 안됨
-> B 컴포넌트의 성능이 확연히 좋아짐 (A: 1.5ms, B: 0.1ms)
React는 먼저 컴포넌트를 렌더링 한 뒤, 이전에 렌더링 된 결과와 비교하여 DOM 업데이트를 결정
만약 렌더링 결과가 이전과 다르다면, React는 DOM을 업데이트 함
이 과정에서 만약 컴포넌트가 React.memo()로 둘러 쌓여 있다면, React는 컴포넌트를 렌더링하고 결과를 메모이징(Memoizing)한다. 그리고 다음 렌더링이 일어날 때 렌더링하는 컴포넌트의 props가 같다면, React는 메모이징된 내용을 재사용한다.
메모제이션 (Memoization) 이란?
Memoization은 주어진 입력값에 대한 결과를 저장함으로써 같은 입력값에 대해 함수가 한 번만 실행되는 것을 보장함
React.memo()는 props 혹은 props의 객체를 비교할 때 얕은 비교를 함
비교 방식을 원하는 대로 수정하고 싶다면 React.memo()의 두 번째 매개변수로 비교함수를 넣어주면 됨
예시)
React.memo(Component, [compareFunction(prevProps, nextProps)]);
function compareFunction(prevProps, nextProps) {
return (
prevProps.a === nextProps.a &&
prevProps.b === nextProps.b
)
}
렌더링 될 때 props가 다른 경우가 대부분인 컴포넌트인 경우, 메모이제이션 기법의 이점을 얻기 힘듬
props가 자주 변하는 컴포넌트를 React.memo()로 래핑 할지라도, React는 두 가지 작업을 렌더링 할때마다 수행함
1. 이전 props와 다음 props의 동등 비교를 위해 비교 함수 수행
2. 비교 함수는 거의 항상 false를 반환할 것이기 때문에, React는 이전 렌더링 내용과 다음 렌더링 내용을 비교함
-> 비교 함수 결과가 대부분 false 라면 props 비교는 불필요
React에서 메모이제이션은 성능 개선을 위한 하나의 도구
대부분의 상황에서 React는 메모이징 된 컴포넌트의 리 렌더링을 피할 수 있지만, 렌더링을 막기 위해 메모이제이션에 너무 의존하면 안됨 (버그 유발 가능성)
리액트에서 렌더링 성능 최적화를 위해서는 컴포넌트를 분리하고 React.memo를 사용하면 됨
또한 React.memo 사용은 항상 좋은 것은 아니기에 profiler를 이용해서 성능상 이점이 있는지 확인 후 사용하면 좋음
숫자, 문자열 등 원시 자료형은 값을 비교
배열, 객체 등 참조 자료형은 값 혹은 속성을 비교하지 않고, 참조되는 위치를 비교
const obj1 = { a: 1, b: 2 };
const obj2 = { a: 1, b: 2 };
console.log(obj1 === obj2); // false
얕은 비교와 달리 깊은 비교는 객체의 경우에도 갚으로 비교함
1. Object depth가 깊지 않은 경우: JSON.stringify() 사용
2. Object depth가 깊은 경우: lodash 라이브러리의 isEqual() 사용
const obj1 = { a: 1, b: 2 };
const obj2 = { a: 1, b: 2 };
console.log(JSON.stringify(obj1) === JSON.stringify(obj2)); // true
참고) 리액트에서 컴포넌트가 리랜더링 되는 경우
1. state 변경이 있을 때
2. 부모로부터 받은 값(props)이 바뀐 경우
3. 부모가 리랜더링 되는 경우
+ shouldComponentUpdate에서 true가 반환될 때
+ forceUpdate가 실행 될 때
참고) App.js 에는 꼭 필요한 경우가 아니면 상태를 넣지 않는다
-> 렌더링 최적화를 위해. 컴포넌트화를 해서 거기에서 상태관리
원래 컴포넌트가 렌더링 될 때 그 안에 있는 함수도 다시 만들게 됨. 하지만 똑같은 함수를 컴포넌트가 리 렌더링 된다고 해서 계속 다시 만드는 것은 좋은 현상이 아님
그리고 만약 리 렌더링이 되는 컴포넌트에서 이 함수를 자식 컴포넌트에 props로 내려 준다면 그 함수를 포함하고 있는 컴포넌트가 리 렌더링 될 때마다 자식 컴포넌트도 함수가 새롭게 만들어지니 계속 리 렌더링을 하게 됨
-> 원래 React.memo로 감싸줘서 리 렌더링이 되지 않던 컴포넌트들이 함수를 props로 받으니 input에 문자를 입력할 때 마다 다시 리 렌더링됨
useCallback은 메모이제이션된 함수를 반환하는 함수
useCallback 적용은 useCallback 안에 콜백함수와 의존성 배열을 순서대로 넣어주면 됨
예시)
const testFunction = useCallback(() => {}, []);
-> useCallback 함수로 감싸주니 리 렌더링이 안됨
메모이제이션은 비용이 많이 드는 함수 호출의 결과를 저장하고 동일한 입력이 다시 발생할 때 캐시된 결과를 반환하여 컴퓨터 프로그램의 속도를 높이는데 주로 사용되는 최적화 기술
function Component({ a, b }) {
const result = conpute(a, b);
return <div>{result}</div>
}
Component 내의 compute 함수가 만약 복잡한 연산을 수행하면 결과 값을 리턴하는데 오랜 시간이 걸리게 됨
이럴 때 컴포넌트가 리 렌더링 된다면 연산을 계속 수행하는데 오랜 시간이 걸려서 성능에 안 좋은 영향을 미치게 되며, UI 지연 현상이 일어남
-> 이런 현상을 useMemo를 이용해 해결
compute 함수에 넘겨주는 a, b의 값이 이전과 동일하다면 컴포넌트가 리 렌더링 되더라도 연산을 다시 하지 않고 이전 렌더링 때 저장해 두었던 값을 재사용
useMemo로 감싸준 후에 첫번째 인수에 compute함수를, 두번째 인수인 의존성 함수에는 compute 함수에서 사용하는 값을 넣어줌