- React.memo
- 함수형 업데이트
- useReducer
- react-virtualized
리액트를 다루는 기술 11장
렌더링 되는 순서
나는 앞서 간단한 일정관리 앱을 만들어보았다. 그 과정에서 데이터가 엄청나게 많아지면 리액트 성능이 어떻게 안좋아지고 이를 어떻게 개선할 수 있는지 한 번 알아보려고 한다.
react가 리렌더링 되는 이유는 크게 네 가지가 있다.
- 자신이 전달 받은 props가 변경될 때
- 자신의 state가 바뀔 때
- 부모 컴포넌트가 리렌더링 될 때
- forceUpdate함수가 실행될 때
만약에 TodoListItem 컴포넌트의 한 항목만 변경하려고 한다. (ex) 앱에서 체크 버튼 누르기)
그렇게 된다면 리액트 앱에서 하나의 정보의 값을 바꾸기 위해 전체 항목을 다시 만들고 재랜더링하는 과정을 거친다. 그렇기 때문에 우리는 해당 값이 바뀌지 않게 되면 리렌더링을 하지 않겠다는 선언이 필요하다. class 문법에서는 shouldComponentUpdate
라는 라이프 사이클 메서드가 존재하지만 hooks 문법에는 존재하지 않습니다.
따라서 hooks문법에는 다음과 같이 React.memo
를 사용해주면 됩니다.
const TodoListItem = ({todo, onRemove, onToggle}) => {
(...)
}
export default React.memo(TodoListItem);
=> 이렇게 될 경우 TodoListItem 컴포넌트는 todo, onRemove, onToggle이 바뀌지 않으면 리렌더링을 하지 않습니다.
위의 코드를 보시게 되면 우리는 App 컴포넌트에서 removeTodo, toggleTodo, addTodo를 선언했다.
위의 코드는 다음과 같이 선언하였다.
const removeTodo = useCallback(
(id) => {
setTodos(todos.filter((todo) => todo.id !== id));
},
[todos]
);
불필요한 함수 재생성을 막기 위해 useCallback을 사용하였지만 여전히 todos가 변경될 때는 함수를 재생성하게 됩니다. (하나의 todo가 변경되도 함수가 재생성된다는 뜻!!)
즉, 이를 해결하기 위해서는 setTodos인자에 새로 생긴 배열을 넣는 것이 아닌 업데이트 함수를 정의해야 한다.
const removeTodo = useCallback((id) => {
setTodos((todos) => todos.filter((todo) => todo.id !== id));
}, []);
위의 코드처럼 업데이트함수를 선언할 경우 두번째 배열 인자에 아무것도 안넣을 수 있게 된다.
또 다른 방법으로는 useReducer가 존재한다.
reducer를 통해서 상태를 관리하게 되면 아무래도 App 컴포넌트 바깥에서 정의를 할 수 있게 된다. 따라서 앞서 했던 함수형 업데이트로 정의할 필요없고 앱이 렌더링될 때마다 함수가 재생성되는 걱정을 하지 않아도 된다.
const INSERT = "INSERT";
const REMOVE = "REMOVE";
const TOGGLE = "TOGGLE";
export const todoReducer = (todos, action) => {
switch (action.type) {
case INSERT:
return [...todos, action.todo];
case REMOVE:
return todos.filter((todo) => todo.id !== action.id);
case TOGGLE:
return todos.map((todo) => {
return todo.id === action.id
? { ...todo, checked: !todo.checked }
: todo;
});
default:
return todos;
}
};
const removeTodo = useCallback((id) => {
dispatch({ type: "REMOVE", id });
}, []);
const toggleTodo = useCallback((id) => {
dispatch({ type: "TOGGLE", id });
}, []);
앞서 수많은 리스트항목들을 보려고 하지만 우리 눈에 보이는 리스트항목의 개수는 한정적이다. (화면의 크기에 따라 보여지는게 한정적이다.)
그럼에도 불구하고 우리는 보여지지도 않는 항목을 미리 렌더링해주고 있다.
이번에 배울 react-virtualized 라이브러리는 스크롤하는 범위에 따라 해당 값이 재렌더링이 일어나 불필요한 렌더링을 줄여준다.
import React, { useCallback } from "react";
import { List } from "react-virtualized";
import TodoListItem from "./TodoListItem";
const TodoList = ({ todos, removeTodo, toggleTodo }) => {
const rowRenderer = useCallback(
({ index, key, style }) => {
const todo = todos[index];
return (
<TodoListItem
todo={todo}
key={key}
removeTodo={removeTodo}
toggleTodo={toggleTodo}
style={style}
/>
);
},
[todos, removeTodo, toggleTodo]
);
return (
<>
<List
width={512}
height={513}
rowCount={todos.length}
rowHeight={57}
rowRenderer={rowRenderer}
list={todos}
style={{ outline: "none" }}
/>
</>
);
};
export default React.memo(TodoList);
=> 이로써 엄청나게 반응이 느리던 코드가 꽤 빨리진 것을 볼 수 있다.
추가적으로 스크롤 이벤트를 스로틀이나 디바운싱을 구현한다면 훨씬 더 안정적인 앱이 될 수 있을 것 같다.