메모이제이션
은 컴퓨터 프로그램이동일한 계산
을반복
해야할 때, 이전에 계산한 값을메모리에 저장
함으로써 동일한 계산의 반복 수행을 제거하여 프로그램 실행 속도를 빠르게 하는 기술
동일한 props로 렌더링을 한다면, React.memo를 사용하여 성능 향상
을 누릴 수 있습니다.
memo를 사용하면 React는 컴포넌트를 렌더링하지 않고 마지막으로 렌더링된 결과
를 재사용
합니다.
Memo.jsx
import React, { useState, useEffect } from 'react'
import Comments from './Comments';
const 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, setComments] = useState(commentList);
useEffect(() => {
const interval = setInterval(() => {
setComments((prevComments) => [
...prevComments,
{ title: `comment${prevComments.length + 1}`, content: `message${prevComments.length + 1}`, likes: 1},
])
}, 1000);
return () => {
clearInterval(interval);
}
}, [])
return (
<Comments commentList={comments} />
)
}
useEffect 코드의 실행 결과로 comment가 1개씩 계속 증가
useEffect(() => {
const interval = setInterval(() => {
setComments((prevComments) => [
...prevComments,
{ title: `comment${prevComments.length + 1}`, content: `message${prevComments.length + 1}`, likes: 1},
])
}, 1000);
return () => {
clearInterval(interval);
}
}, [])
Comments.jsx
import React from 'react'
import CommentItem from './CommentItem'
export default function Comments({ commentList }) {
return (
<div>
{commentList.map(comment => <CommentItem
key={comment.title}
title={comment.title}
content={comment.content}
likes={comment.likes}
/>)}
</div>
)
}
CommentItem.jsx
(Profiler API란?)
import React, { Profiler, memo } from "react";
import "./CommentItem.css";
function CommentItem({ title, content, likes }) {
function onRenderCallback(
id, // 방금 커밋된 Profiler 트리의 "id"
phase, // "mount" (트리가 방금 마운트가 된 경우) 혹은 "update"(트리가 리렌더링된 경우)
actualDuration, // 커밋된 업데이트를 렌더링하는데 걸린 시간
baseDuration, // 메모이제이션 없이 하위 트리 전체를 렌더링하는데 걸리는 예상시간
startTime, // React가 언제 해당 업데이트를 렌더링하기 시작했는지
commitTime, // React가 해당 업데이트를 언제 커밋했는지
interactions // 이 업데이트에 해당하는 상호작용들의 집합
) {
// 렌더링 타이밍을 집합하거나 로그...
console.log(`actualDuration(${title}: ${actualDuration})`);
}
return (
<Profiler id="CommentItem" onRender={onRenderCallback}>
<div className="CommentItem">
<span>{title}</span>
<br />
<span>{content}</span>
<br />
<span>{likes}</span>
<br />
</div>
</Profiler>
);
}
export default CommentItem;
콘솔 결과
useEffect setInterval로 인해 comment가 1초에 한개씩 증가하는데 console창을 보면
1
2
3을 그리고
4가 바로 추가되는게 아니라
//
1
2
3을 그리고
1
2
3
4로 123을 다시 그리고 4를 추가한다.
즉, 1초 뒤에는 다시
1
2
3
4를 그리고
5가 추가됨을 예상할 수 있다.
한 개의 컴포넌트가 추가될 때마다 모든 컴포넌트들이 다 새로 그려진다. 너무 비효율적이다 ❗
export default memo(CommentItem);
memo를 적게되면 아래 이미지처럼 추가된 컴포넌트만 그린다.
왜냐하면 아래 내용처럼 props
가 바뀌지 않기 때문에!!
동일한 props로 렌더링을 한다면, React.memo를 사용하여
성능 향상
을 누릴 수 있습니다.
위에서 React.memo는 동일한 props로 렌더링 할 때, 전체를 다시 그리는 것이 아니라 추가된 컴포넌트만 그리게끔 했는데 useCallback은 언제 사용하는 것일까 ?
위 코드에서 실험한 Component는 CommentItem
으로 아래와 같이 title
, content
, likes
를 props로 받았다.
function CommentItem({ title, content, likes }) {
function onRenderCallback(
id, // 방금 커밋된 Profiler 트리의 "id"
phase, // "mount" (트리가 방금 마운트가 된 경우) 혹은 "update"(트리가 리렌더링된 경우)
actualDuration, // 커밋된 업데이트를 렌더링하는데 걸린 시간
baseDuration, // 메모이제이션 없이 하위 트리 전체를 렌더링하는데 걸리는 예상시간
startTime, // React가 언제 해당 업데이트를 렌더링하기 시작했는지
commitTime, // React가 해당 업데이트를 언제 커밋했는지
interactions // 이 업데이트에 해당하는 상호작용들의 집합
) {
// 렌더링 타이밍을 집합하거나 로그...
console.log(`actualDuration(${title}: ${actualDuration})`);
}
return (
<Profiler id="CommentItem" onRender={onRenderCallback}>
<div className="CommentItem">
<span>{title}</span>
<br />
<span>{content}</span>
<br />
<span>{likes}</span>
<br />
</div>
</Profiler>
);
}
만약에 onClick
을 부모한테 props
로 받는다면 어떻게 작동할까?
Comment.jsx (부모)
import React from 'react'
import CommentItem from './CommentItem'
export default function Comments({ commentList }) {
return (
<div>
{commentList.map(comment => <CommentItem
key={comment.title}
title={comment.title}
content={comment.content}
likes={comment.likes}
onClick={() => console.log('눌림')} // 추가
/>)}
</div>
)
}
// onClick props 추가
CommentItem({ title, content, likes, onClick })
// handleClick 정의
const handleClick = () => {
onClick();
}
// 자식인 CommentItem에 onClick event 추가
<div className="CommentItem" onClick={handleClick}>
이 상태에서 onClick
하게 되면 memo
를 했음에도 불구하고 모든 컴포넌트들이 전부 그려짐.
Comments.jsx (부모)
onClick={() => console.log('눌림')}
이 코드를
const handleChange = () => {
console.log('눌림');
}
onClick={handleChange}
으로 바꾸더라도 Comments
가 리렌더링 되기 때문에 함수가 계속 새로 생성되어서 모든 컴포넌트가 다시 그려진다.
💡 이 때 usCallback
을 사용
const handleChange = useCallback(() => {
console.log('눌림');
}, []);
onClick={handleChange}
이제 추가된 컴포넌트만 그려진다.
요구 사항이 추가되었다.
likes 가 10이 넘으면 rate 값에 string으로 Good을 넣고 아니면 Bad를 넣자.
const rate = () => {
console.log('rate check');
return likes > 10 ? 'Good' : 'Bad';
}
function CommentItem({ title, content, likes, onClick }) {
return (
<div className="CommentItem" onClick={handleClick}>
<span>{title}</span>
<br />
<span>{content}</span>
<br />
<span>{likes}</span>
<br />
<span>{rate()}</span>
</div>
);
}
그럼 이제 CommentItem이 그려질 때마다 rate check
가 콘솔에 찍힐 것이다.
그리고 자신이 몇번 클릭되었는 지를 count하는 값을 handleClick 이벤트에 추가해주자.
const [clickCount, setClickCount] = useState(0);
const handleClick = () => {
setClickCount((prev) => prev + 1);
}
자 이제 CommentItem을 클릭해보자 ! clickCount의 값이 바뀌기 때문에 컴포넌트가 다시 그려진다. 하지만 우리는 위에서 Memoization을 잘해줬기 때문에 전체가 그려지지 않고 클릭된 컴포넌트만 다시 그려질 것이다. 그려지는 과정에서 clickCount 값이 1 증가 된다.
이 때 중요한 것은
const rate = () => {
console.log('rate check');
return likes > 10 ? 'Good' : 'Bad';
}
위 rate 함수도 다시 실행된다는 점이다.
clickCount 상태값이 변했기 때문에 다시 그려지는 것은 당연한 것이지만 rate 함수가 다시 실행될 필요는 없다.
rate 함수를 useMemo
를 이용하면 이 문제는 해결된다.
const rate = useMemo(() => {
console.log('rate check');
return likes > 10 ? 'Good' : 'Bad';
}, [likes]);
// 기존 코드 <span>{rate()}</span>
// 바뀐 코드
// useMemo를 쓰면 기존 코드처럼 따로 함수를 실행할 필요없다
<span>{rate}</span>
이렇게 바꾸고 나면 clickCount가 바뀌는 등 rate와 관련없는 동작으로 다시 렌더링 될 때 rate가 실행되지 않고 likes
가 바뀌었을 때만 실행된다.
특정한 값을 Memoization 할 때는 useMemo
특정한 함수를 Memoization 할 때는 useCallback