-
Notifications
You must be signed in to change notification settings - Fork 0
MSG-582 fix: 네이버 지도 RN 라이브러리 커스텀 뷰 마커 비트맵 recycle 크래시 패치 — 지도 조작 중 앱 강제 종료 해소 #151
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| # MSG-582: [모바일] 지도 조작 중 앱 강제 종료 — 네이버 지도 RN 라이브러리 커스텀 뷰 마커 비트맵 recycle 크래시 패치 | ||
|
|
||
| > 원문: https://soma17-msg.atlassian.net/browse/MSG-582 (하네스 생성, 2026-09-07 — 사용자 QA "갑자기 앱이 꺼지는데") | ||
| > **수정 범위: `patches/@mj-studio__react-native-naver-map@2.9.0.patch` 1줄 + `pnpm-lock.yaml` 패치 해시 + `docs/MOBILE_RUNBOOK.md` 함정 12.** 앱 코드(`apps/mobile/src`)·`apps/web`·`packages/**` 0줄. | ||
|
|
||
| ## 기획 요약 | ||
| 지도 홈에서 조작 중 붉은 에러 화면 없이 앱이 통째로 사라진다. 에뮬레이터 재가동 직후 약 3분 뒤 재현. | ||
|
|
||
| ## 0. 착수 실측 (develop `43f425e`, 에뮬레이터 FillMap_Pixel8) | ||
|
|
||
| | 사실 | 실측 | 함의 | | ||
| |---|---|---| | ||
| | 크래시 종류 | `adb logcat -b crash`: `signal 6 (SIGABRT)`, `Abort message: 'JNI DETECTED ERROR IN APPLICATION: ... bitmap decoding: could not lock pixels'`, `from com.naver.maps.map.renderer.MapRenderer.nativeRender()`, 스레드 `GLThread 139` | JS 에러가 아닌 네이티브 abort — RN 에러 화면이 뜰 수 없다 | | ||
| | 직전 경고 | `W/Bitmap: Called getDensity() on a recycle()'d bitmap!` (크래시 6ms 전) | 지도 SDK가 이미 recycle된 비트맵을 아이콘으로 그리려 했다 | | ||
| | 라이브러리 코드 | `RNCNaverMapMarker.kt:91 removeCustomView()`: `customViewBitmap.recycle()` → `setImageWithLastImage()` 순서 | 아이콘이 아직 그 비트맵을 참조하는 상태에서 recycle | | ||
| | 앱의 커스텀 뷰 마커 | `grid-map.tsx` 경로 번호 마커·클러스터 마커·미션 이름표 (`NaverMapMarkerOverlay` 자식 뷰) | 줌 변경·코스 진입/이탈·칩 해제마다 언마운트 = 트리거 | | ||
| | 기존 패치 | `pnpm-workspace.yaml` `patchedDependencies` → 같은 패치 파일에 onLoad(`topLoaded`) 패치 존재 | 새 패치를 같은 파일에 병합 | | ||
| | 메모리·GPU | AVD RAM 2G/가용 1.2G, GPU auto | 자원 부족 아님 | | ||
|
|
||
| ## 1. 결정 | ||
|
|
||
| | # | 결정 | 근거 | | ||
| |---|---|---| | ||
| | D1 | `removeCustomView()`에서 `recycle()` 호출을 제거하고 `customViewBitmap = null`로 참조만 끊는다 | Android O 이후 비트맵 픽셀은 네이티브 힙을 GC가 회수 — 명시 recycle 불필요. 1줄 diff | | ||
| | D2 | 순서 교체(아이콘 교체 후 recycle)는 기각 | GL 스레드가 다음 프레임까지 옛 아이콘을 잡을 수 있어 레이스가 남는다 | | ||
| | D3 | 라이브러리 업그레이드 기각, 2.9.0 유지 | 기존 onLoad 패치 재작업 + 네이티브 의존성 재검증 비용. 업스트림 제보는 후속 | | ||
| | D4 | `pnpm patch --edit-dir` → 수정 → `pnpm patch-commit`으로 기존 패치 파일에 병합 | 패치 파일 1개 유지, lock 해시 자동 갱신 | | ||
|
|
||
| ## 2. 수용 기준 | ||
|
|
||
| | # | 기준 | 유형 | 검증 | | ||
| |---|---|---|---| | ||
| | L1 | 패치 파일에 `recycle()` 제거 diff가 있고, 설치된 라이브러리 소스에 반영된다 | 로직 | `grep "customViewBitmap = null" patches/…` + `node_modules` 심링크 대상 소스 | | ||
| | G1 | dev client 재빌드 시 `:mj-studio_react-native-naver-map:compileDebugKotlin` 재실행, BUILD SUCCESSFUL | 게이트 | 빌드 로그 | | ||
| | S1 | 패치 APK 콜드 스타트 후 앱이 Metro에 붙어 정상 기동 | 화면 | 실기 | | ||
| | S2 | 줌 인/아웃 반복(클러스터 교체)·코스 진입/이탈에도 앱이 종료되지 않고 `adb logcat -b crash` 무출력 | 화면 | 실기 | | ||
|
|
||
| ## 3. 리스크 | ||
| - recycle 제거로 비트맵 회수가 GC 타이밍에 맡겨진다(클로드 리뷰 🟡). 실측(에뮬레이터, 핀치 줌 인/아웃 10회 반복, `dumpsys meminfo`): Java 힙 22.0→27.9MB, 3분 뒤 22.5MB로 복귀 · 네이티브 힙 407→416→412MB(지도 타일 캐시 변동 범위). 마커 비트맵은 `ARGB_4444` 수십 px라 개당 수 KB이고 GC가 회수함을 확인 — 누적 OOM 리스크는 낮다. 크래시 없이 느려짐·OOM이 보이면 이 티켓과 연결 | ||
| - 확률적 재현이라 S2는 "재현 안 됨"이 곧 증명은 아니다. 시그니처가 다시 보이면 APK가 패치 이후 빌드인지부터 확인(런북 함정 12). | ||
|
|
||
| --- | ||
|
|
||
| ## 작업 로그 (2026-09-07) | ||
|
|
||
| **변경 파일** | ||
| - `patches/@mj-studio__react-native-naver-map@2.9.0.patch` — `RNCNaverMapMarker.kt` hunk 추가 (+15) | ||
| - `pnpm-lock.yaml` — 패치 해시 갱신 | ||
| - `docs/MOBILE_RUNBOOK.md` — 함정 12 신설 | ||
| - `docs/STATUS.md` · `docs/decisions/DECISIONS.md` · 본 파일 | ||
|
|
||
| **실측** | ||
| - L1 ✅ 패치 파일 grep 1건, `apps/mobile/node_modules/@mj-studio/react-native-naver-map` 심링크가 새 해시(`b41f33e…`) 디렉토리로 교체, 그 소스에 `customViewBitmap = null` 반영 | ||
| - G1 ✅ `expo run:android --no-bundler --device FillMap_Pixel8` — `compileDebugKotlin` 재실행, BUILD SUCCESSFUL in 26s | ||
| - S1 ✅ force-stop 후 `localhost:8081` 딥링크 콜드 스타트, `Running "main"` 로그, 로그인 화면 표시 | ||
| - S2 ⏳ 사용자 실기 진행 중(세션의 원래 목적이 안드로이드 테스트) — 재현 조작 반복은 사용자 몫. 스크린샷 없음(크래시 부재는 화면으로 증명되지 않음) | ||
|
|
||
| **검토한 대안** — 결정 D2·D3 참조. | ||
|
|
||
| **후속** — 업스트림(mj-studio/react-native-naver-map) 이슈 제보. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -77,6 +77,21 @@ index c92093f93ff92013db5574361eb0a21853f356cd..7dacb623e928c3bfedc973775f7b70ae | |
| registerDirectEvent(this, NaverMapOptionChangeEvent.EVENT_NAME) | ||
| registerDirectEvent(this, NaverMapCameraChangeEvent.EVENT_NAME) | ||
| registerDirectEvent(this, NaverMapCameraIdleEvent.EVENT_NAME) | ||
| diff --git a/android/src/main/java/com/mjstudio/reactnativenavermap/overlay/marker/RNCNaverMapMarker.kt b/android/src/main/java/com/mjstudio/reactnativenavermap/overlay/marker/RNCNaverMapMarker.kt | ||
| index 509e1ec715d475671f26797a70fc2872cd6fde9c..1d771624fe55010629419f231079f91ba0f6c6a8 100644 | ||
| --- a/android/src/main/java/com/mjstudio/reactnativenavermap/overlay/marker/RNCNaverMapMarker.kt | ||
| +++ b/android/src/main/java/com/mjstudio/reactnativenavermap/overlay/marker/RNCNaverMapMarker.kt | ||
| @@ -88,7 +88,9 @@ class RNCNaverMapMarker( | ||
| fun removeCustomView(index: Int) { | ||
| customView = null | ||
| ViewChangesTracker.getInstance().removeMarker(this) | ||
| - if (customViewBitmap != null && !customViewBitmap!!.isRecycled) customViewBitmap!!.recycle() | ||
| + // FillMap 패치: overlay.icon이 아직 이 비트맵을 참조하는 상태에서 recycle하면 GL 렌더 스레드가 | ||
| + // "could not lock pixels"로 네이티브 abort(앱 종료)한다. GC에 맡긴다 — 2026-09-07 실측. | ||
| + customViewBitmap = null | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 권장 — recycle 제거로 인한 비트맵 GC 지연(메모리) 트레이드오프가 문서화 안 됨
D2(순서만 바꾸는 대안)가 기각된 근거는 납득되지만, 대안으로 "언마운트 시 GL 스레드가 이번 프레임 렌더를 마쳤다고 보장된 뒤(예: 다음 카메라 idle·프레임 콜백)에 recycle" 같은 지연 recycle 경로도 검토해볼 만합니다. 최소한 지속적인 줌 반복 상황에서 메모리 사용량( (추측 포함:
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 실측으로 확인했습니다 (에뮬레이터, 핀치 줌 인/아웃 10회 반복, |
||
| isImageSetFromSubview = false | ||
| setImageWithLastImage() | ||
| super.removeView(children.elementAt(index)) | ||
| diff --git a/lib/typescript/commonjs/src/component/NaverMapView.d.ts b/lib/typescript/commonjs/src/component/NaverMapView.d.ts | ||
| index 83f41067be85014cf8c43f5d970c9b361e12cf12..f6e7e8041b27b25ea1100095dbe16e342b4a105d 100644 | ||
| --- a/lib/typescript/commonjs/src/component/NaverMapView.d.ts | ||
|
|
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟢 사소한 제안 — "3. 리스크"에 메모리 트레이드오프 한 줄 추가 검토
patch 파일 인라인 코멘트에도 적었듯, recycle 제거는 크래시 레이스는 없애지만 대신 비트맵 회수를 GC 타이밍에 맡기는 트레이드오프가 생깁니다. 줌 반복처럼 커스텀 뷰 마커가 빈번히 교체되는 조작에서 메모리 사용량이 늘어날 가능성이 있는데, 이 리스크 절에는 확률적 재현 이슈만 있고 언급이 없습니다. D1 결정 근거("GC가 회수하므로 안전")와 짝을 이루는 리스크로 한 줄 추가해두면 함정 12를 보는 다음 사람이 "이 시그니처는 없는데 대신 느려짐/OOM이 보인다"는 경우도 바로 이 티켓과 연결 지을 수 있습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
실측으로 확인했습니다 (에뮬레이터, 핀치 줌 인/아웃 10회 반복,
dumpsys meminfo): Java 힙 22.0→27.9MB, 3분 뒤 22.5MB로 복귀. 네이티브 힙 407→416→412MB(지도 타일 캐시 변동 범위). 크래시 0. 마커 비트맵은ARGB_4444수십 px라 개당 수 KB이고 GC가 회수하는 것이 확인돼 지연 recycle 경로는 도입하지 않았습니다. 스펙 "3. 리스크"에 트레이드오프 + 실측치를 추가했습니다.