본문 바로가기
Frontend/ReactJS(완)

React - 11장 : 컴포넌트 성능 최적화

by VictorMeredith 2023. 2. 18.

<리액트를 다루는 기술>

React - 1장 : React 이해

React - 2장 : JSX

React - 3장 : 컴포넌트

React - 4장 : 이벤트 핸들링

React - 5장 : ref. DOM에 이름 달기 

React - 6장 : 컴포넌트 반복
React - 7장 : 컴포넌트의 LifeCycle
React - 8장 : React Hooks 총정리

React - 9장 : 컴포넌트의 스타일링

React - 10장 : 빠르게 TODO앱 실습

React - 11장 : 컴포넌트 성능 최적화(현재)

 

지난 10장에 이어서 같은 프로젝트를 이용해서 설명하므로, 10장의 프로젝트를 만든 다음에 실습해보자.

11.1 많은 데이터 렌더링해보기

- 실제로 Lag을 경험할 수 있또록 많은 데이터를 렌더링 해보자.

 

App.js

import {useState, useRef, useCallback} from 'react';
import TodoTemplate from './components/TodoTemplate';
import TodoInsert from './components/TodoInsert';
import TodoList from './components/TodoList';
import './App.css';

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

function App() {
  const [todos, setTodos] = useState(createBulkTodos)
  // usetState(createBulkTodos()) 라고 작성하면 리렌더링 될때마다 함수가 호출되지만,
  // useState(createBulkTodos) 라고 작성하면 컴포넌트가 처음 렌더링 될때만 함수가 실행된다.

  const nextId = useRef(5001);

/// ...

export default App;

- 5000개의 할일을 만들었으므로 개느려졌다.

 

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

- React DevTools를 이용하여 측정하면 된다.

- 크롬 확장프로그램 깔고 Profiler 탭을 연다.

크롬 확장프로그램 React DevTools

 

- 녹화버튼을 누르고 할 일 아무거나 체크한 다음 화면에 변화가 반영되면 녹화버튼을 한 번 더 눌러 녹화를 끝낸다.

녹화 끝낸 후

- 우측의 Render duration은 리렌더링에 소요된 시간.

- 변화를 화면에 반영하는 데 423.4ms 가 걸렸다.

- 이번엔 상단의 Ranked chart 아이콘을 눌러본다

끔찍한 렌더링

- 초록색 아무거나 누르고 내려가보면서 확인해보면 더 자세히 확인이 가능하다.

- 이를 보면 변화를 일으킨 컴포넌트랑 관계없는 컴포넌트들도 리렌더링 된 것을 확인 할 수 있다.

11.3 느려지는 원인 분석

- 컴포넌트는 다음과 같은 상황에서 리렌더링이 발생한다.

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

 2) 자신의 state가 바뀔 때

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

 4) forceUpdate 함수가 실행될 때

- 지금 상황에서는, 할 일 아무거나 체크할 경우 App컴포넌트의 state가 변경되면서  App컴포넌트가 리렌더링된다. 부모 컴포넌트가 리렌더링 되었으니 TodoList 컴포넌트가 리렌더링되고, 그 안의 무수한 컴포넌트들도 리렌더링된다.

- 리렌더링이 불필요할 경우 리렌더링 방지 방법을 알아보자.

 

11.4 React.memo 

- 컴포넌트의 props가 바뀌지 않았다면, 리렌더링하지 않도록 설정할 수있다.

- 컴포넌트를 만들고나서 감싸 주기만 하면 된다!

 

TodoListItem.jsx

import React from 'react';

//... 

export default React.memo(TodoListItem);

- 근데 이게 끝이 아니다

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

- 현재 프로젝트에서는 todos 배열이 업데이트되면 onRemove와 onToggle함수도 새롭게 바뀌므로, 최적화가 끝나지 않는다.

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

- 방지하는 방법은 useState의 함수형 업데이트 기능을 쓰거나 useReducer를 사용하면 된다.

11.5.1 useState의 함수형 업데이트

- 기존에 setTodos 함수에서는 새로운 상태를 파라미터로 넣어주었는데, 새로운 상태 대신 상태 업데이트를 어떻게 할지 정의해주는 업데이트 함수를 넣을 수도 있다.

- 이를 함수형 업데이트라고 한다.

예시.jsx

const [number, setNumber] = useState(0);
//prevNumbers는 현재 number값
const onIncrease = useCallback(
    ()=> setNumber(prevNumber => prevNumber + 1),
    []
);

- setNumber(number+1) 대신 어떻게 업데이트할지 정의해주는 함수를 넣어준다. 그러면 useCallback을 사용할 때 두번째 파라미터로 넣는 배열에 numbers를 넣지 않아도 된다.

- App.js 를 수정해보자

 

App.js

import {useState, useRef, useCallback} from 'react';
import TodoTemplate from './components/TodoTemplate';
import TodoInsert from './components/TodoInsert';
import TodoList from './components/TodoList';
import './App.css';

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

function App() {
  const [todos, setTodos] = useState(createBulkTodos)
  // usetState(createBulkTodos()) 라고 작성하면 리렌더링 될때마다 함수가 호출되지만,
  // useState(createBulkTodos) 라고 작성하면 컴포넌트가 처음 렌더링 될때만 함수가 실행된다.

  const nextId = useRef(5001);

  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((e)=>{
          return(
            e.id === id ? {...e, checked : !e.checked} : e
          )
        })
      )
    },
    []
  )

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

export default App;

- onInsert, onRemove, onToggle 에서 setState함수들에 콜백으로 todos => 이거만 추가해주면 되더라

- 다시 React DevTools로 확인해보면,

많이 줄었다

- Render duration이 15.1ms 로 엄청나게 줄었다.

11.5.2 useReducer 사용하기

- useReduer를 사용해도 onToggle과 onRemove가 계속 새로워지는 문제를 해결할 수 있다.

 

App.js

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

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

function todoReducer(todos, action){
  switch(action.type){
    case 'INSERT': //{type: 'INTERT', 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: 'TOGGLE', id:1}
      return todos.map(todo => todo.id === action.id ? {...todo, checked: !todo.checked} : todo)
    default:
      return todos;
  }
}

function App() {
  const [todos, dispatch] = useReducer(todoReducer, undefined, createBulkTodos);
  // usetState(createBulkTodos()) 라고 작성하면 리렌더링 될때마다 함수가 호출되지만,
  // useState(createBulkTodos) 라고 작성하면 컴포넌트가 처음 렌더링 될때만 함수가 실행된다.

  const nextId = useRef(5001);

  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 (
    <div className="App">
      <TodoTemplate>
        <TodoInsert onInsert={onInsert}/>
        <TodoList todos={todos} onRemove={onRemove} onToggle={onToggle} />
      </TodoTemplate>
    </div>
  );
}

export default App;

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

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

- 이렇게 하면 컴포넌트가 맨 처음 렌더링될 때만 createBulkTodos 함수가 호출된다.

- useReducer는 코드가 귀찮아지긴 하지만 상태업데이트 로직을 컴포넌트 바깥에 둘 수 있다는 장점이 있다.

 

11.6 불변성의 중요성

- 불변성을 지킨 예시함수를 잠깐 살펴보면,

  const example = ()=>{
    const newArr = [...stateSomethinig];
    newArr[0] = '새로운요소'
    setStateSomething(newArr);
  }

- 이와 같이 데이터를 수정할 때 직접 수정하지 않고, 새로운 배열을 만든 다음 필요한 부분을 교체해서 수정하는 방식을 사용한다.

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

- 하지만 Spread Operator는 얕은 복사이므로, 깊이가 깊은 배열/객체는 복사되지 못한다. 

- 배열/객체에서 구조가 복잡해진다면 불변성을 유지하면서 업데이트하는 것이 까다롭고 귀찮아진다.

- 이럴땐 immer 라이브러리를 써보자. (12장 내용)

 

11.7 TodoList 컴포넌트 최적화하기

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

 

TodoList.jsx

import React from "react";

//...

export default React.memo(TodoList);

- 지금 프로젝트에서는 상관없지만, 미리 해준 것.

- 책에는 react-visualized를 사용한 렌더링최적화에 대해 이야기하는데, 이는 react17 이상부터 버전 호환성 이슈가 있다.

- react-window를 사용해보자. 리스트 가상화를 통한 렌더링최적화의 기능을 동일하게 가지고 있다.

댓글