리액트 랜더링 최적화

fe_sw·2022년 8월 3일
0
post-thumbnail

리액트 성능최적화 8가지 방법

1. state 선언치

리액트는 특정 state가 변경되면, 그 state가 선언된 컴포넌트와 그 하위 컴포넌트들을 모두 리렌더링 시킨다.

따라서 state가 선언되는 위치를 잘 설계하는 것은 리렌더링 횟수에 엄청난 영향을 끼친다.

기본적으로 state의 선언위치는 이렇다.해당 state를 사용하는 컴포넌트들을 잘 구분해놓은 뒤 그 컴포넌트들 중 가장 최상위 컴포넌트에 선언한다.

만약 그 state를 사용하는 최상위 컴포넌트보다 더 상위 컴포넌트에 state를 선언하면 state를 사용하지 않는 더 많은 컴포넌트들이 state변경에 의해 불필요한 리렌더링을 겪게 됩니다.

예를 들어 다음과 같은 컴포넌트 구조가 있다고 하면,

IndexGroupUserListUserItem

UserList와 UserItem에서만 사용되는 users state가 있다. 이 users state는 UserItem에서 보여줘야 할 데이터들을 가지고 있다. 이 데이터는 두 컴포넌트에서만 사용하기 때문에 그 중 가장 상위 컴포넌트인 UserList에 선언해야 한다.

import { useState } from "react";

import UserItem from "components/section/examples/example1/UserItem";
import Button from "components/atom/Button";

function UserList() {
  console.log("UserList component render");

  const [users, setUsers] = useState([
    {
      id: 0,
      name: "Kim",
      age: 27,
    },
    {
      id: 1,
      name: "Jo",
      age: 25,
    },
  ]);

  const addUser = () => {
    setUsers([
      ...users,
      {
        id: 2,
        name: "Jung",
        age: 30,
      },
    ]);
  };

  return (
    <div>
      <Button
        value="새 유저 생성"
        disabled={users.length >= 3}
        onClick={addUser}
      />
      {users.map(user => {
        return (
          <UserItem
            key={user.id}
            id={user.id}
            name={user.name}
            age={user.age}
          />
        );
      })}
    </div>
  );
}

export default UserList;

그런데 만약 이 users state를 UserList보다 더 상위 컴포넌트인 Index에 선언하면 어떻게 될까?

users state가 변경되면 index.js가 리렌더링되고 그 하위 컴포넌트가 모두 리렌더링 된다. 이에 따라 이전과 다르게 users 데이터를 사용하지 않는 Index컴포넌트와 Group 컴포넌트까지 리렌더링이 발생하게 된다.

2. 객체 타입의 state는 최대한 분할하여 선언

객체가 크고 복잡한 구조인 경우 분할할 수 있는 만큼 최대한 분할하는 것이 좋다.

해당 state에서 일부의 프로퍼티만 사용하는 하위 컴포넌트가 있다면,
그 컴포넌트는 해당 프로퍼티가 변경될 때에만 리렌더링 되는 것이 바람직 하다.

만약 복잡한 객체로 선언된 state를 분할하지 않으면, 하위 컴포넌트가 사용하지 않는 다른 프로퍼티의 값이 업데이트될 때에도 불필요한 리랜더링이 발생한다.

다음과 같은 타입의 state를 가장 상위 컴포넌트인 index.js에 선언할 수 있다.

- index.js
import { useState } from "react";

import Group from "components/section/examples/example3/Group";
import UserList from "components/section/examples/example3/UserList";

function Example3() {
  const [state, setState] = useState({
    group: {
      name: "coco",
      description: "rendering optimization pracitice",
    },
    users: [
      {
        id: 0,
        name: "Kim",
        age: 27,
      },
      {
        id: 1,
        name: "Jo",
        age: 25,
      },
    ],
  });

  return (
    <div>
      <Group group={state.group} />
      <UserList
        users={state.users}
        setUsers={newUsers => {
          setState({ ...state, users: newUsers });
        }}
      />
    </div>
  );
}

export default Example3;

여기서 만약 users 배열에 원소가 하나 추가되면 어떻게 될까?

users데이터를 이용하는 UserList는 리렌더링되어야 한다.
그런데 굳이 users데이터를 이용하지 않는 Group 컴포넌트까지도 state변경으로 인해 리렌더링 될 수 있다.

이번엔 group state와 users state를 나눠서 선언해보면,

- index.js
import { useState } from "react";

import Group from "components/section/examples/example4/Group";
import UserList from "components/section/examples/example4/UserList";

function Example4() {
  const [group] = useState({
    name: "coco",
    description: "rendering optimization pracitice",
  });
  const [users, setUsers] = useState([
    {
      id: 0,
      name: "Kim",
      age: 27,
    },
    {
      id: 1,
      name: "Jo",
      age: 25,
    },
  ]);

  return (
    <div>
      <Group group={group} />
      <UserList users={users} setUsers={setUsers} />
    </div>
  );
}

export default Example4;

이렇게 나눈 후 다시 users 배열에 원소를 하나추가하는 경우 어떻게 될까?

이전과 마찬가지로 users state변화로 인해 index 컴포넌트가 리렌더링되고, 이에 따라 Group컴포넌트는 memo로 감싸면 group배열은 변한게 없으므로 리랜더링이 발생하지 않는다.

3. hooks에서 shouldComponentUpdate를 대체하는 방법

shouldComponentUpdate는 클래스형 컴포넌트에서 리렌더링 여부를 결정하는 로직을 만드는 생명주기 메소드입이다.

이것은 리렌더링을 방지할 수 있기 때문에 렌더링 최적화를 위해서 클래스형 컴포넌트에서 자주 사용되던 메소드 이다.

그러나 함수형 컴포넌트는 생명주기 메소드를 사용할 수 없기 때문에,shouldComponentUpdate를 이용한 리렌더링 방지를 사용할 수 없다.

리액트 공식 문서에서는 shouldComponentUpdate를 구현하는 방법으로 React.memo를 제시하고 있다.부모 컴포넌트가 리랜더링 될 시props가 변경되지 않으면 해당 컴포넌트는 리랜더링 되지않는다.

import { useState } from "react";

import UserItem from "components/section/examples/example5/UserItem";
import Button from "components/atom/Button";

function UserList() {
  console.log("UserList component render");

  const [users, setUsers] = useState([
    {
      id: 0,
      name: "Kim",
      age: 27,
      score: 80,
    },
    {
      id: 1,
      name: "Jo",
      age: 25,
      score: 70,
    },
  ]);

  const addUser = () => {
    setUsers([
      ...users,
      {
        id: 2,
        name: "Jung",
        age: 30,
        score: 90,
      },
    ]);
  };

  return (
    <div>
      <Button
        value="새 유저 생성"
        disabled={users.length >= 3}
        onClick={addUser}
      />
      {users.map(user => {
        return <UserItem key={user.id} user={user} />;
      })}
    </div>
  );
}

export default UserList;
import React from "react";

function UserItem({ user }) {
  console.log(`UserItem (id: ${user.id}) component render`);

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

export default React.memo(UserItem);

4. 컴포넌트를 매핑할 때에는 key값으로 index를 사용x.

리액트에서 컴포넌트를 매핑할 때에는 반드시 고유 key를 부여하도록 강제 하고 있다.

어떤 배열에 중간에 어떤 요소가 삽입되면 그 중간보다 이후에 위치한 요소들은 전부 인덱스가 변경된다.

이로 인해 key값이 변경되고 리마운트가 일어나게 된다.
또한, 데이터가 key와 매치가 안되어 서로 꼬이는 부작용도 발생합니다.

이번에는 UserList에서 UserItem을 매핑할때 key에 users 배열의 index를 넣어주고, 배열의 맨 앞에 원소를 추가하는 버튼을 만들어봅니다.

5. useMemo

만약 컴포넌트 내에 어떤 함수가 값을 리턴하는데 많은 시간을 소요한다면 이 컴포넌트가 리렌더링 될 때마다 함수가 호출되면서 많은 시간을 소요하게 된다.

그리고 그 함수가 반환하는 값을 하위 컴포넌트가 사용한다면 그 하위 컴포넌트는 매 함수호출마다 새로운 값을 받아 리렌더링할 것이다.

useMemo는 종속 변수들이 변하지 않으면 함수를 굳이 다시 호출하지 않고 이전에 반환한 참조값을 재사용한다.

6. useCallback

상위 컴포넌트에서 하위컴포넌트로 함수를 props로 넘겨줄 때 상위 컴포넌트가 리렌더링 될 때마다,

상위 컴포넌트 안에 선언된 함수를 새로 생성하기 때문에 그때마다 새 참조 함수를 하위 컴포넌트로 넘겨주게 된다.

이에 따라 하위 컴포넌트도 props가 달라졌으므로 또다시 리렌더링 하게 된다.

그러나 useCallback으로 함수를 선언해주면 종속 변수들이 변하지 않으면 굳이 함수를 재생성하지 않고

이전에 있던 참조 변수를 그대로 하위 컴포넌트에 props로 전달하여 하위 컴포넌트도 props가 변경되지 않았다고 인지하게 된다.

이에 따라 하위 컴포넌트의 리렌더링을 방지할 수 있다.

7. 하위 컴포넌트에 props로 객체를 넘겨주는 경우 새 객체 생성을 주의

하위 컴포넌트의 props값으로 객체를 넘겨주는 경우가 많이 있다.

이 때에는 컴포넌트 안에서 생성자 함수나 객체 리터럴 등으로 새로 생성한 객체를 넘겨주는 것을 주의해야 합니다.

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

이렇게 하는 것은 리덕스 스토어나 props 혹은 선언된 state에 참조하는 것이 아니라 새로 생성된 객체가 props로 들어가므로 컴포넌트가 리렌더링 될 때마다 새로운 객체가 생성되어 하위 컴포넌트로 전달되므로,

아무리 렌더링 최적화 기법을 사용해줬다고 하더라도 하위 컴포넌트에 대한 메모이제이션이 되지않는다.

props로 전달한 객체가 동일한 값을 보유하고 있다고 하더라도 새로 생성된 객체는 이전 객체와 다른 참조 주소를 가진 객체이기 때문에 메모이제이션이 통하지 않는 것이다.

따라서 생성자 함수나 객체 리터럴로 객체를 생성해서 하위 컴포넌트로 넘겨주는 방식보다는, state를 그대로 하위컴포넌트에 넘겨주어 필요한 데이터 가공을 그 하위컴포넌트에서 해주는 것이 좋다.

0개의 댓글