TIL

[TIL] 리랜더링 방지 최적화

youngble 2023. 3. 15. 00:41

다시한번 느끼게 된 최적화에 대한 이해한 부분에 대해서 회고록 형식으로 남겨놓고자 이렇게 쓰게 되었다.
요 몇일 여러개의 기능구현 과제를 진행을 하면서 피드백을 요청하여 받았던 부분중 하나가 메모이제이션, 최적화 부분에 신경을 쓰지 않은거 같다 라는 거였다.
 
솔직히 몇일안에 요구사항을 구현하는것에 포커싱을 맞추고 그 후 리팩토링 작업같은 중요하지만 부가적인 부분은 보지않는다고 생각했다.
또한 무분별하게 사용하는 최적화, 메모이제이션은 오히려 많은 비용을 발생할뿐이라는 것이 많은 사람들이 이야기하는 부분이다.
그럼에도 중요하게 생각하는 부분이 최적화부분이라고 하였고, 그렇기에 이번에 다시한번 최적화 부분을 보게 되었다. 특히 리랜더링을 방지하기 위한 React.memo 와 useCallback부분이였다. React.memo가 클래스 기반 컴포넌트일때 ShouldComponentUpdate에 해당하고 이를 대체하기위한 리액트 팀에서 내놓았던 것이다.

찾아보니 한가지 더 알게된 부분은 위의 사진처럼 sort라던가 for문이라던가 정말 1억개 10억개 등들 많은 로직을 수행하지 않는이상 heavy하게 오래걸리지 않고 오히려 heavy한 부분은 return 으로 랜더링되는 컴포넌트 들이라는 것이다. 그렇기 때문에 리랜더링을 방지하는 최적화는 비용적으로 효과가 있기에 노력한다는 것이다.
 
먼저 구현한 화면은 다음과 같다

 
이용자의 디테일한 정보를 받는 페이지였기 때문에 많은 input 필드들이 있었다. 각 필드마다 해당하는 정규식을 통한 유효성 검사를 진행하였고 이를 실시간으로 체크하기위해 ref가 아닌 onChange를 통한 state변경을 해주었다.
 
이렇게 하다보면 입력할때마다 state변경으로 인해 재랜더링이 발생하는데 재사용을 위해서 Input 을 재사용하였다. 그렇기 때문에 입력하지않더래도 해당 컴포넌트 들이 불필요하게 리랜더링 되는 것이다. 
 
하지만 중요한건 리랜더링 되더라도 React.memo나 useCallback 을 사용해서 발생하는 비용과 성능 최적화에 아무런 효과가 없다면 쓰지 않는게 더 중요하다고 생각했다.
 
그래서 이번엔 한번 테스트를해보았다. 검색했을때 React devtools의 profiler를 사용하였고 녹화를 진행하면서 각 input 필드를 입력하면서 랜더링되는 부분들을 기록하여 얼마나 걸리는지 확인하기 위해서였다.
 
 

With 최적화

React.memo로 재사용했단 Input 컴포넌트를 셋팅을하였고, 해당 input으로 넘겨주는 props 들중 변수에 담는 함수부분을 useCallback으로 묶어줌으로써 state가 바뀔때마다 변수가 초기화 되지 않도록 설정하였다. 그리고 나서 아래와 같이 Profiler를 통해 확인하였다.
 
어떤부분을 봐야되는지 간단하게 설명하자면 상단에 1 / 7 로 되어있고 <-  -> 를 통해 이동하면서 랜더링된부분과 랜더링 되는 시간 등을 받아볼수있다.
 

먼저 1 / 7 부분은 처음 리액트를 실행하고 해당 초기 랜딩부분을 보여준다. App이라는 컴포넌트 0.2ms, ApplyForm 컴포넌트 0.2ms, styled.div, ... 이렇게 되어있는데 기본적인 App컴포넌트를 제외하고 ApplyForm 부분이 위의 페이지에 해당하는 페이지에 들어간 로직과 페이지의 전체 컴포넌트였다.
 
먼저 리랜더링 방지를 위해서는 이부분은 보지 않아도 되고 초기에 얼마나 걸리는지만 Durations 의 render 부분만 체크해도된다.

다음은 2 ~ 7 사이의 중에 5번에 해당하는 부분을 선택하여 보았는데 이유는 2 ~7 부분은 각 input필드에 입력할때마다 발생하는 state변화로 인해서 찍힌 부분들이라 그중하나만 골라 스크린샷을 찍었다.
 
이제 중요한 부분은 Memo 라고 써져있으며 해당컴포넌트이름(InutWithLabel)을 알려주고 해당 컴포넌트가 did not render 되었다고 알려준다 이부분이 바로 React.memo로 감싸준 부분이다.
 
Duration을 보면 1.1ms 가 걸린것을 확인할수있다.

다음은 또다른 input입력되었을때의 시간인데, memo로 감싼부분이 랜더링될때는 다음과같이 색깔이 칠해지면 왜 랜더링 되었는지를 설명해준다 이때 props가 변경되었고 해당 Props 이름은 value, validation 이기 때문이라는 것이다. 그부분을 제외한 오른쪽 부분의 검은 영역이 랜더링 되지않고 넘어간 부분이다 이렇게해서 Render 시간은 3.7ms 가 걸린것이다.
 
각 랜더링에 소요된 시간을 쓰면
17ms, 1.1ms, 3.5ms, 3.5ms, 3.2ms, 3.2ms 가 걸렸다. 초기 랜더시간을 제외한 나머지 input 재랜더링 소요시간 평균은 2.9ms 가 걸렸다.
 

Without 최적화

그렇다면 React.memo 와 useCallback을 사용하지 않고 input 필드에 입력하면 얼마나 시간이 걸리는지 비교하기로 해보았다.
 
먼저 초기 랜더 소요시간, 각 input시의 랜더 소요시간을 쓰면
20.7ms , 1.7ms, 3.7ms, 6.5ms, 5.2ms, 3.6ms 이다. 초기 랜더링 시간을 제외한 input 랜더링 시간 평균은 4.14ms 이다
이는 최적화보다 거의 2배에 가까운 차이이다. ms 이기때문에 사용자에겐 엄청 차이가 있을거 같지는 않지만 수치로 봤을때는 어마한 차이라고 생각한다. 

위에가 초기 랜더링 시간

 

두번째가 input을 입력할때 발생한 리랜더링 소요시간인데 스크린샷처럼 Inputd으로 시작하는 부분들이 다 주황색으로 채워지고 있다. 그리고 props가 변했기떄문에 랜더링을했고 그 결과적으로 전체 랜더 시간이 6.5ms 라는 것이다.
 
또한 trade-off 로 발생하는 메모이제이션 크기는 어떻게 체크하는지 찾아보았지만 react 내부 자체적으로 메모리 캐싱을 수행하고 이를 확인하는 방법은 없다고 한다. 하지만 메모리 크기는 상대적으로 작기 때문에 크게 신경쓰지 않아도 된다고한다.
 
끄읏