Skip to main content
Version: Next

프로파일링

프로파일링은 앱의 성능, 리소스 사용량, 동작을 분석해 잠재적인 병목 현상이나 비효율성을 찾아내는 과정이다. 다양한 기기와 환경에서 앱이 원활하게 작동하도록 보장하려면 프로파일링 도구를 활용하는 것이 중요하다.

iOS에서는 Instruments가 필수 도구이며, Android에서는 Android Studio Profiler를 사용하는 방법을 익혀야 한다.

하지만 먼저 개발 모드가 꺼져 있는지 확인하자! 앱 로그에 __DEV__ === false, development-level warning are OFF, performance optimizations are ON이 표시되어야 한다.

안드로이드 UI 성능 프로파일링: 시스템 트레이싱 활용

안드로이드는 1만 종류가 넘는 다양한 스마트폰을 지원하며, 소프트웨어 렌더링을 기본으로 한다. 이 프레임워크 아키텍처와 다양한 하드웨어를 지원해야 하는 특성 때문에, iOS에 비해 기본적으로 제공되는 성능 최적화가 상대적으로 부족할 수 있다. 하지만 때로는 성능을 개선할 수 있는 여지가 있으며, 대부분의 경우 문제가 네이티브 코드에 있는 것은 아니다!

UI 지연 현상을 디버깅하는 첫 번째 단계는 각 16ms 프레임 동안 시간이 어디에 소비되는지 파악하는 것이다. 이를 위해 안드로이드 스튜디오에 내장된 시스템 트레이싱 프로파일러를 사용한다.

1. 트레이스 수집하기

먼저, 조사하고자 하는 버벅임 현상을 보이는 기기를 USB로 컴퓨터에 연결한다. Android Studio에서 프로젝트의 android 폴더를 열고, 오른쪽 상단 패널에서 해당 기기를 선택한 후 프로파일링 가능한 상태로 프로젝트를 실행한다.

앱이 프로파일링 가능한 상태로 빌드되어 기기에서 실행 중일 때, 프로파일링하고자 하는 네비게이션 또는 애니메이션 직전까지 앱을 진행시킨다. 그런 다음 Android Studio의 Profiler 패널에서 "시스템 활동 캡처" 작업을 시작한다.

트레이스 수집이 시작되면, 관심 있는 애니메이션 또는 상호작용을 수행한다. 이후 "녹화 중지"를 누른다. 이제 Android Studio에서 직접 트레이스를 확인할 수 있다. 또는 "과거 기록" 패널에서 트레이스를 선택한 후 "녹화 내보내기"를 눌러 Perfetto와 같은 도구에서 열어볼 수도 있다.

2. 트레이스 읽기

Android Studio나 Perfetto에서 트레이스를 열면 다음과 같은 화면을 볼 수 있다:

예시

힌트

WASD 키를 사용해 화면을 이동하거나 확대할 수 있다.

사용하는 도구에 따라 UI가 다를 수 있지만, 아래 설명은 어떤 도구를 사용하든 적용할 수 있다.

VSync 하이라이트 활성화

화면 오른쪽 상단에 있는 이 체크박스를 선택해 16ms 프레임 경계를 강조 표시한다:

VSync 하이라이트 활성화

위 스크린샷처럼 줄무늬가 보여야 한다. 그렇지 않다면 다른 기기에서 프로파일링을 시도해 보자. 삼성 기기에서는 VSync 표시에 문제가 있는 것으로 알려져 있으며, Nexus 시리즈는 일반적으로 안정적으로 표시된다.

3. 프로세스 찾기

스크롤을 내리면서 패키지 이름의 일부를 찾는다. 이 경우, com.facebook.adsmanager 프로파일링을 진행했는데, 커널의 스레드 이름 제한으로 인해 book.adsmanager로 표시되었다.

왼쪽에는 타임라인 행과 대응되는 스레드 목록이 보인다. 우리가 주목해야 할 주요 스레드는 다음과 같다: UI 스레드(패키지 이름 또는 UI Thread로 표시), mqt_js, 그리고 mqt_native_modules. 안드로이드 5.0 이상을 사용 중이라면 Render Thread도 확인해야 한다.

  • UI 스레드. 여기서 표준 안드로이드의 측정(measure), 레이아웃(layout), 그리기(draw) 작업이 진행된다. 오른쪽의 스레드 이름은 패키지 이름(이 경우 book.adsmanager) 또는 UI Thread로 표시된다. 이 스레드에서 볼 수 있는 이벤트는 Choreographer, traversals, DispatchUI와 관련된 내용으로 구성된다.

    UI 스레드 예시

  • JS 스레드. 자바스크립트가 실행되는 곳이다. 스레드 이름은 mqt_js 또는 <...>로 표시되며, 이는 디바이스 커널의 상태에 따라 달라진다. 이름이 없을 경우 JSCall, Bridge.executeJSCall과 같은 항목을 찾아 확인한다.

    JS 스레드 예시

  • 네이티브 모듈 스레드. 네이티브 모듈 호출(예: UIManager)이 실행되는 곳이다. 스레드 이름은 mqt_native_modules 또는 <...>로 표시된다. 이름이 없을 경우 NativeCall, callJavaModuleMethod, onBatchComplete와 같은 항목을 찾아 확인한다.

    네이티브 모듈 스레드 예시

  • 추가: 렌더 스레드. 안드로이드 L(5.0) 이상을 사용 중이라면 애플리케이션에 렌더 스레드가 존재한다. 이 스레드는 UI를 그리기 위해 실제 OpenGL 명령어를 생성한다. 스레드 이름은 RenderThread 또는 <...>로 표시된다. 이름이 없을 경우 DrawFrame, queueBuffer와 같은 항목을 찾아 확인한다.

    렌더 스레드 예시

문제 원인 파악

부드러운 애니메이션은 다음과 같은 모습이어야 한다:

부드러운 애니메이션

색상이 변할 때마다 하나의 프레임을 나타낸다. 프레임을 표시하려면 모든 UI 작업이 16ms 내에 완료되어야 한다. 어떤 스레드도 프레임 경계에 가깝게 작업하지 않는다는 점에 주목한다. 이렇게 렌더링되는 애플리케이션은 60 FPS로 동작한다.

하지만 애니메이션이 끊기는 경우, 다음과 같은 상황을 볼 수 있다:

JS로 인한 끊김 현상

JS 스레드가 거의 항상 실행되고 있으며, 프레임 경계를 넘나들고 있다! 이 앱은 60 FPS로 렌더링되지 않는다. 이 경우, 문제는 JS에 있다.

다음과 같은 상황도 발생할 수 있다:

UI로 인한 끊김 현상

이 경우, UI와 렌더 스레드가 프레임 경계를 넘나드는 작업을 수행한다. 각 프레임에서 렌더링하려는 UI가 너무 많은 작업을 요구한다. 이 경우, 문제는 렌더링되는 네이티브 뷰에 있다.

이 시점에서, 다음 단계를 결정하는 데 매우 유용한 정보를 얻을 수 있다.

자바스크립트 문제 해결하기

자바스크립트 문제를 발견했다면, 실행 중인 특정 자바스크립트 코드에서 단서를 찾아보자. 위 시나리오에서는 RCTEventEmitter가 프레임마다 여러 번 호출되는 것을 확인할 수 있다. 아래는 트레이스에서 자바스크립트 스레드를 확대한 화면이다:

Too much JS

이 상황은 정상적이지 않다. 왜 이렇게 자주 호출되는 걸까? 실제로 다른 이벤트들이 발생하고 있는 건가? 이 질문에 대한 답은 대부분 여러분의 제품 코드에 달려 있다. 그리고 많은 경우, shouldComponentUpdate를 살펴보는 것이 좋다.

네이티브 UI 문제 해결

네이티브 UI 문제를 발견했다면, 일반적으로 두 가지 상황을 고려해야 한다:

  1. 매 프레임마다 그리려는 UI가 GPU에서 처리하기에 너무 많은 작업을 요구하는 경우
  2. 애니메이션이나 인터랙션 중에 새로운 UI를 생성하는 경우 (예: 스크롤 중에 새로운 콘텐츠를 로드하는 경우)

이 두 가지 상황은 모두 UI의 성능 저하를 유발할 수 있다. 첫 번째 경우에는 GPU의 작업량을 줄이는 방법을 고민해야 하며, 두 번째 경우에는 애니메이션 중에 새로운 UI를 생성하지 않도록 설계를 변경해야 한다.

GPU 작업 과부하

첫 번째 시나리오에서 UI 스레드와 렌더 스레드가 다음과 같은 상태로 나타나는 트레이스를 확인할 수 있다:

과부하된 GPU

DrawFrame에서 프레임 경계를 넘어가는 긴 시간을 주목하자. 이는 이전 프레임의 커맨드 버퍼를 GPU가 처리하기를 기다리는 시간이다.

이 문제를 해결하려면 다음 방법을 고려해야 한다:

  • 애니메이션이나 변환이 적용되는 복잡하고 정적인 콘텐츠(예: Navigator의 슬라이드/알파 애니메이션)에 대해 renderToHardwareTextureAndroid 사용을 검토한다.
  • 기본적으로 비활성화되어 있는 needsOffscreenAlphaCompositing을 사용하지 않도록 주의한다. 대부분의 경우 이 옵션은 GPU의 프레임당 부하를 크게 증가시킨다.

UI 스레드에서 새로운 뷰 생성하기

두 번째 시나리오에서는 다음과 같은 상황을 볼 수 있다:

뷰 생성

먼저 JS 스레드가 잠시 생각하는 시간을 보이고, 그 다음 네이티브 모듈 스레드에서 작업이 이루어지며, 마지막으로 UI 스레드에서 비용이 많이 드는 탐색 작업이 진행된다.

이 문제를 빠르게 해결할 수 있는 방법은 없으며, 새로운 UI 생성을 상호작용이 끝난 후로 미루거나, 생성할 UI를 단순화하는 방법밖에 없다. 리액트 네이티브 팀은 이 문제를 해결하기 위해 인프라 수준의 솔루션을 개발 중이다. 이 솔루션은 새로운 UI를 메인 스레드 외부에서 생성하고 설정할 수 있게 해주며, 상호작용이 원활하게 계속될 수 있도록 한다.

네이티브 CPU 핫스팟 찾기

문제가 네이티브 측면에 있는 것 같다면, CPU 핫스팟 프로파일러를 사용해 더 자세한 정보를 얻을 수 있다. Android Studio 프로파일러 패널을 열고 "Find CPU Hotspots (Java/Kotlin Method Recording)"을 선택한다.

Java/Kotlin 기록 선택하기

"Find CPU Hotspots (Java/Kotlin Recording)"을 선택해야 한다. "Find CPU Hotspots (Callstack Sample)"과 아이콘이 비슷하지만 기능이 다르다.

상호작용을 수행한 후 "Stop recording"을 누른다. 기록 작업은 리소스를 많이 사용하므로 상호작용 시간을 짧게 유지하는 것이 좋다. 그런 다음 Android Studio에서 결과 트레이스를 확인하거나, 이를 내보내서 Firefox Profiler와 같은 온라인 도구에서 열 수 있다.

시스템 트레이스와 달리, CPU 핫스팟 프로파일링은 속도가 느려 정확한 측정값을 제공하지는 않는다. 하지만 어떤 네이티브 메서드가 호출되고, 각 프레임 동안 시간이 어디에 비례적으로 소비되는지에 대한 대략적인 정보를 제공할 것이다.