📖 리액트/리액트를 다루는 기술

11장 컴포넌트 성능 최적화

놀러와요 버그의 숲 2022. 2. 7. 11:13
728x90
반응형

 

이 글은 『리액트를 다루는 기술』(개정판/ 김민준 저 / 길벗 출판사)이라는 책을 참고하여 썼습니다.

 

학습범위: p290~p310

 

새로 알게된 내용

 

1.  useState안에 함수 넣어주기

 

useState(createBulkTodos()) vs useState(createBulkTodos)

 

function createBulkTodos() {
  const array = [];
  for (let i = 1; i <= 2500; i++) {
    array.push({
      id: i,
      text: </span><span class="cd2 co31">할</span> <span class="cd2 co31">일</span> <span class="cd2 co49">${</span><span class="cd2 co33">i</span><span class="cd2 co49">}</span><span class="cd2 co31">,
      checked: false,
    });
  }
  return array;
}



const App = () => {
  const [todos, setTodos] = useState(createBulkTodos);



// 고윳값으로 사용될 id
  // ref를 사용하여 변수 담기
  const nextId = useRef(2501);



(…)
};

 

useState(createBulkTodos())라고 작성하면 리렌더링 될 때마다 createBulkTodos 함수가 호출된다.

useState(createBulkTodos)처럼 파라미터를 함수 형태로 넣어주면

컴포넌트가 처음 렌더링 될 때만 createBulkTodos 함수가 호출된다.

 

 

 

 

2.  React DevTools를 사용하여 성능 측정하는 법

 

 

개발자 도구를 열고 옆에 Profiler를 누르면 성능 측정을 할 수 있다.

동그라미 녹화 버튼을 누르고 기능을 작동시키면 기능이 작동하는데 얼마나 걸리는지를 알려준다.

회색 빗금쳐있는 박스들은 리렌더링 되지 않는 컴포넌트를 나타낸다고한다.

가장 많이 이용하게 될 기능은 녹화버튼, 불꽃모양, 그리고 Ranked chart 이정도이지 않을까 싶다.

 

 

 

 

3. 리렌더링 발생원인

 

1. 자신이 전달받은 props가 변경될 때

2. 자신의 state가 바뀔 때

3. 부모 컴포넌트가 리렌더링 될 때

4. forceUpdate 함수가 실행 될 때 

 

지금 상황을 분석해 보면, ‘할 일 1’ 항목을 체크할 경우 App 컴포넌트의 state가 변경되면서 App 컴포넌트가 리렌더링된다.

부모 컴포넌트가 리렌더링되었으니 TodoList 컴포넌트가 리렌더링되고 그 안의 무수한 컴포넌트들도 리렌더링된다.

‘할 일 1’ 항목은 리렌더링되어야 하는 것이 맞지만, ‘할 일 2’부터 ‘할 일 2500’까지는 리렌더링을 안 해도 되는 상황인데 모두 리렌더링되고 있으므로 이렇게 느린 것이다.

컴포넌트의 개수가 많지 않다면 모든 컴포넌트를 리렌더링해도 느려지지 않는데, 지금처럼 약 2,000개가 넘어가면 성능이 저하된다.

 

 

 

 

4. React.memo를 사용하여 컴포넌트 성능 최적화 

 

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

 

import React from ‘react‘;
import {
  MdCheckBoxOutlineBlank,
  MdCheckBox,
  MdRemoveCircleOutline,
} from ‘react-icons/md‘;
import cn from ‘classnames‘;
import ‘./TodoListItem.scss‘;


const TodoListItem = ({ todo, onRemove, onToggle }) => {
  (…)
};



export default React.memo(TodoListItem);

 

이제 TodoListItem 컴포넌트는 todo, onRemove, onToggle이 바뀌지 않으면 리렌더링을 하지 않는다.

 

 

 

 

 

5. 함수형 업데이트를 통한 함수가 계속 만들어지는 현상 방지

 

간단한 예시

const [number, setNumber] = useState(0);
// prevNumbers는 현재 number 값을 가리킵니다.
const onIncrease = useCallback(
() => setNumber(prevNumber => prevNumber + 1),
[],
);

setNumber(number+1)을 하는 것이 아니라, 위 코드처럼 어떻게 업데이트 할지 정의해준다.

그러면 useCallback을 사용할 때 두 번째 파라미터로 넣는 배열에 number를 넣지 않아도 된다. (왜일까?) 

 

 

 

프로젝트 적용

const onInsert = useCallback(text => {
    const todo = {
      id: nextId.current,
      text,
      checked: false,
    };
    setTodos(todos => todos.concat(todo));
    nextId.current += 1; // nextId 1씩 더하기
  }, []);



const onRemove = useCallback(id => {
    setTodos(todos => todos.filter(todo => todo.id != = id));
  }, []);



const onToggle = useCallback(id => {
    setTodos(todos =>
      todos.map(todo =>
        todo.id === id ? { …todo, checked: !todo.checked } : todo,
      ),
    );
  }, []);

 

예전에 노마드코더가 언젠가 이것의 필요성을 느낄 때가 있을 것이라고 했는데, 이거인것 같다.

그때 setToDo(current => current +1) 이런식으로 했던 기억이 났다.

 

 

 

 

6. useReducer를 사용하여 함수가 계속 만들어지는 현상 방지

 

 

장점: 상태 업데이트 로직을 모아 컴포넌트 바깥에 둘 수 있다.

 

import { useReducer, useState, useRef, useCallback } from "react";
import TodoTemplate from "./components/TodoTemplate";
import TodoInsert from "./components/TodoInsert";
import TodoList from "./components/TodoList";

function createBulkTodos() {
  const array = [];
  for (let i = 1; i <= 2500; i++) {
    array.push({
      id: i,
      text: `할일 ${i}`,
      checked: false,
    });
  }
  return array;
}

function todoReducer(todos, action) {
  switch (action.type) {
    case "INSERT":
      return todos.concat(action.todo);
    case "REMOVE":
      return todos.filter((todo) => todo.id !== action.id);
    case "TOGGLE":
      return todos.map((todo) =>
        todo.id === action.id ? { ...todo, checked: !todo.checked } : todo
      );
    default:
      return todos;
  }
}
// useReducer는 기존 코드를 많이 고쳐야하지만, 상태를 업데이트하는 로직을 컴포넌트 바깥에 둘 수 있는 장점이있다.

function App() {
  const [todos, dispatch] = useReducer(todoReducer, undefined, createBulkTodos);
  // useReducer 사용할때 원래 두 번째 파라미터에 초기 상태를 넣어주어야 한다.
  // 지금은 그 대신 undefined를 넣어주고 세번째 파라미터에 초기상태를 만들어주는 함수 createBulkTodos 넣음
  // 이렇게 하면 컴포넌트가 맨 처음 렌더링 될 때만 createBulkTodos 함수가 호출된다.

  const nextId = useRef(2501);

  console.log(nextId);

  const onInsert = useCallback((text) => {
    const todo = {
      id: nextId.current,
      text,
      checked: false,
    };
    dispatch({ type: "INSERT", todo });
    nextId.current += 1;
  }, []);

  const onRemove = useCallback((id) => {
    dispatch({ type: "REMOVE", id });
  }, []);

  const onToggle = useCallback((id) => {
    dispatch({ type: "TOGGLE", id });
  }, []);

  return (
    <TodoTemplate>
      <TodoInsert onInsert={onInsert} />
      <TodoList todos={todos} onRemove={onRemove} onToggle={onToggle} />
    </TodoTemplate>
  );
}

export default App;

 

 

 

7. 불변성의 중요성

 

기존의 값을 수정하지 않으면서 새로운 값을 만들어내는 것을 불변성을 지킨다고 한다.

 

왜냐하면 업데이트가 필요한 곳에는 아예 새로운 배열 혹은 새로운 객체를 만들기 때문에, React.memo를 사용했을때 props가 바뀌었는지 혹은 바뀌지 않았는지를 알아내서 리렌더링 성능을 최적화해 줄 수 있기 때문이다.

 

불변성이 지켜지지 않으면 객체 내부의 값이 새로워져도 바뀐 것을 감지하지 못한다.

그러면 React.memo에서 서로 비교하여 최적화하는 것이 불가능 할 것이다.

 

const array = [1, 2, 3, 4, 5];


const nextArrayBad = array; // 배열을 복사하는 것이 아니라 똑같은 배열을 가리킵니다.
nextArrayBad[0] = 100;
console.log(array === nextArrayBad); // 완전히 같은 배열이기 때문에 true



const nextArrayGood = […array]; // 배열 내부의 값을 모두 복사합니다.
nextArrayGood[0] = 100;
console.log(array === nextArrayGood); // 다른 배열이기 때문에 false



const object = {
  foo: ‘bar‘,
  value: 1
};



const nextObjectBad = object; // 객체가 복사되지 않고, 똑같은 객체를 가리킵니다.
nextObjectBad.value = nextObjectBad.value + 1;
console.log(object = = = nextObjectBad); // 같은 객체이기 때문에 true



const nextObjectGood = {
  …object, // 기존에 있던 내용을 모두 복사해서 넣습니다.
  value: object.value + 1 // 새로운 값을 덮어 씁니다.
};
console.log(object === nextObjectGood); // 다른 객체이기 때문에 false

 

음..앞으로도 전개 연산자(...)를 많이 보게되겠구나...이 정도 생각만 했다.

내부 값 복사도 해주어야 한다는데...이 부분은 아직 잘 모르겠다.

 

const nextComplexObject = {
  ...complexObject,
  objectInside: {
    ...complexObject.objectInside,
    enabled: false
  }
};
console.log(complexObject === nextComplexObject); // false
console.log(complexObject.objectInside === nextComplexObject.objectInside); // false

이건 나중에 이해해보자...ㅎㅎ 미래의 내가...이해하겠지!

 

 

 

 

 

8. TodoList 컴포넌트 최적화하기

 

import React from "react";
import TodoListItem from "./ToddoListItem";
import styled from "@emotion/styled";

const TodoList = ({ todos, onRemove, onToggle }) => {
  return (
    <Cont>
      {todos.map((todo) => (
        <TodoListItem
          key={todo.id}
          todo={todo}
          onRemove={onRemove}
          onToggle={onToggle}
        />
      ))}
    </Cont>
  );
};

const Cont = styled.div`
  min-height: 320px;
  max-height: 513px;
  overflow-y: auto;
`;

export default React.memo(TodoList);

// APP 컴포넌트에 다른 state가 추가되어 해당 값들이 업데이트 될 때는 TOdoList 컴포넌트가 불필요한
// 리렌더링을 할수도 있다. 그렇기 때문에 지금 React.memo를 사용해서 미리 최적화 해준것이다.

 

 

 

 

9. react-virtualized를 사용한 렌더링 최적화

 

react-virtualized를 사용하면 리스트 컴포넌트에서 스크롤되기 전에 보이지 않는 컴포넌트는 렌더링하지 않고 크기만 차지한다.

 

현재 컴포넌트가 맨 처음 렌더링 될 때 2,500개 컴포넌트 중 2,491개 컴포넌트는 스크롤하기 전에는 보이지 않음에도 렌더링이 

이루어지기에 비효율적이다.

 

이런게 있다는 것만 알아두자! 

 

 

 

 

정리

 

저자님이 다음과 같이 말씀해주셨다.

 

"리액트 컴포넌트의 렌더링은 기본적으로 빠르기 때문에 컴포넌트를 개발할 때 최적화 작업에 대해 너무 큰 스트레스를 받거나 모든 컴포넌트에 일일이 React.memo를 작성할 필요는 없습니다. 단, 리스트와 관련된 컴포넌트를 만들 때 보여 줄 항목이 100개 이상이고 업데이트가 자주 발생한다면, 이 장에서 학습한 방식을 사용하여 꼭 최적화하길 바랍니다."

 

 

이 말씀이 어찌나 나를 안도하게 만드시는지...

 

느낀점

 

이번 장을 공부하면서, 아직은 내가 성능 최적화에 그렇게 관심이 많지 않다는 것을 알게되었다.

왜냐하면 아직 나의 레벨에서는 기능 구현하기도 벅차기 때문이다.

훗날 내가 조금 더 개발 실력이 향상된 이후에는 아마 더 관심있게 볼 것이다.

지금은 솔직히 그렇게 흥미가가지는 않는다. 그냥 이런것이 있구나. 나중에 최적화 필요할 때 써봐야지... 이정도? 

지금 상황에서 내가 받아들 일 수 있는 정도는 다음과 같다.

 

1. 함수를 props로 전달할 때 useCallback으로 해주는 것이 좋다

 

2. React.memo를 통해 props가 바뀌지 않았다면 리렌더링 시키지 않을 수 있다.

 

3. 함수형 업데이트를 통해 함수가 계속 만들어지는 것을 방지하자.