상세 컨텐츠

본문 제목

<리액트를 다루는 기술> 11장: 컴포넌트 성능 최적화

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

by dev otcroz 2022. 1. 17. 13:00

본문

728x90

INDEX

11장 컴포넌트 성능 최적화

1. 많은 데이터 렌더링하기

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

3. 느려지는 원인 분석

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

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

6. 불변성의 중요성

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

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

9. Question 개념 정리 및 코드 문제

 


컴포넌트 성능 최적화

많은 데이터 렌더링하기

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

->React.memo를 통한 컴포넌트 리렌더링 성능 최적화

->onToggle과 onRemove가 새로워지는 현상 방지하기

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


1. 많은 데이터 렌더링하기

랙을 경험해 볼 수 있도록 많은 데이터 추가해본다.

import React, { 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);

  const nextId=useRef(2501);

  const onInsert=useCallback(
    text=>{
      const todo={
        id: nextId.current,
        text,
        checked: false,
      };
      setTodos(todos.concat(todo));
      nextId.current += 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;

 


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

성능을 분석할 때는 느낌만으로 할 수 없다.

->정확히 몇 초가 걸렸는지 확인해야 한다.

->크롬 개발자 도구의 Performance 탭 이용

녹화 버튼을 누르고 '할 일1' 항목을 체크한 다음 화면 변화가 생기면 Stop버튼을 누른다.

Profiler을 보면 각 시간대에 컴포넌트의 어떤 작업이 처리되었는지 알 수 있다.

커서를 올리면 실행에 걸리는 시간을 알 수 있다.

0.0015초가 걸린다(성능이 좋지 않다)


3. 느려지는 원인 분석

컴포넌트에서 리렌더링이 발생하는 상황

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

2. 자신의 state가 바뀔 때

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

4. forceUpdate 함수가 실행될 때


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

컴포넌트의 리렌더링을 방지할 때

-shouldComponentUpdate라는 라이프사이클을 사용한다.(함수형 컴포넌트에서는 라이프사이클 메서드 사용 불가-대신 React.memo라는 함수 사용)

-React.memo사용법(컴포넌트를 만들고 감싸 주기만 하면 된다.)

 

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

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

-이제 todo, onRemove, onToggle이 바뀌지 않으면 렌더링하지 않는다.


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

함수가 계속 만들어지는 상황을 방지하는 방법

1. useState의 함수형 업데이트 기능을 사용

2. useReducer사용

 

5.1. useState의 함수형 업데이트

함수형 업데이트

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

const [number, setNumber]=useState(0);
const onIncrease = useCallback(
    ()=>setNumber(prevNumber=>prevNumber+1),
    [],
);

-useCallback을 사용할 때 두 번째 파리미터로 넣는 배열에 number을 넣지 않아도 된다.

import React, { 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);

  const nextId=useRef(4);

  const onInsert=useCallback(
    text=>{
      const todo={
        id: nextId.current,
        text,
        checked: false,
      };
      setTodos(todos=>todos.concat(todo));
      nextId.current += 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,),
      );
    },
    []
  );

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

export default App;

0.0004초로 시간이 줄어들었다.(성능이 좋아졌다)

 

**프로덕션 모드로 구동해 보고 싶다면..

$ yarn build
$ yarn global add serve
$ serve -s build

5.2 useReducer 사용하기

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

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

export default App;

useReducer를 사용하는 방법은 기존 코드를 많이 고친다는 단점이 있다.

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


6. 불변성의 중요성

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

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

const nextArrayBad=array;
nextArrayBad[0]=100;
console.log(arrray===nextArrayBad);

const nextArrayGood = [...array];
nextArrayGood[0]=100;
console.log(array===nextArrayGood);

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

const nextObjectBad=object;
nextObjectBad.value=nextObjectBad.value+1;
console.log(object===nextObjectBad);

const nextArrayGood={
  ...object,
  value: object.value+1;
};
console.log(object===nextObjectGood);

 

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

nextTodos[0].checked=false;
console.log(todos[0]===nextTodos[0]);

nextTodos[0]={
  ...nextTodos[0],
  checked: false
};
console.log(todos[0]===nextTodos[0]);

 

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

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

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

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

 

import React from 'react';
import TodoListItem from './TodoListItem';
import './TodoList.scss';

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

export default React.memo(TodoList);

현재 프로젝트 성능에는 영향이 없다

-todos배열이 업데이트 될 때만 리렌더링되기 때문이다.

 

**리스트 관련 컴포넌트를 작성할 때는 리스트 아이템과 리스트, 두 가지 컴포넌트를 반드시 최적화

-내부 데이터가 100개를 넘지 않거나 업데이트가 자주 발생한다면 최적화 작업을 반드시 할 필요는 없다.


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

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

8.1. 최적화 준비

$ yarn add react-virtualized

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

각 항목의 실제 크기를 px단위로 알아내야 한다.(개발자도구 사용)

가로 495.33px 세로 56px이다.

**꼭 두번째 항목으로 확인해야 한다. 첫번째 항목은 테두리가 없기 때문에 56px이다.

8.2. TodoList 수정

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={495.33}
        height={496.33}
        rowCount={todos.length}
        rowHeight={57}
        rowRenderer={rowRenderer}
        list={todos}
        style={{ outline: 'none' }}
        />
    );
};

export default React.memo(TodoList);

 

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

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

 

 

8.3. TodoListItem 수정

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,
    (prevProps, nextProps)=>prevProps.todo===nextProps.todo,);

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

.TodoListItem {
  padding: 1rem;
  display: flex;
  align-items: center;
  &:nth-child(even) {
    background: #f8f9fa;
  }
  .checkbox {
    cursor: pointer;
    flex: 1;
    display: flex;
    align-items: center;
    svg {
      font-size: 1.5rem;
    }
    .text {
      margin-left: 0.5rem;
      flex: 1;
    }

    &.checked {
      svg {
        color: #22b8cf;
      }
      .text {
        color: #adb5bd;
        text-decoration: line-through;
      }
    }
  }
  .remove {
    display: flex;
    align-items: center;
    font-size: 1.5rem;
    color: #ff6b6b;
    cursor: pointer;
    &:hover {
      color: #ff8787;
    }
  }
  & + & {
    border-top: 1px solid #dee2e6;
  }
}

.TodoListItem-virtualized {
  & + & {
    border-top: 1px solid #dee2e6;
  }
  &:nth-child(even) {
    background: #f8f9fa;
  }
}

0.0002초로 줄어들었다(성능이 좋아졌다)


9. Question 개념 정리 및 코드 문제

● 개념 복습 문제

1. 컴포넌트에서 리렌더링이 발생하는 상황은 자신이 전달받은 (props가 변경될 때), (자신의 state가 바뀔 때), 부모 컴포넌트가 리렌더링될 때, (forceUpdate 함수가 실행될 때) 등이 있다.

2. 컴포넌트의 리렌더링을 방지할 때 (shouldComponentUpdate)라는 라이프사이클을 사용한다.

3. 함수형 컴포넌트에서는 라이프사이클 메서드 사용 불가-대신 (React.memo)라는 함수 사용한다.

4. 함수가 계속 만들어지는 상황을 방지하는 방법은 (useState의 함수형 업데이트 기능을 사용하기)와 (useReducer사용하기)가 있다.

5. (함수형 업데이트)는 새로운 상태를 파라미터로 넣는 대신, 상태 업데이트를 어떻게 할지 정의해 주는 업데이트 함수를 사용하는 것을 말한다.

6. useReducer를 사용하는 방법은 (기존 코드를 많이 고친다)는 단점과 (상태를 업데이트하는 로직을 모아서 컴포넌트 바깥에 둘 수 있다)는 장점이 있다.

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

 

● 코드 문제

1. React.memo를 사용하여 컴포넌트 성능을 최적화하세요

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

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

2. useState의 함수형 업데이트를 사용하는 대신, useReducer을 사용해서 onToggle과 onRemove가 계속 새로워지는 문제를 해결하세요

import React, { 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);

  const nextId=useRef(4);

  const onInsert=useCallback(
    text=>{
      const todo={
        id: nextId.current,
        text,
        checked: false,
      };
      setTodos(todos=>todos.concat(todo));
      nextId.current += 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,),
      );
    },
    []
  );

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

export default App;

Corner React Starter #2

Editor 성민

728x90

관련글 더보기