이전 장에서 만들어 본 일정 관리 앱은 문제점이 있는데 바로 최적화가 되지않았다는 것이다
그냥 사용해 본다면 무엇이 문제인지 알 수 없지만 아래와 같이 더미 데이터들을 넣고 리액트를 실행시켜보자
const creatDummyTodos= ()=>{
const arr = [];
for(let i=0; i<2501;i++){
arr.push({
id:i,
text : `할 일 ${i}`,
checked : false,
});
}
return arr;
}
const App = () => {
const [todos, setTodos] = useState(creatDummyTodos);
const nextId = useRef(todos.length+1);
...
}
이전에 넣었던 3개의 데이터와는 달리 2500개의 데이터를 넣고 실행시켜봤더니
이전과는 다르게 상당히 느리게 동작하는 웹사이트를 볼 수 있다
11-1. 원인 분석
컴포넌트는 다음과 같은 상황에서 리렌더링이 발생한다
- 자신이 전달받은 props가 변경될 때
- 자신의 state가 바뀔 때
- 부모 컴포넌트가 리렌더링될 때
- forceUpdate함수가 실행될 때
그럼 여기서 우리가 만든 앱은 어떻게 동작할까?
- 할 일 0을 체크한다면 props로 전달해준 onToggle함수가 호출되어서 실행된다
- onToggle함수가 실행되면 App이 가지고 있는 state인 todos가 변경된다
- state가 바뀌었기 때문에 App이 리렌더링된다
- 부모인 App이 리렌더링됐기 때문에 자식인 todoList 또한 리렌더링된다
- todoList가 가지고있는 2500개의 item들이 전부 리렌더링된다
이렇게 돌아가기 때문에 속도가 느려지는 것이다
이럴 때는 컴포넌트 리렌더링 방지해서 최적화를 해주어야하는데 어떻게 방지하는지 알아보자
11-2. React.memo
우리는 7장에서 컴포넌트의 리렌더링 방지를 위한 라이프사이클 메소드를 배웠지만 함수형 컴포넌트에서는 라이프사이클 메서드를 사용할 수 없기 때문에 React.memo라는 함수를 사용해주면 된다
11-2-1. React.memo
React.memo는 컴포넌트를 받아 새로운 컴포넌트로 변환하는 고차 컴포넌트이다
일반적으로 리액트는 컴포넌트를 렌더링한 뒤 이전에 렌더링된 결과와 비교해서 값이 다르면 DOM을 이번 업데이트한다
이 과정에서 컴포넌트를 React.memo()로 감싼다면 리액트는 컴포넌트의 렌더링 결과를 메모이징한 뒤에 다음 렌더링부터 넘어오는 props가 동일하다면 새롭게 렌더링하지 않고 이전에 메모이징된 렌더링을 재사용한다
즉 props를 받아서 화면에 렌더링하는 컴포넌트를 memo로 wrapping했을 때 동일한 props를 받아서 동일한 렌더링결과가 나오는 컴포넌트의 리렌더링을 방지해 성능을 최적화시키는 것
11-2-2. 사용법
사용법은 매우 간단하다
const TodoListItem = ({todo, onRemove, onToggle})=>{
const {id, text, checked} = todo;
return(
<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>
);
}
export default React.memo(TodoListItem);
이처럼 컴포넌트를 memo로 감싸주기만 하면 된다
11-2-3. props
이제 최적화가 끝난 것일까?
이 과정에서 컴포넌트를 React.memo()로 감싼다면 리액트는 컴포넌트의 렌더링 결과를 메모이징한 뒤에 다음 렌더링부터 넘어오는 props가 동일하다면 새롭게 렌더링하지 않고 이전에 메모이징된 렌더링을 재사용한다
그렇다면 우리는 TodoListItem 컴포넌트의 props에 변화가 있는 지를 주목해야한다
1. todo
App에서 onToggle이 실행된다고 해서 2500개의 todo가 모두 바뀌진 않는다 클릭한 하나의 todo.checked가 바뀔 뿐
2. onToggle, onRemove
그럼 함수들은 어떨까?
//in App.js
...
const onToggle = (id)=>{
setTodos(todos.map(todo=> {
return todo.id === id ? {...todo, checked: !todo.checked}:todo;
}));
}
const onRemove = (id)=>{
setTodos(todos.filter(todo=>todo.id!==id));
}
...
이 두 함수가 실행될 때 어떤 것을 참조하는지 한번 살펴보면 알 수 있는데
바로 todos배열을 참조하고 있다
체크하거나 객체하나가 삭제될 때마다 todos전체가 새로 업데이트되는데 그 때마다 이 두 함수들도 새롭게 만들어지게되어서 props가 바뀌고 실제로 성능에 최적화가 되지않는다
11-3. 진짜 리렌더링 방지하기
그렇다면 위의 두 함수를 살짝 바꿔주면 되는데 업데이트시에 바뀔 todos를 직접 넣는 대신에 어떻게 바뀔지 함수상태로 알려주는 것이다
onToggle이나 onRemove가 가지는 상태 변경 함수는 계속 동일한 상태이고 그 콜백 함수들이 변경되는 todos를 참조하고 있기 때문에 함수들은 동일한 상태로 취급된다
11-3-1. useState의 함수형 업데이트 사용하기
함수 상태로 업데이트를 지시하는 것을 함수형 업데이트라고 부르는데 엄청 간단하다
말 그대로 콜백 함수로 변경 상태를 알려주면된다
const onToggle = (id)=>{
setTodos(todos=> todos.map(todo=> {
return todo.id === id ? {...todo, checked: !todo.checked}:todo;
}));
}
const onRemove = (id)=>{
setTodos(todos=> todos.filter(todo=>todo.id!==id));
}
이와 같은 형태로 setState메서드의 인자로 함수를 넣어주면된다
나는 이렇게 하고도 성능에 변화가 없었는데 useCallback을 사용해주지 않아서였다
App이 리렌더링될 때 onRemve와 onToggle이 새로 생성되어서 memo가 props를 비교할 때 항상 다른 값이 나오게 되어서 그런 것 같았다
const onToggle = useCallback(id=>{
setTodos((todos)=>
todos.map(v=> v.id === id ? {...v, checked: !v.checked} : v)
);
},[]);
const onRemove = useCallback(id=>{
setTodos((todos)=> todos.filter(v=> v.id!==id)
);
},[]);
함수들은 변경될 필요가 없으므로 의존배열을 비워두고 useCallback을 사용해서 감싸주면 이 함수들은 새로 생성되지도 않고 todos를 직접적으로 참조하지도 않아서 리렌더링될 때마다 같은 값을 가지게 된다
11-3-2. useReducer 사용하기
위의 함수형 업데이트를 사용하는 대신 useReducer를 사용해서 함수의 참조 문제를 해결할 수 있다
const todoReducer = (todos, action) => {
switch (action.type){
case 'INSERT' :
return todos.concat(action.todo);
case 'REMOVE' :
return todos.filter(v=>v.id!==action.id);
case 'TOGGLE' :
return todos.map(v=>v.id===action.id ? {...v,checked: !v.checked}: v);
default : return todos;
}
};
const App = () => {
const [todos, dispatch] = useReducer(todoReducer,undefined,creatDummyTodos);
const nextId = useRef(todos.length+1);
const onInsert = useCallback(text=>{
const todo = {
id : nextId,
text : text,
checked: false
};
dispatch({type:'INSERT',todo});
nextId.current++;
},[]);
const onRemove =useCallback(id=>{
dispatch({type:'REMOVE', id});
},[]);
const onToggle =useCallback(id=>{
dispatch({type:'TOGGLE', id});
},[]);
...
}
함수형 업데이트와 reducer는 성능 차이가 거의 없으니 취향에 따라 결정하면된다
11-4. react-virtualized
간단하게 설명하면 이 라이브러리를 사용하면 내 화면에 보이지 않는 부분은 렌더링하지 않는다
// react-virtualized
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'}}
/>
)
}
우선 List로 렌더링을 하는데
이 List는 todos배열을 받아와서 뿌려 줄 리스트 컨테이너이다
이 컨테이너의 크기와 총 길이, 칸 하나의 높이 css스타일, 출력할 배열 등을 설정해주고 렌더링할 함수를 설정해주면 되는데 렌더링할 함수는 rowRenderer로 선언해주었다
이렇게 설정해주고 scss와 태그들을 살짝 수정해주면 List라는 리스트컨테이너 안에서 컴포넌트들이 보여줄 수 있는 만큼만 렌더링되어서 화면에 보이게된다
'React > 리액트를 다루는 기술' 카테고리의 다른 글
13. 리액트 라우터로 SPA 개발하기 (0) | 2021.02.06 |
---|---|
12. immer (0) | 2021.02.05 |
10. 일정 관리 웹 애플리케이션 만들기 (0) | 2021.01.30 |
9. 컴포넌트 스타일링 (0) | 2021.01.25 |
8. Hooks (0) | 2021.01.22 |