React Native 시동 속도 2배 높이기: Hermes 엔진과 번들 최적화 실전 로그

최근 릴리스한 사내 메신저 앱의 구글 Play Console 바이탈 지표를 확인하다가 등골이 서늘해진 경험이 있습니다. 하이엔드 기기에서는 쾌적하게 동작하던 앱이, 갤럭시 A 시리즈나 구형 픽셀 기기에서는 'Cold Start(콜드 스타트)' 시간이 무려 4초를 넘어가고 있었습니다. "모바일 개발"에서 3초 이상의 로딩은 사용자의 53%가 이탈한다는 통계를 굳이 들지 않더라도, 이는 명백한 성능 결함이었습니다. 주범은 바로 무거워진 JS 번들과 JavaScriptCore(JSC)의 런타임 컴파일 비용이었습니다. 이 글에서는 "React Native 성능"의 핵심인 "Hermes Engine"을 도입하고, 극한의 다이어트를 통해 "번들 사이즈 축소"를 이뤄낸 과정을 디버깅 로그 형식으로 공유합니다.

레거시 환경과 JSC의 한계 분석

문제의 프로젝트는 React Native 0.68 버전을 기반으로 시작되어, 수많은 서드파티 라이브러리가 엉겨 붙어 있는 상태였습니다. 당시 안드로이드 빌드 설정은 기본값인 JSC(JavaScriptCore)를 사용하고 있었습니다. JSC는 훌륭한 엔진이지만, 안드로이드 환경에서는 앱이 시작될 때 자바스크립트 번들을 파싱하고 컴파일하는 JIT(Just-In-Time) 과정이 메인 스레드에서 발생합니다.

프로파일러를 돌려본 결과, `ScriptPreload` 단계에서만 1.5초가 소모되고 있었습니다. "앱 최적화"를 위해 코드 스플리팅(Code Splitting)을 시도했지만, 초기 구동에 필수적인 Core 모듈 자체가 너무 비대해져 있어 효과는 미미했습니다. 우리는 엔진 교체라는 근본적인 해결책이 필요했습니다.

Critical Issue: 안드로이드 보급형 기기(Snapdragon 4xx 계열)에서 앱 아이콘 클릭 후 첫 화면 렌더링까지 4.2초 소요. ANR(Application Not Responding) 경고 발생 빈도 증가.

우리는 페이스북(Meta)이 "모바일 개발" 환경에 최적화하여 만든 Hermes Engine으로 마이그레이션을 결정했습니다. Hermes는 AOT(Ahead-of-Time) 컴파일을 지원하여, 빌드 타임에 JS를 미리 바이트코드로 변환해 둡니다. 이는 런타임에서의 파싱 비용을 제거하여 TTI(Time to Interactive)를 획기적으로 줄여줍니다.

실패 사례: 무작정 켜기만 하면 될까?

처음에는 단순히 `build.gradle`에서 플래그만 `true`로 바꾸고 배포를 시도했습니다. 하지만 앱은 실행되자마자 크래시를 일으켰습니다. 원인은 `Intl` (국제화) API 지원 문제와 일부 라이브러리의 리플렉션(Reflection) 사용 때문이었습니다. 구형 Hermes 버전은 `Proxy` 객체나 `Intl` 모듈을 완벽하게 지원하지 않아, `date-fns`나 `Intl.NumberFormat`을 사용하는 코드에서 런타임 에러가 발생했습니다. 단순히 엔진을 켜는 것이 능사가 아니라, 이에 맞는 폴리필(Polyfill) 전략과 ProGuard 규칙 수정이 반드시 동반되어야 함을 깨달았습니다.

Hermes 활성화 및 ProGuard 최적화 솔루션

안정적인 Hermes 도입과 APK 사이즈 감량을 위해 적용한 최종 설정입니다. React Native 0.70 이상 버전을 기준으로 작성되었으나, 구형 버전에서도 맥락은 동일합니다.

먼저 안드로이드 빌드 설정입니다. Hermes를 활성화하고, 불필요한 아키텍처를 제거하여 바이너리 크기를 줄입니다.

// android/app/build.gradle

project.ext.react = [
    enableHermes: true,  // Hermes 활성화
]

android {
    defaultConfig {
        // ...
        // 구형 안드로이드 기기 호환성을 위해 멀티덱스 활성화
        multiDexEnabled true 
    }

    // 빌드 타입별 최적화 설정
    buildTypes {
        release {
            // 디버그 심볼 제거 및 코드 난독화 (ProGuard/R8)
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
            
            // 네이티브 라이브러리 압축
            ndk {
                abiFilters "armeabi-v7a", "arm64-v8a"
            }
        }
    }
}

위 코드에서 `enableHermes: true`는 JS를 바이트코드로 컴파일하게 만듭니다. 하지만 더 중요한 것은 `minifyEnabled true`와 함께 설정해야 할 ProGuard 규칙입니다. Hermes는 바이트코드를 사용하므로, 난독화 도구인 R8/ProGuard가 JS와 상호작용하는 네이티브 메서드나 클래스 이름을 실수로 제거하면 앱이 죽습니다.

다음은 Hermes와 호환성을 유지하면서 "번들 사이즈 축소"를 극대화하기 위한 ProGuard 규칙입니다. 특히 `OkHttp`나 `Retrofit` 같은 네트워크 라이브러리와 충돌을 방지해야 합니다.

# android/app/proguard-rules.pro

# React Native 기본 유지 규칙
-keep class com.facebook.react.** { *; }
-keep class com.facebook.hermes.reactexecutor.** { *; }

# Hermes 엔진 관련 경고 무시 (빌드 실패 방지)
-dontwarn com.facebook.hermes.**

# 서드파티 라이브러리 리플렉션 방어 (예: OkHttp)
-keepattributes *Annotation*
-keep class okhttp3.** { *; }
-keep interface okhttp3.** { *; }
-dontwarn okhttp3.**

# Flipper 제거 (릴리스 빌드에서 제외하여 사이즈 감소)
-assumenosideeffects class com.facebook.flipper.** {
    public *;
}

이 설정은 R8 컴파일러가 사용하지 않는 자바 클래스를 공격적으로 제거하도록 유도하되, React Native의 브릿지 통신에 필수적인 클래스는 보존합니다. 또한, 개발 도구인 Flipper 관련 코드를 릴리스 빌드에서 완전히 도려내어 약 2~3MB의 용량을 추가로 확보할 수 있습니다.

성능 검증: 숫자가 증명하는 변화

최적화 적용 전후를 동일한 기기(Galaxy A32)에서 테스트한 결과입니다. "React Native 성능" 개선의 지표가 되는 3가지 핵심 항목을 비교했습니다.

지표 (Metric) JSC (Before) Hermes + ProGuard (After) 개선율
APK 사이즈 42.5 MB 29.8 MB 🔻 29.8%
Cold Start (TTI) 4.2초 1.8초 🚀 57% 단축
메모리 사용량 (Avg) 240 MB 185 MB 🔻 23%

APK 용량이 약 30% 감소한 것은 단순히 Hermes의 바이트코드 압축 효율 때문만이 아닙니다. `enableHermes`를 켜면 기존의 거대한 JSC 엔진 바이너리가 빠지고, 훨씬 가벼운 Hermes 런타임이 탑재되기 때문입니다. 또한, 바이트코드는 텍스트 형태의 JS보다 로드 속도가 월등히 빠르며, 메모리 매핑(mmap)을 효율적으로 사용하여 저사양 기기에서의 OOM(Out of Memory) 크래시 비율도 현저히 낮아졌습니다.

공식 벤치마크 데이터 확인하기

주의사항 및 엣지 케이스 (Edge Cases)

Hermes 도입이 만능열쇠는 아닙니다. "앱 최적화" 과정에서 겪을 수 있는 몇 가지 부작용을 반드시 체크해야 합니다.

  • iOS 빌드 시간 증가: Hermes는 빌드 시점에 JS를 컴파일하므로, CI/CD 파이프라인에서 빌드 시간이 소폭(약 10~20초) 증가할 수 있습니다.
  • 디버깅 경험의 변화: 기존 Chrome Debugger 대신 Flipper나 Chrome DevTools 프로토콜을 통한 직접 연결을 사용해야 합니다. 처음에는 이 환경 설정이 낯설 수 있습니다.
  • Intl 지원 문제: React Native 구버전에서는 안드로이드용 Hermes에 Intl(국제화) 지원이 빠져 있는 경우가 있습니다. 이 경우 날짜나 통화 포맷팅이 깨질 수 있으므로, 반드시 `org.webkit:android-jsc-intl` 대신 Hermes 전용 Intl 빌드 설정을 확인하거나 폴리필을 주입해야 합니다.
Best Practice: 앱 배포 전 반드시 release 변형(Variant)으로 빌드하여 실제 기기에서 테스트하세요. 디버그 모드에서는 Hermes 최적화 효과가 100% 발휘되지 않으며, R8 난독화로 인한 크래시를 잡을 수 없습니다.

결론

Hermes 엔진 도입과 ProGuard 최적화는 "React Native 성능" 튜닝의 시작이자 가장 효과적인 수단입니다. 우리는 이를 통해 별도의 기능 변경 없이 사용자 경험을 극적으로 개선하고, 앱 삭제율을 유의미하게 낮출 수 있었습니다. "모바일 개발" 생태계가 고도화됨에 따라 단순한 기능 구현을 넘어, 엔진 레벨에서의 이해와 "번들 사이즈 축소" 전략이 시니어 개발자의 핵심 역량이 될 것입니다. 지금 당장 여러분의 `build.gradle`을 점검해 보시기 바랍니다.

Post a Comment