[REACT] 리액트로 TODO 리스트 구현하기 :: (3) 컴포넌트 성능 최적화

2020. 8. 25. 18:57React

https://codecrafting.tistory.com/12 에서 이어지는 내용입니다.

 

[REACT] 리액트로 TODO 리스트 구현하기 :: (2) 기능 구현하기

https://codecrafting.tistory.com/11 에서 이어집니다! [REACT] 리액트로 TODO 리스트 구현하기 :: (1) UI 구성하기 서론 요즘 리액트를 다루는 기술이라는 책으로 코딩을 시작하고 처음으로! 리액트를 접하고.

codecrafting.tistory.com

지난시간에 만든 일정관리 어플리케이션은 아직까지 많은 데이터가 추가되지 않았기 때문에 무리없이 정상적으로 작동하는 모습을 보입니다. 하지만 할일 목록이 1000개를 넘어가면 처리속도가 기하급수적으로 느려지는 것을 확인할 수 있습니다. 이는 리액트의 특징인 '대용량 데이터 처리'에 전혀 부합하지 않습니다. 따라서 이번 포스팅에서는 작성했던 코드를 최적화 하는 작업을 다뤄보도록 하겠습니다.

 

우선 데이터를 2500개 추가하는 함수를 만들어 보겠습니다.

 

 

createBulkTodos() 함수를 만들어 할 일 목록을 2500개 만든 후 배열에 담아 setTodos 로 값을 설정해줍니다. 여기서 주의해서 봐야할 점은, useState(createBulkTodos())라고 작성하면 리렌더링될 때마다 createBulkTodos 함수가 호출되지만, useState(createBulkTodo)처럼 파라미터를 함수 형태로 넣어주면 컴포넌트가 처음 렌더링될 때만 createBulkTodo 함수가 실행된다는 점입니다.

 

 

이제 코드를 실행하면 2500개의 데이터가 넣어집니다.

데이터를 삽입하고 Toggle 기능을 사용하면 1초 정도 멈춰버린 후 비로소 실행됩니다.

 

왜 느려질까요?

컴포넌트는 다음과 같은 상황에서 리렌더링이 발생합니다.

  • 자신이 전달받은 props가 변경될 때
  • 자신의 state가 바뀔 때
  • 부모 컴포넌트가 리렌더링 될 때
  • forceUpdate 함수가 실행될 때

 

지금 상황을 분석해보면, '할 일1' 항목을 체크할 경우 App 컴포넌트의 state가 변경되면서 App 컴포넌트가 리렌더링 됩니다. 부모 컴포넌트가 리렌더링 됐으니 TodoList 컴포넌트가 리렌더링 되고 그 안의 무수한 Todo 들이 또 리렌더링 되버리는 것입니다...

 

 

'할 일1' 항목은 리렌더링 되어야 하는 것이 맞지만, '할 일2' 부터 '할 일2500' 까지는 리렌더링을 안해도 되는 상황임에도 모두 리렌더링되고 있으므로 느려지는 것입니다. 이럴 때 컴포넌트 성능 최적화를 해주어야 할 필요성이 생깁니다.

 

React.memo를 이용한 컴포넌트 성능 최적화

컴포넌트의 리렌더링을 방지할 때는 React 라이프사이클의 shouldComponentUpdate 함수를 사용해주면 됩니다. 그런데 함수형 컴포넌트에서는 라이프사이클 메서드를 사용할 수 없습니다. 그 대신 React.memo 라는 함수를 사용합니다.

 

React.memo가 뭔데요

기본적으로 리액트는 컴포넌트를 렌더링 한 후, 이전 렌더링 결과와 비교해서 DOM 업데이트를 결정합니다. 이 때 이전 결과와 렌더링 결과가 다르면 리액트는 DOM을 업데이트 합니다.

 

그런데 어떠한 상황에서는 이 과정을 좀 더 빠르게 할 수 있습니다.

만약 컴포넌트가 React.memo 로 래핑(wrapping) 되어 있다면 다음 렌더링이 일어날 때 prop에 변경이 일어나지 않는 한, 해당 컴포넌트는 리렌더링 되지 않고 이전 렌더링된 DOM을 그대로 사용합니다. 

 


다시 코드로 돌아와서, 만들었던 TodoListItem.js를 보면 todo, onRemove, onToggle 같은 요소를 props로 받고 있는데 이때 memoization 을 활용하면 이들 요소가 바뀌지 않는 한 리렌더링을 하지 않습니다.

정말 간단하게 구현을 했습니다.

 

하지만 현재 프로젝트에서는 todos 배열이 업데이트되면 onRemove와 onToggle 함수도 새롭게 바뀝니다. 기존의 함수는 새로운 상태를 만들면 그것을 파라미터로 넣어서 재참조 하기때문에 새로운 항목이 추가되면 2500개의 onRemove와 onToggle 함수가 줄줄히 리렌더링 되는 참사가 발생합니다.

 

이 때, useState의 함수형 업데이트를 이용하면 쉽고 간편하게 업데이트 되는 내용만 리렌더링을 하게 됩니다.

 

useState의 함수형 업데이트 활용

 

이런 함수가 있다고 해봅시다. onIncrease는 state로 정의된 number의 값을 1 증가시키고, onDecrease는 number의 값을 1 감소시킵니다. 그런데 동일한 수 많은 요소들이 이러한 함수들을 호출시키는 상황이라고 할때, useState의 함수형 업데이트를 활용하면 성능저하를 최소화 할 수 있습니다.

 

 

 

 

 

이렇게 말이죠. 여기서 쌩뚱맞게 prevNumber이 튀어나온다고 당황할 필요 없습니다. useState에서 첫 번째 인자로 '함수'가 호출된다면 이 함수의 인자를 prevState, 즉 바로 전 랜더링 값의 state를 참조하면서 update를 진행하게 됩니다.

 

 

 

 

프로젝트에서 마찬가지로 useState를 활용해보겠습니다.

 

각 함수의 setTodos 마다 todos => 인자를 추가합니다.

 

그러면 todos 업데이트가 진행될 때 이전의 todos 값이 각각의 함수에 들어가 있기 때문에 빠르게 리렌더링이 진행됩니다. 만약에 todos => 없이 함수를 썼다면 2500개의 todos를 일일히 다시 참조하고 filter함수, map함수로 재계산하게 되면서 엄청나게 느려지는 일이 생깁니다. 

 


 

이렇게 해서 최적화가 끝났습니다. useReducer을 이용한 방법도 있지만 useState에 비해서 코드량이 매우 많이 늘어나기 때문에 사용하지 않았습니다. 또 react-virtualized를 사용한 랜더링 최적화 기법도 나중에 기회가 된다면 포스팅하고 싶습니다 ㅎㅎ

 

 

리액트 프로젝트 첫 포스팅 시리즈를 읽어주셔서 감사합니다!