상세 컨텐츠

본문 제목

[리액트스타터2] 11장. 컴포넌트 성능 최적화

22-23/22-23 리액트 스타터 2

by mk020 2022. 12. 29. 10:00

본문

728x90

데이터가 무수히 많아지면 앱 속도가 느려지는 것을 체감할 정도의 지연이 발생한다.

 

=> App 컴포넌트를 다음과 같이 수정하여 데이터를 추가하면 랙(lag)이 발생한다. createBulkTodos 함수를 만들어 2500개의 데이터를 자동 생성한다.

//App.js

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);
  
  const nextId = useRef(2501);
  
  (...)
  };
  
  export default App;

주의할 점은 useState의 기본값에 함수를 넣어 주었다는 것이다.

useState( createBulkTodos() ) : 리렌더링될 때마다 createBulkTodos() 함수 호출된다

vs

useState( createBulkTodos ) : 이처럼 파라미터를 함수 형태로 넣어주면, 컴포넌트가 처음 렌더링될 때만 createBulkTodos 함수가 실행된다

 

=> React DevTools를 사용하면 정확히 몇 초 걸리는지 측정이 가능하다. 개발자 도구의 Profiler 탭을 열어, 좌측 상단의 파란색 녹화 버튼을 누르고 항목을 체크한 뒤, 다시 녹화 버튼을 누르면 된다.

Render Duration은 리렌더링에 소요된 시간을 의미하며, 현재 325.3ms가 소요되었다. 랭크 차트 아이콘을 클릭하면 아래와 같이 리렌더링된 컴포넌트를 오래 걸린 순으로 정렬해 나열해준다.

이를 통해 현재 프로그램에서는 변화를 일으킨 컴포넌트와 관계없는 컴포넌트도 리렌더링되었음을 확인할 수 있다.

 

=>컴포넌트의 리렌더링이 발생하는 경우 

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

 

현재 프로그램에서는 부모 컴포넌트인 App 컴포넌트 리런테링 시, TodoList 컴포넌트와 그 안의 컴포넌트 모두 리렌더링된다. 이렇게 되면 불필요한 컴포넌트가 리렌더링되기 때문에, 리렌더링 방지( = 성능 최적화 작업 )가 필요하다. 

 

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

=> shouldComponentUpdate라는 라이프사이클을 사용하여 컴포넌트 리렌더링을 방지할 수 있다. 그러나, 함수 컴포넌트에서는 이 메서드를 사용할 수 없고, 그 대신에 React.memo 함수를 사용한다. 이를 통해 컴포넌트의 props가 바뀌지 않았다면, 리렌더링을 하지 않도록 설정하여 성능을 최적화할 수 있다. 컴포넌트를 만들고 나서 감싸주기만 하면 된다.

//TodoListItem.js

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이 바뀌지 않으면 리렌더링하지 않는다.

 

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

=> onToggle, onRemove 함수는 배열 상태 업데이트 과정에서 최신 상태의 todos를 참조하기 때문에, 현재 todos 배열이 업데이트되면, 두 함수는 새롭게 바뀐다.

이렇게 함수가 계속 만들어지는 상황을 방지하는 방법은 2가지이다.

기존에 setTodos 함수에서는 새로운 상태를 파라미터로 넘겨 주었다. 그 대신에 상태 업데이트를 어떻게 할지 정의해주는 업데이트 함수를 넣을 수 있고, 이것을 함수형 업데이트라고 부른다. 이 방법을 사용하는 경우, useCallback에서도 두 번째 파라미터로 넣는 배열에 todos를 넣지 않아도 된다.

//App.js
(...)
const onInsert = useCallback(
    text => {
      const todo = {
        id: nextId.current,
        text,
        checked: false,
      };
      setTodos(todos=> todos.concat(todo));
      nextId.current+=1;
    }, []);
  (...)
  
  export default App;

setTodos를 사용할 때 그 안에 todos => 만 앞에 넣어주면 된다.

렌더링 소요 시간이 12ms으로 줄어든 것을 확인할 수 있다.

useReducer를 통해서도 같은 문제를 해결할 수 있다.

//App.js

(...)

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;
  }
}

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

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

(...)

export default App;

useReducer를 사용할 때는 원래 2번째 파라미터에 초기 상태를 넣어주어야 하는데, 여기서는 undefined를 넣고, 3번째 파라미터에 초기 상태를 만들어주는 함수인 createBulkTodos를 넣어주었다. 이렇게 하면, 컴포넌트가 맨 처음 렌더링될 때만 createBulkTodos 함수가 호출된다.

 

VI. 불변성의 중요성

=> 컴포넌트에서 상태를 업데이트할 때 불변성을 지키는 것은 매우 중요하다. 불변성을 지킨다는 것은 기존의 값은 직접 수정하지 않으면서 새로운 값을 만들어 내는 것을 의미한다. 배열 또는 객체의 구조가 복잡해지면 불변성을 유지하며 업데이트하는 것이 까다로워지기 때문에 , 이런 경우 immer 라이브러리의 도움을 받아 편하게 작업할 수 있다.

 

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

=> 리스트 관련 컴포넌트를 최적화할 때는 리스트 내부에서 사용하거나 리스트로 사용되는 컴포넌트 자체 모두를 최적화해주는 것이 좋다. 즉, 리스트 아이템과 리스트, 이 2가지 모두 최적화해주어야 한다. 그러나 내부 데이터가 많지 않거나 업데이트가 자주 발생하지 않는다면, 이런 최적화 작업을 반드시 해 줄 필요는 없다.

 

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

=> 스크롤되기 전에 보이지 않는 컴포넌트는 렌더링하지 않고, 스크롤되면 보이는 컴포넌트를 자연스럽게 렌더링시킨다.

yarn add react-virtualized 명령어를 사용해 이를 설치하고, react-virtualized에서 제공하는 List 컴포넌트를 사용하여 TodoList 컴포넌트의 성능을 최적화한다.

최적화를 수행하려면 먼저 항목의 실제 크기를 px 단위로 알아야 한다. ( 크롬 개발자 도구 이용 )

첫 번째 항목은 테두리가 없기 때문에 두 번째 항목을 이용해 크기를 측정한다.

크기를 알아낸 후, TodoList 컴포넌트를 다음과 같이 수정한다.

//TodoList.js

import React, {useCallback} from "react";
import { List } from 'react-virtualized';
import TodoListItem from "./TodoListItem";
import './TodoList.scss';

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' }}
    />
  );
};

export default React.memo(TodoList);

List 컴포넌트를 사용하기 위해 rowRenderer 함수를 작성했고, index, key, style 값을 객체 타입으로 받아와서 파라미터에 사용했다. List 컴포넌트를 사용할 때는 배열, 항목, 렌더링할 때 쓰는 함수(rowRenderer), 전체 크기, 각 항목 높이 등을 props로 넣어주어야 한다.

TodoList를 저장하고 나면 스타일이 깨져서 나타나는데, 이는 TodoListItem 컴포넌트를 다음과 같이 수정하면 해결된다.

render 함수에서 기존에 보여주던 내용을 div로 한 번 감싸고 props로 받아온 style을 적용시킨다.

//TodoListItem.js

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

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);

2.2ms까지 성능이 최적화된 것을 확인할 수 있다.

Quiz

 

Q1) 리액트에서는 ( ----- -------- )를 사용하여 정확한 성능 분석을 할 수 있다.

Q2) 컴포넌트에서 리렌더링이 발생하는 경우는 4가지로, 자신이 전달받은 ( ---- )가 변경될 때, 자신의 ( ----- )가 바뀔 때, ( -- ---- )가 리렌더링될 때, ( ----------- ) 함수가 실행될 때이다.

Q3) 컴포넌트의 리렌더링을 방지할 때는 ( ------------------- )라는 라이프사이클을 사용하면 된다.

Q4) 함수 컴포넌트에서는 라이프사이클 메서드를 사용할 수 없기 때문에, 대신 ( ----------)라는 함수를 사용한다.

Q5) onToggle, onRemove 함수는 배열 상태 업데이트 과정에서 최신 상태의 todos를 참조하기 때문에, 현재 todos 배열이 업데이트되면, 두 함수는 새롭게 바뀐다. 이렇게 함수가 계속 만들어지는 상황을 방지하는 방법은 2가지로, ( -------- )의 함수형 업데이트 기능을 사용하는 방법과 ( ---------- )을 사용하는 것이다.

Q6) useReducer를 사용할 때는 원래 두 번째 파라미터에 ( -- -- )를 넣어 주어야 한다.

Q7) ( --- )을 지킨다는 것은 기존의 값은 직접 수정하지 않으면서 새로운 값을 만들어 내는 것을 의미한다.

Q8) 아래의 TodoListItem 컴포넌트에 Recat.memo를 적용하는 코드를 작성하시오.

//TodoListItem.js

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 TodoListItem;

Q9) react-virtualized를 사용한 렌더링 최적화 방법에서 일정 관리 앱의 TodoList 컴포넌트를 아래와 같이 수정하는 경우 스타일이 깨져서 나타난다. 이를 해결하기 위해 아래의 TodoListItem 컴포넌트를 수정하시오.

//TodoList.js
import React, {useCallback} from "react";
import { List } from 'react-virtualized';
import TodoListItem from "./TodoListItem";
import './TodoList.scss';

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' }}
    />
  );
};

export default React.memo(TodoList);
//TodoListItem.js

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

const TodoListItem = ({todo, onRemove, onToggle, style}) => {
  const { id, text, checked } = todo;
  return (
      <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>
  );
};

export default React.memo(TodoListItem);

A1) React DevTools

A2) props, state, 부모 컴포넌트, forceUpdate

A3) shouldComponentUpdate

A4) React.memo

A5) useState, useReducer

A6) 초기 상태

A7) 불변성

A8 ) 예시 코드

//TodoListItem.js

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);

A9 ) 예시 코드

//TodoListItem.js

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

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);

 

 엘리

728x90

관련글 더보기