React Native 성능개선 1) React

황다운
None
Published in
11 min readFeb 13, 2020

내가 짠 코드, 리액트 퍼포먼스 관점으로 다시보기

안녕하세요, 휴먼스케이프 주니어 프론트엔드 개발자 Tasha입니다. 오늘은 React Native 성능 개선에 대한 이야기를 진행하려고 합니다.

먼저 리액트 네이티브 동작 원리를 보면 자바스크립트 thread(코드작성부분), UI thread(화면의 UI와 인터렉션), 그리고 그 중간에서 소통하는 Bridge 부분으로 되어있습니다.

리액트 네이티브 thread 구조 [출처: https://www.notion.so/React-Native-2019-506432aa91a646dcb2d1fbbb166b7dd3]

제대로 성능 개선을 하기 위해서는 각 영역에서 동작하는 원리를 이해하고 속도개선을 위한 방법들을 고민해봐야겠지만, 속도개선 초반이니만큼 아는 부분에서 확실하게 개선할 부분을 찾아 개선해보아야겠다는 생각이 들었습니다. 따라서 자바스크립트 스레드에서 React Native의 기본이 되는 React 부분부터 살펴보기로 결정하였습니다.

이제 어떤 면에서 개선해야할지 선택하였으니, 어떤 부분부터 개선할지도 선택해야합니다. 얼마 전, 휴먼스케이프에서는 희귀질환 환자들을 위한 어플 레어노트가 출시되었는데요.🥳 레어노트 QA과정에서 가장 많이 들었던 피드백 중 하나가 ‘연구 소식’탭이 로딩 되는데에 매우 오래걸린다는 것이었습니다.

맞춤 최신의학정보와 치료제개발 현황을 알려주는 탭(레어노트)

출시 전 개선방법

이를 빠르게 해결하기 위해 출시 전에 적용한 방법은 다음과 같습니다.

  1. 캐싱을 적용한다.
  2. 서버에서 클라이언트에 불필요한 정보는 최대한 줄여서 보낸다.
  3. 어쩔수 없이 오는 불필요한 정보는 클라이언트 상태관리하는 부분에서 제거한다.
  4. 애니메이션이 있는 로딩을 넣는다.

위와 같이 렌더 속도 / 유저 경험부분에서 빠르게 할 수 있는 개선안을 적용시켰습니다. 렌더 속도 면에서는, 불필요한 정보를 클라이언트에서 최대한 들고 있지 않도록 서버에서 조정을 한다던가, 클라이언트 상태저장을 UI적으로 필요한 부분만 저장을 한다던가 해서 개선시켰습니다. 또한 유저 경험부분에서는 처음 앱을 켜서 탭에 들어온 이후로는 캐싱을 적용시켜 이미 가지고 있는 정보를 이용해 빠르게 보여준다던가, 애니메이션 로딩을 보여주는 방법으로 개선시켰습니다. 그러나 아직 만족스럽지 않아 속도개선 첫 타자로 연구소식탭 렌더부분을 골랐습니다.

What? 연구 소식 탭,
How? React 퍼포먼스 관점으로
속도 개선시키기

Reconciliation

Reconciliation(재조정)은 React가 DOM에 업데이트되는 원리이자, React 성능 개선에 있어 가장 많이 회자되는 부분입니다. Reconciliation은 회계에서 두 레코드세트가 일치하는지 확인하는 과정인데, 계정을 떠나는 돈이 실제 지출 한 돈과 일치시키는데에 있어 사용하는 용어라고 합니다.

출처 : https://www.investopedia.com/terms/r/reconciliation.asp

리액트에서 Reconciliation은 원래 DOM트리와 새로운 DOM트리를 비교하는 것을 말합니다. React에서는 DOM트리를 level-by-level로 탐색하는데, 각 노드의 상위 Root를 비교한 다음 그 값이 다르면 이전 것은 버리고 새로 업데이트를 합니다.

level-by-level 탐색 https://www.huskyhoochu.com/virtual-dom/

노드의 Root 타입이 같은 경우에는 DOM은 그대로 두고 그 속성을 비교하는데, 속성이 다른 경우 업데이트가 일어나는 형태로 진행됩니다.

여기서 왜 React의 reconciliation과정이 Performance와 관련이 있냐하면, 개발자 의도와 다르게 상위 컴포넌트 부분만 업데이트 되어도 하위 컴포넌트가 렌더될 수가 있고, 속성값을 객체형태라던지 화살표함수형태로 넣으면 비교에 있어 다른 속성값으로 받아드려 불필요한 렌더가 이루어지고, 불필요한 렌더들은 성능저하의 원인이 되기 때문입니다.

따라서 먼저, 연구소식 메인탭에서 불필요한 렌더가 일어나지는 않는지 먼저 React Profiler를 이용해 측정을 해 보았습니다.

React Profiler 출처: https://reactjs.org/blog/2018/09/10/introducing-the-react-profiler.html#ranked-chart

React Profiler는 리액트 컴포넌트의 렌더 횟수/ 속도, 마운트/언마운트 시기 등을 시각화해줍니다. DEV모드에만 실행되어 실제 PROD 모드에서의 퍼포먼스를 정확히 측정해주지는 않지만 관계없는 UI가 실수에 의해 업데이트되는 현상과 UI 업데이트의 깊이와 빈도를 알 수 있도록 해줍니다. (React-Native 의 React Profiling은 react-devtools에서 가능합니다.)

위 도구를 이용하여 레어노트 연구소식탭을 위에서 끌어당겨 새로고침을 했을 때(pull-to-refresh)화면 렌더 까지의 부분을 프로파일링을 해보았는데, 메인탭의 경우 렌더 횟수가 다음과 같이 조회되었습니다.

  1. 의도하지 않은 상태 업데이트

연구소식 탭은 총 5번 렌더되었고, 렌더시간 도합 376.4ms입니다. 제가 의도한 렌더 횟수는 1)데이터를 불러오는 hooks 실행을 위해 상태변경 2) 로딩시작 상태변경 3)로딩끝 상태변경을 위해 이렇게 총 세 번인데 의도와 다르게 총 5번이 실행되었습니다. (캐싱된 데이터와 값이 변경이 안되었기 때문에 데이터를 global state에 저장하여 상태가 변경되는 일은 일어나지 않아야 합니다.)

나머지 두번은 어디서 일어났나 — 하고 살펴보니 의도와 다르게 새로 불러온 데이터가 컨텍스트에 저장되어 상태변경이 일어나 렌더되는 현상이 발생하였습니다. 객체배열 형태의 데이터를 JSON.stringify()하여 값을 비교한다음 같으면 상태저장을 시키지 않아야하는데, 데이터가 다르지 않음에도 다르다고 판단이 된 것입니다.

이유를 살펴보니, 위에서 이전 속도개선에서 언급한 ‘클라이언트에서는 UI에 필요한 부분만 변경해서 상태저장하기’ 때문에 캐싱된 데이터가 변형되어 새로 불러온 값과 다른 것이었습니다. 메인탭에서는 최신의학소식/개발중인치료제/질환분류 세가지 데이터가 호출되는데, 앞에 두개만 데이터를 변형해 저장하기 때문에 추가적으로 새로 두 번 더 업데이트되어 의도한 바 3번과 함께 총 5번 렌더가 일어나게 된 것입니다.

2. 불필요한 속성 업데이트

이와 같은 잘못된 로직을 변경하고, 더불어 의도하지 않은 Reconciliation을 발생시키는 코드들이 있는지 살펴보았습니다. 먼저 DOM의 형태가 같더라도 그 속성이 다르면 rerender가 일어나게 되는데, 속성값을 객체 또는 화살표함수 형태로 줄 경우가 그 예에 해당이 됩니다.

Practical Performance for React (Native) — Anna Doubková 출처: https://www.youtube.com/watch?v=jTdi9oTM22c

위와 같이 속성에 화살표함수, 객체 형태로 넣어주게 되면 렌더시킬때마다 값을 새로 생성하기 때문에 같은 값임에도 불구하고 속성이 다른값으로 인지되어 불필요한 업데이트가 일어납니다. 이와 같은 사실은 인지하고 있어서 useMemo, useCallback을 이용해 dependency가 업데이트 될때만 값을 생성시켜서 넘겨주는 형태로 잘 쓰고 있다고 생각했는데, 코드를 살펴보니 미처 적용되지 않은 레거시 코드들도 있어서 함께 수정하였습니다.

3. 상위 노드 상태변경에 따른 불필요한 하위 노드 업데이트

하위 노드에서는 변경사항이 없어도 상위 노드의 상태가 변경되면 자동으로 불필요한 업데이트가 일어납니다. 이를 막기 위한 방법으로는 shouldComponentUpdate라는 라이프사이클을 이용하거나 React.memo로 컴포넌트를 감싸는 방법이 있습니다. shouldComponentUpdate의 경우에는 라이프사이클에서 props값을 비교해 렌더할지 말지를 결정하는 구간이고, React.memo의 경우에는 렌더한 값을 저장하고 있다가 props가 같은 경우에 새로 렌더하지않고 이전 값을 재사용하도록 합니다.

레어노트 코드의 경우에는 함수형 컴포넌트로 이루어졌기 때문에 React.memo를 이용하였습니다. 하위 컴포넌트의 props값이 단순하고, 단순히 props만 넘겨받는 단순한 컴포넌트들을 먼저 1차적으로 적용하였습니다.

결과

아직 전부 적용한 것은 아니지만 1차적으로 위의 세 가지 방법으로 코드개선을 해본 뒤 pullToRefersh에서 프로파일링을 다시 해보았습니다.

의도한 바와 같이 렌더 횟수가 5번 렌더 -> 3번렌더 되었고, 렌더시간 도합 376.4ms -> 176.8ms으로 줄어들었습니다.

사용자가 느낄만한 수준은 아니지만, 유저가 테스트하는 QA에서 발견하지 못한 버그를 발견할 수 있어 만족스러웠고, 실제 크게 문제가 되지 않는 부분을 조심스럽게 적용한 것이기 때문에 더 큰 문제가 느껴지는 부분에서는 더 효율적인 결과를 얻을 수 있을 것이라고 예상합니다. 또한 프로파일링을 하면서 코드를 점검해보니 더 신중하게 잘 작성해야겠다는 부분이 더 크게 와 닿았습니다.

TO-DO

리액트 프로파일링을 렌더부분에서만 보았는데, 더 깊이 알아보면 더 많이 개선할 수 있을거라고 판단됩니다. 위 프로파일링을 보시면 검정색 부분은 새로 fetch된 데이터가 global state에 저장되는 시기입니다. 이를 통해 fetch된 데이터가 update되는지 시점을 파악함으로써 네트워크 시간이 컴포넌트 렌더에 어떤 영향을 미치는지 대략적으로 유추해볼 수가 있습니다. 또한 노란색 부분 이후로 보시면 데이터가 렌더된 이후로 불필요한 업데이트가 계속 일어나고 있는데, 살펴보니 특정 외부 라이브러리에서 이와같은 현상이 발생하였습니다. 이렇게 어떤부분에서 지연이 되는지 통합적으로도 살펴볼 수 있습니다.

속도를 개선문제를 겪으면서 시각적으로 인지되지 않는 부분들이 있어 답답했는데, 이런 툴들을 활용하니 많이 도움이 된 것 같습니다.

이상으로 React 관점으로 React-Native 속도 개선하는 법에 대한 글을 마치겠습니다. 앞으로도 속도 부분에 있어서 더 자세히 공부해보고, 공유하도록 하겠습니다. 읽어주셔서 감사합니다 🙂

[출처]

Get to know us better!
Join our official channels below.

Telegram(EN) : t.me/Humanscape
KakaoTalk(KR) : open.kakao.com/o/gqbUQEM
Website : humanscape.io
Medium : medium.com/humanscape-ico
Facebook : www.facebook.com/humanscape
Twitter : twitter.com/Humanscape_io
Reddit : https://www.reddit.com/r/Humanscape_official
Bitcointalk announcement : https://bit.ly/2rVsP4T
Email : support@humanscape.io

--

--