할일 2500개를 생성하고 할일 0을 체크해서 개발자 도구의 Performance 눌러서 성능을 테스트해보았다.
Timings를 확인해보면 컴포넌트 업데이트에 1.09초가 걸렸다고 나온다. 이는 성능이 매우 나쁜 것이다.
100ms 미만의 UI 응답 지연은 유저들이 즉시 느낄 수 있고 100ms에서 300ms가 지연되면 이미 유저들은 느리다고 생각한다. 가급적 0.3초 이내로 모든 일을 끝내야 한다.
이는 재렌더링이 되기 때문에 발생하는 일이다. 리액트 컴포넌트가 재랜더링되는 경우는 다음과 같다.
자신의 props이 변경될 때, 자신의 state가 변경될 때, 부모 컴포넌트가 재렌더링될 때, forceUpdate 함수가 실행될 때.
즉, 현재 할일을 뿌려주는 2500개의 컴포넌트가 전부 재 렌더링되고 있는 상태인 것이다. 재렌더링이 불필요할 때도 나머지 2499의 컴포넌트를 렌더링이 되지 않도록 만들어보자.
* 여기서 주의할 점이, 자신의 "state가 변경될 때", "props가 변경될 때"를 비교하는 방식이 얕은 비교라는 것이다. state를 사용할 때 Primitive type이 아닌 객체나 배열, 함수와 같은 reference type을 주었을 경우에는 실제 값까지 비교하지 않고 해당 참조하는 메모리 주소가 같은 지를 참고한다.
이와 같은 일로 인하여 발생할 수 있는 문제점은
상위 컴포넌트의 state가 변경되면 => 상위 컴포넌트가 재렌더링 되며 하위 컴포넌트에 넘겨주는 props가 새롭게 생성되고 => props에 참조 타입이 있다면 동일한 값이라도 동일 참조 값이 아니므로 얕은 비교를 통해 새로운 값으로 판단하여 재렌더링을 실행한다는 것이다.
따라서 이러한 문제점을 해결하기 위해 useCallback와 React.memo를 사용한다.
🚀 React.memo로 감싸기
memo는 react에서 기본적으로 제공하는 HOC입니다.
React.memo()로 래핑 될 때, React는 컴퍼넌트를 렌더링하고 결과를 메모이징(Memoizing)한다. 그리고 다음 렌더링이 일어날 때 props가 같다면, React는 메모이징(Memoizing)된 내용을 재사용합니다. - 출처
즉, 컴포넌트의 props가 바뀌지 않는 이상 재랜더링하지 않도록 설정합니다.
뭔가 반복적으로 렌더링하는 것이 있다면(배열에서 map, filter 등으로 컴포넌트를 뿌려주는 게 대표적이겠죠) React.memo로 감싸주는 것이 좋습니다.
다음 컴포넌트는 React.memo로 감싸져 있으므로 todo, onRomove, onToggle에 변화가 있지 않는 이상 재렌더링되지는 않을 것입니다.
function TodoListItem({ todo, onRemove, onToggle }) {
const { id, checked, text } = todo;
return (
<Item>
<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>
</Item>
);
}
export default React.memo(TodoListItem);
🚀 useState의 함수형 업데이트
그런데 위 방법에서 onRemove와 onToggle함수가 상위 컴포넌트에 작성된 방식을 살펴보면, todos가 바뀔 때마다 다시 생성되는 것을 볼 수 있습니다. 이를 막기 위해서는 useState의 함수형 업데이트를 사용하는 것이 좋습니다.
이게 무슨 말이냐면
const [number, setNumber] = useState(0);
// 함수형 업데이트
const onIncrease = useCallback(
let prevNumber = number
() => setNumber(prevNumber => prevNumber + 1), []
);
// 그냥 업데이트 (직접 새로운 값을 넣어주기)
const onIncrease = useCallback(
() => setNumber(number + 1), [number]
);
함수형 업데이트를 진행하면 useCallback에 deps를 넣어줄 필요가 없습니다. 따라서 매번 deps가 바뀔 때마다 다시 리렌더링되는 걸 막을 수 있게 됩니다.
이를 적용한 코드는 다음과 같습니다.
// 일반 업데이트
const onInsert = useCallback(
(text) => {
const todo = {id: nextId.current, text, checked: false};
// 새로운 todo를 만들어 todos에 concat으로 직접 합쳐주기
settodos(todos.concat(todo));
nextId.current += 1;
},
[todos]
);
// 함수형 업데이트
const onInsert = useCallback((text) => {
const todo = {id: nextId.current, text, checked: false};
// 함수로 업데이트 방식을 알려주기
settodos((todos) => todos.concat(todo));
nextId.current += 1;
}, []);
51ms. 빠르다.
🚀 결론
리스트를 렌더링할 때는
1. React.memo를 통해 리스트 아이템과 리스트를 감싸줘서 props가 동일하면 리렌더링이 되는 것을 방지하고
2. 인라인 함수들 (대개 onClick, onSubmit 등)을 useCallback으로 감싸줘야 하며
3. setState를 이용하여 새로운 state를 반영할 시 그 업데이트는 함수형 업데이트로 이루어져야 한다
이외에
+ 무한 스크롤을 구현하여 필요한 만큼만 리스트 아이템을 렌더하도록 합시다.
참고한 글)
벨로퍼트님의 서적 <리액트를 다루는 기술>
'React, Next, Redux > ⚛ React.JS' 카테고리의 다른 글
Loadable Components를 통한 코드 스플리팅 (0) | 2020.07.10 |
---|---|
React.lazy와 Suspense 컴포넌트를 통한 코드 스플리팅 (0) | 2020.07.10 |
history stack과 push/replace/go (0) | 2020.07.04 |
dorpzone을 만들어 이미지, 비디오 첨부하기 (0) | 2020.06.28 |
커스텀 Hoc 만들기 (0) | 2020.06.17 |