Skip to main content
네이티브
입문네이티브React Native 플랫폼 · 29

React Native 성능 측정: JavaScript·UI·네이티브 병목을 어떻게 나누는가

느린 상호작용의 전체 시간만 보고 JavaScript를 원인으로 지목하는 실패를 재현하고, JavaScript 작업·React Render·UI 그리기·다른 네이티브 함수 실행의 증거를 같은 시도에 연결해 병목 후보를 좁힌다.

마지막 검증 재검증 정책: 제품 버전 의존: 새 주요 버전마다 재검증
목차
표준·구현·측정·해석 표시는 무엇인가요?
  • 표준웹 표준이나 언어 명세가 정한 동작
  • 구현특정 기술이나 브라우저가 실제로 구현한 동작
  • 측정명시한 환경에서 직접 실행해 관찰한 결과
  • 해석앞선 근거에서 도출한 설계 판단
  • 미확인아직 공식 근거나 재현 결과를 확인하지 못한 내용

30초 요약

메트릭은 “얼마나 느린가?”, 프로파일은 “어느 코드가 실행 시간을 많이 썼는가?”, 시스템 성능 트레이스는 “같은 시간축에서 어느 실행 흐름이 실행되거나 기다렸는가?”를 묻는 증거다. 병목은 전체 결과를 늦추는 주된 제한 지점이다.

screen-ready는 제품이 “다음 화면을 사용할 준비가 끝났다”고 정한 앱 사건이며 실제 픽셀 표시 시각과 다를 수 있다. actualDuration은 선택한 React 트리의 이번 Render 시간이다. 여기서 React Render는 컴포넌트 트리를 계산하는 단계이고, 플랫폼 그리기 실행 흐름은 Android·iOS가 계산된 화면을 실제 프레임으로 만드는 작업이다. 둘은 같은 Render가 아니다.

사용자 결과       버튼 입력 → 제품이 정한 screen-ready까지의 시간
JavaScript 작업   이름 붙인 동기 작업 구간과 JavaScript 실행 흐름
React Render      선택한 React 트리의 actualDuration
UI 그리기          UI 스레드·플랫폼 그리기 실행 흐름과 프레임
네이티브 함수      UI 밖을 포함한 플랫폼 함수의 CPU 실행 기록

사용자 결과와 네 계층 기록은 측정 범위와 도구가 달라 단순히 더하는 숫자가 아니다. 같은 시도 식별값·시간 구간·build·기기와 연결해 비교한다. React Render는 보통 JavaScript 경로 안의 더 구체적인 증거다.

반복해서 기억할 문장은 이것이다.

먼저 사용자 결과를 재고, 같은 시도의 실행 계층 증거로 병목 후보를 좁힌다.

이 글을 관통하는 상황: 느린 영수증 화면을 모두 JavaScript 문제로 분류했다

영수증 화면의 네 동작이 모두 느리다는 신고가 들어왔다. 팀은 버튼을 누른 뒤 다음 화면 준비가 끝났다고 앱이 기록한 시점까지의 interactionMs만 확인했다. 300ms를 넘으면 모두 javascript-work로 저장했다.

하지만 네 동작의 실제 원인 후보는 달랐다.

영수증 정렬      긴 일반 JavaScript 계산
영수증 행 갱신   비싼 React Render
바텀 시트 열기   화면 아래에서 올라오는 패널의 UI·플랫폼 그리기
Portable Document Format(PDF) 영수증 만들기
                   문서 모양을 고정해 저장·전달하는 파일을 만드는 네이티브 처리

시작과 끝의 제품 계약을 먼저 고정해야 같은 값을 비교할 수 있다.

이 글의 질문은 하나다.

JavaScript, React Render, UI·플랫폼 그리기와 다른 네이티브 함수 중 병목 위치를 어떻게 찾는가?

고정된 네 증거 묶음으로 잘못된 분류를 재현한다

1편에서 사용한 React Native 0.86 앱의 App.tsx를 아래 코드로 교체한다. 외부 패키지는 필요 없다. USE_LAYER_EVIDENCE = false가 전체 시간만 보는 잘못된 기준선이다.

이 fixture는 실제 CPU·프레임 시간을 측정하지 않는다. MeasurementRecord 네 개는 같은 입력을 반복한 뒤 이미 검증·정규화했다고 가정한 가상 증거 묶음이다. interactionMs는 제품 결과 구간, jsBusyMs는 JavaScript 실행 흐름의 바쁜 구간, reactRenderMs는 선택한 React 트리 Render, uiRenderingBusyMs는 UI 스레드·플랫폼 그리기 구간, nativeFunctionBusyMs는 그 밖의 네이티브 함수 실행 구간을 뜻한다. 이 값들은 서로 다른 관찰 범위이므로 합계로 사용하지 않는다.

attempt ID는 한 번의 측정 시도에 속한 기록을 잇는 식별값이고, build ID는 실행한 앱 산출물을 구분한다. repeatCount는 같은 조건을 몇 번 실행해 만든 증거인지 남긴다. 이 fixture의 값은 다섯 번의 실제 표본을 계산한 결과가 아니라 반복 조건까지 포함한 가상 입력이다.

import React, { useState } from 'react'
import { Button, SafeAreaView, ScrollView, StyleSheet, Text, View } from 'react-native'
 
const USE_LAYER_EVIDENCE = false
 
type CandidateLayer =
  | 'javascript-work'
  | 'react-render'
  | 'ui-rendering'
  | 'native-function'
  | 'unresolved'
 
type MeasurementRecord = {
  id: string
  scenario: string
  attemptId: string
  buildId: string
  device: string
  repeatCount: number
  interactionMs: number
  jsBusyMs: number
  reactRenderMs: number
  uiRenderingBusyMs: number
  nativeFunctionBusyMs: number
}
 
const RECORDS: MeasurementRecord[] = [
  {
    id: 'sort',
    scenario: '영수증 정렬',
    attemptId: 'perf-sort-101',
    buildId: 'mobile-417-release',
    device: 'fixture-device-a',
    repeatCount: 5,
    interactionMs: 850,
    jsBusyMs: 780,
    reactRenderMs: 18,
    uiRenderingBusyMs: 20,
    nativeFunctionBusyMs: 12,
  },
  {
    id: 'rows',
    scenario: '영수증 행 갱신',
    attemptId: 'perf-rows-102',
    buildId: 'mobile-417-release',
    device: 'fixture-device-a',
    repeatCount: 5,
    interactionMs: 420,
    jsBusyMs: 335,
    reactRenderMs: 310,
    uiRenderingBusyMs: 25,
    nativeFunctionBusyMs: 10,
  },
  {
    id: 'sheet',
    scenario: '바텀 시트 열기',
    attemptId: 'perf-sheet-103',
    buildId: 'mobile-417-release',
    device: 'fixture-device-a',
    repeatCount: 5,
    interactionMs: 480,
    jsBusyMs: 20,
    reactRenderMs: 15,
    uiRenderingBusyMs: 390,
    nativeFunctionBusyMs: 30,
  },
  {
    id: 'pdf',
    scenario: 'PDF 영수증 만들기',
    attemptId: 'perf-pdf-104',
    buildId: 'mobile-417-release',
    device: 'fixture-device-a',
    repeatCount: 5,
    interactionMs: 560,
    jsBusyMs: 25,
    reactRenderMs: 10,
    uiRenderingBusyMs: 30,
    nativeFunctionBusyMs: 430,
  },
]
 
function classifyFromTotal(record: MeasurementRecord): CandidateLayer[] {
  return record.interactionMs >= 300 ? ['javascript-work'] : ['unresolved']
}
 
function classifyFromLayerEvidence(record: MeasurementRecord): CandidateLayer[] {
  const candidates: CandidateLayer[] = []
 
  if (record.reactRenderMs >= 250) {
    candidates.push('react-render')
  } else if (record.jsBusyMs >= 250) {
    candidates.push('javascript-work')
  }
 
  if (record.uiRenderingBusyMs >= 250) candidates.push('ui-rendering')
  if (record.nativeFunctionBusyMs >= 250) candidates.push('native-function')
 
  return candidates.length > 0 ? candidates : ['unresolved']
}
 
const MULTI_CANDIDATE_CHECK = classifyFromLayerEvidence({
  id: 'multi',
  scenario: 'UI·네이티브 동시 후보 점검',
  attemptId: 'perf-multi-105',
  buildId: 'mobile-417-release',
  device: 'fixture-device-a',
  repeatCount: 5,
  interactionMs: 620,
  jsBusyMs: 20,
  reactRenderMs: 15,
  uiRenderingBusyMs: 390,
  nativeFunctionBusyMs: 430,
})
 
function summarize(record: MeasurementRecord): string {
  const candidates = USE_LAYER_EVIDENCE
    ? classifyFromLayerEvidence(record)
    : classifyFromTotal(record)
 
  return [
    `${record.scenario} → ${candidates.join(' + ')}`,
    `전체 ${record.interactionMs}ms`,
    `JS ${record.jsBusyMs}ms`,
    `React ${record.reactRenderMs}ms`,
    `UI ${record.uiRenderingBusyMs}ms`,
    `native ${record.nativeFunctionBusyMs}ms`,
    `반복 ${record.repeatCount}`,
  ].join(' / ')
}
 
export default function App() {
  const [rows, setRows] = useState<string[]>([])
 
  return (
    <SafeAreaView style={styles.screen}>
      <ScrollView contentContainerStyle={styles.content}>
        <Text style={styles.title}>성능 병목 분류 fixture</Text>
        <Text>모드: {USE_LAYER_EVIDENCE ? '계층 증거' : '전체 시간만'}</Text>
        <Text>다중 후보 점검: {MULTI_CANDIDATE_CHECK.join(' + ')}</Text>
        <Button title="4개 증거 분류" onPress={() => setRows(RECORDS.map(summarize))} />
        {rows.map(row => (
          <View key={row} style={styles.card}>
            <Text>{row}</Text>
          </View>
        ))}
      </ScrollView>
    </SafeAreaView>
  )
}
 
const styles = StyleSheet.create({
  screen: { flex: 1 },
  content: { gap: 12, padding: 20 },
  title: { fontSize: 22, fontWeight: '700' },
  card: { borderWidth: 1, borderColor: '#bbb', borderRadius: 8, padding: 12 },
})

실행 전에 예측한다.

  1. 전체 시간이 300ms를 넘는 네 동작은 모두 어느 계층으로 분류될까?
  2. 영수증 행 갱신의 React Render 310ms는 화면에 드러날까?
  3. 바텀 시트 열기의 UI 그리기 390ms와 PDF 영수증 만들기의 네이티브 함수 430ms는 구분될까?
  4. UI 그리기와 다른 네이티브 함수가 함께 기준을 넘는 점검값은 두 후보를 모두 남길까?

개발자 메뉴에서 Reload한 뒤 4개 증거 분류를 누른다.

버튼 위에는 다음 점검값이 먼저 보인다.

다중 후보 점검: ui-rendering + native-function
영수증 정렬 → javascript-work / 전체 850ms / JS 780ms / React 18ms / UI 20ms / native 12ms / 반복 5
영수증 행 갱신 → javascript-work / 전체 420ms / JS 335ms / React 310ms / UI 25ms / native 10ms / 반복 5
바텀 시트 열기 → javascript-work / 전체 480ms / JS 20ms / React 15ms / UI 390ms / native 30ms / 반복 5
PDF 영수증 만들기 → javascript-work / 전체 560ms / JS 25ms / React 10ms / UI 30ms / native 430ms / 반복 5

네 동작이 모두 느리다는 증상은 맞지만, interactionMs만 본 규칙은 서로 다른 실행 위치를 모두 JavaScript로 덮어썼다.

전체 시간은 증상이지 실행 위치가 아니다

interactionMs는 사용자 영향과 회귀 여부를 찾는 출발점이다. 원인 위치는 다음처럼 별도 증거로 좁힌다.

전체 결과가 느림
  ├─ JavaScript 실행 흐름이 길게 바쁨      → 일반 JavaScript 작업 후보
  ├─ 그 안에서 React Render가 큼           → 컴포넌트 트리 계산 후보
  ├─ UI·플랫폼 그리기가 프레임을 넘김      → 플랫폼 화면 작업 후보
  ├─ 다른 네이티브 함수 실행이 오래 걸림    → 네이티브 함수 후보
  └─ 어느 상자도 설명하지 못함             → 네트워크·저장·GPU·대기 구간을 다시 계측

한 시스템 성능 기록 안에서도 작업은 겹치거나 기다릴 수 있다. 서로 다른 도구가 계산한 시간도 시작·끝 정의가 다르다. 따라서 전체 = JS + React + UI + native 같은 산술식을 만들지 않는다. 같은 attempt ID와 시간 범위에 연결한 뒤 어느 증거가 지연 구간을 설명하는지 비교한다.

이름 붙인 JavaScript 구간은 일반 작업 후보를 좁힌다

예를 들어 동기 정렬 함수만 감싸면 해당 JavaScript 호출 구간을 반복 비교할 수 있다. 아래 recordPerformanceSample은 제품이 구현할 측정 수집 함수다. attempt ID와 경과 시간뿐 아니라 실제 구현에서는 build·기기·시나리오 문맥을 함께 저장하며, 영수증 원문이나 인증값은 넣지 않는다.

performance.mark(`receipt-sort:${attemptId}:start`)
const sorted = sortReceipts(receipts)
performance.mark(`receipt-sort:${attemptId}:end`)
performance.measure(
  `receipt-sort:${attemptId}`,
  `receipt-sort:${attemptId}:start`,
  `receipt-sort:${attemptId}:end`,
)
 
const entries = performance.getEntriesByName(`receipt-sort:${attemptId}`, 'measure')
const latest = entries[entries.length - 1]
if (latest) recordPerformanceSample({ attemptId, duration: latest.duration })

여기서 동기는 함수가 결과를 반환할 때까지 같은 호출 흐름에서 일을 끝낸다는 뜻이다. await를 사이에 둔 구간은 네트워크·timer·다른 작업을 기다린 시간까지 포함할 수 있으므로 “JavaScript CPU가 그 시간 내내 바빴다”는 증거가 아니다.

React Native DevTools는 Hermes를 쓰는 개발 build의 JavaScript와 React를 조사하는 공식 개발 도구다. Performance panel은 기록한 구간의 JavaScript 실행, React 성능 행, 네트워크 사건과 mark·measure로 만든 사용자 지정 시간 구간을 한 시간축에 보여 준다.

실제 제품 화면에서 JavaScript 후보를 조사할 때는 다음 순서로 닫는다.

  1. Hermes 개발 build에서 개발자 메뉴의 Open DevTools를 고르거나 CLI에서 j를 눌러 DevTools를 연다.
  2. Performance panel의 기록을 시작하고 표시한 attempt의 실제 동작을 한 번 실행한 뒤 기록을 멈춘다.
  3. 사용자 지정 시간 구간과 겹치는 JavaScript 실행·React 성능 행을 선택해 어떤 작업이 시간을 설명하는지 본다.
  4. 주석을 붙인 성능 기록을 내려받아 build·시나리오·attempt와 함께 보존한다.
  5. 같은 후보를 release build의 mark·measure 기록과 사용자 결과 분포에서 다시 확인한다.

Performance panel에서 긴 JavaScript 실행이 표시한 구간과 겹치지 않으면 JavaScript 병목으로 확정하지 않고 unresolved로 돌린다. 플랫폼 내부 작업은 DevTools가 아니라 Android Studio와 Xcode 도구로 조사한다.

개별 측정마다 화면 로그를 많이 출력하면 측정 자체가 JavaScript 비용을 더할 수 있다. 메모리에 짧게 모으거나 측정이 끝난 뒤 내보내고, 계측을 켠 상태와 끈 상태의 차이도 확인한다.

React Profiler는 선택한 트리의 Render만 잰다

phase는 첫 Mount인지 이후 업데이트인지 구분하는 값이다. 아래 recordRenderSample은 이 제품이 따로 구현할 측정 수집 함수이며, 인증 비밀값이나 영수증 원문을 넣지 않는다.

import { Profiler } from 'react'
 
<Profiler
  id="ReceiptRows"
  onRender={(id, phase, actualDuration, _baseDuration, startTime, commitTime) => {
    recordRenderSample({ attemptId, id, phase, actualDuration, startTime, commitTime })
  }}
>
  <ReceiptRows receipts={receipts} />
</Profiler>

fixture의 영수증 행 갱신은 JS 335ms 가운데 선택한 트리의 Render가 310ms인 가상 입력이다. 이때는 react-render를 더 구체적인 병목 후보로 고른다. 반대로 JS 780ms인데 React Render가 18ms인 영수증 정렬은 React 트리보다 일반 JavaScript 작업부터 조사한다.

교정 모델은 같은 시도의 계층 증거를 함께 본다

상수를 바꾼다.

const USE_LAYER_EVIDENCE = true

개발자 메뉴에서 Reload한 뒤 같은 버튼을 누른다.

영수증 정렬 → javascript-work / 전체 850ms / JS 780ms / React 18ms / UI 20ms / native 12ms / 반복 5
영수증 행 갱신 → react-render / 전체 420ms / JS 335ms / React 310ms / UI 25ms / native 10ms / 반복 5
바텀 시트 열기 → ui-rendering / 전체 480ms / JS 20ms / React 15ms / UI 390ms / native 30ms / 반복 5
PDF 영수증 만들기 → native-function / 전체 560ms / JS 25ms / React 10ms / UI 30ms / native 430ms / 반복 5

React Render가 250ms를 넘으면 같은 JavaScript 경로를 일반 javascript-work로 한 번 더 기록하지 않고 더 구체적인 react-render 후보로 남긴다. UI 그리기와 다른 네이티브 함수는 서로 독립적인 경계이므로 둘 다 기준을 넘으면 후보 배열에 둘 다 보존한다. 250ms 기준은 이 fixture의 분기값이지 모든 앱에 통용되는 성능 기준이 아니다. 실제 제품은 사용자 결과 목표와 지원 기기의 반복 분포로 기준을 정한다.

어느 값도 기준을 넘지 않으면 unresolved로 남긴다. 가장 큰 숫자를 억지로 원인으로 승격하지 않고 네트워크·저장·그래픽 처리 장치(GPU)·스레드 대기 같은 누락된 경계를 다시 측정한다.

실제 제품 화면은 Android와 iOS 플랫폼 기록으로 닫는다

이 절은 위의 가상 분류 App.tsx에서 네 동작을 실행하는 절차가 아니다. 실제 영수증 정렬·행 갱신·바텀 시트 열기·PDF 영수증 만들기와 시작·screen-ready 표시가 이미 있는 제품 화면에 적용하는 통합 절차 템플릿이다. 제품 화면에 그 동작이나 표시가 없다면 먼저 측정할 실제 동작과 시작·끝 사건을 구현하고, 같은 build의 해당 화면에서 아래 기록을 수집한다.

Android

profileable build는 디버거 기능 전체를 켜지 않고 성능 도구가 앱을 관찰할 수 있게 만든 build다. Perfetto는 Android와 운영체제 전체의 시스템 성능 기록을 시간축으로 여는 도구다. Android Studio의 Capture System Activities는 연결한 기기에서 같은 종류의 기록을 수집하는 작업이다.

Android Studio에서 프로젝트의 android 폴더를 열고 지원 실기기를 선택한 뒤 다음 순서로 기록한다.

  1. Build > Select Build Variant에서 앱의 release variant를 고른다. variant는 debug·release처럼 build 설정을 구분한 항목이다.
  2. Android Studio의 More actions > Profile 'app' with low overhead를 누른다. 앱 run configuration 이름이 다르면 'app' 자리에도 그 이름이 보인다.
  3. 기기에서 앱이 열리고 Profiler 창이 나타나면 Home의 process 목록에서 앱을 구분하는 package 이름이 붙은 맨 위 process를 고른다. 이 과정이 profileable release 앱을 기록 대상으로 잡았다는 성공 관찰이다.
  4. 조사할 build·입력·화면 상태와 attempt ID를 기록하고 Capture System Activities를 시작한다.
  5. 영수증 동작을 같은 순서로 실행하고 기록을 멈춘다.
  6. 느린 구간에서 JavaScript, UI 스레드와 플랫폼 그리기 실행 흐름 중 무엇이 실행 중이거나 기다렸는지 본다.
  7. 함수 단위가 필요하면 같은 짧은 시나리오를 CPU profiler로 다시 기록한다.

Profile with low overhead가 보이지 않으면 Android 앱 설정 파일인 android/app/src/main/AndroidManifest.xml<application> 아래에서 <profileable android:shell="true" /> 설정을 확인한다. Android build를 구성하는 Gradle 플러그인인 Android Gradle Plugin과 기기 요구 조건은 위의 Android Studio 공식 profileable 절차를 따른다.

최소 성공 산출물은 내보낸 시스템 성능 기록 파일, 선택한 attempt 시간 범위, 관련 실행 흐름·프레임의 관찰 메모다. 함수 원인을 주장한다면 같은 범위의 CPU profile도 보존한다. 기록의 특정 행 이름은 Android, React Native와 도구 버전에 따라 달라질 수 있으므로 이름 하나에 판정을 고정하지 않는다. 앱 프로세스, 실행 흐름의 역할, 프레임과 직접 표시한 시도 구간을 함께 대조한다.

iOS

Instruments는 Xcode가 제공하는 성능 기록·분석 앱이다. 호출 트리는 어떤 함수가 어떤 함수를 불렀는지 계층으로 모은 보기이고, animation hitch는 스크롤이나 애니메이션의 화면 움직임이 잠깐 끊긴 현상이다.

scheme은 어떤 앱 설정으로 build·실행할지 묶은 이름이다.

  1. Xcode에서 지원 실기기와 측정할 scheme을 고른다.
  2. Product > Profile로 Instruments를 열어 CPU 작업은 Time Profiler, 움직임은 Animation Hitches를 고른다.
  3. 같은 영수증 시나리오를 실행하고 느린 시간 범위를 선택한다.
  4. 주 UI 스레드와 관련 실행 흐름, 높은 비중의 함수, Commit·Render·프레임 수명을 함께 본다.

최소 성공 산출물은 저장한 Instruments 기록, 선택한 hitch 또는 attempt 시간 범위, Time Profiler 호출 트리와 관련 실행 흐름의 관찰 메모다. Android 시스템 성능 기록과 iOS Instruments 기록은 같은 파일 형식이 아니고 플랫폼 구현도 다르다. 공통 질문은 “같은 사용자 시도에서 어느 실행 흐름이 실행되거나 기다렸는가?”이고, 실제 판정은 플랫폼별 원본으로 한다.

네 시나리오의 실제 기록은 다음 가설을 확인하거나 기각해야 한다.

시나리오후보를 지지하는 실제 관찰지지하지 않을 때
영수증 정렬표시한 attempt 구간과 긴 JavaScript 작업·관련 함수 표본이 겹침unresolved로 돌리고 누락 경계를 측정
영수증 행 갱신같은 attempt의 Profiler Render가 지연 구간 대부분을 설명일반 JavaScript·플랫폼 기록을 다시 확인
바텀 시트 열기JavaScript·React Render는 짧고 UI·플랫폼 그리기 구간이 프레임 기한을 넘음네트워크·저장·GPU·대기 증거를 추가
PDF 영수증 만들기JavaScript·React Render·UI 그리기는 짧고 CPU profile의 네이티브 파일 처리 함수가 지연 구간을 설명파일 입출력·운영체제 대기·다른 실행 흐름을 추가 측정

같은 환경에서 반복하고 실기기로 닫는다

Macrobenchmark는 앱 시작이나 큰 UI 상호작용을 별도 Android 테스트 앱에서 반복 측정하는 도구다. iteration은 같은 시나리오를 한 번 실행해 만든 측정 1회다.

한 번의 숫자는 우연한 캐시·발열·백그라운드 작업·네트워크 상태에 흔들릴 수 있다. 이 글의 비교 정책은 다음 축을 기록하고 같은 조건에서 여러 번 반복하는 것이다.

Android Macrobenchmark로 시작 성능까지 잰다면 COLD는 측정 전에 앱 프로세스를 끝내고, WARM은 프로세스를 유지한 채 Activity(현재 Android 화면 단위)를 다시 시작한다. 다른 플랫폼·도구에 이 이름을 그대로 일반화하지 않고, 측정 전 프로세스·화면·저장값·캐시 상태를 각각 기록한다.

시나리오       시작·끝 사건, 입력 크기, 화면 상태
실행 산출물    JavaScript bundle ID, 네이티브 build ID, 설정
환경           플랫폼·운영체제, 기기 모델, 전원·발열 상태
실행 조건      프로세스·화면·저장값·캐시 시작 상태, 네트워크, 계측 도구
결과           개별 표본, 미리 정한 요약값, 실패·제외 이유

Simulator·Emulator는 같은 순서의 기능 재현과 도구 연습에 유용하다. 최종 성능 기준은 대표적인 지원 실기기와 성능 하한에 가까운 기기에서도 확인한다. 개인 기기를 연결·기록할 때는 사용자 승인을 받고, 알림·계정·민감 입력이 시스템 성능 기록이나 화면 기록에 들어가지 않게 한다.

fixture와 실제 측정의 증명 범위를 나눈다

검증 층fixture가 확인하는 것실제 통합에서 추가할 증거
사용자 결과고정 interactionMs를 보존제품이 정한 시작·screen-ready 사건, 반복 표본
JavaScript고정 jsBusyMs로 일반 JS 후보 분리mark·measure와 JavaScript 프로파일의 실제 구간
React Render고정 reactRenderMs 우선 분기같은 attempt의 Profiler id·phase·actualDuration
UI·플랫폼 그리기고정 uiRenderingBusyMs 분기시스템 성능 기록·Animation Hitches의 실행 흐름과 프레임
네이티브 함수고정 nativeFunctionBusyMs 분기Android CPU profiler·iOS Time Profiler의 함수 기록
회귀 판정네 후보를 덮어쓰지 않음같은 build·기기·조건의 변경 전후 반복 분포

사용자 결과와 플랫폼 성능 기록 사이에 같은 attempt ID를 직접 전달할 수 없다면, build·시나리오·단조 시계의 시작과 끝을 함께 기록해 범위를 맞춘다. 단조 시계는 기기 시간이 바뀌어도 한 실행 안에서 경과 시간을 비교하는 시계다. 연결이 불확실한 표본은 임의의 함수나 스레드에 붙이지 않는다.

pull request에서 달라져야 할 행동

성능 개선 pull request에는 “더 빨라졌다” 대신 다음 표가 있어야 한다.

사용자 결과      시작·끝 사건과 목표, 변경 전후 반복 분포
JavaScript       이름 붙인 작업 구간과 프로파일의 높은 비용 함수
React Render     Profiler 범위·phase·actualDuration, 일반 JS와의 구분
Android          profileable 시스템 성능 기록, UI·플랫폼 그리기·운영체제 실행 흐름
네이티브 함수    Android CPU profiler·iOS Time Profiler의 같은 attempt 함수 기록
iOS 화면         Animation Hitches의 실기기 UI·플랫폼 그리기 기록
실행 산출물      JavaScript bundle·네이티브 build·설정 일치
환경             도구가 정의한 시작 모드와 측정 전 프로세스·화면·저장값·캐시 상태, 기기·발열·네트워크
계측 비용        계측을 켠 상태와 끈 상태의 차이
결론 경계        fixture·가상 환경·실기기가 각각 증명한 범위

검토자는 다음 질문을 실제 기록으로 닫는다.

  • 사용자 결과가 정말 개선됐는가, 아니면 내부 함수 하나만 짧아졌는가?
  • 긴 JavaScript 구간 가운데 React Render가 차지한 범위는 얼마인가?
  • JavaScript가 비어 있는데 UI·플랫폼 그리기 실행 흐름이 프레임 기한을 넘지 않았는가?
  • UI 그리기가 짧아도 다른 네이티브 함수 실행·대기가 지연 구간을 설명하지 않는가?
  • Android와 iOS에서 같은 증상을 같은 원인이라고 가정하지 않았는가?
  • 지원 하한 기기와 느린 쪽 표본에서도 개선이 유지되는가?
  • 계측 로그·프로파일·시스템 성능 기록에 인증값이나 사용자 입력 원문이 들어가지 않았는가?

한 장으로 다시 보기

문제       전체 결과가 300ms를 넘으면 모두 JavaScript 병목으로 분류
실패       JS 780 / React 310 / UI 390 / native 430의 서로 다른 증거를 덮어씀
출발       제품이 정한 입력 → screen-ready 결과 시간
JavaScript mark·measure + JavaScript 프로파일
React      선택 트리 Profiler actualDuration, JS 경로 안의 구체적 증거
Android    profileable 시스템 성능 기록 + 필요할 때 CPU profiler
iOS        Time Profiler + Animation Hitches
교정 결과  javascript-work / react-render / ui-rendering / native-function
반복       같은 build·시나리오·기기와 도구별 시작 상태를 고정한 분포 비교
경계       가상 fixture 분류 성공은 실기기 성능을 증명하지 않음

성능 측정은 밀리초 하나에 원인 이름을 붙이는 일이 아니다. 먼저 사용자가 기다린 결과 구간을 고정하고, 같은 시도의 JavaScript 작업·React Render·UI와 네이티브 성능 기록을 연결한다. 그 뒤 반복한 실기기 증거에서 지연 구간을 실제로 설명하는 계층부터 개선해야 한다.

스스로 확인할 질문

  1. 전체 상호작용 시간 하나로 JavaScript 병목을 판정할 수 없는 이유는 무엇인가?
  2. React Profiler의 actualDuration은 무엇을 측정하고 무엇을 측정하지 않는가?
  3. JavaScript 335ms와 React Render 310ms를 단순히 더하면 안 되는 이유는 무엇인가?
  4. Android 시스템 성능 트레이스와 CPU profiler는 각각 어떤 질문에 답하는가?
  5. iOS의 Time Profiler와 Animation Hitches는 어떤 증거를 좁히는가?
  6. 개발 빌드·Simulator의 한 번 측정으로 실제 사용자 성능을 결론 내리면 안 되는 이유는 무엇인가?

Active recall

기억에서 꺼내 보기

답을 쓰지 않아도 됩니다. 먼저 머릿속으로 답한 뒤 펼쳐서 정답·이유·흔한 오해를 비교하세요.

  1. 01버튼을 누른 뒤 결과가 800ms 만에 보였다는 값 하나로 JavaScript 병목이라고 결론 내릴 수 있을까?

    정답

    아니다. 전체 결과 시간은 느림을 확인하지만 JavaScript 작업, React Render, UI·플랫폼 그리기, 다른 네이티브 함수 중 어디에서 시간이 쓰였는지는 각 경계의 기록을 같은 시도에 연결해야 알 수 있다.

    왜 그런가

    사용자 결과 메트릭과 원인 위치를 좁히는 프로파일·시스템 성능 트레이스는 서로 다른 질문에 답한다.

    관련 설명 다시 읽기
    지금 어느 정도 기억났나요?

    선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.

  2. 02React Profiler의 actualDuration이 짧으면 전체 상호작용과 네이티브 화면 반영도 빠르다고 보장할 수 있을까?

    정답

    아니다. actualDuration은 선택한 React 트리의 이번 업데이트 Render 시간이며, 전체 JavaScript 작업·네트워크·Fabric Mount·플랫폼 그리기를 모두 재는 값이 아니다.

    왜 그런가

    React Render는 JavaScript 경로 안의 구체적인 부분 증거로 사용한다.

    관련 설명 다시 읽기
    지금 어느 정도 기억났나요?

    선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.

  3. 03JavaScript 구간과 React Render가 짧은데 화면 움직임이 끊기면 다음에는 무엇을 확인해야 할까?

    정답

    Android는 같은 상호작용의 시스템 성능 트레이스에서 UI 스레드·플랫폼 그리기·운영체제 실행 흐름을 확인하고, iOS는 Instruments의 Animation Hitches와 CPU 프로파일을 사용해 프레임·스레드·함수 증거를 좁힌다.

    왜 그런가

    플랫폼 화면 작업은 JavaScript 시간이나 React Profiler 값으로 대신 판정할 수 없다.

    관련 설명 다시 읽기
    지금 어느 정도 기억났나요?

    선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.

  4. 04개발 빌드의 Simulator에서 한 번 얻은 숫자를 실제 사용자 성능 기준으로 삼아도 될까?

    정답

    아니다. 같은 시나리오·입력·build를 반복하고, 개발 검사 비용을 뺀 release 또는 목적에 맞는 profileable build를 지원 실기기에서 다시 측정한다.

    왜 그런가

    가상 환경은 결정적 재현에 유용하지만 실기기 성능과 기능을 그대로 재현하지 않는다.

    관련 설명 다시 읽기
    지금 어느 정도 기억났나요?

    선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.

출처와 검증 범위

아래 날짜는 링크를 마지막으로 열어 본 날입니다. 문서 상단의 검증일은 글의 설명과 적용 범위를 다시 확인한 날입니다.

  1. Performance OverviewReact Native · 공식 문서 · 확인 2026-08-09
  2. ProfilingReact Native · 공식 문서 · 확인 2026-08-09
  3. performanceReact Native · 공식 문서 · 확인 2026-08-09
  4. React Native DevToolsReact Native · 공식 문서 · 확인 2026-08-09
  5. Debugging BasicsReact Native · 공식 문서 · 확인 2026-08-09
  6. <Profiler>React · 공식 문서 · 확인 2026-08-09
  7. Render, Commit, and MountReact Native · 공식 문서 · 확인 2026-08-09
  8. Overview of system tracingAndroid Developers · 공식 문서 · 확인 2026-08-09
  9. Profile your app performanceAndroid Developers · 공식 문서 · 확인 2026-08-09
  10. Write a MacrobenchmarkAndroid Developers · 공식 문서 · 확인 2026-08-09
  11. Writing and running performance testsApple Developer · 공식 문서 · 확인 2026-08-09
  12. Analyzing CPU profiles with call tree viewsApple Developer · 공식 문서 · 확인 2026-08-09
  13. Improving app responsivenessApple Developer · 공식 문서 · 확인 2026-08-09
  14. Running your app on simulated or physical devicesApple Developer · 공식 문서 · 확인 2026-08-09
이 문서의 마지막까지 읽었습니다.