React

[React] 렌더링 성능 개선하기

oagree0123 2022. 4. 19. 00:14

먼저, 컴포넌트가 리렌더링되는 경우는 다음과 같습니다.

 

  • 컴포넌트의 state가 변경되었을 때
  • 부모 컴포넌트에서 받은 props가 변경될 때
  • 부모 컴포넌트가 리렌더링 될 때

위와 같은 특성은 필요없는 렌더링을 일으키기도 합니다.

부모 컴포넌트의 state가 바뀌면 props를 받은 자식 컴포넌트와
변경된 state를 받지 않은 자식 컴포넌트 모두 렌더링 됩니다. 

 

이러한 불필요한 렌더링을 줄이고 성능을 높이는 방법을 설명하겠습니다.

useMemo

useMemo는 React에서 CPU 소모가 심한 함수들을 캐싱하기 위해 사용됩니다.

useMemo를 사용하면 복잡한 계산식의 계산한 값을 재사용할 수 있습니다.

 

만약, 컴포넌트의 어떤 함수가 값을 리턴하는데 하나의 변화에도 많은 시간을 소요한다면, 

리렌더링될 때마다 함수가 호출되면서 많은 시간을 소요하게 될 것입니다.

또한, 이 함수의 리턴 값을 자식 컴포넌트가 사용한다면 함수 호출마다 리렌더링할 것입니다.

 

아래 코드를 보겠습니다.

import { useState, useMemo, useRef } from "react";
import Item from "./Item";
import Average from "./Average";

function UserList() {
  const [users, setUsers] = useState([
    {
      id: 0,
      name: "gildong",
      age: 24,
      score: 100
    },
    {
      id: 1,
      name: "chulsu",
      age: 28,
      score: 10
    }
  ]);

  const cal_average = (() => {
    console.log("시간이 오래걸려요!!!");
    return users.reduce((acc, cur) => {
      return acc + cur.age / users.length;
    }, 0);
  })();

  return (
      <div>
       	<Average average={cal_average} />
      </div>
  );
}

export default UserList;

Average 컴포넌트를 보면 cal_average함수가 실행되면, 리턴 값이 props로 전달되는 것을 볼 수 있습니다.

실제 함수 간단하지만 엄청 오래걸리는 함수라고 가정해 보겠습니다. 

UserList 컴포넌트가 리렌더링 될 때마다 매번 연산을 수행해야 합니다.

 

이때, useMemo를 사용하면 데이터가 변했을 경우에만 cal_average함수를 수행하도록 할 수 있습니다.

useMemo(()=> function, [input_dependency]) //useMemo 기본 형태

const cal_average = useMemo(() => {
   console.log("시간이 오래걸려요!!!");
   return users.reduce((acc, cur) => {
     return acc + cur.score / users.length;
   }, 0);
 }, [users]);

위 코드를 보면 input_dependency에 state를 넣어주었습니다.

useMemo는 input_dependency의 값이 변하지 않을 경우 다시 함수를 호출하지 않고, 이전에 리턴 값을 재사용합니다.

이는 함수 호출 시간을 줄여줄 것이고, 자식 컴포넌트의 리렌더링도 방지해 줄 것입니다.


React.memo 컴포넌트 메모이제이션

React.memo는 렌더링 결과를 메모이제이션 합니다.

컴포넌트의 props가 바뀌지 않았다면, 리렌더링이 일어나지 않도록 설정하여

함수형 컴포넌트의 렌더링 성능을 최적화할 수 있습니다. 

또한, 콜백함수를 이용하여 메모이제이션을 적용하는지에 대한 여부를 파악할 수 있습니다.

//UserList.jsx
import { useState, useRef } from "react";
import Item from "./Item";
import Average from "./Average";

function UserList() {
  let numberRef = useRef(0);
  const [text, setText] = useState("");
  const [users, setUsers] = useState([
    {
      id: 0,
      name: "gildong",
      age: 24,
      score: 100
    },
    {
      id: 1,
      name: "chulsu",
      age: 28,
      score: 10
    }
  ]);

  const cal_average = useMemo(() => {
    console.log("시간이 오래걸려요!!");
    return users.reduce((acc, cur) => {
      return acc + cur.score / users.length;
    }, 0);
  }, [users]);
  
   const add_user =() => {
    setUsers([
      ...users,
      {
        id: (numberRef.current += 1),
        name: "yeonkor",
        age: 30,
        score: 90
      }
    ]);
  }

  return (
      <div>
       <input
         type="text"
         value={text}
         placeholder="내용을 입력하세요."
         onChange={(event) => setText(event.target.value)}
        />
       <Average average={cal_average} />
       <button className="button" onClick={add_user}>
        새 유저 생성
       </button>
      {users.map((user) => {
        return (
          <Item key={user.id} user={users} />
        );
      })}
      </div>
  );
}

export default UserList;
//Item.jsx
import React,{ memo } from "react";

function Item({ user }) {
  console.log("Item component render");

  return (
    <div className="item">
      <div>이름: {user.name}</div>
      <div>나이: {user.age}</div>
      <div>점수: {user.score}</div>
      <div>등급: {result.grade}</div>
    </div>
  );
}

export default memo(Item);

위 코드에서는 Item 컴포넌트에 React.memo로 감싸 사용하였습니다.

이는 버튼을 누르면 users 배열의 값이 추가되고 UserList 컴포넌트가 리렌더링 되더라도,

새로 추가된 Item 컴포넌트만 새로 렌더링 되고 이미 존재하던 Item 컴포넌트가 리렌더링 되지 않습니다.


useCallback

useCallback은 함수 선언을 메모이제이션 합니다.

메모이제이션된 함수는 dependency에 담긴 state가 변경되었을 때에만 변경된다.

또한, 콜백 안에서 참조되는 모든 state는 dependency에 나타나야 합니다.

 

위 코드의 버튼을 아래의 컴포넌트로 바꿔보겠습니다.

// Button.jsx
import React.{memo} from "react";

function Button({ onClick }) {
    console.log("Button component render");

  return (
    <button type="button" onClick={onClick}>
      버튼
    </button>
  );
}

export default memo(Button);

onClick 함수는 UserList 컴포넌트에서 props로 받고 있습니다.

위의 React.memo 에서 본 것 처럼  onClick props가 동일한지 체크한 후 동일하다면 리렌더링 되지 않아야 합니다.

그런데, 위의 코드에서는 리렌더링이 일어납니다.

그 이유는 함수는 객체이고, 새로 생성된 함수는 다른 참조 값을 가지고 있어

Button 컴포넌트에서는 새로 생성된 함수를 받을 때 props가 변한 것으로 파악합니다.

 

이런 상황일 때 useCallback을 사용합니다.

useCallback으로 함수를 선언하면, dependency가 변하지 않는 이상 함수를 재생성하지 않고,

이전에 참조 값을 props로 전달하여 props가 변하지 않도록 파악하게 합니다.

const add_user = useCallback(() => {
    setUsers([
      ...users,
      {
        id: (numberRef.current += 1),
        name: "yeonkor",
        age: 30,
        score: 90
      }
    ]);
  }, [users]);

 

정리

useMemo

- 계산이 오래걸리는 복잡한 함수나 지속적으로 업데이트 되는 onChange같은 함수를 캐싱하는데 사용합니다.

 

React.memo

- props를 받는 자식 컴포넌트는 React.memo로 감싸서 생성할 수 있습니다.

- React.memo는 렌더링 결과를 메모이제이션하여 props가 바뀌지 않으면 리렌더링 되지 않게 합니다.

 

useCallback

- useCallback은 함수 선언을 메모이제이션 합니다.

- props로 함수를 전달할 때,
  useCallback으로 함수를 선언하면 dependency가 변하지 않는 이상 함수를 재생성하지 않고,

  이전에 참조 값을 props로 전달하여 props가 변하지 않도록 파악하게 합니다.