풀타임 사무직(Sr. React Native Engineering, USA, remote)을 제외하고 매일 5~6시간씩 작업하여 이 글을 완성하는 데 16일이 걸렸습니다.
참고: 저는 React Native 공식 문서와 CallStack 성능 최적화 책(최고의 React Native 표준을 만드는 회사)을 기반으로 React Native 디버깅
에 대한 저의 경험을 바탕으로 전체 표준 여정을 다루었습니다. 이 글을 다 읽고 나면 여러분은 분명 React Native 디버깅의 전문가가 될 것입니다. 저는 모든 R&D를 RN v0.71
로 진행했습니다.
🔥 저는 약 1000개의 GitHub STAR를 확보한 심층적인 "React Native 고급 가이드북"을 출간했습니다. (무료 책 링크)
긴 글을 읽으면서 심호흡을 하고 커피 한 잔을 마시세요. 그러면 졸리지 않을 거예요 😃.
이 글에서 배울 수 있는 내용을 요약해 보겠습니다.
React Native는 여러 디버깅 옵션을 제공하는 인앱 개발자 메뉴를 제공합니다. 디바이스를 흔들거나 키보드 단축키를 사용하여 Dev Menu에 액세스할 수 있습니다.
또는 Android 디바이스 및 에뮬레이터의 경우 터미널에서 adb shell input keyevent 82
를 실행할 수 있습니다.
또한 VSCode 확장 프로그램인 React Native Tools를 사용하여 Dev Menu (Bridge)를 열 수 있습니다. 확장 프로그램을 설치한 다음 Cmd ⌘ + Shift + P
를 누르고 React Native: Show Dev Menu
를 입력하고 Enter 키를 누릅니다.
릴리스(프로덕션) 빌드에서는 Dev Menu가 비활성화됩니다.
Dev Menu는 아래와 같습니다(IOS 및 Android).
Fast Refresh는 React 컴포넌트의 변경 사항에 대해 거의 즉각적인 피드백을 받을 수 있는 React Native 기능입니다. Fast Refresh는 기본적으로 활성화되어 있으며, React Native Dev Menu에서 "Enable Fast Refresh"를 토글할 수 있습니다. Fast Refresh를 활성화하면 대부분의 수정 내용을 1~2초 이내에 볼 수 있습니다.
React 컴포넌트만 export하는 모듈을 수정하는 경우 Fast Refresh는 해당 모듈에 대해서만 코드를 업데이트하고 컴포넌트를 다시 렌더링합니다. 스타일, 렌더링 로직, 이벤트 핸들러, 이펙트 등 해당 파일의 모든 것을 수정할 수 있습니다.
React 컴포넌트가 아닌 것을 export하는 모듈을 수정하면 Fast Refresh는 해당 모듈과 해당 모듈을 import하는 다른 모듈을 모두 다시 실행합니다. 따라서 Button.js
와 Modal.js
가 모두 Theme.js
를 가져오는 경우 Theme.js
를 수정하면 두 컴포넌트가 모두 업데이트됩니다.
마지막으로 React 트리 외부의 모듈에서 가져온 파일을 수정하면 Fast Refresh는 전체를 다시 로딩합니다. React 컴포넌트를 렌더링하는 파일과 React 컴포넌트가 아닌 다른 컴포넌트에서 가져온 값을 내보내는 파일이 있을 수 있습니다. React 컴포넌트를 렌더링하지만 React가 아닌 컴포넌트에서 가져온 값을 내보내는 파일이 있을 수 있습니다.
Fast Refresh 세션 중에 syntax error가 발생하면 이를 수정하고 파일을 다시 저장할 수 있습니다. 그러면 redbox가 사라집니다. syntax error가 있는 모듈은 실행되지 않으므로 앱을 다시 로드할 필요가 없습니다.
모듈 초기화 중에 runtime error가 발생한 경우(예: StyleSheet.create
대신 Style.create
입력) 오류를 수정하면 Fast Refresh 세션이 계속됩니다. 이후, redbox가 사라지고 모듈이 업데이트됩니다.
컴포넌트 내에서 런타임 오류를 발생시키는 실수를 한 경우에도 오류를 수정하면 Fast Refresh 세션이 계속됩니다. 이 경우 React는 업데이트된 코드를 사용하여 애플리케이션을 다시 마운트합니다.
Fast Refresh는 수정 중인 컴포넌트의 로컬 React 상태를 유지하려고 시도하지만, 그렇게 하는 것이 안전한 경우에만 가능합니다. 파일을 수정할 때마다 로컬 상태가 초기화되는 몇 가지 이유는 다음과 같습니다.
클래스 컴포넌트의 경우 로컬 상태는 보존되지 않습니다(함수 컴포넌트와 Hook만 상태를 보존합니다).
수정 중인 모듈에 React 컴포넌트 외에 다른 내보내기가 있을 수 있습니다.
때로는 모듈이 createNavigationContainer(MyScreen)
같은 HOC를 호출한 결과를 내보내는 경우가 있습니다. 반환된 컴포넌트가 클래스인 경우 상태가 재설정됩니다.
때로는 상태를 강제로 리셋하고 컴포넌트를 다시 마운트하고 싶을 수도 있습니다. 예를 들어 마운트할 때만 발생하는 애니메이션을 조정할 때 유용할 수 있습니다. 이렇게 하려면 수정 중인 파일의 아무 곳에나 // @refresh reset
을 추가하면 됩니다. 이 지시문은 파일에 로컬로 적용되며, 수정할 때마다 해당 파일에 정의된 컴포넌트를 다시 마운트하도록 Fast Refresh에 지시합니다.
Fast Refresh는 가능하면 수정 사이에 컴포넌트의 상태를 보존하려고 시도합니다. 특히, useState
와 useRef
는 인수나 Hook 호출 순서를 변경하지 않는 한 이전 값을 유지합니다.
useEffect
, useMemo
, useCallback
등 종속성이 있는 Hook은 Fast Refresh 중에 항상 업데이트됩니다. Fast Refresh가 진행되는 동안 해당 종속성 목록은 무시됩니다.
개발 빌드의 오류 및 경고는 앱 내부의 LogBox에 표시됩니다.
릴리스(프로덕션) 빌드에서는 LogBox가 비활성화됩니다.
Console 오류 및 경고는 빨간색 또는 노란색 배지와 함께 화면 알림으로 표시되며, Console에서 오류 또는 경고의 개수가 각각 표시됩니다. Console 오류나 경고를 보려면 알림을 탭하여 로그에 대한 전체 화면 정보를 보고 Console의 모든 로그를 페이지네이션할 수 있습니다.
이러한 알림은 LogBox.ignoreAllLogs()
를 사용하여 숨길 수 있습니다. 이 기능은 예를 들어 제품 데모를 제공할 때 유용합니다. 또한 LogBox.ignoreLogs()
를 통해 로그별로 알림을 숨길 수 있습니다. 이 기능은 서드파티 종속성처럼 수정할 수 없는 시끄러운 경고가 있을 때 유용합니다.
undefined is not a function
와 같이 처리되지 않은 JavaScript error
는 오류의 소스와 함께 전체 화면 LogBox 오류가 자동으로 열립니다.
syntax error
가 발생하면 syntax error의 스택 추적 및 위치와 함께 전체 화면 LogBox 오류가 자동으로 열립니다. 이 오류는 앱을 계속 진행하기 전에 수정해야 하는 잘못된 JavaScript 실행을 나타내므로 무시할 수 없습니다.
다음 6단계를 따라 React Native 앱용 'Chrome Developer Tool'을 활성화하세요.
다음은 제가 React Native용 chrome debug tool을 활성화하기 위해 사용한 명령어입니다.
yarn global add react-devtools
react-devtools
adb reverse tcp:8097 tcp:8097
USB를 통해 연결된 Android 5.0 이상 디바이스에서는 adb
command line tool을 사용하여 디바이스에서 컴퓨터로 포트 포워딩을 설정할 수 있습니다.
명령어: adb reverse tcp:8081 tcp:8081
또는 개발자 메뉴에서 'Settings'를 선택한 다음 'Debug server host for device' 설정을 컴퓨터의 IP 주소와 일치하도록 업데이트하세요.
iOS 디바이스에서 RCTWebSocketExecutor.mm
파일을 열고 "localhost"를 컴퓨터의 IP 주소로 변경한 다음 개발자 메뉴에서 "Debug JS Remotely"를 선택합니다.
문제가 발생하면 Chrome 확장 프로그램 중 하나가 디버거와 예기치 않은 방식으로 상호 작용하고 있을 수 있습니다. 실제로 오래된 디버그 도구에 연결되어 있는 위치를 철저하게 조사한 후 내 문제를 해결했으므로 그냥 노트북/PC를 다시 시작하세요.
아래와 같이 Dev Menu에서 'Show Perf Monitor'를 선택하여 성능 오버레이를 활성화하면 성능 문제를 디버깅하는 데 도움이 됩니다.
아래는 Performance Monitor를 연 후, React Native 앱 (IOS + Android)의 이미지입니다.
React Native 앱의 performance monitor는 앱의 성능과 사용자 경험을 최적화하는 데 도움이 되는 몇 가지 메트릭을 보여줍니다. 각 숫자의 의미는 다음과 같습니다.
RAM: 앱에서 사용하는 메모리 양입니다. 여기에는 네이티브 메모리와 JavaScript 메모리 사용량이 모두 포함됩니다. 메모리 압박과 충돌을 피하려면 이 수치를 가능한 한 낮게 유지해야 합니다.
JSC: JavaScript 코드를 실행하는 JavaScriptCore 엔진에서 사용하는 메모리 양입니다. 이는 RAM 사용량의 하위 집합입니다. 가비지 콜렉션 일시 중지 및 메모리 누수를 방지하려면 이 수치를 가능한 한 낮게 유지하는 것이 좋습니다.
Views: 앱에서 생성 및 소멸된 네이티브 view(UI 컴포넌트)의 개수입니다. 첫 번째 숫자는 현재 view의 개수이고 두 번째 숫자는 최대 view의 개수입니다. 불필요한 렌더링 및 메모리 할당을 피하려면 이 숫자를 가능한 한 낮게 유지하는 것이 좋습니다.
UI: 네이티브 UI 렌더링과 사용자 상호작용을 처리하는 메인 스레드의 프레임 속도입니다. 초당 프레임 수(FPS)로 측정됩니다. 부드럽고 반응이 빠른 UI를 위해 이 수치를 가능한 한 60에 가깝게 유지하는 것이 좋습니다.
JS: 비즈니스 로직, API 호출, 터치 이벤트 등을 처리하는 JavaScript 스레드의 프레임 속도입니다. 역시 FPS로 측정됩니다. 빠르고 안정적인 앱을 사용하려면 이 수치를 가능한 한 60에 가깝게 유지하는 것이 좋습니다.
WebView 기반 도구 대신 React Native를 사용하는 설득력 있는 이유는 초당 60프레임과 앱의 네이티브 look and feel을 구현하기 위해서입니다.
여러분의 조부모 세대가 영화를 '움직이는 그림'이라고 불렀던 이유는 동영상의 사실적인 움직임은 정적인 이미지를 일정한 속도로 빠르게 변화시켜 만들어내는 착시 현상이기 때문입니다. React Native 팀은 이러한 각 이미지를 프레임이라고 부릅니다. 초당 표시되는 프레임 수는 동영상(또는 사용자 인터페이스)이 얼마나 매끄럽고 궁극적으로 실제와 같이 보이는지에 직접적인 영향을 미칩니다. iOS 디바이스는 초당 60프레임을 표시하므로 사용자와 UI 시스템은 해당 간격 동안 화면에 표시되는 정적 이미지(프레임)를 생성하는 데 필요한 모든 작업을 수행하는 데 약 16.67ms의 시간이 주어집니다. 할당된 16.67ms 내에 해당 프레임을 생성하는 데 필요한 작업을 완료하지 못하면 '프레임 드롭'이 발생하고 UI가 응답하지 않는 것처럼 보입니다.
이제 문제를 조금 혼란스럽게 만들기 위해 앱에서 Dev Menu를 열고 Show Perf Monitor
를 토글합니다. 그러면 두 가지 프레임 속도가 있음을 알 수 있습니다.
프레임 속도에는 두 가지 유형이 있습니다.
대부분의 React Native 애플리케이션의 경우 비즈니스 로직은 JavaScript 스레드에서 실행됩니다. 여기에서 React 애플리케이션이 실행되고, API 호출이 이루어지고, 터치 이벤트가 처리되는 등... 네이티브 지원 뷰에 대한 업데이트가 일괄 처리되어 이벤트 루프의 각 반복이 끝날 때마다 프레임 마감 시간 전에(모든 것이 순조롭게 진행된다면) 네이티브 측으로 전송됩니다.
JavaScript 스레드가 프레임에 대해 응답하지 않으면 프레임이 삭제된 것으로 간주됩니다. 예를 들어 복잡한 애플리케이션의 루트 컴포넌트에서 this.setState
를 호출하여 계산 비용이 많이 드는 컴포넌트 하위 트리를 다시 렌더링하는 경우 200ms가 소요되어 12프레임이 드롭될 수 있습니다. 이 시간 동안 JavaScript로 제어되는 모든 애니메이션이 멈추는 것처럼 보일 것입니다. 100ms보다 오래 걸리는 것이 있으면 사용자가 이를 느낄 수 있습니다.
🤜 Navigator
전환 중에 JS 스레드 낮은 FPS가 발생합니다.
new route를 푸시하면 JavaScript 스레드가 장면에 필요한 모든 컴포넌트를 렌더링하여 적절한 명령을 네이티브 측으로 전송하여 백킹 뷰를 생성해야 합니다. 전환이 JavaScript 스레드에 의해 제어되기 때문에 여기서 수행되는 작업은 몇 프레임이 걸리고 끊김 현상이 발생하는 것이 일반적입니다. 때로는 컴포넌트가 componentDidMount
에서 추가 작업을 수행하여 전환에서 두 번째 끊김 현상이 발생할 수 있습니다.
🤜 또 다른 예는 낮은 JS FPS로 인해 뷰가 터치에 반응하지 않는 경우입니다.
예를 들어 JavaScript 스레드에서 여러 프레임에 걸쳐 작업을 수행하는 경우 TouchableOpacity
에 대한 응답이 지연될 수 있습니다. 이는 JavaScript 스레드가 사용 중이어서 메인 스레드에서 전송된 raw 터치 이벤트를 처리할 수 없기 때문입니다. 결과적으로 TouchableOpacity
는 터치 이벤트에 반응하여 네이티브 뷰에 불투명도를 조정하도록 명령할 수 없습니다.
많은 사람들이 NavigatorIOS
의 성능이 Navigator
보다 더 뛰어나다는 것을 알고 있습니다. 그 이유는 전환 애니메이션이 전적으로 메인 스레드에서 이루어지기 때문에 JavaScript 스레드에서 프레임 드롭으로 인해 중단되지 않기 때문입니다.
마찬가지로, ScrollView
가 메인 스레드에 있기 때문에 JavaScript 스레드가 잠겨 있어도 ScrollView
를 통해 위아래로 자유롭게 스크롤할 수 있습니다. 스크롤 이벤트는 JS 스레드로 디스패치되지만 스크롤이 발생하기 위해 이벤트 수신이 필요하지는 않습니다.
개발 모드에서 실행: (dev=true
) FPS가 낮아집니다.
console.log
문을 사용하면 FPS가 낮아집니다.
ListView
가 FPS를 낮춥니다: 큰 목록의 경우 ListView
초기 렌더링이 너무 느리거나 스크롤 성능이 좋지 않습니다. FPS가 낮다면 FlashList를 사용하는 것이 좋습니다. FlashList에 대한 심도있는 글을 참조하세요. 여전히 FlatList를 사용하는 경우 렌더링된 항목의 측정을 건너뛰어 렌더링 속도를 최적화할 수 있도록 getItemLayout
을 구현했는지 확인하세요. getItemLayout
을 올바르게 설정하는 방법에 대한 글을 참조하세요.
거의 변경되지 않는 뷰를 다시 렌더링하면 FPS가 낮아집니다: ListView를 사용하는 경우 행을 다시 렌더링해야 하는지 여부를 빠르게 판단하여 많은 작업을 줄일 수 있는 rowHasChanged
함수를 제공해야 합니다. 변경 불가능한 데이터 구조를 사용하는 경우에는 참조 동일성 검사만 수행하면 됩니다. 마찬가지로 shouldComponentUpdate
를 구현하고 컴포넌트를 다시 렌더링할 정확한 조건을 지정할 수 있습니다.
JS 스레드에서 동시에 많은 작업을 수행하면 FPS가 낮아집니다: "Slow Navigator transitions"이 가장 흔하게 나타나지만 다른 경우에도 이런 문제가 발생할 수 있습니다. InteractionManager
를 사용하는 것도 좋은 방법이지만 사용자 경험 비용이 너무 높아 애니메이션 중 작업을 지연시킬 수 없다면 LayoutAnimation
을 고려할 수 있습니다. 현재 Animated API는 useNativeDriver: true
를 설정하지 않는 한 JavaScript 스레드에서 각 키프레임을 온디맨드 방식으로 계산하지만, LayoutAnimation은 코어 애니메이션을 활용하며 JS 스레드 및 메인 스레드 프레임 드롭의 영향을 받지 않습니다. LayoutAnimation
은 fire-and-forget 애니메이션("정적" 애니메이션)에만 작동하며, 중단할 수 있어야 하는 경우에는 Animated
를 사용해야 합니다.
화면에서 뷰를 움직이면(스크롤, 이동, 회전) UI 스레드 FPS가 떨어집니다.: 이미지 위에 투명한 배경의 텍스트가 있거나 각 프레임에서 뷰를 다시 그리기 위해 알파 합성이 필요한 다른 상황이 있는 경우 특히 그렇습니다. shouldRasterizeIOS
또는 renderToHardwareTextureAndroid
를 활성화하면 이 문제를 해결하는 데 큰 도움이 될 수 있습니다. 이를 과도하게 사용하지 않도록 주의하세요. 그렇지 않으면 메모리 사용량이 급증할 수 있습니다. 이러한 속성을 사용할 때는 성능과 메모리 사용량을 프로파일링하세요.
이미지 크기를 애니메이션하면 UI 스레드 FPS가 떨어집니다.: iOS에서는 이미지 컴포넌트의 너비나 높이를 조정할 때마다 원본 이미지에서 다시 잘라내고 크기를 조정합니다. 이는 특히 큰 이미지의 경우 비용이 매우 많이 들 수 있습니다. 대신 transform: [{scale}]
스타일 속성을 사용하여 크기에 애니메이션을 적용합니다. 예를 들어 이미지를 탭하여 전체 화면으로 확대할 때 이 작업을 수행할 수 있습니다.
TouchableX 뷰는 낮은 FPS 때문에 반응이 좋지 않습니다: 터치에 반응하는 컴포넌트의 불투명도 또는 하이라이트를 조정하는 것과 같은 프레임에서 작업을 수행하는 경우, onPress
함수가 반환될 때까지 해당 효과가 표시되지 않는 경우가 있습니다. onPress
가 많은 작업을 수행하여 몇 프레임이 중단되는 setState
를 수행하는 경우 이러한 문제가 발생할 수 있습니다. 이에 대한 해결책은 onPress
핸들러 내부의 모든 액션을 requestAnimationFrame
으로 래핑하는 것입니다.
handleOnPress() {
requestAnimationFrame(() => {
this.doExpensiveAction();
});
}
Navigator
애니메이션은 JavaScript 스레드에 의해 제어됩니다. "push from right" 장면 전환을 상상해 보세요. 매 프레임마다 새 장면이 화면 밖에서 시작하여 (x-offset이 320이라고 가정해 보겠습니다.) 궁극적으로 장면이 x-offset 0에 위치할 때 정착합니다. 이 전환 중 매 프레임마다 JavaScript 스레드는 새로운 x-offset을 메인 스레드로 보내야 합니다. JavaScript 스레드가 잠겨 있으면 이 작업을 수행할 수 없으므로 해당 프레임에서 업데이트가 발생하지 않고 애니메이션이 끊어집니다. 이에 대한 한 가지 해결책은 JavaScript 기반 애니메이션을 메인 스레드로 오프로드할 수 있도록 허용하는 것입니다. "useNativeDriver"
를 true
로 설정하면 이 문제가 해결됩니다.🔥
코드 레벨
에서 성능 최적화에 대해 자세히 알아보세요: 코드 레벨에서의 React Native 앱 성능 최적화👇
개발자가 알아야 할 세 가지 주요 스레드가 있습니다.
또한 Render 스레드는 Android 5.0 이상에서 사용할 수 있습니다. 각 스레드는 React Native 애플리케이션이 작동하는 방식에서 고유한 역할을 합니다.
Main (UI) 스레드는 모든 네이티브 UI 컴포넌트가 생성되고 조작되는 기본 스레드입니다. 사용자 상호작용을 처리하고, UI 컴포넌트를 렌더링하며, 디바이스 화면 업데이트를 관리합니다.
모든 React Native UI 업데이트는 이 스레드에서 발생합니다. 따라서 상태를 자주 조작하는 경우 이 스레드가 바빠져 성능 문제가 발생할 수 있습니다.
이 스레드의 주요 역할은 인터페이스를 원활하고 반응성 있게 유지하는 것입니다. Animated API를 사용하여 멤버에 애니메이션을 적용하는 것이 그 예입니다.
Animated.timing(this.state.fadeAnim, {
// this executes on the UI thread
toValue: 1,
duration: 2000,
}).start();
위의 코드 스니펫에서 `Animated.timing`
은 컴포넌트의 불투명도를 2초에 걸쳐 업데이트합니다. 이 애니메이션은 Main 스레드에서 발생하여 원활한 UI 업데이트를 보장합니다.
React Native 애플리케이션은 별도의 JavaScript 엔진에서 JavaScript 코드를 실행하며, 이는 JavaScript 스레드에서 이루어집니다. 여기에는 API 호출, 터치 이벤트 처리, JavaScript 코드 실행이 포함됩니다.
JavaScript 스레드는 실제 React 및 JavaScript 코드가 실행되는 스레드입니다. API에서 데이터를 가져온 후 상태를 설정하는 것을 예로 들 수 있습니다.
fetchData = async () => {
const response = await fetch("https://api.example.com/data");
const json = await response.json();
this.setState({ data: json }); // this line is executed in the JS thread
}
이 전체 작업은 JavaScript 스레드에서 실행됩니다.
React Native를 사용하면 JavaScript 없이 작업을 수행할 때 네이티브 언어(예: Android의 경우 Java, iOS의 경우 Objective-C 또는 Swift)로 코드를 작성할 수 있습니다. 이를 네이티브 모듈이라고 하며, 이 네이티브 코드의 실행은 Native Modules 스레드에서 이루어집니다.
React Native 앱에서 네이티브 코드를 사용하는 경우 여기에서 실행됩니다. 또한 Native Modules 스레드는 JavaScript 스레드에서 무거운 계산을 오프로드하여 애플리케이션의 응답성을 유지할 수 있습니다.
간단한 예로 Android에서 토스트 모듈을 만드는 것을 들 수 있습니다.
// This is Java code that will run on the Native Modules Thread
@ReactMethod
public void show(String message, int duration) {
Toast.makeText(getReactApplicationContext(), message, duration).show();
}
이 예제에서는 `show` 메서드가 JavaScript에서 호출되지만 Native Modules 스레드에서 실행됩니다.
Render 스레드는 Android 5.0(롤리팝)에 도입되었습니다. 이 스레드를 활용하도록 빌드된 앱의 경우 Main 스레드에서 렌더링을 제거합니다. 높은 프레임 속도가 필요한 복잡한 애니메이션에 특히 유용합니다.
프로파일링은 메모리 또는 시간 복잡도, 함수 호출의 빈도 및 지속 시간 등을 측정하는 분석을 통해 앱의 런타임 성능을 이해하는 데 필수적입니다. 이 모든 정보를 확보하면 앱의 성능을 추적하고 적절한 솔루션을 제공하여 앱의 성능과 사용자 참여를 유지하는 데 도움이 됩니다.
특정 작업에 대한 응답이 너무 오래 걸리면 70%의 사용자가 앱을 떠납니다. 그리고 한 번 이탈한 사용자는 다시 돌아오지 않을 가능성이 높기 때문에 비즈니스에 심각한 피해를 줄 수 있습니다. 이러한 이유만으로도 iOS 및 Android용 네이티브 툴로 React Native 애플리케이션을 프로파일링하면 조직과 사용자 기반 모두의 판도를 바꿀 수 있습니다.
Xcode는 첫 번째 리포트를 위한 몇 가지 기본 도구를 제공합니다. CPU, 메모리 및 네트워크를 모니터링할 수 있습니다.
Xcode에서 앱을 실행하면 아래와 같이 왼쪽 표시줄 상단에 Xcode의 "Debug Navigator" 버튼에서 성능 모니터가 표시됩니다.
이 "성능 모니터"에서 각 모니터가 의미하는 바를 분석하고 알아봅시다.
CPU 모니터: CPU 모니터는 작업량을 측정합니다.
메모리 모니터: 메모리 모니터는 앱 사용을 관찰하기 위한 것입니다. 모든 iOS 디바이스는 영구 저장소로 SSD를 사용하므로 이 데이터에 액세스하는 속도가 RAM에 비해 느립니다.
디스크 모니터: 디스크 모니터는 앱의 디스크 쓰기 성능을 파악하기 위한 것입니다.
네트워크 모니터: 네트워크 모니터는 iOS 앱의 TCP/IP 및 UDP/IP 연결을 분석합니다.
각 항목을 탭하여 자세한 정보를 확인할 수 있습니다.
CPU 모니터를 클릭하면, 아래와 같은 화면이 표시됩니다.
이제 메모리 모니터를 클릭해 보겠습니다. 아래와 같은 화면이 표시됩니다.
이제 디스크 모니터를 클릭해 보겠습니다. 아래와 같은 화면이 표시됩니다.
이제 네트워크 모니터를 클릭해 보겠습니다. 아래와 같은 화면이 표시됩니다.
Xcode는 기본적으로 표시되지는 않지만 UI를 검사하는 데 도움이 되는 추가 모니터를 제공하는데, 이것이 바로 View Hierarchy입니다. 실행 중인 앱의 검사하려는 화면에서 아래와 같이 View UI Hierarchy
디버그 버튼을 클릭합니다. 그러면 현재 UI가 2D/3D 모델과 뷰 트리로 표시됩니다.
이렇게 하면 겹치는 부분을 감지하거나(컴포넌트가 보이지 않는 경우) 컴포넌트 트리를 평탄하게 만들고자 할 때 도움이 됩니다. RN이 뷰 평탄화를 수행하더라도 모든 뷰를 평탄화할 수 없는 경우가 있으므로 여기서는 특정 항목에 초점을 맞춘 최적화를 수행할 수 있습니다.
아래와 같이 Xcode의 "Open Developer Tool"에서 "Instruments"를 엽니다.
"Instruments"를 클릭하면 아래와 같은 패널이 표시됩니다.
여기서부터 Time Profiler
를 사용하겠습니다. 자세히 살펴봅시다. Time Profiler를 클릭하고 엽니다. Time Profiler를 열면 아래와 같은 화면이 표시됩니다.
Time Profiler의 왼쪽 상단 모서리에 아래와 같은 내용이 표시됩니다(빨간색 사각형 🟥 과 녹색 사각형 🟩 을 보세요).
이 빨간색 사각형 🟥 과 녹색 사각형 🟩 을 클릭하면 드롭다운이 표시되고, 드롭다운에서 아래와 같이 디바이스를 선택하고 그에 따라 앱을 선택합니다.
선택한 디바이스의 왼쪽에 시작(⭕️) 버튼이 있습니다. 이 버튼을 클릭하고 평소처럼 앱을 사용하세요. 제 경우에는 시뮬레이터에서 FlatList에 리스트 아이템을 로드했습니다.
40초 동안 앱을 사용하여 리스트를 로드하고 위아래로 계속 스크롤하면서 타임 킬러 문제를 일으켰습니다. "Profiler Recording"을 중지한 후 큰 파란색 사각형(아래 이미지 참조)을 발견했는데, 이는 완료하는 데 많은 시간이 걸린다는 것을 의미합니다(제 경우 파란색 사각형은 약 3초가 걸렸습니다).
스레드를 살펴봅시다. 스레드 섹션에는 전체 프로파일링 기간 동안 수행된 모든 활동이 표시됩니다(제 경우에는 40초). 차트의 특정 부분을 선택하여 스레드 및 해당 활동을 확인할 수 있습니다. 그래프에서 마우스 왼쪽 버튼을 클릭하여 검사하려는 그래프의 범위를 선택합니다. 아래 이미지와 같이 빨간색 부분만 선택했습니다.
위 이미지와 같이 그래프에서 선택한 부분(빨간색 상자 🟥)에 대해 아주 적은 양의 활동 목록(녹색 상자 🟩)만 표시됩니다.
셰브론(➡️) 위로 option + 마우스 클릭을 하면 확장되어 유용한 정보가 표시됩니다. 현재로서는 메모리 주소가 표시되지만 문제가 있는 위치를 찾을 수 있는 다른 방법을 찾아야 합니다.
Flipper
를 사용하여 Hermes Debugger (RN)
라는 모니터와 페어링해 보겠습니다. 앱을 열고 실행 중인 상태에서 Flipper
로 이동하여 실행 중인 앱이 아직 선택되어 있지 않은 경우 선택한 다음 Hermes Debugger (RN)
-> Profiler
로 이동합니다.
먼저 MacOS에 flipper를 설치해 보겠습니다. 성공적으로 설치한 후 Hermes Debugger (RN)
와 페어링하는 방법을 살펴보겠습니다.
Flipper를 설치하려면 Homebrew
명령어를 사용하거나 공식 Flipper 다운로드 링크에서 Flipper의 .dmg
파일을 다운로드하세요.
Flipper를 설치하는 Homebrew 명령어: brew install - cask flipper
(저는 Homebrew를 사용했고 Flipper 안정 버전 v0.225.0
이 설치되었습니다).
Flipper 다운로드 공식 링크: https://www.facebook.com/fbflipper/public/mac
Flipper를 설치한 후, MacOS에서 Flipper 앱을 여는 동안 아래 오류가 발생하면 간단한 해결 방법이 있습니다.
해결 방법: MacBook 터미널의 루트 디렉터리(제 경우에는 iTerm)에서 다음 명령을 실행합니다xattr -d com.apple.quarantine /Applications/Flipper.app
. 이제 다시 Flipper 앱을 엽니다.
이제 Flipper를 열면 아래와 같은 메시지가 표시될 수 있습니다. 또 다른 오류 🤜🙇♂️.
하지만 제가 한 일은 Cmd + Q
로 Flipper 앱을 완전히 닫은 다음 런치패드에서 다시 Flipper를 열었습니다. 이제 작동했습니다 😄. Flipper를 처음 봤을 때의 느낌입니다.
네, Flipper 홈 페이지의 React Native 섹션 아래에 "Hermes Debugger (RN)"
탭이 이미 보입니다. 꽤 멋지네요 🎉.
마지막으로 멈추고 Flipper 설치를 시작한 부분으로 돌아가 보겠습니다. 네, “Pair Flipper with Hermes Debugger (RN) -> Profiler”이었습니다.
🛑 참고: Flipper 앱을 열기 전에 항상 디바이스/시뮬레이터에서 앱(iOS/안드로이드)을 실행해야 한다는 점을 기억하세요.
"Hermes Debugger (RN)"
버튼을 클릭하면 아래와 같은 텍스트가 표시됩니다.
제 React Native 앱은 v0.71이었기 때문에 기본적으로 Hermes가 true지만 여전히 Flipper는 Hermes 앱을 찾지 못했다고 말합니다. 그래서 시뮬레이터에서 앱을 다시 로드했습니다. 이제 성공적으로 연결되었고 "Hermes Debugger (RN)"
버튼을 다시 클릭하면 아래와 같이 표시됩니다.
React Native 앱에서 Hermes를 활성화하지 않았거나 "앱에서 Hermes가 이미 활성화되어 있습니까?"를 확인하려면 이에 대한 짧은 글을 참조하세요.
글 링크:
Hermes 상세 정보 🔥
이제 start
를 클릭하면 프로파일러가 시작됩니다. Time Profiler
로 프로파일링할 때와 동일한 흐름과 작업을 앱에서 수행합니다. 중지하면 수집된 모든 데이터를 볼 수 있습니다.
기본적으로 데이터는 아래에서 위로 정렬되며 무거운 작업이 맨 위에 있습니다. 제 경우에는 checkType()
이라는 함수가 최대 122밀리초밖에 걸리지 않는 것을 확인할 수 있었습니다. 따라서 제가 프로파일링한 FlatList에 대해 React Native 앱에서 작성한 코드가 매우 효율적이라는 것을 알 수 있습니다.
또한 이전에 Xcode Instruments에서 "Time Profiler"
를 통해 시간이 많이 걸리는 큰 파란색
동작이 리스트 데이터를 가져오는 API 호출이라는 것을 알게 되었습니다. 그래서 React Native 코드 대신 백엔드 코드를 최적화해야 했습니다. 백엔드 최적화가 잘 된 후에는 그래프에서 더 이상 큰 파란색이 보이지 않습니다. 아래는 Xcode "Time Profiler" 도구의 최종 결과물로, 더 이상 큰 파란색
시간 소모 동작이 보이지 않습니다 🔥.
성능 문제가 발생하면 대부분 React Profiler를 사용하여 문제를 해결합니다. 대부분의 성능 문제는 JS 영역에서 발생하기 때문에 보통은 그 이상의 조치를 취할 필요가 없습니다. 하지만 때때로 Android 런타임에서 직접 발생하는 버그나 성능 문제가 발생할 수 있습니다. 이러한 경우 디바이스에서 다음과 같은 지표를 수집하는 데 도움이 되는 정밀한 프로파일링 도구가 필요합니다.
CPU
메모리
네트워크
배터리 사용량
이 데이터를 기반으로 앱이 평소보다 더 많은 에너지를 소비하는지, 경우에 따라서는 CPU 전력을 더 많이 사용하는지 확인할 수 있습니다. 특히 저사양(LE) Android 디바이스에서 실행된 코드를 확인하는 데 유용합니다. 일부 알고리즘은 일부 디바이스에서 더 빠르게 실행될 수 있으며 최종 사용자는 결함을 발견하지 못할 수 있지만 일부 고객은 저사양 디바이스를 사용할 수 있으며 알고리즘이나 기능이 휴대폰에 너무 무거울 수 있다는 점을 기억해야 합니다. 하이엔드 디바이스는 하드웨어가 강력하기 때문에 이를 처리할 수 있습니다.
Android Studio는 JetBrains에서 개발한 IDE입니다. Google에서 공식적으로 지원하며 모든 Android 앱을 개발하는 데 사용할 수 있는 공식 IDE입니다. 매우 강력하며 한 곳에 많은 기능이 포함되어 있습니다. 그 도구 중 하나는 이름에서 알 수 있듯이 Android에서 React Native 프로파일링이 필요한 경우 유용한 Android Profiler
입니다.
아직 Android Studio를 설치하지 않은 경우 이 링크를 사용하여 설치할 수 있습니다.
Android Studio를 설치한 후, Android Studio에서 React Native 앱 android folder
를 엽니다. 이제 Android Studio가 리액트 네이티브 앱에 대한 Gradle의 모든 종속성을 완료할 수 있도록 시간을 줍니다. 시간이 다소 걸릴 수 있습니다(10분 이상 걸릴 수도 있음).
모든 종속성을 성공적으로 설치한 후 아래와 같이 Profiler를 열고, Android Studio 상단 메뉴 바에서 View > Tool Windows > Profiler를 선택합니다.
또는 도구 모음에서 Profile
버튼을 클릭할 수도 있습니다.
영향을 받는 실제 Android 디바이스에서 앱을 실행하고, 디바이스가 없는 경우 가급적 저사양 휴대폰이나 에뮬레이터를 사용합니다. 앱에 런타임 모니터링이 설정되어 있는 경우 사용자가 가장 많이 사용하는 모델 또는 특정 문제의 영향을 받는 모델을 사용하세요.
개발 모드를 끕니다. 앱이 해당 번들을 제공하는 메트로 서버 대신 JS 번들을 사용하는지 확인해야 합니다. 아래 단계에 따라 "JS DEV MODE"
를 해제하세요.
"JS DEV MODE"
를 끄는 단계 👇
먼저 다음 npx 명령을 사용하여 앱을 실행합니다: npx react-native run-android
앱을 실행한 후 디바이스(디바이스를 흔들어)/시뮬레이터(Cmd + M
을 함께 눌러)에서 DEV MENU를 엽니다.
DEV MENU를 연 후 Settings를 클릭합니다.
“JS DEV MODE” 옵션이 표시되면 이를 선택 해제하고 앱을 다시 로드합니다.
다시 로드하는 동안 앱이 0-100% 빌드되는 것을 다시 한 번 확인할 수 있습니다. 이 빌드에서는 앱이 디바이스/시뮬레이터에서 서버를 번들로 로드합니다.
빌드가 완료되면 METRO에 더 이상 로그가 표시되지 않습니다.
다음은 디바이스/시뮬레이터 개발 메뉴의 "JS DEV MODE" 사진입니다.
다음은 METRO의 마지막 라인 사진입니다.
이제 Profiler tab(앞서 프로파일러를 여는 방법 또는 Android Studio 하단에 표시된 두 가지 방법)으로 이동하여 아래와 같이 새 프로파일러 세션을 추가합니다. Profiler Session에서 디바이스/시뮬레이터가 실행 중인 것을 확인할 수 있습니다.
세션이 앱에 연결될 때까지 기다렸다가 스와이프, 스크롤, 네비게이션 등 일부 성능 문제를 일으킬 수 있는 작업을 수행하기 시작합니다. 완료되면 아래와 같은 몇 가지 지표가 표시됩니다.
각 그린필드 React Native 앱에는 하나의 Android 액티비티만 있습니다. 앱에 두 개 이상의 액티비티가 있는 경우 브라운필드 앱일 가능성이 높습니다. 여기에서 브라운필드 접근 방식에 대해 자세히 알아보세요. 위의 예에서는 흥미로운 내용이 보이지 않습니다. 모든 것이 결함 없이 정상적으로 작동합니다. 각 지표를 확인해 보겠습니다.
CPU 지표
는 일부 계산을 수행하는 데 더 많은 에너지가 필요하기 때문에 에너지 소비와 밀접한 관련이 있습니다.
앱을 사용하는 동안 메모리 지표
는 변경되지 않으며, 이는 예상되는 현상입니다. 메모리 사용량은 새 화면을 열 때와 같이 증가할 수 있고, 가비지 컬렉터(GC)가 화면을 네비게이팅 할 때와 같이 여유 메모리를 해제할 때 감소할 수 있습니다. 메모리가 예기치 않게 증가하여 계속 늘어나는 경우 메모리 누수를 의미할 수 있으며, 메모리 부족(OOM) 오류로 앱이 충돌할 수 있으므로 피해야 합니다.
네트워크 섹션
은 Network Tab이라는 별도의 도구로 이동되었습니다. 이 지표는 대부분 백엔드 인프라와 관련이 있으므로 대부분의 경우 필요하지 않습니다. 네트워크 연결을 프로파일링하려면 여기에서 자세한 정보를 확인할 수 있습니다.
에너지 섹션
에서는 앱의 에너지 사용량이 낮거나 중간 또는 높은 시점에 대한 힌트를 제공하여 일상적인 앱 사용 경험에 영향을 미칩니다.
이전 예제에서 각 지표 간의 관계를 확인할 수 있었습니다.
더 자세한 내용을 보려면 탭을 두 번 클릭해야 합니다. 이제 더 자세한 내용을 볼 수 있습니다. 사용자가 터치 동작(위 예시에서는 스와이프)을 시작하면 더 많은 CPU 작업을 볼 수 있습니다. 각 앱에는 고유한 CPU 급증 및 저하의 시그니처가 있습니다. 앱과 상호 작용하고 터치 이벤트와 같은 특정 활동을 사용량 증가와 연관시켜 직관적으로 파악하는 것이 중요합니다. 즉, 작업을 수행해야 하므로 어느 정도의 사용량 급증이 예상됩니다. 문제는 장기간 또는 예상치 못한 곳에서 CPU 사용량이 매우 높을 때 시작됩니다.
저사양 디바이스에서 최고의 성능을 발휘하는 React Native 앱에 가장 적합한 리스트 또는 스크롤 뷰 컴포넌트를 선택하려고 한다고 가정해 봅시다. 현재 솔루션을 개편하거나 개선할 수 있다는 사실을 발견하고 이 작업을 시작했습니다. 실험에서 위에서 설명한 솔루션을 사용하여 저사양 디바이스에서 솔루션이 어떻게 작동하는지 확인하고자 합니다. CPU를 더블클릭하면 아래 데이터를 확인할 수 있습니다.
그래서 제가 여기서 발견한 것은 RenderThread
가 시간이 걸린다는 것입니다. 안드로이드 L(5.0) 이상을 사용하는 경우 애플리케이션에 RenderThread
가 있습니다. 이 스레드는 UI를 그리는 데 사용되는 실제 OpenGL 명령을 생성합니다. 스레드 이름은 RenderThread
또는 <...>
입니다.
OpenGL
은 2D 및 3D 그래픽 렌더링을 위한 오픈 소스 크로스 플랫폼 API입니다. React Native에서 GPU에 액세스하고 복잡한 연산을 수행할 수 있는 네이티브 모듈을 만드는 데 사용됩니다. React Native는 OpenGL을 사용하여 모바일 애플리케이션에서 애니메이션, 전환, 3D 효과와 같은 기능을 구현합니다.
프로파일링 차트에서 mqt_js 스레드
는 어떤지 살펴봅시다. 아래를 참조하세요.
mqt_js 스레드
에 녹색 필드가 너무 많이 표시되는 경우는 mqt_js 스레드
가 거의 항상 사용되며 계산이 JS 측에서 수행되기 때문에 일부 무거운 계산을 수행하는 것입니다. 이를 개선하는 방법에 대해 생각해 볼 수 있습니다. 확인할 수 있는 옵션은 여러 가지가 있습니다.
통신 측면에서 브릿지를 JSI로 대체 - JSI가 브릿지보다 빠른지 테스트를 해보세요. JSI가 새 아키텍처에서 어떻게 작동하는지 자세히 알아보세요.
코드의 일부를 네이티브 측으로 이동 - 네이티브 측에서는 스레드 실행을 더 잘 제어할 수 있고 JS 또는 UI 스레드를 차단하지 않도록 일부 작업을 예약할 수 있습니다.
다른 네이티브 컴포넌트 사용 - 네이티브 스크롤 뷰를 커스텀 솔루션으로 대체합니다.
섀도우 노드 사용 - C++로 값비싼 계산을 수행한 후 네이티브 측에 전달합니다.
모든 솔루션을 시도해보고 서로의 효과를 비교할 수 있습니다. 프로파일러가 지표를 제공하며, 이를 바탕으로 특정 문제에 가장 적합한 접근 방식을 결정할 수 있습니다.
Android Studio CPU Profiler를 사용하여 System Tracing을 할 수도 있습니다. 적절한 함수가 언제 호출되었는지 확인할 수 있습니다. 모든 스레드를 분류하고 어떤 함수가 가장 비용이 많이 드는지 확인할 수 있으며, 이는 UX에 영향을 미칩니다. System Tracing을 활성화하려면 (프로파일링 세션 레코드가 실행 중인 상태에서) CPU 섹션을 클릭하고 System Trace Recording을 선택합니다. 그런 다음 "Record"를 클릭합니다.
앱에서 약간의 상호작용을 하고 기록을 중지하면 세부 정보가 포함된 모든 스레드를 볼 수 있습니다.
저장 버튼을 클릭하여 데이터를 저장할 수도 있습니다.
이제 저장된 데이터를 다른 도구(예: Perfetto)에서 사용할 수 있습니다.
위의 iOS 프로파일링 파트에서 Flipper를 설치하고 디버깅에 사용하는 방법을 살펴봤습니다. Flipper에서는 Flipper 플러그인 Flashlight를 사용할 수 있습니다. 하지만 CLI에서 Flashlight를 사용하는 것이 더 좋습니다. Flashlight를 설치하고 웹에서 시작하려면 아래 단계를 따르세요.
MacOS/Linux에서는 터미널에 curl https://get.flashlight.dev | bash
명령어를 실행하여 설치하고 Windows에서는 터미널에 iwr https://get.flashlight.dev/windows -useb | iex
명령어를 실행합니다.
설치 후 새 터미널을 열고 다음 명령을 실행합니다. flashlight measure
웹에서 포트가 열립니다 http://localhost:3000/
이제 웹에서 USB/시뮬레이터를 통해 연결된 장치를 선택하고 측정값 기록을 시작하세요. 아래와 같은 출력이 표시됩니다. 앱의 FlashList에 측정값을 적용했습니다.
향후 출시될 React Native 버전에서 걱정하지 마세요. 0.73 이상 버전에서도 Flipper를 사용할 수 있으며, 사전 설치되어 제공되지 않으므로 직접 설치해야 합니다.
아래 내 트윗을 참조하세요 👇
🔥 🔥 React Native의 New Architecture에 대한 자세한 내용을 읽어보세요. 글 링크: React Native — New Architecture in depth (Hermes, JSI, Fabric, Yoga, Turbo Module, Codegen).