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

신은수·2023년 7월 16일
0

ReactJS

목록 보기
9/13

1. useCallback

1) useCallback이란?

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

2) useCallback 사용법

const memoizedCallback = useCallback(function, deps);

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


2. 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);

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


3. 예시를 통해 알아보는 렌더링 최적화 - 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 값이 같으므로(얕은비교) 리렌더링 되지 않는다.

4. 예시를 통해 알아보는 렌더링 최적화 - 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("눌림");
}, []);

  • 사진을 보면 아까처럼 다시 렌더링 되지 않고 하나씩 렌더링 되는 것을 볼 수 있다.

4. 언제 렌더링 최적화를 해야할까?

많은 개발자들이 고민하고 있는 부분이라고 한다. 하지만 useCallback, useMemo, React.memo를 너무 남용하지는 말아야한다는 것이 내가 찾아 본 결과이다. useCallback, useMemo, React.memo 도 하나의 코드이고 내부적으로 특정한 동작을 실행시켜줘야하기 때문에 하나하나가 모두 비용이라고 생각해야한다고 한다.

useCallback이나 memo를 많이 써보진 못해서 더 공부해야겠다는 생각이 들었다.
또한 이 영상에서 render phase와 commit phase가 있고 useCallback은 render phase를 막지는 못한다는 것을 알게 되었다.

profile
🙌꿈꾸는 프론트엔드 개발자 신은수입니당🙌

0개의 댓글