[React 🔯] 10. 최적화
![[React 🔯] 10. 최적화](/images/useBlog/react.jpg)
최적화
최적화 (Optimization)란?
- 웹 서비스의 성능을 개선하는 모든 행위를 일컫는다.
- 아주 단순한 것부터 아주 어려운 방법까지 매우 다양하다.
일반적인 웹 서비스 최적화 방법
- 서버의 응답속도를 개선한다.
- 이미지, 폰트, 코드 파일 등의 정적 파일 로딩을 개선한다.
- 불필요한 네트워크 요청을 줄인다.
- ....
React App 내부의 최적화 방법
- 컴포넌트 내부의 불필요한 연산을 방지한다.
- 컴포넌트 내부의 불필요한 함수 재생성을 방지한다.
- 컴포넌트의 불필요한 리렌더링을 방지한다.
useMemo
"메모제이션"기법을 기반으로 불필요한 연산을 최적화하는 리액트 훅이다.
메모제이션이란? Memoization : 기억해두기, 메모해두기라는 뜻
useMemo 활용하여 연산 최적화하기
const getAnalyzedData = () => {
const totalCount = todos.length;
const doneCount = todos.filter((todo) => todo.isDone).length;
const notDoneCount = totalCount - doneCount;
return {
totalCount,
doneCount,
notDoneCount,
};
};
const { totalCount, doneCount, notDoneCount } = getAnalyzedData();
return (
<div className="List">
<h4>Todo List 📋</h4>
<div>total: {totalCount}</div>
<div>done:{doneCount}</div>
<div>notDone: {notDoneCount}</div>
.... 이후 기준 코드와 동일
getAnalyzedData()는 그냥 일반 함수라서 컴포넌트가 리렌더링될 때마다 무조건 다시 실행된다. todos가 바뀌든 안 바뀌든 상관없이, 컴포넌트가 렌더링되면 항상 계산을 새로 한다.
const { totalCount, doneCount, notDoneCount } = useMemo(() => {
const totalCount = todos.length;
const doneCount = todos.filter((todo) => todo.isDone).length;
const notDoneCount = totalCount - doneCount;
return {
totalCount,
doneCount,
notDoneCount,
};
}, [todos]);
useMemo는 두 번째 인자로 받은 의존성 배열 [todos]를 감시한다. todos가 바뀌었을 때만 첫 번째 인자의 함수를 실행해서 값을 다시 계산하고, todos가 바뀌지 않았다면 이전에 계산해둔 결과를 그대로 재사용한다. 이걸 메모이제이션이라고 한다.

React.memo
컴포넌트를 인수로 받아, 최적화된 컴포넌트로 만들어 반환한다.


컴포넌트를 memo()로 감싸면 props가 바뀌었을 때만 리렌더링된다. props가 동일하면 리렌더링을 건너뛴다.
Header 컴포넌트가 좋은 예다. Header는 아무 props도 받지 않고 날짜만 표시한다. 그런데 App에서 todos가 바뀔 때마다 App이 리렌더링되면 Header도 덩달아 리렌더링됐었다. memo(Header)로 감싸면 props가 없으니 사실상 처음 한 번만 렌더링되고 이후로는 리렌더링되지 않는다.
import './Header.css';
import { memo } from 'react';
const Header = () => {
return (
<div className="Header">
<h3>오늘은 🗓️</h3>
<h1>{new Date().toDateString()}</h1>
</div>
);
};
export default memo(Header);
TodoItem도 마찬가지다. Todo가 10개 있을 때 1번 항목을 체크하면, 기존에는 나머지 9개도 전부 리렌더링됐다. memo(TodoItem)을 적용하면 props가 바뀐 1번 항목만 리렌더링되고 나머지 9개는 건너뛴다.
import './TodoItem.css';
import { memo } from 'react';
const TodoItem = ({ id, isDone, content, date, onUpdate, onDelete }) => {
const onChangeCheckbox = () => {
onUpdate(id);
};
const onClickDeleteButton = () => {
onDelete(id);
};
return (
<div className="TodoItem">
<input onChange={onChangeCheckbox} readOnly checked={isDone} type="checkbox" />
<div className="content">{content}</div>
<div classame="date">{new Date(date).toLocaleDateString()}</div>
<button onClick={onClickDeleteButton}>삭제</button>
</div>
);
};
export default memo(TodoItem);
여기서 문제가 생긴다. onCreate, onUpdate, onDelete는 App이 리렌더링될 때마다 함수가 새로 만들어진다. JavaScript에서 함수는 매번 새로 생성되면 이전과 다른 참조값을 가진다. memo는 props를 얕은 비교로 확인하기 때문에, 함수 내용이 똑같아도 참조값이 달라지면 "props가 바뀌었다"고 판단해서 결국 리렌더링이 발생한다. memo를 써도 소용이 없어지는 것이다.
useCallback - 불필요한 함수 재생성 방지하기
useCallback은 함수를 메모이제이션한다. 의존성 배열 안의 값이 바뀌지 않으면 이전에 만들어둔 함수를 그대로 재사용한다.
const onCreate = useCallback((content) => {
dispatch({
type: 'CREATE',
data: {
id: idRef.current++,
isDone: false,
content: content,
date: new Date().getTime(),
},
});
}, []);
const onUpdate = useCallback((targetId) => {
dispatch({
type: 'UPDATE',
targetId: targetId,
});
}, []);
const onDelete = useCallback((targetId) => {
dispatch({
type: 'DELETE',
targetId: targetId,
});
}, []);
세 함수 모두 의존성 배열이 []다. dispatch는 useReducer가 안정성을 보장하는 함수라 배열에 넣지 않아도 된다. 결과적으로 이 세 함수는 처음 마운트될 때 딱 한 번만 생성되고, 이후 App이 리렌더링돼도 같은 참조값을 유지한다. 덕분에 TodoItem 입장에서는 함수 props가 바뀌지 않은 것으로 인식되고, memo가 제대로 작동하게 된다.
