일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 오블완
- JPA
- Chakra
- css
- Gin
- java
- 티스토리챌린지
- frontend
- hook
- satisfiles
- golang
- component
- go
- springboot
- javascript
- react-hook-form
- React
- 웹애플리케이션서버
- JavaSpring
- RTK
- Spring
- designpatterns
- tanstackquery
- storybook
- Redux
- test
- ReactHooks
- typescript
- backend
- Today
- Total
bkdragon's log
반복되는 요소 렌더링 최적화 하기 본문
할 일 목록을 지닌 todoContainer와 할 일 아이템을 개별적으로 보여주는 todoItem, 두 개의 컴포넌트가 있을 때 할 일 아이템을 추가하거나 삭제해서 todoContainer 가 리렌더링 되면 모든 todoItem이 리렌더링이 되는데 이걸 최적화 하고 싶었다. 최적화에 필요한 도구들은 이전 글 중에 정리해둔 부분이 있다. 헌데 그 글은 단순 이론이고 이번에 실전에 적용하면서 조금 더 디테일하게 다뤄보려고 한다.
React 리렌더링 과정
리렌더링 과정을 세 단계로 구분 할 수 있다.
- 트리거
- 렌더링이 트리거되는 단계 state 값이 불변을 지켜 대체되었는지만 확인한다. 좀 더 자세히 이야기하면 기본형 변수일 땐 값이 변경되었는지만 확인, 참조형 변수일 땐 메모리 주소만 변경되었는지 얕은 비교를 통해 확인하고 렌더링을 트리거한다.
- 렌더 단계
- 새로운 가상 DOM을 생성한 후 이전 가상 DOM과 비교하여 달라진 부분을 탐색하고 실제 DOM에 반영할 최소한의 부분을 결정한다. (재조정 과정)
- 커밋 단계
- 실제 DOM에 반영한다.
useCallback과 useMemo로 props가 변경 안되게 했을 때
리렌더링은 상태의 얕은 비교로 트리거 된다. props도 마찬가지로 얕은 비교를 통해 리렌더할지 말지를 결정한다. todoItem으로 가는 모든 props를 useCallback과 useMemo를 통해 변경되지 않게 하면 리렌더링은 발생하지 않을까?
정답은 '발생한다' 이다. Props의 변경과 상관 없이 부모 컴포넌트가 리렌더링 되었기 때문이다. 다만 이 경우에는 렌더링 과정 중 커밋 단계는 발생하지 않는다. 재조정 과정에서 변경 점이 없었기 때문이다. 렌더링 측면에서 효과가 제로는 아니라는 것이다.
memo
완전하게 효과를 보려면 memo도 사용해줘야 한다. memo로 감싼 컴포넌트의 Props를 이전과 얕은 비교를 하여 변하지 않았다면 리렌더링이 되지 않는다. 렌더 단계에도 진입하지 않게 된다.
이제 코드를 확인해보자.
todoItem.tsx
const TodoItem: React.FC<TodoItemProps> = ({
description,
id,
done,
handleCheckBox,
handleDelete,
}) => {
console.log(`${description} 리렌더링`);
return (
<div
style={{
display: 'flex',
}}
className={styles.todoItem}
>
<input type="checkbox" onChange={() => handleCheckBox(id)} />
<div
style={{
textDecoration: done ? 'line-through' : 'none',
}}
>
{description}
</div>
<button onClick={() => handleDelete(id, description, done)}>완료</button>
</div>
);
};
export default memo(TodoItem);
memo로 감쌌다. 이제 props를 얕은 비교해서 변경 되었을 때만 리렌더링 된다.
todoContainer.tsx
const Todo = () => {
const dispatch = useAppDispatch();
const todos = useAppSelector((state) => state.todo);
const handleCheckBox = useCallback(
(id: string) => dispatch(changeDone(id)),
[dispatch]
);
const handleComplete = useCallback(
(id: string, description: string, done: boolean) => {
if (window.confirm('완료 목록에 등록하시겠습니까?')) {
dispatch(setTodo({ description, done, id }));
dispatch(openModal());
}
dispatch(deleteTodo(id));
},
[dispatch]
);
return (
<div>
{!todos?.length ? (
<div>할 일 없음</div>
) : (
<div>
{todos.map((props) => (
<TodoItem
key={props.id}
description={props.description}
done={props.done}
id={props.id}
handleCheckBox={handleCheckBox}
handleDelete={handleComplete}
/>
))}
</div>
)}
</div>
);
};
이제 todos에 새로운 아이템이 추가되거나 삭제되어도 todoItem의 props를 얕은 비교상 변하지 않았기 때문에 리렌더링이 발생하지 않는다.
이 과정에서 함수를 약간 변경한 과정에 대해서도 설명하면 handleComplete 함수는 원래 아래와 같은 형태였다.
const handleComplete = (id: string) => {
if (window.confirm('완료 목록에 등록하시겠습니까?')) {
const current = todos.find((todo) => todo.id === id);
if (!current) return;
if (window.confirm('완료 목록에 등록하시겠습니까?')) {
dispatch(setTodo(current));
dispatch(openModal());
}
dispatch(deleteTodo(id));
};
이 함수 그대로 useCallback으로 감싸게 되면 최초의 todos 값만 사용할 수 있어서 추가나 삭제가 되지 않을 것이다.
memo, useCallback, useMemo를 사용해서 반복되는 요소의 렌더링 최적화를 구현해보았다. react-devTools를 이용하여 가시적으로 확인 할 수도 있다.
'React' 카테고리의 다른 글
Server Component (0) | 2023.08.21 |
---|---|
useEffect 친해지기 1 (0) | 2023.08.09 |
React 이벤트 위임 (0) | 2023.07.30 |
클로저와 useState (0) | 2023.07.10 |
[React] 성능 최적화 (0) | 2023.06.29 |