MSG-582 fix: 네이버 지도 RN 라이브러리 커스텀 뷰 마커 비트맵 recycle 크래시 패치 — 지도 조작 중 앱 강제 종료 해소 - #151
Conversation
…강제 종료 해소 - `@mj-studio/react-native-naver-map` 2.9.0 `RNCNaverMapMarker.removeCustomView()`가 지도 아이콘이 아직 참조하는 비트맵을 먼저 `recycle()`해 GL 렌더 스레드가 `could not lock pixels` 네이티브 abort(SIGABRT)로 앱을 종료시키던 버그. 기존 pnpm 패치 파일에 recycle 제거(`customViewBitmap = null`) 1줄 추가, GC에 맡김 - Kotlin 변경이라 dev client 재빌드 필요 — MOBILE_RUNBOOK 함정 12 신설(시그니처·원인·대응) - STATUS·DECISIONS·spec/MSG-582 영속화 (앱 코드 0줄) Claude-Session: https://claude.ai/code/session_012MtDBFuKP9N4wqmsuoXE7P
|
Claude finished @s13121312's task in 3m 14s —— View job 🔍 코드 리뷰 완료작업 목록
결과 요약원인 진단·수정 범위(패치 1줄)·문서화가 모두 꼼꼼하고 일관됩니다. 🔴 반드시 수정할 이슈는 없습니다.
전체 요약은 별도 코멘트에 게시했습니다 (마커: |
| - 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.
🟡 권장 — recycle 제거로 인한 비트맵 GC 지연(메모리) 트레이드오프가 문서화 안 됨
recycle()을 없애고 GC에 맡기는 방향 자체는 크래시(레이스) 해소로는 타당합니다. 다만 이 마커들은 스펙에도 적혀 있듯 "줌 변경으로 클러스터가 교체"될 때마다 대량으로 마운트/언마운트되는 경로입니다 (grid-map.tsx의 클러스터·경로 번호 마커). recycle 없이 GC에만 의존하면:
- 커스텀 뷰 마커 비트맵은 보통
View.draw()로 렌더한 소프트웨어 비트맵이라 Java 힙에 남습니다. 줌을 빠르게 반복하면 이전 프레임들의 비트맵이 GC 사이클 전까지 누적돼 메모리 압박(→ 다른 원인의 OOM/버벅임)을 유발할 수 있습니다. docs/spec/MSG-582.md의 "3. 리스크" 절이 확률적 재현 이슈만 다루고 이 메모리 트레이드오프는 언급하지 않습니다.
D2(순서만 바꾸는 대안)가 기각된 근거는 납득되지만, 대안으로 "언마운트 시 GL 스레드가 이번 프레임 렌더를 마쳤다고 보장된 뒤(예: 다음 카메라 idle·프레임 콜백)에 recycle" 같은 지연 recycle 경로도 검토해볼 만합니다. 최소한 지속적인 줌 반복 상황에서 메모리 사용량(adb shell dumpsys meminfo)을 실기로 한 번 확인해두면 좋겠습니다 — 사용자 QA가 "지도 조작 중"이라 정확히 이 패턴입니다.
(추측 포함: customViewBitmap이 커스텀 뷰 스냅샷용 소프트웨어 비트맵이라는 전제 하의 지적이며, 하드웨어 비트맵이라면 해당 없음)
There was a problem hiding this comment.
실측으로 확인했습니다 (에뮬레이터, 핀치 줌 인/아웃 10회 반복, dumpsys meminfo): Java 힙 22.0→27.9MB, 3분 뒤 22.5MB로 복귀. 네이티브 힙 407→416→412MB(지도 타일 캐시 변동 범위). 크래시 0. 마커 비트맵은 ARGB_4444 수십 px라 개당 수 KB이고 GC가 회수하는 것이 확인돼 지연 recycle 경로는 도입하지 않았습니다. 스펙 "3. 리스크"에 트레이드오프 + 실측치를 추가했습니다.
| | S1 | 패치 APK 콜드 스타트 후 앱이 Metro에 붙어 정상 기동 | 화면 | 실기 | | ||
| | S2 | 줌 인/아웃 반복(클러스터 교체)·코스 진입/이탈에도 앱이 종료되지 않고 `adb logcat -b crash` 무출력 | 화면 | 실기 | | ||
|
|
||
| ## 3. 리스크 |
There was a problem hiding this comment.
🟢 사소한 제안 — "3. 리스크"에 메모리 트레이드오프 한 줄 추가 검토
patch 파일 인라인 코멘트에도 적었듯, recycle 제거는 크래시 레이스는 없애지만 대신 비트맵 회수를 GC 타이밍에 맡기는 트레이드오프가 생깁니다. 줌 반복처럼 커스텀 뷰 마커가 빈번히 교체되는 조작에서 메모리 사용량이 늘어날 가능성이 있는데, 이 리스크 절에는 확률적 재현 이슈만 있고 언급이 없습니다. D1 결정 근거("GC가 회수하므로 안전")와 짝을 이루는 리스크로 한 줄 추가해두면 함정 12를 보는 다음 사람이 "이 시그니처는 없는데 대신 느려짐/OOM이 보인다"는 경우도 바로 이 티켓과 연결 지을 수 있습니다.
There was a problem hiding this comment.
실측으로 확인했습니다 (에뮬레이터, 핀치 줌 인/아웃 10회 반복, dumpsys meminfo): Java 힙 22.0→27.9MB, 3분 뒤 22.5MB로 복귀. 네이티브 힙 407→416→412MB(지도 타일 캐시 변동 범위). 크래시 0. 마커 비트맵은 ARGB_4444 수십 px라 개당 수 KB이고 GC가 회수하는 것이 확인돼 지연 recycle 경로는 도입하지 않았습니다. 스펙 "3. 리스크"에 트레이드오프 + 실측치를 추가했습니다.
🔍 코드 리뷰 요약 — MSG-582 네이버 지도 마커 비트맵 recycle 크래시 패치전반 평가: 원인 진단(크래시 로그 → GL 렌더 스레드가 recycle된 비트맵을 잠그려는 레이스)이 명확하고, 수정 자체는 라이브러리 패치 1줄( 잘한 점
이슈 (인라인 코멘트 참고)
검증 확인 사항
전체적으로 원인 분석과 diff 범위가 타당하고 문서화가 꼼꼼합니다. 위 메모리 트레이드오프는 머지를 막을 정도는 아니라고 보며(🔴 없음), 참고용 권장 사항입니다. |
… + 줌 10회 메모리 실측 추가 - 실측: Java 힙 22.0→27.9→22.5MB(3분 후 복귀), 네이티브 407→416→412MB, 크래시 0 Claude-Session: https://claude.ai/code/session_012MtDBFuKP9N4wqmsuoXE7P
|
Claude finished @s13121312's task in 1m 59s —— View job 🔍 코드 리뷰 완료 (재검토)작업 목록
결과 요약최신 커밋 🔴 반드시 수정할 이슈 없음. 새 인라인 코멘트 없음 — 이전 지적이 해소됐습니다. 전체 요약은 별도 코멘트에 게시했습니다 (마커: |
🔍 코드 리뷰 요약 (재검토) — MSG-582 네이버 지도 마커 비트맵 recycle 크래시 패치대상: 최신 커밋 결론: 이전 지적 사항 해소 확인, 추가로 반드시 고칠 이슈 없음 (🔴 0건). 이전 지적 사항 반영 확인
주관적 우려("GC에 맡기면 메모리가 늘 수 있다")를 추측으로 남기지 않고 실측으로 검증한 점이 좋습니다. 특히 3분 뒤 Java 힙이 원상 복귀했다는 수치가 "GC가 실제로 수거한다"는 가장 중요한 주장을 직접 뒷받침합니다. 나머지 diff 재확인
참고 (추측 포함, 확정적 이슈 아님)
전체적으로 이번 커밋은 이전 리뷰의 유일한 권장 사항을 실측 데이터로 충실히 반영했습니다. 승인 가능하다고 봅니다. |
🎫 관련 티켓
📌 작업 내용
지도 홈에서 조작하다 보면 붉은 에러 화면 없이 앱이 통째로 사라지는 문제입니다(사용자 QA "갑자기 앱이 꺼지는데"). 앱 코드가 아니라 네이버 지도 RN 래퍼 라이브러리(
@mj-studio/react-native-naver-map2.9.0)의 안드로이드 네이티브 버그입니다.크래시 로그1는 이렇습니다.
원인은 라이브러리
RNCNaverMapMarker.removeCustomView()가 지도 아이콘이 아직 참조하고 있는 비트맵을 먼저recycle()2하고 그 다음에야 아이콘을 교체하는 순서입니다. 커스텀 뷰 마커(경로 번호 마커·클러스터 마커·미션 이름표)가 언마운트되는 순간 GL 렌더 스레드3가 이미 회수된 비트맵을 잠그려다 네이티브 abort로 프로세스가 죽습니다. 줌 변경으로 클러스터가 교체되거나 코스 상세를 나올 때 확률적으로 재현됩니다.수정은 기존 pnpm 패치4 파일에 1줄을 더한 것입니다.
patches/@mj-studio__react-native-naver-map@2.9.0.patch:removeCustomView()의recycle()호출을 제거하고customViewBitmap = null로 참조만 끊습니다. Android O 이후 비트맵 픽셀은 GC가 회수하므로 명시 recycle 없이 안전합니다pnpm-lock.yaml: 패치 해시 갱신docs/MOBILE_RUNBOOK.md: 함정 12 신설 — 이 시그니처가 보이면 코드 결함이 아니라 패치 전 APK인지 먼저 확인docs/spec/MSG-582.md· STATUS 2행 · DECISIONS 1행Kotlin 변경이라 dev client 재빌드가 필요합니다(JS만 바뀐 게 아닙니다). 앱 코드(
apps/mobile/src)·웹·packages는 0줄입니다.✅ 체크리스트
pnpm lint/pnpm typecheck해당 없음 (JS·TS 변경 0줄, pre-commit 훅 통과)🔍 검증 요약
apps/mobile/node_modules/@mj-studio/react-native-naver-map심링크가 새 패치 해시 디렉토리로 교체, 소스에customViewBitmap = null확인expo run:android --no-bundler --device FillMap_Pixel8:mj-studio_react-native-naver-map:compileDebugKotlin재실행 · BUILD SUCCESSFULRunning "main"로그, 로그인 화면)logcat -b crash무출력📸 스크린샷 (선택)
없음 — 크래시 부재는 화면으로 증명되지 않습니다. 재현 시그니처와 확인 명령은 런북 함정 12에 있습니다.
💡 추가 논의할 사항
topLoaded) 패치와 같은 파일입니다. 라이브러리 버전을 올릴 때 두 hunk 모두 다시 얹어야 합니다.https://claude.ai/code/session_012MtDBFuKP9N4wqmsuoXE7P
Footnotes
adb logcat -d -b crash— 안드로이드가 네이티브 크래시만 따로 모아 두는 로그 버퍼. ↩Bitmap.recycle()— 비트맵의 픽셀 메모리를 즉시 해제하는 호출. 해제 뒤 그 비트맵을 그리려 하면 예외 또는 네이티브 크래시가 난다. ↩GL 렌더 스레드 — 네이버 지도 SDK가 OpenGL로 지도를 그리는 별도 스레드. UI 스레드에서 비트맵을 회수해도 이 스레드는 다음 프레임에서 그 비트맵을 다시 읽는다. ↩
pnpm 패치 —
pnpm patch/patch-commit으로 node_modules 안 라이브러리 소스에 diff를 입혀 설치 때마다 재적용하는 기능.pnpm-workspace.yaml의patchedDependencies에 등록돼 있다. ↩