목표 : Memoization의 개념에 대해서 알아보자!

memoization ?
메모이제이션은 컴퓨터 프로그램이 동일한 계산을 반복해야 할때, 이전에 계산한 값을 메모리에 저장함으로써 동일한 계산의 반복 수행을 제거하여 프로그램 실행 속도를 빠르게 하는 기술이다.
예제)
[App.js]
import "./App.css";
import Memo from "./components/3.8 Memoization/Memo";
function App() {
return (
<div className="App">
<Memo />
</div>
);
}
export default App;
[Memo.jsx]
import React, { useEffect, useState } from "react";
import Comments from "./Comments";
let commentList = [
{ title: "comment1", content: "message1", likes: 1 },
{ title: "comment2", content: "message2", likes: 1 },
{ title: "comment3", content: "message3", likes: 1 },
];
export default function Memo() {
const [comments, setComment] = useState(commentList);
useEffect(() => {
const interval = setInterval(() => {
setComment((prevComments) => [
...prevComments,
{
title: `comment${prevComments.length + 1}`,
content: `message${prevComments.length + 1}`,
likes: 1,
},
]);
}, 1000);
return () => {
clearInterval(interval);
};
});
return <Comments commentList={comments} />;
}
[Comments.jsx]
import React, { useCallback } from "react";
import CommentItem from "./CommentItem";
export default function Comments({ commentList }) {
const handleChange = useCallback(() => console.log("눌림"), []);
return (
<div
style={{
display: "flex",
gap: "10px 0",
padding: "10px 0",
flexDirection: "column",
width: "fit-content",
margin: "0 auto",
}}
>
{commentList.map((comment) => (
<CommentItem
key={comment.title}
title={comment.title}
content={comment.content}
likes={comment.likes}
onClick={handleChange}
/>
))}
</div>
);
}
[CommentItem.jsx]
import React, { memo, useState, useMemo, Profiler } from "react";
import "./CommentItem.css";
function CommentItem({ title, content, likes, onClick }) {
const [count, setCount] = useState(0);
function onRenderCallback(
id, // 방금 커밋된 Profiler 트리의 "id"
phase, // "mount" (트리가 방금 마운트가 된 경우) 혹은 "update"(트리가 리렌더링된 경우)
actualDuration, // 커밋된 업데이트를 렌더링하는데 걸린 시간
baseDuration, // 메모이제이션 없이 하위 트리 전체를 렌더링하는데 걸리는 예상시간
startTime, // React가 언제 해당 업데이트를 렌더링하기 시작했는지
commitTime, // React가 해당 업데이트를 언제 커밋했는지
interactions // 이 업데이트에 해당하는 상호작용들의 집합
) {
// 렌더링 타이밍을 집합하거나 로그...
console.log(`actualDuration(${title}:${actualDuration})`);
}
const handleClick = () => {
onClick();
setCount((prev) => prev + 1); // 리렌더링!
alert(`${title}나 눌림!`);
};
const rate = useMemo(() => {
console.log("click rate");
return likes > 10 ? "good" : "bad";
}, [likes]);
return (
<Profiler id="CommentItem" onRender={onRenderCallback}>
<div className="CommentItem" onClick={handleClick}>
<span>{title}</span>
<span>{content}</span>
<span>{likes}</span>
<span>{rate}</span>
<span>{count}</span>
</div>
</Profiler>
);
}
export default memo(CommentItem);
예제를 살펴보면 알겠지만 Memo 컴포넌트에서는 매 초마다 commetList에 있는 객체들을 하나씩 추가해서 뿌려주고 있다.
자식 컴포넌트인 CommentItem은 부모 컴포넌트로부터 props를 전달받아 title, content likes.. 등의 값을 호출한다.
부모 컴포넌트인 Memo에서 객체가 하나씩 들어갈때마다 자식컴포넌트도 한번씩 리렌더링이 일어날 수밖에 없는 구조가 되어버린 것.
하지만 컴포넌트의 props 가 바뀌지 않은 상태에서 같은 내용을 반복해서 업데이트하는 경우에는 리렌더링하는 과정에서 많은 자원의 낭비를 할 수 밖에 없다.
리액트의 컴포넌트가 렌더링 되는 상황은 많겠지만 보통 state나 props의 상태가 변했을 때 혹은 부모의 컴포넌트가 렌더링 되었을 때 일어난다.
많은 분들이 코딩을 통해 프로젝트를 만들게 되면서 느끼는 거겠지만 규모가 커질수록 다른요소들에 의해 리렌더링이 일어나는 모습을 흔히 볼 수 있다.
이것은 메모리 낭비적인 측면이나 사용자의 서비스사용에 큰 불편함을 초래하는 것이기 때문에 리렌더링이 일어나지않도록 최적화를 해주는 습관이 필요하다.
리액트는 메모이제이션을 위한 세개의 api를 제공한다.
1. memo
2. useCallback
3. useMemo
memo
React는 먼저 컴퍼넌트를 렌더링 한 뒤, 이전 렌더된 결과와 비교하여 DOM 업데이트를 결정한다.
만약 렌더 결과가 이전과 다르다면, React는 DOM을 업데이트한다.
React.memo는 고차 컴포넌트(Higher Order Component이다.
컴포넌트가 동일한 props로 동일한 결과를 렌더링해낸다면, React.memo를 호출하고 결과를 메모이징(Memoizing)하도록 래핑하여 경우에 따라 성능 향상을 누릴 수 있다.
즉, React는 컴포넌트를 렌더링하지 않고 마지막으로 렌더링된 결과를 재사용을 한다.
function CommentItem({ title, content, likes, onClick }) {
return (
<div className="CommentItem">
<span>{title}</span>
<span>{content}</span>
<span>{likes}</span>
</div>
);
}
export default memo(CommentItem);
위 예제에서는 컴포넌트 자체를 memo함수의 인자로 넣어 호출한다.
그렇다면 memo를 사용함과 사용하지 않는 상태를 비교할 수 있는 방법은 어떤 것이 있을까?
성능을 측정하기 위해 Profiler를 사용해보자.
Profiler
<Profiler id="CommentItem" onRender={onRenderCallback}>
<div className="CommentItem" onClick={handleClick}>
<span>{title}</span>
<span>{content}</span>
<span>{likes}</span>
<span>{rate}</span>
<span>{count}</span>
</div>
</Profiler>
Profiler는 React 트리 내에 어디에나 추가될 수 있으며 트리의 특정 부분의 렌더링 비용을 계산해준다.
이는 두 가지 props를 요구하는데,
id (문자열) 와 onRender 콜백 (함수)이며 React 트리 내 컴포넌트에 업데이트가 “커밋”되면 호출한다.
자식들 중에 이미 그려졌던 애들은 반복해서 사용하기 때문에 비효율을 줄일 수 있다.
이 함수는 무엇이 렌더링 되었는지 그리고 얼마나 걸렸는지 설명하는 입력값을 받게 됨
function onRenderCallback(
id, // 방금 커밋된 Profiler 트리의 "id"
phase, // "mount" (트리가 방금 마운트가 된 경우) 혹은 "update"(트리가 리렌더링된 경우)
actualDuration, // 커밋된 업데이트를 렌더링하는데 걸린 시간
baseDuration, // 메모이제이션 없이 하위 트리 전체를 렌더링하는데 걸리는 예상시간
startTime, // React가 언제 해당 업데이트를 렌더링하기 시작했는지
commitTime, // React가 해당 업데이트를 언제 커밋했는지
interactions // 이 업데이트에 해당하는 상호작용들의 집합
) {
// 렌더링 타이밍을 집합하거나 로그...
console.log(`actualDuration(${title}:${actualDuration})`);
}
※ 주의사항
Profiler는 가벼운 컴포넌트이지만 조금의 CPU와 메모리 비용을 추가하게 되기 때문에 필요할 때만 사용해야 한다.
useCallback
useCallback함수는 메모이제이션된 콜백을 반환한다. 특정 함수를 새로 만들지 않고 재사용하고 싶을때 사용한다.
현재 Comments 에서는 handleChange라는 함수를 새로 만들어서 자식 컴포넌트인 CommentItem에 전해주고 있다.
그렇다면 props로 전달해주는 새로운 함수를 리렌더링해줘야 하기때문에 memo는 사용할 수 가 없게 된다.
export default function Comments({ commentList }) {
const handleChange = () => console.log("눌림");
return (
<div
style={{
display: "flex",
gap: "10px 0",
padding: "10px 0",
flexDirection: "column",
width: "fit-content",
margin: "0 auto",
}}
>
{commentList.map((comment) => (
<CommentItem
key={comment.title}
title={comment.title}
content={comment.content}
likes={comment.likes}
onClick={handleChange}
/>
))}
</div>
);
}
하지만 useCallback 함수를 사용하게 되면 함수를 새로 만들지 않고 재사용하기 때문에 리렌더링을 하지 않는다.
const handleChange = useCallback(() => console.log("눌림"), []);
'React 내용 정리' 카테고리의 다른 글
React REDUX (0) | 2022.11.30 |
---|---|
Context (0) | 2022.11.04 |
고차컴포넌트(HOC, Higher Order Component) (0) | 2022.11.02 |
List and Key (0) | 2022.10.28 |
조건부 렌더링 (0) | 2022.10.28 |