🐕 렌더할 데이터가 많다면?
function createBulkTodos() {
const array = [];
for (let i = 1; i <= 2500; i++) {
array.push({
id: i,
text: '할일',
checked: false,
});
}
return array;
}
const App = () => {
const [todos, setTodos] = useState(createBulkTodos);
간단한 리액트 투두 리스트 앱을 저번에 만들어봤는데
다음 차트는 성능 최적화 부분이었습니다.
만약 2500개의 데이터를 렌더링하면??
안타깝게도 제 사이트는 1~2초 정도의 렉이 발생하고
뭔가 이벤트를 발생시켜도 느리게 동작해요
이걸 해결하는 것이 중요하겠네용
👻리액트에서 성능을 체크하는 법
먼저 크롬 확장에서 React DevTools를 설치해주고
개발자도구를 켜서 Profiler를 켜줍니다.
전 하도 안되서 새로고침해봤는데 새로고침하니까 잘되네요
녹화버튼을 누르고 어떤 동작을 한 뒤 녹화버튼을 종료시키면
해당 작업을 수행하는 데에 걸린 시간이 나타나게됩니다.
랭크를 확인해보면 어떤 작업에 얼마의 시간이 걸렸는지도 쭉 나타나네요
제 스크롤바가 압도적인 이유는 아마 저 밑으로 쭉 2500개의 for문이 있지않나 싶은
🥶 느려지는 원인이 뭔가
우리는 useState에 저 2500개의 컴포넌트를 다 때려박아 넣은 덕분에
한개의 컴포넌트의 상태가 변경되었을 때
모든 컴포넌트가 리렌더링 된다는 게 가장 큰 문제겠네요
변경되어야할 요소만 변경되고 나머지는 그대로 재활용하고 싶다는 욕구가 올라오죠?
그럴 때 고려할 수 있는 훅으로는 일단... 메모이제이션을 지원하는
useMemo가 크게 떠오르긴하네요
책을 읽어보니 컴포넌트의 리렌더링을 방지하고 싶을 때
클래스컴포넌트에서는 shouldComponentUpdate를 사용하면 뚝딱이라는데
대신 함수형 컴포넌트에서는 useMemo를 사용한다네요
TodoListItem.js
export default React.memo(TodoListItem);
에엥...?!! 내가 알던 useMemo 사용법과는 다르네요
이렇게도 useMemo를 할 수 있다니 정말 어메이징합니다.
굉장히 멋지지만
여전히 toDos 배열이 업데이트 되면 함수도 새로 만들어집니다.
이렇게 함수가 계속 만들어지는 상황을 방지하기 위해서는
1. useState의 함수형 업데이트 기능 사용하기
2. useReducer를 사용한다!!
두가지 선택지가 있는데 뭘 쓰는게 좋을지는 아직 모르겠네요 따라가보겠습니다.
🌞useState의 함수형 업데이트 기능
기존 setTodos 함수를 사용할 때는
const onToggle = useCallback((id) => {
setTodos(
todos.map((todo) =>
todo.id === id ? { ...todo, checked: !todo.checked } : todo,
),
);
});
이런식으로 완전히 새로운 상태를 파라미터로 넣어줬습니다.
근데 이거 대신에 상태 업데이트를 어떻게 할지 정의해주는
업데이트 함수를 넣는 방법도 있어요!
아니 근데 그거 어떻게 하는건데
const onToggle = useCallback((id) => {
setTodos(todos =>
todos.map((todo) =>
todo.id === id ? { ...todo, checked: !todo.checked } : todo,
),
);
});
어라...? 이게...끝...?
그냥 setTodos( todos => todos.map((todo) => todo.id ~~~))
이게 진짜 끝..? 너무 맛있는데..?
근데 전 왜 최적화가 안되죠..? 일단 넘어가겠음..
왜 안되는지 원인을 알았읍니다...
useCallback의 의존성 배열들을 안만져줘서
계속 리렌더링이 일어나는거였어요..
const onToggle = useCallback((id) => {
setTodos(todos =>
todos.map((todo) =>
todo.id === id ? { ...todo, checked: !todo.checked } : todo,
),
);
},[]);
따라서 저 위의 코드는 이렇게 의존성 배열에 빈배열을 추가해줘야했죠..
다시 리마인드 하면
의존성 배열을 안넣어주면 렌더링 될때마다 호출되고
의존성 배열에 빈배열 []을 넣어주면 최초 렌더링 1회만 호출됩니다.
의존성 배열에 뭔가 넣어주면 최초 1회 호출되고 그 값이 바뀔때 호출이 되고용..
따라서 useState의 함수형 업데이트 기능을 이용한 코드는 다음과 같습니다.
import TodoInsert from './components/TodoInsert';
import TodoTemplate from './components/TodoTemplate';
import TodoList from './components/TodoList';
import { useState, useRef, useCallback, useReducer } from 'react';
function createBulkTodos() {
const array = [];
for (let i = 1; i <= 2500; i++) {
array.push({
id: i,
text: `할일 ${i}`,
checked: false,
});
}
return array;
}
const App = () => {
const [todos, setTodos] = useState(createBulkTodos);
const nextId = useRef(4);
const onInsert = useCallback((text) => {
const todo = {
id: nextId.current,
text,
checked: false,
};
setTodos((todos) => todos.concat(todo));
nextId.current += 1;
}, []);
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,
),
);
}, []);
return (
<TodoTemplate>
<TodoInsert onInsert={onInsert} />
<TodoList todos={todos} onRemove={onRemove} onToggle={onToggle} />
</TodoTemplate>
);
};
export default App;
😋useReducer로 최적화
import TodoInsert from './components/TodoInsert';
import TodoTemplate from './components/TodoTemplate';
import TodoList from './components/TodoList';
import { useState, useRef, useCallback, useReducer } from 'react';
function createBulkTodos() {
const array = [];
for (let i = 1; i <= 2500; i++) {
array.push({
id: i,
text: `할일 ${i}`,
checked: false,
});
}
return array;
}
function 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(2501);
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의 세번째 인자로는 초기상태를 만들어줄 값을 넣어줄 수 있다.
와우 정말 놀라워
이렇게 useReducer를 사용하면 코드를 좀 많이 고쳐야하는 단점이 있지만
컴포넌트 바깥에 로직을 둘 수 있다는 큰 장점이 있어요
예전에 케이크처럼 쉽게 리듀서를 먹어볼 때 다 알고 간 내용이네요
성능상으로는 비슷합니다.
저도 궁금해서 찍어봤는데 제 환경에선 둘이 아무 오차도 없이
11.8ms이 걸리더라구요(최적화 이전에는 300~400ms 걸림)
🤢불변성의 중요성을 araboza
이미 아니까 넘어가겠습니다.
...스프레드 문법은 shallow copy를 하게되는데
그러다보니까 저 투두리스트를 짜면서도 스프레드 문법으로 가져온 객체 안에
들어있는 객체를 스프레드로 펼치는 짓을 하니 두번밖에 안했는데도 개헷갈리더라구요
깊은 복사가 마려운 순간이었습니다.
이런 상황에서 immer를 쓰면 참 좋겠네요
🤮TodoList도 최적화를 하자
import './TodoList.scss';
import React from 'react';
import TodoListItem from './TodoListItem';
const TodoList = ({ todos, onRemove, onToggle }) => {
return (
<div className="TodoList">
{todos.map((todo) => (
<TodoListItem
todo={todo}
key={todo.id}
onRemove={onRemove}
onToggle={onToggle}
/>
))}
</div>
);
};
export default React.memo(TodoList);
export default React.memo(TodoList);
요 부분만 TodoListItem처럼 React.memo로 감싸주었습니다.
요 코드는 사실 성능에 아무런 영향도 주지 않아요
왜냐면 TodoList의 부모 컴포넌트 APP이 리렌더링 되는 유일한 시점은
todos 배열이 업데이트 될 때 뿐이니까요!
하지만 APP 컴포넌트에 뭔가 다른 state를 추가한다든지 하면 불필요하게 리렌더 될 수 있을테니
이렇게 미리 최적화를 해줬습니다.
단 memo 기법 역시도 공간을 사용해서 성능을 얻는 기법이니 무분별하게 남용하면 좋지 않겠죵
내부 데이터가 100개를 넘지 않거나 업데이트가 자주 발생하지 않는 한
이런 작업은 꼭 해줄 필요는 없답니다.
🐶마치며
react-virtualized라는 라이브러리를 사용하는 파트는 아직 안했는데
내용이 좋아보여서 따로 빼두려고요
'react' 카테고리의 다른 글
useEffect를 async와 함께 사용할 때 유의할 점 (0) | 2023.03.31 |
---|---|
리액트의 SPA (SIngle Page Application) (0) | 2023.03.28 |
라이프사이클 메서드 폼 미쳤다. (5) | 2023.03.21 |
커스텀 훅으로 맛있는 훅 만들기 (4) | 2023.03.19 |
useCallback을 케이크처럼 쉽게 먹는 방법 (0) | 2023.03.17 |