React Native 관점으로 RN앱 성능 개선점 찾아보기
안녕하세요, 휴먼스케이프 프론트엔드 개발자 Tasha입니다. 이전까지 React Native App 성능을 개선하는 데에 컴포넌트 렌더 부분을 중심으로 살펴봤다면, 이번 편에서는 React Native가 동작하는 방식을 통해 레어노트앱에 적용시킬 수 있는 성능 개선점에 대해 살펴보려고 합니다.
그 전에, React Native 앱 개선을 위해 이전에 다루었던 렌더 문제가 React Native 관점에서 보았을 때 왜 중요한지 다시 한 번 짚고 넘어가려고 합니다. React Native는 자바스크립트를 사용하는 라이브러리이기 때문에 대부분의 로직은 자바스크립트 코드로 쓰여집니다. 따라서 대부분의 비지니스 로직은 JS Thread에서 이루어지며, JS Thread에서 쓰여진 코드가 직렬화된 형태로 bridge를 통해 Native Thread와 소통하면서 UI 업데이트가 이루어지게 됩니다.
출처: React Native: See the Past, the Present and the Future — Lorenzo Sciandra (youtube)
네이티브에서 UI 업데이트가 이루어지기 위해 필요한 작업이 수행되는데에 필요한 프레임 주기가 있습니다 (초당 60프레임). 만약 할당된 시간 내에 프레임을 생성할 수 없다면 그 프레임은 버려지고 UI가 응답하지 않게 됩니다. 이 부분이 바로 JS thread의 렌더 부분과 관련이 있는 부분입니다.
React Advanced London Conference — Practical Performance for React (Native) — Anna Doubková
하나의 상태 변화가 일어나면 위와 같은 과정들 — 렌더 함수가 실행되고, 새 가상돔이 그려지고, 재조정이 일어나서 렌더가 되는 — 이 JS Thread에서 일어나게 됩니다. 이 때 렌더 과정을 고려하지 않는 코드를 작성하면 한 곳에서만 이루어져야 할 업데이트가 부수적으로 다른 컴포넌트들에도 영향을 미쳐 새로 돔이 그려지고 재렌더되고 하면서 렌더시간이 증가하게 됩니다. 이렇게 JS Thread에서의 시간이 길어지면 앞서 말한 프레임 주기가 끝나버려 해당 프레임이 drop되게 됩니다. 이렇게 되면 원래 일어나야 할 UI 업데이트가 이루어지지 않게 되면서 화면이 동작하지 않는 것 처럼 보이게 되는 것입니다.
따라서 JS Thread에서 불필요한 렌더를 막아 렌더에 필요한 시간을 최대한 줄이는 것이 React Native 성능 개선에 핵심적인 부분이라고 볼 수 있습니다. 이렇게 렌더에 관한 중요성을 한번 더 강조하며, 이제 진짜로 본 주제인 Native Thread영역에서의 개선사항을 보도록 하겠습니다.
Inline Requires
보통 라이브러리를 임포트해서 사용할 때는 다음과 같이 작성합니다.
import React, { useEffect } from 'react'; import codePush from 'react-native-code-push';
function App() { useEffect(() => { codePush.restartApp(false); } }
이렇게 global하게 import를 사용하게 되면 처음에 import하는 라이브러리를 모두 로드하게 됩니다. 로드 되는데에 시간이 오래걸리는 라이브러리들, 네이티브 라이브러리, 큰 이미지들을 이런식으로 초반에 로드하게 되면 초기 실행시간이 더 늘어나게 됩니다. 따라서 만약 이 라이브러리들을 바로 사용할 것이 아니라면, 이부분은 다음과 같은 방식으로 가져오는 것이 더 좋습니다.
React Advanced London Conference — Practical Performance for React (Native) — Anna Doubková
이렇게 CommonJS의 require를 이용하면 라이브러리가 쓰이는 시점에 불러와서 사용할 수 있으므로, 초기에 필요하지 않는 부분을 로드하는 데에 들이는 시간을 줄일 수 있습니다.
Animations
React Native에서 애니메이션은 JS 스레드에서 계산되어서 Bridge를 통해 Native Thread로 보내집니다. 따라서 처음에 언급한 것 처럼, JS 스레드에서 계산되는 시간이 길어지면 블락되어 Native Thread에서 제대로 애니메이션이 표현되지 않습니다.
이런 현상을 막기 위해 React Native는 일부 View 애니메이션에서(transform, opacity) useNativeDriver라는 기능을 제공하는데요. 이 프로퍼티를 true로 해서 사용하면 애니메이션이 실행되기 전 애니메이션 계산 과정을 전부 Native Thread로 넘기기 때문에 자바스크립트 실행시간이 지연되어 UI가 업데이트 되지 않는 부분을 막아줍니다. 레어노트에서는 보통 애니메이션 구현을 위해 이 기능이 디폴트로 들어가있는 react-native-reanimated 라이브러리를 이용하곤 합니다.
이와 같이 JS Thread가 블락되어 애니메이션이 잘 나타나지 않는 현상은 TouchableOpacity, TouchableHighlight와 같은 컴포넌트에서도 발생할 수 있습니다. onPress와 같은 터치 이벤트에서 setState가 많이 이루어져 JS Thread에서 실행되는 일이 지연되면, 개발자가 의도했던 opacity와 같은 동작이 잘 나타나지 않을 수도 있습니다. 이럴 때에는 requestAnimationFrame를 이용하라고 RN문서에서 권고하고 있습니다.
handleOnPress() { requestAnimationFrame(() => { this.doExpensiveAction(); }); }
requestAnimationFrame을 이용하면, 원하는 opacity와 같은 애니메이션 동작이 먼저 일어나 expensiveAction을 처리하기 위해서 block되는 현상을 막아줍니다.
ScrollView vs FlatList
보통 스크롤이 필요한 경우에는 ScrollView를 많이 사용하는데, 성능을 위해서는 가급적 FlatList를 이용하라고 권장합니다. 그 이유는 ScrollView는 한번에 JS 컴포넌트들과 Native View를 렌더하기 때문에, 렌더하는 속도도 느려지고 메모리도 많이 사용하기 때문입니다. 반면 FlatList를 이용하게 되면 보여지는 부분만 렌더하고, 보여지지 않는 부분은 제거해버리기 때문에 속도, 메모리 면에서 모두 이점이 있습니다.
이 FlatList를 성능적으로 좋게 사용하기 위해 FlatList에 사용될 아이템 컴포넌트가 지키면 좋은 규칙들이 몇 가지 있습니다. 첫번째로는 nesting과 logic이 적은 베이직한 컴포넌트를 사용하는 것이 좋고, 불필요한 렌더 업데이트를 막기 위해 shouldComponentUpdate와 같은 로직을 이용할 것을 권장합니다. 두번째로는 리스트 아이템이 동일한 height를 가지고 있어 FlatList가 비동기적인 layout을 계산할 필요가 없게끔 하는 것이 좋습니다. 문서에서는 만약 layout height이 다 다를 경우, 디자이너와 협의하여 더 좋은 성능을 위해 같은 height의 레이아웃으로 할 것을 권장합니다.
Fetch Network Data with Startup
두번째 방법은 정보를 가지고 오는 일을 native단에서 진행하는 것입니다. 처음에 javascript vm도 실행시키고, react-native도 실행시키고 하면서 초반에 이루어져야 할 작업들이 많은데 서버로부터의 데이터 fetch는 보통 그 시작 로직이 다 끝난 이후에나 이루어집니다.
출처: Chain React 2019 — Ram Narasimhan — Performance in React Native
이 부분을 native영역에서 이루어지도록 앞당겨오면 초반 실행로직이 줄어들게되므로 앱 초기에 더 빨라진다는 장점이 있습니다. (JS에만 친숙한 개발자에게 네이티브에서 데이터를 fetch하는 일을 하는 것은 낯설고 두려운 일이지만.. network fetch 로직 이전을 통한 이용한 성능개선에 대해 발표한 Ram Narasimhan 페이스북 개발자는 JS, Native 모두 라이브러리 지원하는 Apollo와 같은 툴을 이용하면 어렵지 않게 할 수 있다고 독려해줍니다.)
지금까지 React Native관점으로 RN App 성능 개선에 대해 알아보았습니다. RN을 할수록 native 코드를 다루는 부분이 필요해짐을 점점 더 느끼는 것 같습니다. 할 수 있는 작은 부분부터 적용하면서, 나중에 Native 코드를 이용한 개선방법도 시도해봐야겠다는 생각이 들었습니다. 읽어주셔서 감사합니다 🙂
[출처]
React Native Docs Performance
React Advanced London Conference— Practical Performance for React (Native) — Anna Doubková
Chain React — Ram Narasimhan
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