useMemo
는 특정 결과값을 재사용 할 때 사용하는 반면, useCallback
은 특정 함수를 새로 만들지 않고 재사용하고 싶을 때 사용한다.
const memoizedCallback = useCallback(function, deps);
첫 번째 인자에는 함수
를, 두 번째 인자에는 의존성 배열(deps)
을 전달한다.
리액트 컴포넌트 안에 함수가 선언되어있을 때, 이 함수는 해당 컴포넌트가 렌더링 될 때마다 새로운 함수가 생성되는데, useCallback을 사용하면 해당 컴포넌트가 렌더링 되더라도 그 함수가 의존하는 값(deps)들이 바뀌지 않는 한 기존 함수를 재사용할 수 있다.
React.memo
는 컴포넌트의 props가 바뀌지 않았다면, 컴포넌트를 렌더링하지 않고 마지막으로 렌더링된 결과를 재사용함으로써 성능 최적화를 할 수 있는 함수이다.얕은 비교
만을 수행하는 것이 기본 동작이다.function MyComponent(props) {
/* props를 사용하여 렌더링 */
}
function areEqual(prevProps, nextProps) {
/*
nextProps가 prevProps와 동일한 값을 가지면 true를 반환하고, 그렇지 않다면 false를 반환
*/
}
export default React.memo(MyComponent, areEqual);
얕은 비교만을 수행하지만, 다른 비교 동작을 원한다면 두 번째 인자로 별도의 비교 함수를 제공하면 된다.
Comments 컴포넌트에 CommentItem 컴포넌트 여러개가 있고, 1초에 하나씩 CommentItem 컴포넌트가 늘어남.
Comments.jsx
import React, { useCallback, useEffect, useState } from "react";
import CommentItem from "./CommentItem";
const commentList = [
{ title: "comment1", content: "message1", likes: 1 },
{ title: "comment2", content: "message2", likes: 1 },
{ title: "comment3", content: "message3", likes: 1 },
];
export default function Comments() {
const [comments, setComments] = useState(commentList);
useEffect(() => {
const interval = setInterval(() => {
setComments((prev) => [
...prev,
{
title: `comment${prev.length + 1}`,
content: `message${prev.length + 1}`,
likes: 1,
},
]);
}, 1000);
return () => {
clearInterval(interval);
};
}, []);
return (
<div>
{comments.map((comment) => (
<CommentItem
key={comment.title}
title={comment.title}
content={comment.content}
likes={comment.likes}
/>
))}
</div>
);
}
import React, { Profiler, memo, useMemo, useState } from "react";
function CommentItem({ title, content, likes }) {
function onRenderCallback(
id, // 방금 커밋된 Profiler 트리의 "id"
phase, // "mount" (트리가 방금 마운트가 된 경우) 혹은 "update"(트리가 리렌더링된 경우)
actualDuration, // 커밋된 업데이트를 렌더링하는데 걸린 시간
baseDuration, // 메모이제이션 없이 하위 트리 전체를 렌더링하는데 걸리는 예상시간
startTime, // React가 언제 해당 업데이트를 렌더링하기 시작했는지
commitTime, // React가 해당 업데이트를 언제 커밋했는지
interactions // 이 업데이트에 해당하는 상호작용들의 집합
) {
// 렌더링 타이밍을 집합하거나 로그...
console.log(` ${title} actualDuration: ${actualDuration}`);
}
return (
<Profiler id="CommentItem" onRender={onRenderCallback}>
<div className={"CommentItem"}>
<span>{title}</span>
<br />
<span>{content}</span>
<br />
<span>{likes}</span>
</div>
</Profiler>
);
}
export default CommentItem;
Profiler
는 리액트 성능분석도구로, 트리의 특정 부분의 렌더링 비용을 계산해준다. 이는 두 가지 props를 요구한다. id(문자열)와 onRender 콜백(함수)이며 React 트리 내 컴포넌트에 업데이트가 “커밋”되면 호출된다.
1초에 1번 state를 set하기 때문에 리렌더링이 일어남
하나의 CommentItem컴포넌트가 추가될 때마다 모든 CommentItem컴포넌트들이 다시 그려지는 것을 볼 수 있음 (사진을 보면 123/1234/12345 이런식으로 렌더링됨), 비효율적이다.
따라서, 이럴 때에 React의 memo
를 쓴다.
(동일한 props로 렌더링할 때 memo를 사용하면 React는 컴포넌트를 렌더링하지 않고 마지막으로 렌더링된 결과를 재사용한다
import React, { Profiler, memo, useMemo, useState } from "react";
.
.
.
export default memo(CommentItem);
아까와는 달리 하나씩만 렌더링 되는 것을 볼 수 있다.
(사진을 보면 12345678910 순으로 하나씩 렌더링된다.)
CommentItem의 props 값이 같으므로(얕은비교) 리렌더링 되지 않는다.
만약에 CommentItem에 어떤 함수를 props로 주면 어떨까?
export default function Comments() {
...
const handleClick = () => {
console.log("눌림");
};
return (
<div>
{comments.map((comment) => (
<CommentItem
key={comment.title}
title={comment.title}
content={comment.content}
likes={comment.likes}
onClick={handleClick}
/>
))}
</div>
);
}
function CommentItem({ title, content, likes, onClick }) {
...
return (
<Profiler id="CommentItem" onRender={onRenderCallback}>
<div className={"CommentItem"} onClick={onClick}>
</div>
</Profiler>
);
}
export default memo(CommentItem);
memo(CommentItem)로 최적화 했음에도 불구하고, CommentItem이 추가될 때 마다, 모든 CommentItem들이 다시 렌더링 되는 것을 볼 수 있다.
(사진을 보면 123/1234/12345... 이런식으로 렌더링)
이유는 Comments컴포넌트가 렌더링될 때 onClick함수가 다시 선언되어 참조값이 달라지기 때문에 memo로 최적화 했다고 하더라도 다시 렌더링 되는 것이다. 이럴 때 useCallback
을 쓰면 된다.
// Comments.jsx
const handleClick = useCallback(() => {
console.log("눌림");
}, []);
많은 개발자들이 고민하고 있는 부분이라고 한다.
하지만, useCallback, useMemo, React.memo를 너무 남용하지는 말아야한다는 것이 내가 찾아 본 결과이다. useCallback, useMemo, React.memo 도 하나의 코드이고, 내부적으로 특정한 동작을 실행시켜줘야하기 때문에 하나하나가 모두 비용으로 생각해야 한다고 한다.
useCallback이나 memo를 많이 써보진 못해서 더 공부해야겠다는 생각이 들었다.
또한, 이 영상에서 render phase와 commit phase가 있고 useCallback은 render phase를 막지는 못한다는 것을 알게 되었다.