React 렌더링 최적화 (feat - useCallback, React.memo)

SangHun Han·2023년 8월 2일
0

📞 useCallback

1) useCallback이란?

useMemo는 특정 결과값을 재사용 할 때 사용하는 반면, useCallback은 특정 함수를 새로 만들지 않고 재사용하고 싶을 때 사용한다.


2) useCallback 사용법

const memoizedCallback = useCallback(function, deps);

첫 번째 인자에는 함수를, 두 번째 인자에는 의존성 배열(deps)을 전달한다.

리액트 컴포넌트 안에 함수가 선언되어있을 때, 이 함수는 해당 컴포넌트가 렌더링 될 때마다 새로운 함수가 생성되는데, useCallback을 사용하면 해당 컴포넌트가 렌더링 되더라도 그 함수가 의존하는 값(deps)들이 바뀌지 않는 한 기존 함수를 재사용할 수 있다.



📝 React.memo

1) React.memo란?

  • React.memo는 컴포넌트의 props가 바뀌지 않았다면, 컴포넌트를 렌더링하지 않고 마지막으로 렌더링된 결과를 재사용함으로써 성능 최적화를 할 수 있는 함수이다.
  • React.memo를 사용하더라도 함수 컴포넌트 안에서 구현한 state나 context가 변할 때는 리렌더링된다.
  • 또한, props가 갖는 복잡한 객체에 대하여 얕은 비교만을 수행하는 것이 기본 동작이다.

2) React.memo 사용법

function MyComponent(props) {
  /* props를 사용하여 렌더링 */
}
function areEqual(prevProps, nextProps) {
  /*
  nextProps가 prevProps와 동일한 값을 가지면 true를 반환하고, 그렇지 않다면 false를 반환
  */
}
export default React.memo(MyComponent, areEqual);

얕은 비교만을 수행하지만, 다른 비교 동작을 원한다면 두 번째 인자로 별도의 비교 함수를 제공하면 된다.



👀 예시를 통해 알아보는 렌더링 최적화 - React.memo

1) 리액트에서 리렌더링 되는 조건

  • state가 변경되었을 때
  • props가 변경되었을 때
  • context가 변경되었을 때

2) 코드

  • 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>
  );
}

  • CommentItem.jsx
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 트리 내 컴포넌트에 업데이트가 “커밋”되면 호출된다.


3) 결과

  • 1초에 1번 state를 set하기 때문에 리렌더링이 일어남

  • 하나의 CommentItem컴포넌트가 추가될 때마다 모든 CommentItem컴포넌트들이 다시 그려지는 것을 볼 수 있음 (사진을 보면 123/1234/12345 이런식으로 렌더링됨), 비효율적이다.

  • 따라서, 이럴 때에 React의 memo를 쓴다.
    (동일한 props로 렌더링할 때 memo를 사용하면 React는 컴포넌트를 렌더링하지 않고 마지막으로 렌더링된 결과를 재사용한다


4) React.memo 적용

import React, { Profiler, memo, useMemo, useState } from "react";
  .
  .
  .
export default memo(CommentItem);

  • 아까와는 달리 하나씩만 렌더링 되는 것을 볼 수 있다.
    (사진을 보면 12345678910 순으로 하나씩 렌더링된다.)

  • CommentItem의 props 값이 같으므로(얕은비교) 리렌더링 되지 않는다.



👀 예시를 통해 알아보는 렌더링 최적화 - useCallback

1) 코드

만약에 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);

2) 결과

  • memo(CommentItem)로 최적화 했음에도 불구하고, CommentItem이 추가될 때 마다, 모든 CommentItem들이 다시 렌더링 되는 것을 볼 수 있다.
    (사진을 보면 123/1234/12345... 이런식으로 렌더링)

  • 이유는 Comments컴포넌트가 렌더링될 때 onClick함수가 다시 선언되어 참조값이 달라지기 때문에 memo로 최적화 했다고 하더라도 다시 렌더링 되는 것이다. 이럴 때 useCallback을 쓰면 된다.


3) 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를 막지는 못한다는 것을 알게 되었다.

profile
매일매일 성장하는 개발자 🚀

0개의 댓글