본문 바로가기

react

[리액트를 다루는 기술] 11장 컴포넌트 성능 최적화

11.1 많은 데이터 렌더링하기 

 

- createBulkTodos 라는 함수를 만들어서 데이터 2,500개를 자동으로 생성 

- useStaet(createBulkTodos)처럼 파라미터를 함수 형태로 넣어주면 컴포넌트가 처음 렌더링될 때만 createBulkTodos 함수가 실행됨 

 

App.js

import {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;
}
const App = () => {
    const [todos, setTodos] = useState(createBulkTodos);

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

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

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

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

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

export default App;

 

 

 

11.2 크롬 개발자 도구를 통한 성능 모니터링

 

- React DevTools를 사용하여 측정 

 

- Render duration은 리렌더링에 소요된 시간을 의미 

- 변화를 일으킨 컴포넌트랑 관계없는 컴포넌트들도 리렌더링됨 

 

 

11.3 느려지는 원인 분석 

 

- 리렌더링 발생 상황 

 

 1) 자신이 전달받은 props가 변경될 떄 

 2) 자신의 state가 바뀔 떄

 3) 부모 컴포넌트가 리렌더링될 떄 

 4) forceUpdate 함수가 실행될 떄 

 

- '할일 1' 항목을 체크할 경우 App 컴포넌트의 state가 변경되면서 App 컴포넌트가 리렌더링됨 

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

- '할일 2'부터 할일 2500'까지는 리렌더링을 안 해도 되는 상황인데 모두 리렌더링되고 있으므로 느린 것 

 

 

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

 

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

 

- TodoListItem 컴포넌트에 React.memo 적용 

 - todo, onRemove, onToggle이 바뀌지 않으면 리렌더링하지 않음 

 

TodoListItem.js

...
export default React.memo(TodoListItem);

 

 

11.5 onToggle, onRemove 함수가 바뀌지 않게 하기 

 

- onRemove, onToggle 함수는 배열 상태를 업데이트 하는 과정에서 최신 상태의 todos를 참조하기 때문에 todos 배열이 바뀔 때마다 함수가 새로 만들어짐 

 

11.5.1 useStaet의 함수형 업데이트 

 

- 기존에 setTodos 함수를 사용할 때는 새로운 상태를 파라미터로 넣어 줌 

- setTodos를 사용할 때 새로운 상태를 파라미터로 넣는 대신, 상태 업데이트를 어떻게 할지 정의해 주는 업데이트 함수를 넣을 수 있음 

 

ex)

const [number, setNumber] = useState(0);

//prevNumbers는 현재 number의 값을 가리킴
const onIncrease = useCallback(
  () =>setNumber(prevNumber =>prevNumber + 1),
  [],
);

 

- setNumber(number+1)을 하는 것이 아니라 , 위 코드처럼 어떻게 업데이트할지 정의해주는 업데이트 함수를 넣어 줌 

 -> useCallback을 사용할 때 두 번째 파라미터로 넣는 배열에 number을 넣지 않아도 됨 

 

App.js

 

- onToggle, onRemove 함수에서 useState의 함수형 업데이트를 사용 

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

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

        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,),
                );
            },
            [],
        );
...

 

 

11.5.2 useReducer 사용하기 

 

- 리듀서는 현재 상태, 그리고 업데이트를 위해 필요한 정보를 담은 액션(action)값을 전달받아 새로운 상태를 반환하는 함수 

- useReducer를 사용할 떄는 원래 두 번째 파라미터에 초기 상태를 넣어 주어야 함 

- 지금은 그 대신 두 번째 파라미터에 undefined를 넣고, 세 번째 파라미터에 초기 상태를 만들어 주는 함수인 createBulkTodos를 넣어줌

=> 컴포넌트가 맨 처음 렌더링될 때만 createBulkTodos 함수가 호출됨 

 

- useReducer를 사용하면 상태를 업데이트하는 로직을 모아서 컴포넌트 바깥에 둘 수 있음 

 

App.js

import React, { useReducer, 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': // 새로 추가
      // { type: 'INSERT', todo: { id: 1, text: 'todo', checked: false } }
      return todos.concat(action.todo);
    case 'REMOVE': // 제거
      // { type: 'REMOVE', id: 1 }
      return todos.filter(todo => todo.id !== action.id);
    case 'TOGGLE': // 토글
      // { type: 'REMOVE', id: 1 }
      return todos.map(todo =>
        todo.id === action.id ? { ...todo, checked: !todo.checked } : todo,
      );
    default:
      return todos;
  }
}

const App = () => {
  const [todos, dispatch] = useReducer(todoReducer, undefined, createBulkTodos);

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

  const onInsert = useCallback(text => {
    const todo = {
      id: nextId.current,
      text,
      checked: false,
    };
    dispatch({ type: 'INSERT', todo });
    nextId.current += 1; // nextId 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;

 

 

11.6 불변성의 중요성 

 

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

 

ex)

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

 

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

  -> React.memo에서 서로 비교하여 최적화하는 것이 불가능 

 

- 전개 연산자 (... 문법)를 사용하여 객체나 배열 내부의 값을 복사할 때는 얕은 복사(shallow copy)를 하게 됨 

 -> 내부의 값이 완전히 새로 복사되는 것이 아니라 가장 바깥쪽에 있는 값만 복사 

 -> 내부의 값이 객체 혹은 배열이라면 내부의 값 또한 따로 복사해줘야 함

 

ex)

const todos = [{id:1, checked: true}, {id:2, checked:true}];
const nextTodos = [...todos];

nextTodos[0].checked = false;
console.log(todos[0] === nextTodos[0]); //아직까지는 똑같은 객체를 가리키고 있기 때문에 true

nextTodos[0] = {
    ...nextTodos[0],
    checked:false
};

console.log(todos[0] === nextTodos[0]); //새로운 객체를 할당해 주었기에 false

 

 

- 객체 안에 있는 객체라면 불변성을 지키면서 새 값을 할당해야 함

ex)

const nextComplexObject = {
    ...complexObject,
    objectInside: {
        ...complexObject.objectInside,
        enabled:false
    }
};

console.log(complexObject === nextComplexObject); //false
console.log(complexObject.objectInside === nextComplexObject.objectInside); //false

 

 

11.7 TodoList 컴포넌트 최적화하기 

 

- 리스트에 관련된 컴포넌트를 최적화할 때는 리스트 내부에서 사용하는 컴포넌트도 최적화해야 하고, 리스트로 사용되는 컴포넌트 자체도 최적화해 주는 것이 좋음 

 

 

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

 

- react-virtualized를 사용하면 리스트 컴포넌트에서 스크롤되기 전에 보이지 않는 컴포넌트는 렌더링하지 않고 크기만 차지하게끔 할 수 있음

- 만약 스크롤되면 해당 스크롤 위치에서 보여 주어야 할 컴포넌트를 자연스럽게 렌더링 시킴

- 이 라이브러리를 사용하면 낭비되는 자원을 쉽게 아낄 수 있음

 

 

11.8.1 최적화 준비 

 

- npm 설치 

npm install react-virtualized --save

 

- react-virtualized에서 제공하는 List 컴포넌트를 사용하여 TodoList 컴포넌트의 성능을 최적화 

- 최적화를 수행하려면 각 항목의 실제 크기를 px 단위로 알아내야 함 

 

 

11.8.2 TodoList 수정 

 

- List 컴포넌트를 사용하기 위해 rowRenderer라는 함수를 새로 작성 

- rowRenderer 함수는 react-virtualized의 List 컴포넌트에서 각 TodoItem을 렌더링할 때 사용하며, 이 함수를 List 컴포넌트의 props로 설정해 주어야 함 

- roRenderer 함수는 파라미터에 index, key, style 값을 객체 타입으로 받아와서 사용 

 

- List 컴포넌트를 사용할 때는 해당 리스트의 전체 크기각 항목의 높이, 각 항목을 렌더링할 때 사용해야 하는 함수, 그리고 배열을 props로 넣어주어야 함 

- 이 컴포넌트가 전달받은 props를 사용하여 자동으로 최적화해 줌 

 

TodoList.js

const TodoList = ({ todos, onRemove, onToggle }) => {
  const rowRenderer = useCallback(
    ({ index, key, style }) => {
      const todo = todos[index];
      return (
        <TodoListItem
          todo={todo}
          key={key}
          onRemove={onRemove}
          onToggle={onToggle}
          style={style}
        />
      );
    },
    [onRemove, onToggle, todos],
  );
  return (
    <List
      className="TodoList"
      width={512} // 전체 크기
      height={513} // 전체 높이
      rowCount={todos.length} // 항목 개수
      rowHeight={57} // 항목 높이
      rowRenderer={rowRenderer} // 항목을 렌더링할 때 쓰는 함수
      list={todos} // 배열
      style={{ outline: 'none' }} // List에 기본 적용되는 outline 스타일 제거
    />
  );
};

export default React.memo(TodoList);

 

 

11.8.3 TodoListItem 수정 

 

TodoListItem.js

const TodoListItem = ({ todo, onRemove, onToggle, style }) => {
  const { id, text, checked } = todo;

  return (
    <div className="TodoListItem-virtualized" style={style}>
      <div className="TodoListItem">
        <div
          className={cn('checkbox', { checked })}
          onClick={() => onToggle(id)}
        >
          {checked ? <MdCheckBox /> : <MdCheckBoxOutlineBlank />}
          <div className="text">{text}</div>
        </div>
        <div className="remove" onClick={() => onRemove(id)}>
          <MdRemoveCircleOutline />
        </div>
      </div>
    </div>
  );
};

export default React.memo(TodoListItem);

 

- render 함수에서 기존에 보여 주던 내용을 div로 한 번 감싸고, 해당 div에는 TodoListItem-virtualized라는 className을 설정하고, props로 받아온 style을 적용 시켜 줌