상세 컨텐츠

본문 제목

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

21-22/21-22 리액트 마스터

by Kimpeep 2021. 12. 27. 13:00

본문

728x90

11.1 많은 데이터 렌더링하기

랙(lag) 경험을 위해 많은 데이터 렌더링

//App.js
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:1,
			text:`할 일 ${i}`,
			checked: false,
		});
	}
	return array;
}

const App = () => {
	const [todos, setTodos] = useState(createBulkTodos);
	const nextId = useRef(2501);

	(...)
};
export default App;
  • useState(createBulkTodos()) : 리렌더링 될 때마다 createBulkTodos 함수가 호출됨
  • useState(createBulkTodos) : 처음 렌더링될 때만 createBulkTodos 함수가 실행됨

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

크롬 개발자 도구 - Performance 탭 녹화 ⇒ 성능 분석 결과가 나타남

11.3 느려지는 원인 분석

리렌더링이 발생하는 상황

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

※ 리렌더링이 불필요할 때는 리렌더링을 방지해줘야 함 ⇒ 컴포넌트 리렌더링 성능 최적화

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

방법1) shouldComponentUpdate 라이프 사이클 사용

방법2) 함수형 컴포넌트 - 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);**

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

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

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

  방법1) useState의 함수형 업데이트 기능 사용

  방법2) useReducer 사용

11.5.1 useState의 함수형 업데이트

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

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

const onIncrease = useCallback(
	() => **setNumber(prevNumber => prevNumber + 1)**,
	[],
}

setNumber(number+1)을 하는 것이 아니라, 어떻게 업데이트할지 정의

⇒ 두 번째 파라미터로 넣는 배열에 number를 넣지 않아도 됨

 

▼onToggle, onRemove, onInsert 수정

더보기
//App.js
import React, { useRef, useState, useCallback } from 'react';
import TodoTemplate from './componenets/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:1,
			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;
💡 실제 프로덕션 모드에서는 개발 모드보다 처리 속도가 훨씬 빠름 프로덕션 모드로 구성하고 싶다면 ⇒
yarn build
yarn global add serve
serve -s build

11.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:1,
			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 = () => {
	// 맨처음 렌더링될 때만 createBulkTodos 함수가 호출됨
	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;

11.6 불변성의 중요성

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

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

  • 전개 연산자(... 문법)을 사용하면 얕은 복사가 이루어짐
  • ⇒ 내부의 값이 객체 혹은 배열이라면, 내부의 값 또한 따로 복사해주어야 함

배열 혹은 객체의 구조가 복잡해진다면 불변성 유지가 어려워짐

  ⇒ immer 라이브러리 사용하면 편하게 작업 가능 (12장)

11.7 TodoList 컴포넌트 최적화하기

리스트 관련 컴포넌트 최적화 시, 최적화 해주면 좋은 것

  • 리스트 아이템
  • 리스트

※ 내부 데이터가 100개를 넘지 않거나, 업데이트가 자주 발생하지 않는다면 반드시 해줄 필요는 없음

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

const TodoList = ({ todos, onRemove, onToggle }) => {
	return (...);
};

export default React.memo(TodoList);

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

react-virtualized : 리스트 컴포넌트에서 스크롤되기 전에 보이지 않는 컴포넌트는 렌더링하지 않고 크기만 차지하게끔 함

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

11.8.1 최적화 준비

yarn add react-virtualized

사전 작업

  • 각 항목의 실제 크기를 px 단위로 알아내기
    • 크롬 개발자 도구 - 테두리가 포함되어 있지 않은 두 번째 항목 확인

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

rowRenderer 함수 : react-virtualized List 컴포넌트에서 각 TodoItem을 렌더링할 때 사용

  → list 컴포넌트의 props로 설정

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

11.8.3 TodoListItem 수정

//TodoListItem.js - render
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,
);
  • TodoListItem-virtualized 클래스 : 컴포넌트 사이사이에 테두리를 제대로 쳐주고, 홀수/짝수 번째 항목에 다른 배경 색상 설정 위함
//TodoListItem.scss
.TodoListItem-virtualized {
	& + & {
		border-top : 1px solid #dee2e6;
	}
	&:nth-child(even) {
		background: #f8f9fa;
	}
}


Quiz

Q1. 컴포넌트 성능 최적화를 위해 shouldComponentUpdate 라이프 사이클을 사용하는데 함수형 컴포넌트에서는 같은 기능을 위해 어떤 함수를 사용해야 하는가?

더보기
답 : React.memo

Q2. 함수가 계속 만들어지는 상황을 방지하기 위해 useState의 함수형 업데이트 기능을 사용하거나, _______를 사용한다.

더보기
답 : useReducer

Q3. 전개 연산자를 사용하면 깊은 복사가 이루어진다. (O,X)

더보기

답 : X

728x90

관련글 더보기