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
선택자 한정 스타일 적용.이제 줄무늬 배경 + 테두리가 잘 적용되었다!