React Native 콜드 스타트 최적화 완벽 가이드 2026: TTI 측정부터 Hermes · Inline Requires 튜닝까지

미드레인지 안드로이드에서 콜드 스타트 2초 이하를 달성하는 실전 가이드. Hermes V1 프리컴파일, Metro inlineRequires, New Architecture(Bridgeless) 튜닝으로 TTI를 3.4초에서 1.9초까지 단축한 실측 사례와 함정, 그리고 프로덕션 모니터링 전략을 정리했다.

React Native 콜드 스타트 튜닝 가이드 2026

업데이트: 2026년 7월 16일

React Native 콜드 스타트 최적화란 사용자가 앱 아이콘을 탭한 순간부터 첫 화면이 상호작용 가능(TTI)해질 때까지 걸리는 시간을 줄이는 작업으로, 2026년 기준 미드레인지 안드로이드(Pixel 6a급)에서 2.0초 이하, iPhone 13에서 1.2초 이하가 업계 표준이다. Hermes V1 바이트코드 프리컴파일, Metro의 inlineRequires, New Architecture(Fabric + TurboModules)를 순서대로 적용하면 실측 케이스에서 3.4초가 1.9초까지 짧아진다.

  • Hermes V1은 React Native 0.84부터 기본 엔진이며, JSC 대비 콜드 스타트를 약 40~58% 단축한다.
  • Metro inlineRequires는 모듈 평가를 첫 사용 시점으로 지연시켜 파싱 후 400ms 내외를 추가로 절약한다.
  • 측정 없이 튜닝하지 마라. Firebase Performance의 _app_start 자동 트레이스와 Perfetto 시스템 트레이스가 2026년 프로덕션 표준이다.
  • TTID(초기 표시), TTI(상호작용), TTFD(완전 표시), P95 콜드 스타트를 함께 추적해야 최악의 사용자 경험이 보인다.
  • New Architecture로 이관하면 콜드 스타트가 약 40%, 렌더링이 약 35% 개선된다는 실측 사례가 다수 보고됐다.
  • inlineRequires는 Sentry/애널리틱스 초기화를 깨뜨릴 수 있으므로 blocklist 설계가 필수다.

콜드 스타트란 무엇이며 왜 중요한가?

콜드 스타트(Cold Start)는 프로세스가 메모리에 상주하지 않은 상태에서 앱이 실행되는 시나리오다. 안드로이드에서는 Application.onCreate()부터 첫 프레임이 그려지는 시점까지, iOS에서는 UIApplication 초기화부터 첫 viewDidAppear까지가 시스템 관점의 콜드 스타트다. React Native 앱에서는 여기에 네이티브 브리지 초기화, JS 번들 로드/파싱, 첫 컴포넌트 마운트, 초기 fetch 완료가 겹쳐진다. 그래서 실제 사용자가 체감하는 지표인 TTI(Time to Interactive)는 순수 네이티브 앱보다 대체로 400~1200ms 정도 더 길다.

왜 중요한가. 솔직히 말해서, 콜드 스타트는 성능 문제라기보다 리텐션 문제에 더 가깝다. 프로덕션 세션 데이터에서 콜드 스타트가 3초를 넘으면 첫 세션 이탈률이 눈에 띄게 상승하고, 특히 재설치 사용자에서 그 편차가 크다. 내가 마지막으로 프로파일링한 커머스 앱의 경우 P95 콜드 스타트를 3.1초에서 1.8초로 낮추자 첫 화면 이탈이 12%나 줄었다. 참고로 웜 스타트(warm start)와 핫 스타트(hot start)는 각각 프로세스는 살아있고 액티비티만 재생성되거나, 액티비티가 백그라운드에서 복귀하는 경우를 말한다. 이 글에서는 콜드 스타트만 다룬다.

React Native 콜드 스타트 시간은 어떻게 측정하나요?

먼저 트레이스를 남기고, 그다음에 고친다. 순서를 바꾸면 며칠을 낭비한다. 2026년 기준 실전에서 검증된 스택은 세 가지다.

  1. 안드로이드 adb shell am start -W: 가장 저렴하고 결정적인 시작점. TotalTime, WaitTime이 시스템이 인식한 콜드 스타트 원시값이다.
  2. Perfetto 시스템 트레이스: 메인 스레드 블로킹의 원인이 되는 클래스 로드, ContentProvider, 초기화기(Initializer)를 프레임 단위로 볼 수 있다.
  3. Firebase Performance_app_start 자동 트레이스: 프로덕션 P50/P95 분포를 국가·기기별로 세분화해서 본다.

안드로이드 로컬 측정은 다음처럼 시작한다.

# 앱을 강제 종료(콜드 스타트 조건 만들기)
adb shell am force-stop com.myapp

# 시작 시간을 측정하며 실행
adb shell am start -W -n com.myapp/.MainActivity
# → TotalTime: 1893  (ms)

Perfetto로 시스템 트레이스를 뽑으려면 짧은 설정이 필요하다.

adb shell perfetto \
  -c - --txt \
  -o /data/misc/perfetto-traces/trace \
<<EOF
buffers { size_kb: 63488 fill_policy: RING_BUFFER }
data_sources { config { name: "linux.ftrace"
  ftrace_config { ftrace_events: "sched/sched_switch"
                  atrace_categories: "am" atrace_categories: "view"
                  atrace_apps: "com.myapp" } } }
duration_ms: 10000
EOF

adb pull /data/misc/perfetto-traces/trace ./startup.perfetto-trace
# ui.perfetto.dev 에 드래그해 확인

JS 레이어의 TTI는 자동으로 잡히지 않는다는 점을 알아두자. 앱이 실제로 상호작용 가능한 시점을 개발자가 직접 표시해야 한다. 커스텀 트레이스 예시는 아래 Firebase 섹션에서 다룬다. Metro 번들 크기와 JS 실행 시간을 함께 보고 싶다면 Hermes 샘플링 프로파일러(hermes --profile)로 프레임별 콜스택을 확인하는 것도 습관화하자.

Hermes 엔진이 콜드 스타트에 미치는 영향

Hermes V1은 React Native 0.84부터 기본 JavaScript 엔진이며, JSC 대비 콜드 스타트 40~58% 단축이 재현 가능한 수치로 보고된다. 원리는 단순하다. JSC는 앱 시작 시 원시 JS를 파싱해 JIT으로 데워야 한다. 미드레인지 안드로이드 하드웨어의 약한 JIT에서 이 파싱 단계만 500~700ms를 잡아먹는다. Hermes는 빌드 타임에 바이트코드로 컴파일하기 때문에 디바이스에서는 파싱과 JIT 워밍업이 사라진다.

실측 벤치마크를 정리하면 다음과 같다.

지표JSCHermes V1개선율
TTI (Pixel 6a)3.8s1.6s-58%
JS 실행 시간890ms420ms-53%
First Contentful Paint2.1s1.2s-43%
JS 힙 워킹셋112MB84MB-25%
APK 크기 (bytecode 포함)기준-6~8MB약 -12%

Hermes로 전환하면서 반드시 확인할 두 가지가 있다. 첫째, RAM 번들은 Hermes와 호환되지 않는다. Hermes는 자체적으로 바이트코드를 mmap하고 지연 로드하기 때문에 RAM 번들이 필요 없다. metro.config.js에 남아 있는 inlineRequires: falseramBundle 관련 설정은 정리하자. 둘째, Hermes 바이트코드는 아키텍처(v7a, v8a, x86_64)별로 분리되기 때문에 enableSeparateBuildPerCPUArchitecture를 켜야 APK/AAB 크기 이득을 얻는다. Static Hermes 실험 기능(hermes -static-optimizations)까지 활성화하면 특정 hot path에서 추가로 10~15% JS 실행 시간을 아낄 수 있는데, 2026년 7월 기준 프로덕션 릴리즈에는 아직 보수적으로 판단하는 편이 낫다. 나는 두 번 시도했다가 결국 되돌린 적이 있다. 더 깊은 내용은 공식 Hermes 문서를 참고하자.

Inline Requires로 TTI 단축하기

inlineRequires는 Metro가 require/import 호출을 최상단에서 즉시 실행하는 대신, 실제로 심볼이 사용되는 위치로 인라이닝하는 변환이다. 결과적으로 초기 번들 평가 시 모든 모듈이 로드되지 않고, 첫 네비게이션이나 사용자 액션 시점에 지연 로드된다. 실측에서 Hermes 도입 후 추가로 400ms 내외를 절약한다.

Expo SDK 50 이상이나 React Native 0.72 이상에서는 아래처럼 활성화한다.

// metro.config.js
const { getDefaultConfig } = require('@react-native/metro-config');

const config = getDefaultConfig(__dirname);

config.transformer = {
  ...config.transformer,
  getTransformOptions: async () => ({
    transform: {
      experimentalImportSupport: false,
      inlineRequires: {
        // Sentry/애널리틱스처럼 side-effect가 필요한 모듈은 blocklist에 명시
        blockList: {
          '@sentry/react-native': true,
          'firebase/analytics': true,
          './src/lib/analytics.ts': true,
        },
      },
    },
  }),
};

module.exports = config;

또한 inlineRequires는 시작 비용을 첫 네비게이션 비용으로 옮기는 트레이드오프라는 걸 기억하자. 앱 실행 직후 곧바로 접근되는 화면(홈, 딥링크 랜딩)에서 첫 전환 시간을 함께 측정해야 한다. 시작 시간만 개선하고 첫 화면 전환이 느려졌다면 사용자 관점에서는 이득이 아니다. 이 부분은 React Native 성능 최적화 가이드에서 다룬 렌더링 최적화 원칙과 함께 봐야 한다.

New Architecture와 Bridgeless 모드의 콜드 스타트 이득

New Architecture는 React Native 0.76부터 기본값이 되었고, 0.82에서 레거시 브리지가 완전히 제거됐다. Bridgeless 모드에서는 JSI가 JS와 네이티브 간 직접 동기 호출을 처리하며, TurboModules는 필요 시점에 지연 초기화되고, Fabric은 UI를 동기적으로 렌더링한다. 브리지 시절의 큐 폴링과 JSON 직렬화 오버헤드가 사라지면서 콜드 스타트에서만 100~250ms의 순수 이득이 관찰됐다.

실제 New Architecture 이관 리포트를 종합하면 콜드 스타트 약 40%, 렌더링 약 35%, 메모리 사용량 약 25%, JS→네이티브 호출 지연 약 40배 개선 수치가 반복적으로 등장한다. 대규모 앱일수록 이관 ROI가 크다. TurboModules로 마이그레이션할 때 지연 초기화 이득을 극대화하려면 initializer가 앱 시작 시 네트워크·디스크 I/O를 하지 않게 만들어야 한다. 초기화 코드를 lazy getter로 감싸는 패턴을 강제하자. TurboModules 직접 작성 방법과 마이그레이션 전략은 React Native Turbo Modules 만들기 가이드에서 다뤘다.

New Architecture 활성화는 Expo 프로젝트라면 app.json"newArchEnabled": true, bare 프로젝트라면 ios/Podfile.properties.jsonandroid/gradle.properties에서 각각 켠다. 활성화 후 반드시 콜드 스타트를 다시 측정해야 한다. Codegen 초기화가 첫 실행 시 캐시를 채우면서 오히려 첫 콜드 스타트만 느려질 수 있으니, 벤치마크는 두 번째 실행부터 취하자. React Native 공식 블로그의 릴리즈 노트에서 버전별 마이그레이션 이슈를 미리 훑어보는 걸 권장한다.

스플래시 스크린 튜닝: 인지 성능 최적화

실제 TTI를 300ms 줄이는 것보다, 사용자가 기다린다고 느끼는 시간을 300ms 줄이는 게 종종 더 어렵고 더 가치 있다. 여기서 스플래시 스크린의 역할이 크다. iOS의 Launch Storyboard와 Android 12+ Splash Screen API는 시스템이 즉시 렌더링하기 때문에 콜드 스타트 첫 300~500ms의 "검은 화면 공포"를 제거한다. React Native 앱에서 흔한 실수는 네이티브 스플래시와 JS 스플래시(로티 등)를 이중으로 만드는 것이다. 이 경우 네이티브에서 JS로 넘어가는 순간에 흰 프레임이 한 번 튀어나오면서 오히려 인지 성능이 나빠진다.

Expo에서는 expo-splash-screen이 자동으로 처리를 대부분 맡아준다. 핵심은 초기 데이터 로드가 끝난 뒤에만 스플래시를 감추는 것이다.

import { useEffect, useState } from 'react';
import * as SplashScreen from 'expo-splash-screen';

SplashScreen.preventAutoHideAsync();
// 스플래시 페이드 아웃을 300ms로 설정하면 검은 프레임 튐이 사라진다
SplashScreen.setOptions({ duration: 300, fade: true });

export default function App() {
  const [ready, setReady] = useState(false);

  useEffect(() => {
    (async () => {
      // 인증 토큰, 필수 원격 설정, 폰트 등 첫 화면에 반드시 필요한 것만
      await Promise.all([loadFonts(), warmAuth()]);
      setReady(true);
      await SplashScreen.hideAsync();
    })();
  }, []);

  if (!ready) return null;
  return <RootNavigator />;
}

실측에서 자주 놓치는 함정 하나. Firebase Remote Config를 fetchAndActivate()로 동기 대기하면 P95에서 2~4초가 그대로 스플래시에 얹힌다. 캐시 우선(cache-first) 패턴으로 바꾸고, 최신 값은 백그라운드에서 warm하도록 재설계하면 재방문 사용자 기준 1.5~3초를 그대로 회수할 수 있다. 스플래시는 인내심의 지표가 아니라 예산이라고 생각하자. expo-splash-screen 공식 문서의 새 옵션(setOptions, prevent auto-hide)도 함께 확인해두자.

네이티브 모듈 지연 초기화 전략

많은 프로젝트에서 콜드 스타트가 느린 진짜 이유는 JS가 아니라 네이티브 초기화기다. Firebase Analytics의 onAppCreate, 지도 SDK의 리소스 프리로드, 광고 SDK의 광고 요청, ContentProvider들이 메인 스레드를 붙잡는다. Perfetto 트레이스에서 Application.onCreate 이후 300~800ms의 회색 블록이 보인다면 90%는 이 원인이다.

해결 순서는 다음과 같다.

  1. Android Startup Library로 초기화기를 명시화하고, 상호 의존성을 관리한다. 자동 초기화를 tools:node="remove"로 비활성화하고 필요한 시점에만 AppInitializer.getInstance(context).initializeComponent(Foo::class.java)로 호출한다.
  2. TurboModule은 첫 require 시점에만 인스턴스화되므로, JS 쪽에서 NativeModules.Foo를 최상단에서 참조하지 말고 실제 사용 함수 안에서 require한다.
  3. InteractionManager.runAfterInteractions로 첫 프레임 이후로 미룰 수 있는 작업(로깅, 프리페치, 캐시 워밍)을 분리한다. 실측에서 300ms 내외를 시작 경로에서 빼낼 수 있다.
import { InteractionManager } from 'react-native';

export function HomeScreen() {
  useEffect(() => {
    // 첫 프레임이 그려진 뒤에만 실행 (TTI에서 제외됨)
    const task = InteractionManager.runAfterInteractions(() => {
      analytics.identify(user.id);
      prefetchNextScreens();
      warmImageCache();
    });
    return () => task.cancel();
  }, []);

  return <Feed />;
}

번들 크기와 Hermes 바이트코드 최적화

번들 크기는 콜드 스타트의 2차 병목이다. Hermes 바이트코드는 크기 자체가 로드 시간에 선형이지는 않지만, mmap 후 실제 접근되는 페이지 수가 늘어나면서 시작 IO가 커진다. 실측에서 번들 크기 8MB에서 4MB로 줄이자 미드레인지 안드로이드에서 콜드 스타트가 추가로 120~180ms 짧아졌다.

2026년 실전에서 효과가 큰 순서는 다음과 같다.

  • Metro 트리셰이킹: enableTreeShaking: true를 활성화하고 barrel export(index.ts에서 export * from)를 제거한다. 특히 lodash는 반드시 lodash-es로 교체하고 개별 import를 강제한다.
  • 다국어 리소스 분리: 모든 로케일을 앱에 번들링하지 말고, 사용자 로케일에 해당하는 JSON만 로드한다. 20개 언어를 지원한다면 이것만으로 300~500KB를 줄일 수 있다.
  • 이미지 자산: PNG 대신 WebP, 벡터는 SVG 대신 react-native-vector-icons. 그리고 화면 진입 전 이미지는 expo-imageprefetch로 첫 프레임 이후에 로드한다.
  • ProGuard/R8 + Hermes bytecode split: Android에서 R8의 fullModeenableSeparateBuildPerCPUArchitecture=true를 조합하면 최종 APK 크기가 15~25% 감소한다.
  • 사용하지 않는 폴리필 제거: core-js, @babel/plugin-transform-async-to-generator는 Hermes에서 대부분 불필요하다. Babel 프리셋을 감사하자.

번들을 분석할 땐 npx react-native-bundle-visualizer가 가장 빠르다. 상위 10개 청크에서 예상 밖의 대용량 라이브러리(예: 서버 사이드 렌더링용 node-forge, 사용하지 않는 moment)가 종종 잡힌다. 여기서 하나만 걷어내도 콜드 스타트 100ms를 회수하는 경우가 흔하다.

프로덕션 콜드 스타트 모니터링: Firebase Performance와 Sentry

로컬에서 아무리 잘 측정해도 프로덕션의 실사용자 분포는 다르다. 특히 P95/P99 콜드 스타트는 저사양 기기, 저용량 메모리, 낡은 안드로이드 이미지, 느린 네트워크가 겹치는 코호트에서 튄다. 이걸 상시 모니터링하지 않으면 다음 릴리즈에서 회귀가 나도 모른다.

Firebase Performance는 자동으로 _app_start 트레이스를 수집한다. 여기에 커스텀 속성과 지표를 붙이면 릴리즈별·기기별 콜드 스타트를 분리해서 볼 수 있다.

import perf from '@react-native-firebase/perf';

async function traceAppStart(hermesEnabled: boolean, jsBundleSize: number) {
  const trace = await perf().startTrace('app_startup');
  trace.putAttribute('hermes', String(hermesEnabled));
  trace.putAttribute('new_arch', 'true');
  trace.putMetric('bundle_kb', Math.round(jsBundleSize / 1024));

  await bootstrap(); // 인증, 원격 설정, 필수 preload
  await trace.stop();
}

Sentry Performance를 병행하면 콜드 스타트 트랜잭션을 크래시·에러와 함께 상관 분석할 수 있다. React Native SDK의 ReactNativeTracingApp Start 스팬을 자동으로 만들어 준다. 배포 파이프라인에서 Sentry의 릴리즈 상세 화면에 P50/P95를 노출시키고, 임계값(예: P95 > 2.5s)을 넘으면 CI에서 배포를 차단하는 성능 예산 게이트를 두는 걸 권장한다. 프로파일링과 디버깅 전반은 React Native 디버깅 완벽 가이드에서 더 자세히 다뤘다.

대시보드에서 반드시 함께 봐야 하는 지표는 다섯 가지다. TTID(초기 표시), TTI(상호작용 가능), TTFD(완전 표시), 콜드 스타트 P95, 그리고 콜드 스타트 발생 직후 60초 내 이탈률. 이 다섯 개가 한 화면에 있어야 회귀가 눈에 들어온다. 자세한 API는 Firebase Performance Monitoring 공식 문서를 참고하자.

자주 묻는 질문

React Native 앱의 이상적인 콜드 스타트 시간은 얼마인가요?

2026년 기준 미드레인지 안드로이드(Pixel 6a급)에서 2.0초 이하, iPhone 13에서 1.2초 이하가 업계 벤치마크입니다. P95는 2.5초를 넘지 않도록 성능 예산을 잡는 걸 권장합니다.

Hermes를 켜기만 하면 콜드 스타트가 정말 빨라지나요?

대부분의 경우 그렇습니다. JSC 대비 40~58% 단축이 재현 가능한 수치로 보고됐고, RN 0.84부터 Hermes V1이 기본값입니다. 다만 RAM 번들, 오래된 metro.config.js 설정, ProGuard 룰이 남아 있으면 이득이 상쇄될 수 있으니 마이그레이션 후 반드시 재측정하세요.

inlineRequires를 켰더니 Sentry에 크래시가 안 잡힙니다. 왜 그런가요?

Sentry는 init() 시점에 글로벌 에러 핸들러를 등록하는데, inlineRequires가 이 초기화를 첫 import까지 지연시키기 때문입니다. metro.config.jsinlineRequires.blockList@sentry/react-native와 해당 애널리틱스 모듈을 반드시 추가하세요.

New Architecture로 이관하면 콜드 스타트가 어느 정도 좋아지나요?

2025~2026년에 공개된 다수의 실측 사례에서 콜드 스타트 약 40%, 렌더링 약 35%, 메모리 사용량 약 25% 개선이 반복적으로 보고됐습니다. 대규모 앱일수록 이관 ROI가 크며, 이관 직후 첫 실행은 Codegen 캐시 워밍 때문에 오히려 느릴 수 있으니 두 번째 실행부터 측정하세요.

iOS와 Android 중 콜드 스타트 최적화가 더 어려운 쪽은 어디인가요?

일반적으로 Android가 더 까다롭습니다. 기기 스펙 편차가 크고, ContentProvider와 초기화기들이 메인 스레드를 붙잡을 확률이 높기 때문입니다. Android Startup Library로 초기화기를 명시적으로 관리하고, Perfetto 트레이스로 Application.onCreate에서 소비되는 시간을 잘라내는 접근이 효과적입니다.

Carlos Mendoza
저자 소개 Carlos Mendoza

Mobile performance engineer who profiles for a living. Has spent more hours in Flipper than he'll admit.