2021-04-06(화)
클래스형 : shouldComponentUpdate
함수형 : React.memo
컴포넌트의 props가 바뀌지 않았다면 리렌더링을 하지 않도록 설정하여 리 렌더링 성능을 최적화 한다.
export default React.memo(TodoListItem);
리렌더링을 방지하기위해 useCallback부분을 아래와 같이 수정한다.
const onInsert = useCallback(
(text) => {
const todo = {
id: nextId.current,
text,
checked: false,
};
// 1. setTodos(todos.concat(todo));
setTodos((todos) => todos.concat(todo));
nextId.current += 1;
},
// 1. [todos],
[],
);
function todoReducer(todos, action) {
switch (action.type) {
case 'INSERT':
// {type:'INSERT', todo: {id:1, text:'todo', checked:false}}
return todos.concat(action.todo);
case 'REMOVE':
// {type:'REMOVE', todo: {id:1}}
return todos.filter((todo) => todo.id !== action.id);
case 'TOGGLE':
// {type:'TOGGLE', todo: {id:1}}
return todos.map((todo) =>
todo.id === action.id
? { ...todo, checked: !todo.checked }
: todo,
);
default:
break;
}
}
const [todos, dispatch] = useReducer(
todoReducer,
undefined,
createBulkTodos,
);
const onInsert = useCallback(
(text) => {
const todo = {
id: nextId.current,
text,
checked: false,
};
// 1. setTodos(todos.concat(todo));
// setTodos((todos) => todos.concat(todo));
dispatch({ type: 'INSERT', todo });
nextId.current += 1;
},
// 1. [todos],
[],
);
const onRemove = useCallback(
(id) => {
// setTodos(todos.filter((todo) => todo.id !== id));
// setTodos((todos) => todos.filter((todo) => todo.id !== id));
dispatch({ type: 'REMOVE', id });
},
// [todos],
[],
);
리액트는 기존의 값과 새로운 값과 비교해서 렌더링 여부를 결정하는데, 불변성을 유지시키기 위해서는 깊은 복사가 필요하다.
const todos = [
{ id: 1, checked: true },
{ id: 2, checked: true },
];
const nextTodos = [...todos];
nextTodos[0].checked = false;
// 얕은복사로 똑같은 객체를 가리키고 있다.
console.log(todos[0] === nextTodos[0]); // true
// 새로운 객체를 할당
nextTodos[0] = {
...nextTodos[0],
checked: false,
};
console.log(todos[0] === nextTodos[0]); // false
// 깊은복사. 오브젝트 안에 오브젝트를 새로운 객체로 받는 방법
const compleObject = [
{
objectInside: {
checked: true,
},
},
{
objectInside: {
checked: true,
},
},
];
const nextCompleObject = {
...compleObject,
objectInside: {
...compleObject.objectInside,
checked: false,
},
};
console.log(compleObject === nextCompleObject); // false
console.log(compleObject.objectInside === nextCompleObject.objectInside); // false
불변성 유지를 위한 모듈 immer, 최적화 모듈 react-virtualized이 있다.
// 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={495}
height={513}
rowCount={todos.length}
rowHeight={57}
rowRenderer={rowRenderer}
list={todos}
style={{ outline: 'none' }}
/>
);
};
export default React.memo(TodoList);
// TodoListItem.js
import React from 'react';
import cn from 'classnames';
import './TodoListItem.scss';
import {
MdCheckBoxOutlineBlank,
MdCheckBox,
MdRemoveCircleOutline,
} from 'react-icons/md';
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,
);
아래는 기본 사용 에제이다.
const originalState = [
{ id: 1, todo: '불변성 유지', cheked: false },
{ id: 2, todo: 'immer 라이브러리 사용하기', cheked: true },
];
// originalState : 수정하고 싶은 상태
// draft : 상태를 어떻게 업데이트할 것인지 정의하는 함수
// 두번째 파라미터로 전달되는 함수 내부에서 원하는 값을 변경하면
// produce함수가 불변성 유지를 대신해 주면서 새로운 상태를 생성해준다.
const nextState = produce(originalState, (draft) => {
const todo = draft.find((t) => t.id === 2);
todo.checked = true; // == draft[1].cheked = true;
draft.push({
id: 3,
todo: '데이터 추가',
cheked: false,
});
draft.splice(
draft.findIndex((t) => t.id === 1),
1
);
});
immer 모듈의 produce를 사용한다면 달라지는 부분은 draft로 상태에 접근해서 바꿔주어도 불변성유지가 가능하다는 것이다.
그리고 useCallback에서 두번째 매개변수로 들어가는 의존상태의 리스트를 설정해주지 않아도 된다
// App.js
import produce from 'immer';
import React, { useState, useRef, useCallback } from 'react';
const App = () => {
const nextId = useRef(1);
const [form, setform] = useState({ name: '', username: '' });
const [data, setData] = useState({
array: [],
uselessValue: null,
});
// input 수정
const onChange = useCallback(
(e) => {
const { name, value } = e.target;
// setform({
// ...form,
// [name]: [value],
// });
setform(
produce(form, (draft) => {
draft[name] = value;
})
);
},
// [form]
[]
);
const onSubmit = useCallback(
(e) => {
e.preventDefault();
const info = {
id: nextId.current,
name: form.name,
username: form.username,
};
// array에 새 항목 등록
// setData({
// ...data,
// array: data.array.concat(info),
// });
setData(
produce(data, (draft) => {
draft.array.push(info);
})
);
// form초기화
// setform({
// name: '',
// username: '',
// });
setform(
produce(form, (draft) => {
draft.name = '';
draft.username = '';
})
);
nextId.current += 1;
},
// [data, form.name, form.username]
[]
);
const onRemove = useCallback(
(id) => {
// setData({
// ...data,
// array: data.array.filter((info) => info.id !== id),
// });
setData(
produce(data, (draft) => {
draft.array.splice(
draft.array.findIndex((info) => info.id === id),
1
);
})
);
},
// [data]
[]
);
return (
<div>
<form onSubmit={onSubmit}>
<input name="username" placeholder="아이디"
value={form.username} onChange={onChange} />
<input name="name" placeholder="이름"
value={form.name} onChange={onChange} />
<button type="submit">클릭</button>
</form>
<div>
<ul>
{data.array.map((info) => (
<li key={info.id}
onClick={() => onRemove(info.id)}>
{info.username}({info.name})
</li>
))}
</ul>
</div>
</div>
);
};
export default App;