Today I Learned ... react.js
🙋♂️ Reference Book
🙋 My Dev Blog
리액트를 다루는 기술 DAY 11
- 일정 관리 App 성능 최적화


컴포넌트가 리렌더링이 발생하는 경우
- 자신이 전달받은
props가 변경될 때- 자신의
state가 변경될 때- 부모 컴포넌트가 리렌더링 될 때
(PureComponent나 React.memo를 이용)- forceUpdate 함수가 실행될 때
state인 todos가 변경되면서const createBulkTodos = () => {
const arr =[];
for(let i = 1; i <= 2500 ; i++) {
arr.push({
id: i,
text: '할일',
checked: false
});
}
return arr;
}
const App = () => {
const [todos, setTodos] = useState(createBulkTodos);
// 초기값으로 설정. createBulkTodos()가 아님. ()은 없애줘야 매번 호출안함.
// ()없이 함수 자체를 넣어줘야 첫 렌더링시 한번만 발생함.
...
}
예를 들면, 위와 같이 할일이 2500개가 있다고 가정하자.
체크를 하나만 했으므로 1개만 리렌더링 되면 되는데, 나머지 2499개도 리렌더링이 발생한다.
이럴때 성능의 저하가 발생하게 된다.
이럴 때는 불필요한 리렌더링을 방지해주면 된다.
React.memo를 사용한다.props가 바뀌지 않았다면 리렌더링하지 않도록 설정.방법 1) 컴포넌트 전체 감싸주기
const TodoListItem = React.memo( ({ todo, onRemove, onToggle}) => {
...
});
방법 2) 컴포넌트 작성 후, export시 감싸주기
const TodoListItem = ({ todo, onRemove, onToggle}) => {
...
}
export default React.memo(TodoListItem);
props들)이 바뀌기 전까지는 렌더링 ❌함수가 계속해서 생성되는 상황을 방지하는 방법
- 함수형 useState
- useReducer
기존에는 setState의 인자로 새로 업데이트할 값을 넣어줬었다.
setState를 사용할 때 상태 업데이트를 정의한 함수를 넣어줄 수 있음.
const onIncrease = useCallback(
() => setNumber(prevNumber => prevNumber + 1),
[]
);
함수형 setState는 useState 내에서 이전 상태(state)를 참고해야 할 때 사용한다.
const onInsert = useCallback((text) => {
const todo = {
id: nextId.current,
text,
checked: false,
}
setTodos(todos.concat(todo));
nextId.current += 1;
}, [todos]);
const onInsert = useCallback((text) => {
const todo = {
id: nextId.current,
text,
checked: false,
}
setTodos(todos => todos.concat(todo));
nextId.current += 1;
}, []);
state인 todos에 의존하지 않아도 됨. (todos가 바뀌어도 함수 새로 생성하지 X)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))
}, []);
다시한번 성능을 체크해보면, 이전보다 훨씬 속도가 빨라진 것을 알 수 있다.
아래에서 회색 빗금으로 표시된 부분은 React.memo로 리렌더링 되지 않은 컴포넌트를 의미한다.

useReducer 을 사용해도 함수가 계속 새로 생성되는 문제를 해결할 수 있다.App.js
import { useReducer, useRef, useCallback } from "react";
import TodoInsert from "./components/TodoInsert";
import TodoList from "./components/TodoList";
import TodoTemplate from "./components/TodoTemplate"
const 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(4);
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에는 두번째 인자로 initialState를 넣어줘야하지만, 여기서는 대량의 2500개의 데이터를 넣어주는 함수인 createBulkTodos를 전달한다.두번째 인자로는 undefined를 넣어줘야 한다.
(맨 처음 렌더링 시에만 createBulkTodos가 실행되도록 하기 위해)
useReducer은 상태를 업데이트 하는 로직(=reducer)을 모아 App 컴포넌트 바깥에 둘 수 있다.
state에는 직접 push나 splice 등이 아닌,업데이트가 필요한 곳에서는 아예 새로운 배열을 만드므로, React.memo 사용시 props가 바뀐 경우에만 리렌더링을 하여 성능 최적화가 가능하다.
만약, 불변성을 지키지 않는다면? = 객체 내부의 값이 바뀌어도 변경된 것을 감지할 수 ❌
React.memo에서 서로 비교하여 최적화 할 수 없음.
참고
spread(...)로 객체나 배열의 값을 복사할 때는 얕은 복사가 이루어짐.- 가장 바깥 쪽의 값만 복사됨.
- 객체나 배열의 구조가 복잡해지면 불변성 유지가 어려워짐 ->
immer라이브러리 사용 가능!
const copiedObj = {
...obj,
objInside: {
...obj.objInside
}
};
✅ 중요
리스트 관련 컴포넌트 작성시 - 리스트 아이템 / 리스트 (두 컴포넌트) 모두 최적화 필요.
만약 todo가 2500개가 있다면, 우리는 화면에 나타나는 todo는 9개임에도 불구하고
스크롤 아래에 가려져 보이지 않는 나머지 2491개의 컴포넌트도 리렌더링이 된다.
react-virtualized를 사용하면, 리스트 컴포넌트에서 스크롤되기 전 보이지 않는 컴포넌트를 렌더링하지 않는 기능을 구현할 수 있다.$ yarn add react-virtualized
최적화에 앞서, 우리는 각 항목의 실제 크기를 px 단위로 알아내야 함.

import React, { useCallback, memo } 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 memo(TodoList);
rowRenderer 함수 작성TodoListItem을 렌더링 할때 사용함.위 항목들을 모두 props로 넣어줘야 함.
TodoList만 수정하면 스타일이 깨지게 되는데, todoListItem 컴포넌트도 일부 수정하면 해결된다.
div.TodoListItem-virtualized 로 감싸고props로 받아온 style을 적용해주면 됨.import React, { memo } from 'react';
import {
MdCheckBoxOutlineBlank,
MdCheckBox,
MdRemoveCircleOutline,
} from 'react-icons/md';
import cn from 'classnames';
import './TodoListItem.scss';
const TodoListItem = memo(({ 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 TodoListItem;
-> 컴포넌트 사이 테두리를 제대로 쳐주고, 홀수/짝수번째 배경색을 설정하기 위해서임.
(스타일이 깨졌던 이유)

TodoListItem.scss에서 설정을 해줘야 함.&:nth-child(even) {
background: #f8f9fa;
}
...
& + & {
border-top: 1px solid #dee2e6;
}
.TodoListItem-virtualized {
&:nth-child(even) {
background: #f8f9fa;
}
& + & {
border-top: 1px solid #dee2e6;
}
}
TodoListItem-virtualized선택자 한정 스타일 적용.
이제 줄무늬 배경 + 테두리가 잘 적용되었다!