[Computer Science] React 렌더링 성능 최적화

이제는 클래스형 컴포넌트를 사용하지 않고 함수형 컴포넌트를 사용하기 때문에 함수형 컴포넌트 기반 성능 최적화에 대한 방법을 알아보자.

1) state 선언

리액트는 특정 state가 변경되면 그 state가 선언된 컴포넌트와 그 하위 컴포넌트들을 모두 리렌더링 시킨다. 따라서 state가 선언되는 위치를 잘 설계하는 것이 리렌더링 횟수에 엄청난 영향을 끼친다. state를 해당 state를 사용하는 컴포넌트 중 가장 최상위 컴포넌트에 선언하는 것이 좋다. 만약 그 state를 사용하는 최상위 컴포넌트보다 더 상위 컴포넌트에 state를 선언하면 state를 사용하지 않는 더 많은 컴포넌트들이 state 변경에 의해 불필요한 리렌더링을 겪게 된다.

만약 state가 객체 타입일 경우 객체가 크고 복잡한 구조이면 분할할 수 있는 만큼 최대한 분할하는 것이 좋다. 해당 state에서 일부의 프로퍼티만 사용하는 하위 컴포넌트가 있다면, 그 컴포넌트는 해당 프로퍼티가 변경될 때에만 리렌더링 되는 것이 바람직하다. 만약 복잡한 객체로 선언된 state를 분할하지 않으면, 하위 컴포넌트가 사용하지 않는 다른 프로퍼티의 값이 업데이트될 때에도 리렌더링이 발생한다.


2) React.memo를 이용한 컴포넌트 메모이제이션

React.memo는 Hook이 아니기 때문에 클래스형 컴포넌트에서도 사용할 수 있다.
함수형 컴포넌트에서는 shouldComponentUpdate를 사용할 수 없는데, 리액트 공식 문서에서는 그 대안으로 React.memo를 제시하고 있다.

React.memo를 통해 컴포넌트의 props가 바뀌지 않았다면, 리렌더링하지 않도록 설정하여 함수형 컴포넌트의 리렌더링 성능을 최적화 해줄 수 있다.

React.memo는 콜백함수를 이용해 메모이제이션을 적용할지 여부를 판단할 수도 있다.

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

function UserList() {
  let numberRef = useRef(2);
  const [text, setText] = useState("");
  const [users, setUsers] = useState([
    {
      id: 0,
      name: "sewon",
      age: 30,
      score: 100
    },
    {
      id: 1,
      name: "kongil",
      age: 50,
      score: 10
    }
  ]);

  const average = useMemo(() => {
    console.log("calculate average. It takes long time !!");
    return users.reduce((acc, cur) => {
      return acc + cur.score / users.length;
    }, 0);
  }, [users]);

  
   const addUser =() => {
    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={average} />
       <button className="button" onClick={addUser}>
        새 유저 생성
       </button>
      {users.map((user) => {
        return (
          <Item key={user.id} user={user} /> // 아래 코드 참고
        );
      })}
      </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);

React.memo를 적용해서 새 유저 생성 버튼을 눌러 users 배열의 길이를 변화시켜 UserList.jsx를 리렌더링 시키더라도 새로 추가된 Item만 새로 렌더되고 이미 렌더된 Item들은 리렌더링 되지 않는다.


3) useMemo

이 함수는 React Hook 중 하나로서 React에서 CPU 소모가 심한 함수들을 캐싱하기 위해 사용된다.

만약 컴포넌트내의 어떤 함수가 값을 리턴하는데 하나의 변화에도 값을 리턴하는데 많은 시간을 소요한다면 이 컴포넌트가 리렌더링 될 때마다 함수가 호출되면서 많은 시간을 소요하게 될 것이다. 또 그 함수가 리턴되는 값이 자식 컴포넌트에도 사용이 된다면, 그 자식 컴포넌트도 함수가 호출 될 때마다 새로운 값을 받아 리렌더링 된다.

useMemo는 종속 변수들이 변하지 않으면 함수를 굳이 다시 호출하지 않고 이전에 반환한 참조값을 재사용한다. 즉, 함수 호출 시간도 세이브할 수 있고 같은 값을 props로 받는 하위 컴포넌트의 리렌더링도 방지할 수 있다.


4) useCallback

useMemo가 리턴되는 값을 메모이즈 시켜주었는데, useMemo와 비슷한 useCallback은 함수 선언을 메모이즈 하는데 사용된다.

상위 컴포넌트에서 하위컴포넌트로 함수를 props로 넘겨줄 때, 상위 컴포넌트가 리렌더링 될 때마다 상위 컴포넌트 안에 선언된 함수를 새로 생성하기 때문에 그때마다 새 참조 함수를 하위 컴포넌트로 넘겨주게 된다. 이에 따라 하위 컴포넌트도 props가 달라졌으므로 또다시 리렌더링 된다.

그러나 useCallback으로 함수를 선언해주면, 종속 변수들이 변하지 않는 이상 굳이 함수를 재생성하지 않고 이전에 있던 참조 변수를 그대로 하위 컴포넌트에 props로 전달하여, 하위 컴포넌트도 props가 변경되지 않았다고 인지하게 되어 하위 컴포넌트의 리렌더링을 방지할 수 있다.


5) 하위 컴포넌트의 props로 객체를 넘겨주는 경우 새 객체 생성을 주의해야 한다.

하위 컴포넌트의 props값으로 객체를 넘겨주는 경우가 많은데 이 때, 컴포넌트 안에서 생성자 함수나 객체 리터럴 등으로 새로 생성한 객체를 넘겨주는 것을 주의해야 한다.

// 생성자 함수
<Component prop={new Obj("x")} />
// 객체 리터럴
<Component prop={{property: "x"}} />

이렇게 하는 것은 리덕스 스토어나 props 혹은 선언된 state에 참조하는 것이 아니라 새로 생성된 객체가 props에 들어가므로 컴포턴트가 리렌더링 될 때마다 새로운 객체가 생성되어 하위 컴포넌트로 전달되므로, 아무리 렌더링 최적화 기법을 사용해줬다고 하더라도 하위 컴포넌트에 대한 메모이제이션이 되지 않는다. props로 전달한 객체가 동일한 값을 보유하고 있다고 하더라도 새로 생성된 객체는 이전 객체와 다른 참조 주소를 가진 객체이기 때문에 메모이제이션이 통하지 않는 것이다. 따라서 생성자 함수나 객체 리터럴로 객체를 생성해서 하위 컴포넌트로 넘겨주는 방식보다는 state를 그대로 하위컴포넌트에 넘겨주어 필요한 데이터 가공을 그 하위컴포넌트에서 해주는 것이 좋다.


6) 컴포넌트를 매핑할 때 key값으로 index를 사용하지 않는다.

사람들이 보통 컴포넌트를 매핑할 때 key값에 index 값을 많이 넣는다. 리액트에서 매핑을 할 때 반드시 고유 key를 부여하도록 강제하고 있는데, 이렇게 index값으로 key값을 부여하면 어떤 배열에 중간에 어떤 요소가 삽입될 때 그 중간 이후 위치한 요소들은 전부 index가 변경된다. 이로 인해 key값이 변경되고 리마운트가 일어나게 된다. 또 데이터가 key와 매치가 안되어 서로 꼬이는 부작용도 발생한다. 따라서 key값에는 index값이 아니라 고유 id를 넣어주는게 좋다.

좋은 웹페이지 즐겨찾기