반갑습니다 ^^
2탄을 이어서 작성해 보겠습니다.
React.memo는 컴포넌트를 넣으면 컴포넌트를 반환해주는 함수인데요. (HOC)
이것들에 대한 설명을 찾아보면, memo는 props를 비교하고 얕은 비교를 한다. 이런 설명이 전부입니다.
이렇게만 알고 가면, 이 케이스에는 렌더링이 일어날까? 이런 것들이 대한 답이 되지 않았습니다.
제가 직접 찾아보고 나니 이제는 대답을 할 수 있을것 같더라구요.
memo에 props를 50개를 넣었지만, 실제로 호출시에 3개만 넘겨준다면, 그 3개만 비교하게 될 것입니다.
React.memo
이 HOC는 주어진 props를 비교하지만 좀 더 자세히 비교를 합니다. 그리고 시간이 꽤나 걸릴것 같은 작업입니다.
areHookEquals보다 가격이 훨씬 비싸답니다.
- deps를 전부 비교하던 areHookEquals + Object 같은것들은 {key,value}가 모두 같은지 확인을 하는 작업이 있습니다.
- Object의 모든 key를 뽑아서 Object[key] === Object[key2] 이런 과정이 있는데.. 같이 보면 됩니다.
- type은 _ref로 내가 쓴 컴포넌트를 기억하고 있습니다.
- 그런데 이게 명확하게 스트링을 비교 하려고 갖고있는건지 어디에 쓰는지는 잘 이해를 못했었습니다.
- 이 부분은 좀 더 리액트 내부 코드를 보는게 익숙해지면 더 보도록 하겠습니다
- compare은 별다른걸 주지 않으면 null이 들어가 있는데, 나중에 보면 알지만 null은 기본 shallowEqual로 변합니다
shallowEqual 내부
- 비교함수를 주지 않으면, shallowEqual이 들어가게 됩니다.
- shallowEqual은 areHookEquals + object 비교가 존재합니다.
- 따라서 {a:3, b:2} 와 {b:2, a:3} 을 같게 봅니다. 만약 JSON.stringify(obj)로 비교했다면 다르다고 나왔지만, 이렇게 key로 보니까 같게 나옵니다.
그리고 여기서 만들어진 컴포넌트는 Hook 이라는곳에 Obj 형태로 global하게 저장되고 있습니다.
따라서 memo류를 사용하면 무거운 Object가 하나씩 추가된다고 볼 수 있습니다.
언제 써야 할까?
컴포넌트를 생성하는 작업이 비싸면서, 다른 input에 의해 re-render가 자주 일어나는 경우에 좋습니다.
import "./App.css";
import React, { useState, useEffect, memo } from "react";
import axios from "axios";
const BaseCard = memo(({ list }) => {
return list.map((elem, idx) => <div key={idx}>{elem.title}</div>);
});
// const BaseCard = ({ list }) => {
// return list.map((elem, idx) => <div key={idx}>{elem.title}</div>);
// };
function App() {
const [page, setPage] = useState(1);
const [list, setList] = useState([]);
const [text, setText] = useState("");
// const MC = React.memo(MemoizedCard);
const handleChange = (e) => {
setText(e.target.value);
};
useEffect(async () => {
const { data } = await axios.get(
`https://jsonplaceholder.typicode.com/comments/${page}/todos`
);
setList([...list, ...data]);
}, [page]);
return (
<div className="App">
<input type="text" value={text} onChange={handleChange} />
<BaseCard list={list} />
<button onClick={() => setPage(page + 1)}>ClickMe</button>
</div>
);
}
export default App;
memo가 된것과, 되지 않은경우에 렌더링 속도에 차이를 느낄 수 있습니다. 실제로 profiler로 찍어봐도 속도차이가 많이 나는걸 볼 수 있습니다.
useCallback
- 렌더링시에 hook을 호출하고, deps를 확인해서 변하지 않았다면 기존의 함수를 반환해 줍니다. (참조가 같습니다)
이건 React.memo와도 많은 관련이 있습니다. (deps 확인은 areHookEquals로 합니다)
useCallback에서 props에 function을 넘길경우, 무조건 re-render 및 불필요한 비교를 하게 됩니다.
사진에서 알 수 있듯, 생긴건 같아도 두 함수의 참조는 다릅니다. 따라서 Object.is로 비교시 false입니다.
하지만 let c = func로 넘겨준다면, 서로 같은 주소를 가리키므로 참이 됩니다.
이걸 이용해서 memo에서 re-render를 막을 수 있습니다.
import "./App.css";
import React, { useState, useEffect, useCallback, memo } from "react";
import axios from "axios";
import Card from "./Card";
const MemoizedCard = memo(({ list, handleTest }) => {
handleTest();
return list.map((elem, idx) => <div key={idx}>{elem.title}</div>);
});
function App() {
const [page, setPage] = useState(1);
const [list, setList] = useState([]);
const [text, setText] = useState("");
const handleChange = (e) => {
setText(e.target.value);
};
const handleTest = useCallback(() => {
console.log("aa");
}, []);
// 이것 또한 props로 넘기는 것이 아니라면 불필요한 과정이다.
// const handleTest = () => {
// console.log("aa");
// };
useEffect(async () => {
const { data } = await axios.get(
`https://jsonplaceholder.typicode.com/comments/${page}/todos`
);
setList([...list, ...data]);
}, [page]);
return (
<div className="App">
<input type="text" value={text} onChange={handleChange} />
<MemoizedCard list={list} handleTest={handleTest} />
<button onClick={() => setPage(page + 1)}>ClickMe</button>
</div>
);
}
export default App;
// Callback을 제거하고 해보면 체감이 된다.
// handleTest={()=>("aa")} 또한 memoization을 깨는 요인이 된다.
useCallback을 사용한것과, 사용하지 않은경우에 차이를 느껴보면, 참조가 달라서 re-render가 많이 일어나는걸 볼 수 있을 것 입니다.
또한 onClick 같은곳에서 내부에서 함수를 선언하면, 매 렌더링마다 새로 생성하므로 좋지 않은 습관이 됩니다.
결론
- 오래 걸리는 컴포넌트 or 함수들에 사용하자
- dependency를 비교하는것 또한 시간이 걸린다. 하지만 shallowEqual은 더 걸린다.
- 자주 변경되는 값들은 그냥 놔두는것이 좋다. 오히려 props를 비교하는 cost가 더 들게 된다.
지금 사진에 있는 예시는 useEffect에 deps를 많이 달아서 re-render를 막아주는 방식을 해본 예시입니다.
- re-render 방지 전
- re-render 방지 이후
결론적으로 확인하면, rendering 시간이 줄어들고, 실제 2600ms -> 2000ms 가 된걸 볼 수 있습니다.
하지만 실제 결과는 한번의 click시에 드는 시간은 오히려 방지 이후에 더 시간이 걸리게 되었습니다.
WHY? 불필요한 render를 막기 위해 useEffect에 deps가 많아서 오히려 시간이 더 들게 된 것이었습니다.
React 자체도 이미 충분히 rendering 최적화가 잘 되어있다.
이상입니다.

'WEB > 깊게 공부하기' 카테고리의 다른 글
[JS] iterable, iterator 살펴보기 - Generator 1 (0) | 2023.10.16 |
---|---|
React가 이벤트를 관리하는 방식 - 3 (완) (2) | 2023.06.19 |
React가 이벤트를 관리하는 방식 - 2 (0) | 2023.06.18 |
React가 이벤트를 관리하는 방식 - 1 (2) | 2023.06.15 |
[React] Optimization, useMemo, useCallback, memo - (1) (0) | 2022.03.07 |