11장 컴포넌트 성능 최적화

mini·2022년 9월 1일
0

What to do?

  • 많은 데이터 렌더링 하기 -> 크롬 개발자 도구를 통한 성능 모니터링 -> React.memo를 통한 컴포넌트 리렌더링 성능 최적화 -> onToggle과 onRemove가 새로워지는 현상 방지하기 -> react-virtualized 사용한 렌더링 최적화

많은 데이터 렌더링 하기

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

항목을 하나 체크 해 보면 이전보다 느려진것을 확인 할 수잇다.

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

React DevTools -> Profiler 탭 -> 좌측 상단 파란색 녹화버튼 , 버튼을 누르고 퍼포먼스 한뒤 -> 다시하번더 더 누르면 성능 분석 결과가 나타난다.

차트 아이콘 클릭 뒤 확인 해보면 컴포넌트랑 관계없는 컴포넌트들도 리렌더링 된것을 확인 할 수있다.

느려지는 원인 분석

<컴포넌트가 리렌더링 되는 상황>

  • 자신이 전달받은 props가 변경될때
  • 자신의 state가 바뀔때
  • 부모 컴포넌트가 리렌더링 될때
  • forceUpdate 함수가 실행될때

현재 예제 상황에선 할일 을 체크할 경우 App 컴포넌트의 state가 변경되면서 App 컴포넌트가 리렌더링 된다. 부모 컴포넌트가 리렌더링 되었으니 TodoList 컴포넌트가 리렌더링 되고 그안의 무수한 컴포넌트도 리렌더링 된다.
-> 즉 할일 1 항목은 리렌더링 되는게 맞지만 나머지 할일 까지 리렌더링을 하지 않아도 되는 상황인데 모두 리렌더링 되므로 느린것이다. 컴포넌트의 개수가 많지 않다면 모든 컴포넌트를 리렌더링 해도 느리지않지만 수가 많아지면 성능이 저하 된다. 어떻게 리렌더링을 방지할까?

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

shouldComponentUpdate라는 라이프사이클을 사용해도 되지만 함수 컴포넌트에서는 라이플사이클 메서드를 사용 할 수 없다. 대신 React.memo 라는 함수를 사용해 컴포넌트의 props가 바뀌지 않앗다면 리렌더링 하지 않도록 설정 성능을 최적화 할수 있다.
export default React.memo(TodoListItem);

onToggle, onRemove 함수가 바뀌지 않게

현재 프로젝트에서는 todo 배열이 업데이트 되면 onRemove와 onToggle 함수도 새롭게 바뀐다. 배열 상태를 업데이트 하는 과정에서 최신 상태의 todos를 참조하기 때문에 todos 배열이 바뀔 때마다 함수가 새로 만들어진다. 이렇게 함수가 계속 만들어지는 상황을 방지 하는 방법은 두가지다. 1. useState의 함수형 업데이트 기능을 사용, 2. useReducer 사용

useState의 함수형 업데이트

기존에 setTodos 함수를 사용할떄는 새로운 상태를 파라미터로 넣어줫지만 상태 업데이트를 어떻게 할지 정해주는 업데이트 함수를 넣을수도 있다. 이를 함수형 업데이트라고 한다.

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

setNumber(number+1)을 하는것이 아니라 위코드 처럼 어떻게 업데이트할지 정의해주는 업데이틑 함수를 넣어준다. 그러면 useCallback을 사용할때 두번째 파라미터로 넣는 배열에 number 를 넣지 않아도 된다.

성능이 향상 되었다!

useReducer 사용하기

useState의 함수형 업데이트를 사용하는 대신, useReducer를 사용해도 onToggle과 onRemove가 계속 새로워지는 문제를 해결 할 수있다.
기존 코드를 많이 고쳐야 한다는 단점 있지만 상태를 업데이트하는 로직을 모아서 컴포넌트 바깥에 둘수 있다는 장점

불변성의 중요성

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

예시코드

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)를 하게된다. 내부의 값이 완전히 새로 복사되는것이 아니라 가장 바깥쪽에 있는 값만 복사된다. 따라서 내부의 값이 객체 혹은 배열이라면 내부의 값또한 따로 복사 해주어야 한다 다음 코드를 보자

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

nextTodos[0].checked = false;

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

nextTodos[0] = {
  ...nextTodos[0],//새로운 객체를 할당
  checked: false
}

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

만약 객체 안에 있는 객체라면 불변성을 지키면서 새값을 할당 해야 하므로 다음과 같이 해야한다.

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

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

배열 혹은 객체의 구조가 정말 복잡해진다면 이렇게 불변성을 유지하면서 업데이트 하는것도 까다로워진다. 이렇게 복잡할 상황일 경우 immer 라는 라이브러리의 도움을 받으면 편하게 작업할수잇다.

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); // memo -> 최적화 //지금은 불필요 하지만 나중에 다른 state가 추가되면 해당 값들이 업데이트 될때는 TodoList 컴포넌트가 불필요한 리렌더링을 할수 있으니 미리 최적화 준다.

리스트 관련 컴포넌트를 작성 할때는 리스트 아이템과 리스트, 이 두가지 컴포넌트를 최적화 해주는것을 잊지마세요. 내부 데이터가 100개를 넘지 않거나 업데이트가 자주 발생하지 않는다면 최적화 작업은 필요없다.

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

일정 관리 앱 초기 데이터가 2,500개 등록 되어있는데 실제 화면에서 나오는 항목은 9개 뿐이다. 나머지는 스크롤 해야 볼수있다. 현제 컴포넌트가 맨처음 렌더링 될떄 2,500개 컴포넌트 중 2,491개 컴포넌트는 스크롤 하기 전인데도 불구하고 렌더링이 이루어진다. 비효율적. 나중에 todos 배열에 변동이 생길때도 TodoList 컴포넌트 내부의 map 함수에서 배열의 처음부터 끝까지 컴포넌트로 변환해주는데 이중에서 2,491개 는 보이지 않으므로 시스템 자원낭비. react-virtualized를 사용하면 리스트 컴포넌트에서 스크롤되기 전에 보이지 않는 컴포넌트는 렌더링 하지않고 크기만 차지 하게끔 한다. 만약 스크롤되면 해당 스크롤 위치에서 보여주어야 할 컴포넌트를 자연스레 렌더링 시킨다. 이 라이브러리를 사용하면 자원을 아낄수 있다.

  • 최적화 준비
    $yarn add react-virsualized

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

  • 사전에 먼저 각 항목의 실제 크기를 px단위로 알아 내야한다. (495px X 57px) 두번째 항목

TodoList 수정

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

const TodoList = ({ todos, onRemove, onToggle }) => {
	const rowRender = 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={rowRender} //항목을 렌더링 할때 쓰는 함수
			list={todos} //배열
			style={{ outline:'none' }} //List에 기본 적용되는 outline 스타일 제거
		/>
	)
}

export default React.memo(TodoList); // memo -> 최적화 //지금은 불필요 하지만 나중에 다른 state가 추가되면 해당 값들이 업데이트 될때는 TodoList 컴포넌트가 불필요한 리렌더링을 할수 있으니 미리 최적화 준다.

rowRenderer라는 함수는 List 컴포넌트에서 각 TodoItem을 렌더링 할때 사용. 이 함수를 List 컴포넌트의 props로 설정 해주어야 한다. 이 함수는 파라미터에 index, key, style 값을 객체타임을 받아 와서 사용한다.

TodoListItem 수정


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

TodoListItem-virtualized 클래스를 만든 이유는 컴포넌트 사이사이에 테두리를 제대로 쳐주고 홀수번쨰/짝수번째 항목에 다른 컬러 입히기 위해...

정리

많은 데이터를 렌더링 하는 리스트를 만들어 지연을 유발해 해결하는 방법을 알아 보앗다.컴포넌트에서 항목이 100개 이상이고 업데이트가 자주 발생한다면 최적화를 해야한다.

0개의 댓글