React Native Android stack 복원: R8 mapping과 native symbols를 어떻게 고르는가
읽기 어려운 Java·Kotlin 호출과 C·C++ 기계 코드 주소를 먼저 구분하고, 공개 build와 함께 만든 각각의 대응표로 원래 함수·파일·줄을 복원한다.
목차
표준·구현·측정·해석 표시는 무엇인가요?
- 표준웹 표준이나 언어 명세가 정한 동작
- 구현특정 기술이나 브라우저가 실제로 구현한 동작
- 측정명시한 환경에서 직접 실행해 관찰한 결과
- 해석앞선 근거에서 도출한 설계 판단
- 미확인아직 공식 근거나 재현 결과를 확인하지 못한 내용
30초 요약
R8은 Android build에서 Java·Kotlin 코드를 줄이고 최적화하며 클래스·함수 이름을 바꿀 수 있는 도구다.
Retrace는 R8이 남긴 이름·줄 대응표인 mapping.txt를 읽어 난독화된 호출을 원래 정의로 되돌리는 도구다.
.so는 Android 앱에 들어가는 C·C++ 공유 라이브러리 파일이다.
Android stack을 복원할 때는 먼저 frame 모양으로 실행 코드 계열을 나눈다. frame은 stack에 기록된 호출 한 항목이다.
at a.a(SourceFile:10) Java·Kotlin / R8 → mapping.txt + Retrace
#00 pc 00000000000123 libreceipt.so C·C++ / native → 같은 ABI symbols + ndk-stack
index.android.bundle:1:132161 JavaScript → 25편 source map + metro-symbolicateABI(Application Binary Interface, 응용 프로그램 이진 인터페이스)는 arm64-v8a, x86_64처럼 기계 코드가
따르는 CPU 명령·함수 호출 규칙의 구분이다. release는 여기서 사용자 공개 행위가 아니라 Android build의
배포용 configuration 이름이고, 실제 공개 결과는 공개 build 417처럼 쓴다.
반복해서 기억할 문장은 이것이다.
frame 계열을 먼저 나누고, 오류가 난 공개 build와 함께 만든 mapping 또는 같은 ABI symbols만 쓴다.
이 글을 관통하는 상황: ProfileActivity가 원인처럼 보였다
공개 Android build 2.4.1(417)에서 다음 Java frame이 수집됐다.
java.lang.IllegalStateException: receipt closed
at a.a(CheckoutFailure.kt:10)팀은 build 417의 mapping을 찾지 못해 현재 main의 build 418 mapping을 넣었다. Retrace 명령은 성공했고
ProfileActivity.open(ProfileActivity.kt:88)이 나왔다. 그 파일을 수정했지만 오류는 그대로였다.
build 417의 실제 mapping으로 다시 실행하자
CheckoutFailure.submit(CheckoutFailure.kt:42)가 나왔다. 같은 보고서 아래의
#00 pc ... libreceipt.so frame은 mapping.txt로 바뀌지 않았다.
Java·Kotlin frame은 바뀐 이름 때문에 원본을 잃고, C·C++ frame은 배포 파일에서 진단 정보가 제거되면 기계 코드 주소만 남는다. 두 갈래의 복원 산출물은 서로 다르다.
이 글의 질문은 하나다.
읽기 어려워진 Android stack을 정확한 원본 함수·파일·줄로 어떻게 되돌리는가?
frame 모양으로 복원 산출물을 먼저 고른다
R8은 Android Java·Kotlin bytecode를 DEX라는 Android 실행 형식으로 바꾸는 과정에서 도달하지 않는 코드를
제거하고, 호출을 합치며, 클래스·함수 이름과 줄을 바꿀 수 있다. C·C++ 코드는 .so 확장자의 공유
라이브러리 기계 코드로 들어가므로 R8 mapping의 대상이 아니다.
Android Native Development Kit(NDK)는 C·C++ 코드를 Android 공유 라이브러리로 만드는 도구 묶음이다.
compile은 원본 코드를 기계가 처리할 형식으로 바꾸는 단계이고, link는 여러 기계 코드 조각을 실행
라이브러리로 연결하는 단계다. stripped .so는 배포 크기를 줄이려고 함수·파일 진단 정보를 제거한
공유 라이브러리다.
Java·Kotlin 원본 → R8 최적화·난독화 → DEX + mapping.txt
C·C++ 원본 → NDK compile·link → stripped .so + native debug symbols
JavaScript 원본 → Metro·Hermes → bundle/bytecode + source map잘못된 mapping도 그럴듯한 원본을 만든다
Retrace 실행 모양을 작은 가상 입력으로 먼저 고정한다. 이 실험은 Android SDK command-line tools의 Retrace 9.0.32에서 직접 확인했다. command-line tools는 Android SDK의 터미널 도구를 모아 설치하는 패키지다. 앱 루트에서 먼저 폴더를 만든다.
mkdir -p android-stack-fixtureWindows PowerShell에서는 다음 명령을 쓴다.
New-Item -ItemType Directory -Force android-stack-fixture이제 편집기로 다음 세 코드 블록을 표시된 정확한 경로에 저장한다.
android-stack-fixture/trace.txt
java.lang.IllegalStateException: receipt closed
at a.a(CheckoutFailure.kt:10)android-stack-fixture/mapping-build-418.txt
# {"id":"com.android.tools.r8.mapping","version":"1.0"}
com.byteloft.profile.ProfileActivity -> a:
# {"id":"sourceFile","fileName":"ProfileActivity.kt"}
10:10:void open(java.lang.String):88:88 -> aandroid-stack-fixture/mapping-build-417.txt
# {"id":"com.android.tools.r8.mapping","version":"1.0"}
com.byteloft.checkout.CheckoutFailure -> a:
# {"id":"sourceFile","fileName":"CheckoutFailure.kt"}
10:10:void submit(java.lang.String):42:42 -> a앱 루트에서 세 파일이 모두 있는지 확인한다.
ls android-stack-fixture/trace.txt \
android-stack-fixture/mapping-build-418.txt \
android-stack-fixture/mapping-build-417.txtPowerShell에서는 다음 명령의 세 결과가 모두 True인지 확인한다.
Test-Path android-stack-fixture/trace.txt
Test-Path android-stack-fixture/mapping-build-418.txt
Test-Path android-stack-fixture/mapping-build-417.txt이 두 mapping은 R8 mapping 문법을 보여 주기 위해 손으로 만든 가상 파일이다. 실제 build의 provenance, 즉 어느 소스·도구·설정에서 만들어졌는지는 증명하지 않는다.
Android SDK command-line tools를 설치하면 Retrace는 보통
$ANDROID_HOME/cmdline-tools/latest/bin/retrace에 있다. $ANDROID_HOME은 Android SDK 설치 위치를 담은
환경 변수다. 앱 루트에서 버전을 확인한다.
"$ANDROID_HOME/cmdline-tools/latest/bin/retrace" --versionPowerShell에서는 Android SDK 설치 위치를 담은 $env:ANDROID_HOME을 사용한다.
& "$env:ANDROID_HOME\cmdline-tools\latest\bin\retrace.bat" --version먼저 잘못된 build 418 mapping으로 실행한다.
"$ANDROID_HOME/cmdline-tools/latest/bin/retrace" \
android-stack-fixture/mapping-build-418.txt \
android-stack-fixture/trace.txtPowerShell 등가 명령은 다음과 같다.
& "$env:ANDROID_HOME\cmdline-tools\latest\bin\retrace.bat" `
android-stack-fixture/mapping-build-418.txt `
android-stack-fixture/trace.txtjava.lang.IllegalStateException: receipt closed
at com.byteloft.profile.ProfileActivity.open(ProfileActivity.kt:88)명령은 성공하고 이름·줄도 읽기 좋지만 오답이다. 이제 같은 명령에 build 417 mapping을 넣는다.
"$ANDROID_HOME/cmdline-tools/latest/bin/retrace" \
android-stack-fixture/mapping-build-417.txt \
android-stack-fixture/trace.txtPowerShell에서는 mapping 경로만 build 417 파일로 바꾼다.
& "$env:ANDROID_HOME\cmdline-tools\latest\bin\retrace.bat" `
android-stack-fixture/mapping-build-417.txt `
android-stack-fixture/trace.txtjava.lang.IllegalStateException: receipt closed
at com.byteloft.checkout.CheckoutFailure.submit(CheckoutFailure.kt:42)공개 build와 mapping을 한 산출물 묶음으로 보관한다
React Native 0.86 Android 프로젝트의 android/app/build.gradle에서 release build의 R8 최적화를
명시한다. signing은 배포 파일의 출처와 무결성을 증명하는 서명 설정이다. 실제 제품 signing 값은
프로젝트의 기존 안전한 설정을 유지한다.
android {
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
}
}
}앱 루트에서 Android App Bundle(AAB, Play에 올리는 앱 코드·자원 묶음)을 build한다. APK는 Android 기기에 직접 설치할 수 있는 앱 파일 형식이다.
cd android
./gradlew :app:bundleRelease
cd ..Windows PowerShell에서는 앱 루트에서 다음 순서로 실행한다.
Set-Location android
.\gradlew.bat :app:bundleRelease
Set-Location ..성공 뒤 다음 파일을 확인한다.
android/app/build/outputs/bundle/release/app-release.aab
android/app/build/outputs/mapping/release/mapping.txtbuild 417 보관 폴더에는 최소 다음 값을 함께 둔다.
app version / versionCode / commit
AAB path / SHA-256 hash
mapping.txt path / SHA-256 hash
Android Gradle Plugin / R8 version
build configuration / build logmacOS·Linux에서는 다음처럼 파일 내용 해시값을 계산한다.
shasum -a 256 \
android/app/build/outputs/bundle/release/app-release.aab \
android/app/build/outputs/mapping/release/mapping.txtWindows PowerShell에서는 다음 두 명령을 실행한다.
Get-FileHash -Algorithm SHA256 android/app/build/outputs/bundle/release/app-release.aab
Get-FileHash -Algorithm SHA256 android/app/build/outputs/mapping/release/mapping.txtversionCode가 같다는 사실만으로 파일 내용까지 같아지지 않으므로 AAB와 mapping hash도 함께 맞춘다.
실제 stack은 앱 루트에서 같은 build의 mapping으로 복원한다.
"$ANDROID_HOME/cmdline-tools/latest/bin/retrace" \
artifacts/android/417/mapping.txt \
stacktrace-build-417.txtPowerShell 등가 명령은 다음과 같다.
& "$env:ANDROID_HOME\cmdline-tools\latest\bin\retrace.bat" `
artifacts/android/417/mapping.txt `
stacktrace-build-417.txt--verbose를 추가하면 함수 매개변수와 반환 타입 같은 정보를 더 볼 수 있다. 원본 함수가 여러 후보로
나오거나 줄이 빠지면 임의로 하나를 고르지 말고 mapping 일치, R8 출력 경고와 원본 stack 보존 여부를
먼저 확인한다.
native frame은 ABI와 debug symbol 수준을 맞춘다
R8 mapping은 .so 주소를 복원하지 않는다.
파일·줄까지 필요한 build는 android/app/build.gradle의 해당 release build에 다음 설정을 둔다.
android {
buildTypes {
release {
ndk {
debugSymbolLevel 'FULL'
}
}
}
}Android Gradle Plugin(AGP)은 Gradle이 Android 앱을 build하도록 연결하는 도구다. AGP 4.1 이상에서 APK를 만들면 보통 다음 파일이 생성된다.
android/app/build/outputs/native-debug-symbols/release/native-debug-symbols.zipAAB는 설정한 symbols를 bundle에 포함해 Play Console로 전달할 수 있다. mapping과 native symbols는 같은 앱 버전에 둘 다 연결할 수 있다.
오프라인 ndk-stack은 zip 이름만 보는 것이 아니라 같은 build·ABI의 symbols가 제거되지 않은 .so가
있는 디렉터리를 -sym에 받는다. AGP build의 일반적인 위치 모양은 다음과 같다.
android/app/build/intermediates/cxx/<build-type>/<build-hash>/obj/<abi>build-hash는 Gradle이 만든 경로 조각이므로 build log와 실제 폴더에서 확인한다. build log는 build
명령이 남긴 단계와 출력 경로 기록이다. 오류 기기의 ABI가
arm64-v8a라면 x86_64 폴더를 대신 쓰지 않는다.
logcat은 연결된 Android 기기의 로그를 읽는 도구다. tombstone은 Android native crash가 남긴 process·thread·signal·주소 기록이며, signal은 운영체제가 native process에 전달한 충돌·중단 같은 사건 번호다.
먼저 앱 루트에서 실제 obj/<abi> 디렉터리를 찾는다.
find android/app/build/intermediates/cxx -type d -path '*/obj/*'PowerShell에서는 다음처럼 찾는다.
Get-ChildItem android/app/build/intermediates/cxx -Directory -Recurse |
Where-Object { $_.FullName -match '\\obj\\' }출력 중 오류 기기와 같은 ABI이며 필요한 .so가 들어 있는 디렉터리를 고른다. 다음 값은 경로 모양을
보여 주는 예시다. NDK 버전, build type, build hash와 ABI를 방금 확인한 실제 폴더명으로 모두 바꾼다.
NDK_STACK="$ANDROID_HOME/ndk/<설치한-NDK-버전>/ndk-stack"
UNSTRIPPED_DIR="android/app/build/intermediates/cxx/<build-type>/<build-hash>/obj/<abi>"
"$NDK_STACK" -sym "$UNSTRIPPED_DIR" -dump tombstone-build-417.txt<...>는 설명용 자리이므로 shell에 그대로 붙여 넣지 않는다. 예를 들어 찾은 폴더가
android/app/build/intermediates/cxx/Release/4d6g5h2a/obj/arm64-v8a라면 그 전체 문자열을
UNSTRIPPED_DIR 값으로 넣는다.
PowerShell에서는 같은 실제 값으로 다음처럼 실행한다.
$NdkStack = "$env:ANDROID_HOME\ndk\<설치한-NDK-버전>\ndk-stack.cmd"
$UnstrippedDir = "android\app\build\intermediates\cxx\<build-type>\<build-hash>\obj\<abi>"
& $NdkStack -sym $UnstrippedDir -dump tombstone-build-417.txtPowerShell에서도 네 <...> 값을 실제 폴더명으로 바꾼 뒤 실행한다. tombstone 파일은 다음 구분선부터
보존한다.
*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***성공 결과는 libreceipt.so 안의 주소가 ReceiptStore.cpp:73 같은 실제 build 417의 파일·줄과 함수로
바뀌는 것이다. 이 글은 가상 native 라이브러리를 build하지 않으므로 그 특정 출력은 실행 증거로 주장하지
않는다. 실제 C·C++ 통합에서 같은 ABI의 unstripped 라이브러리로 닫아야 한다.
Play Console 자동 복원도 version 연결을 검증한다
Play Console의 App bundle explorer는 관련 artifact에 mapping 또는 native symbols를 연결한다. 표준 AAB build에서는 mapping을 bundle에서 자동으로 가져올 수 있고, native symbols는 앞의 debug symbol 설정이 필요하다.
Java·Kotlin version별 ReTrace 호환 mapping → deobfuscated stack
C·C++ version별 native debug symbols → symbolicated stack
둘 다 포함 같은 artifact에 두 파일 계열을 모두 연결 가능파일을 나중에 올렸다고 과거 보고서가 모두 즉시 다시 복원된다고 가정하지 않는다. Play Console 공식 안내는 해당 version에 올린 뒤 들어온 crash·ANR부터 복원된다고 한정한다. 자동 화면에서도 build 식별자와 artifact를 확인하고, 부분 복원이면 앱·제3자 라이브러리 symbols가 모두 포함됐는지 조사한다.
복원된 stack을 공개 build 소스와 대조한다
.so build ID는 해당 공유 라이브러리 build를 구분하는 값이다.
읽기 좋은 이름은 조사 시작점이다. 다음 표로 원본 사건과 다시 연결한다.
build versionCode·commit·AAB hash가 보고서와 맞는가
Java·Kotlin mapping hash와 Retrace 경고·후보를 보존했는가
native ABI·.so build ID·symbol 수준이 맞는가
사건 예외 또는 signal과 복원된 호출 순서가 맞는가
소스 공개 build commit의 실제 파일·줄인가
재현 같은 입력·기기 조건에서 같은 오류 범주가 생기는가stack에 인증값·결제 정보·사용자 입력을 추가하지 않고, mapping과 symbols 접근도 공개 build 진단 담당자로 제한한다.
pull request에서 달라져야 할 행동
공개 Android build의 stack 복원 pull request에는 다음이 있어야 한다.
frame 분류 Java·Kotlin / C·C++ / JavaScript를 나눴는가
build 연결 versionCode·commit·AAB hash를 고정했는가
R8 같은 build mapping hash와 Retrace 출력을 보존했는가
native 같은 ABI·build의 symbols와 ndk-stack 출력을 보존했는가
Play Console artifact에 mapping·symbols가 연결됐는가
원인 검증 예외·signal·호출 순서·재현과 복원 위치를 대조했는가
보안 진단 산출물 접근·보존·삭제 정책이 있는가검토자는 다음을 확인한다.
- 모든 Android frame을 mapping.txt 하나에 넣지 않는가?
- 현재 main build의 mapping을 과거 공개 build stack에 쓰지 않는가?
- 새 build가 mapping.txt를 덮어쓰기 전에 별도 보관하는가?
- 함수 이름만 필요한 SYMBOL_TABLE과 파일·줄까지 필요한 FULL을 구분하는가?
- arm64-v8a tombstone에 x86_64
.so를 쓰지 않는가? - 별표 구분선이 빠진 tombstone을 ndk-stack 실패로 오판하지 않는가?
- 복원된 첫 줄을 원인으로 바로 단정하지 않는가?
- JavaScript frame을 R8 또는 ndk-stack으로 복원하려 하지 않는가?
한 장으로 다시 보기
문제 build 417의 a.a(SourceFile:10)가 ProfileActivity로 잘못 복원됨
원인 build 418 mapping을 사용해 명령 성공을 산출물 일치로 오해
분류 Java·Kotlin frame → R8 mapping, native .so frame → ABI symbols
교정 build 417 mapping → CheckoutFailure.submit(CheckoutFailure.kt:42)
보관 AAB·mapping·native symbols·commit·hash를 build별 묶음으로 저장
native FULL이면 함수·파일·줄, ndk-stack은 같은 ABI unstripped .so 사용
자동화 Play artifact에 version별 mapping과 symbols 연결
검증 공개 build 소스·예외/signal·호출 순서·재현과 대조
경계 JavaScript는 25편, iOS address는 27편 도구가 맡음Android stack 복원은 읽기 어려운 위치를 추측하는 요령이 아니다. frame이 어느 실행 코드 계열인지 먼저 분류하고, 그 공개 build와 함께 생성한 정확한 진단 산출물을 찾는 일이다. 그 뒤 원본 사건과 호출 순서를 대조해야 수정할 코드를 고를 수 있다.
스스로 확인할 질문
- Java·Kotlin frame과 C·C++ frame을 어떤 모양으로 먼저 구분하는가?
- 다른 build의 mapping이 그럴듯한 원본 이름을 낼 수 있는 이유는 무엇인가?
- mapping.txt를 공개 build마다 별도로 보관해야 하는 이유는 무엇인가?
- SYMBOL_TABLE과 FULL native debug symbols의 차이는 무엇인가?
- ndk-stack에 같은 ABI의 unstripped 공유 라이브러리가 필요한 이유는 무엇인가?
- 복원된 함수·줄을 공개 build 소스와 다시 대조해야 하는 이유는 무엇인가?
Active recall
기억에서 꺼내 보기
답을 쓰지 않아도 됩니다. 먼저 머릿속으로 답한 뒤 펼쳐서 정답·이유·흔한 오해를 비교하세요.
01Android crash 보고서의 모든 frame을 R8 mapping.txt 하나로 복원할 수 있을까?
정답
아니다. Java·Kotlin의 난독화된 클래스·함수 frame은 Retrace와 R8 mapping을 쓰고, C·C++ 공유 라이브러리 주소는 같은 ABI의 native debug symbols와 ndk-stack을 쓴다.
관련 설명 다시 읽기왜 그런가
stack에 함께 보인다는 이유로 생성 체계와 복원 산출물이 같아지는 것은 아니다.
선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.
02같은 앱 버전 이름의 다음 build에서 새로 만든 mapping.txt로 이전 build stack을 Retrace해도 될까?
정답
아니다. stack을 만든 DEX와 같은 build의 mapping을 사용하고 build 식별자·AAB hash·mapping hash를 함께 맞춘다.
관련 설명 다시 읽기왜 그런가
R8은 build마다 이름·줄·최적화 결과를 바꿀 수 있고 mapping.txt도 다시 build하면 덮어쓸 수 있다.
선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.
03Play Console에서 native 함수 이름이 보이면 원본 C·C++ 파일과 줄도 항상 복원된 것일까?
정답
아니다. SYMBOL_TABLE은 함수 이름 중심이고 FULL은 함수 이름과 파일·줄을 제공한다. 의존 라이브러리가 이미 진단 정보를 제거했다면 부분 복원일 수도 있다.
관련 설명 다시 읽기왜 그런가
표시된 정보 수준과 실제 보관한 symbols 범위를 함께 확인해야 한다.
선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.
04Retrace나 ndk-stack이 원본 함수·줄을 보여 주면 그 줄이 원인이라고 확정해도 될까?
정답
아니다. 복원된 호출 순서와 예외·signal, 같은 공개 build 소스, 재현 조건을 함께 대조한다.
관련 설명 다시 읽기왜 그런가
위치 복원은 조사 출발점을 되찾는 단계이지 당시 값과 원인을 자동 기록하는 단계가 아니다.
선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.
출처와 검증 범위
아래 날짜는 링크를 마지막으로 열어 본 날입니다. 문서 상단의 검증일은 글의 설명과 적용 범위를 다시 확인한 날입니다.
- Enable app optimization with R8Android Developers · 공식 문서 · 확인 2026-08-09
- Fix optimization problemsAndroid Developers · 공식 문서 · 확인 2026-08-09
- R8 retraceAndroid Developers · 공식 문서 · 확인 2026-08-09
- R8, Retrace and map file versioningR8 Open Source Project · 소스 코드 · 확인 2026-08-09
- ndk-stackAndroid Developers · 공식 문서 · 확인 2026-08-09
- Include native symbols in your release buildAndroid Developers · 공식 문서 · 확인 2026-08-09
- Deobfuscate or symbolicate crash stack tracesGoogle Play Console Help · 공식 문서 · 확인 2026-08-09