Skip to content

MSG-582 fix: 네이버 지도 RN 라이브러리 커스텀 뷰 마커 비트맵 recycle 크래시 패치 — 지도 조작 중 앱 강제 종료 해소 - #151

Merged
s13121312 merged 2 commits into
developfrom
fix/MSG-582-naver-marker-bitmap-recycle
Sep 7, 2026
Merged

Conversation

@s13121312

@s13121312 s13121312 commented Sep 7, 2026

Copy link
Copy Markdown
Member

🎫 관련 티켓

📌 작업 내용

지도 홈에서 조작하다 보면 붉은 에러 화면 없이 앱이 통째로 사라지는 문제입니다(사용자 QA "갑자기 앱이 꺼지는데"). 앱 코드가 아니라 네이버 지도 RN 래퍼 라이브러리(@mj-studio/react-native-naver-map 2.9.0)의 안드로이드 네이티브 버그입니다.

크래시 로그1는 이렇습니다.

W/Bitmap: Called getDensity() on a recycle()'d bitmap!
F/kr.fillmap.app: JNI DETECTED ERROR ... 'bitmap decoding: could not lock pixels'
   from com.naver.maps.map.renderer.MapRenderer.nativeRender()   (GLThread, SIGABRT)

원인은 라이브러리 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: 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 훅 통과)
  • 수용 기준 검증 완료 (검증 리포트 요약을 아래에 첨부)
  • 필요한 경우 문서(README, docs/) 업데이트

🔍 검증 요약

게이트 결과
패치 반영 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 SUCCESSFUL
diff 범위 patches 1 · lock 1 · docs 4. 앱 코드·web·packages 0
codex 리뷰(브랜치 diff) 지적 0건: "premature bitmap recycling 제거, 마커 정리 경로 보존, lock 해시 일치"
# 기준 판정 근거
L1 패치 파일에 recycle 제거 diff, 설치 소스에 반영 통과 grep
G1 Kotlin 재컴파일 후 빌드 성공 통과 빌드 로그
S1 패치 APK 콜드 스타트 → Metro 연결, 정상 기동 통과 실기(Running "main" 로그, 로그인 화면)
S2 줌 변경·코스 진입/이탈 반복에도 앱 유지, logcat -b crash 무출력 진행 중 확률적 재현이라 사용자 실기로 계속 관찰

📸 스크린샷 (선택)

없음 — 크래시 부재는 화면으로 증명되지 않습니다. 재현 시그니처와 확인 명령은 런북 함정 12에 있습니다.

💡 추가 논의할 사항

  • 이 패치 파일은 MSG-445의 onLoad(topLoaded) 패치와 같은 파일입니다. 라이브러리 버전을 올릴 때 두 hunk 모두 다시 얹어야 합니다.
  • 업스트림(mj-studio/react-native-naver-map) 이슈 제보는 후속입니다. 상위 버전이 이 경로를 고쳤는지는 확인하지 않았습니다.

https://claude.ai/code/session_012MtDBFuKP9N4wqmsuoXE7P

Footnotes

  1. adb logcat -d -b crash — 안드로이드가 네이티브 크래시만 따로 모아 두는 로그 버퍼.

  2. Bitmap.recycle() — 비트맵의 픽셀 메모리를 즉시 해제하는 호출. 해제 뒤 그 비트맵을 그리려 하면 예외 또는 네이티브 크래시가 난다.

  3. GL 렌더 스레드 — 네이버 지도 SDK가 OpenGL로 지도를 그리는 별도 스레드. UI 스레드에서 비트맵을 회수해도 이 스레드는 다음 프레임에서 그 비트맵을 다시 읽는다.

  4. pnpm 패치 — pnpm patch / patch-commit으로 node_modules 안 라이브러리 소스에 diff를 입혀 설치 때마다 재적용하는 기능. pnpm-workspace.yamlpatchedDependencies에 등록돼 있다.

…강제 종료 해소

- `@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

claude Bot commented Sep 7, 2026

Copy link
Copy Markdown

Claude finished @s13121312's task in 3m 14s —— View job


🔍 코드 리뷰 완료

작업 목록

  • 컨텍스트 수집 (diff, 변경 파일 확인)
  • patches/@mj-studio__react-native-naver-map@2.9.0.patch 변경 검토
  • pnpm-lock.yaml 해시 정합성 확인
  • docs/MOBILE_RUNBOOK.md, docs/STATUS.md, docs/decisions/DECISIONS.md, docs/spec/MSG-582.md 검토
  • 인라인 코멘트 작성 (2건)
  • 최종 요약 코멘트 게시

결과 요약

원인 진단·수정 범위(패치 1줄)·문서화가 모두 꼼꼼하고 일관됩니다. 🔴 반드시 수정할 이슈는 없습니다.

  • 🟡 recycle() 제거로 크래시 레이스는 해소되지만, 줌 반복처럼 커스텀 뷰 마커가 빈번히 교체되는 상황에서 비트맵 회수가 GC 타이밍까지 지연돼 메모리 사용량이 늘어날 수 있는 트레이드오프가 스펙 리스크 절에 빠져 있음 → 인라인 코멘트
  • 🟢 docs/spec/MSG-582.md "3. 리스크"에 위 메모리 트레이드오프 한 줄 추가 제안 → 인라인 코멘트

전체 요약은 별도 코멘트에 게시했습니다 (마커: claude-review:summary bad8db5db9d825197f755f25ab10f5ac40f56f83).
· 브랜치

- if (customViewBitmap != null && !customViewBitmap!!.isRecycled) customViewBitmap!!.recycle()
+ // FillMap 패치: overlay.icon이 아직 이 비트맵을 참조하는 상태에서 recycle하면 GL 렌더 스레드가
+ // "could not lock pixels"로 네이티브 abort(앱 종료)한다. GC에 맡긴다 — 2026-09-07 실측.
+ customViewBitmap = null

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 권장 — 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이 커스텀 뷰 스냅샷용 소프트웨어 비트맵이라는 전제 하의 지적이며, 하드웨어 비트맵이라면 해당 없음)

Copy link
Copy Markdown
Member Author

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. 리스크"에 트레이드오프 + 실측치를 추가했습니다.

Comment thread docs/spec/MSG-582.md
| S1 | 패치 APK 콜드 스타트 후 앱이 Metro에 붙어 정상 기동 | 화면 | 실기 |
| S2 | 줌 인/아웃 반복(클러스터 교체)·코스 진입/이탈에도 앱이 종료되지 않고 `adb logcat -b crash` 무출력 | 화면 | 실기 |

## 3. 리스크

Copy link
Copy Markdown

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이 보인다"는 경우도 바로 이 티켓과 연결 지을 수 있습니다.

Copy link
Copy Markdown
Member Author

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. 리스크"에 트레이드오프 + 실측치를 추가했습니다.

@claude

claude Bot commented Sep 7, 2026

Copy link
Copy Markdown

🔍 코드 리뷰 요약 — MSG-582 네이버 지도 마커 비트맵 recycle 크래시 패치

전반 평가: 원인 진단(크래시 로그 → GL 렌더 스레드가 recycle된 비트맵을 잠그려는 레이스)이 명확하고, 수정 자체는 라이브러리 패치 1줄(recycle() 제거 → customViewBitmap = null)로 최소 범위입니다. pnpm-lock.yaml 해시 3곳(patchedDependencies·importers·snapshots)이 모두 일치하게 갱신됐고, 런북·STATUS·DECISIONS·스펙 문서가 서로 모순 없이 같은 내용을 기록하고 있습니다. 앱 코드(apps/mobile/src)·웹·packages 변경 0줄이라는 설명도 diff와 일치합니다.

잘한 점

  • 크래시 시그니처(W/Bitmap: recycle()'d bitmapSIGABRT ... could not lock pixels ... nativeRender())를 근거로 원인을 정확히 특정하고, 그 근거를 스펙·런북에 그대로 남겨 재현 시 바로 참조 가능하게 함
  • isRecycled 가드가 있던 기존 recycle() 호출을 제거하면서 참조만 null로 끊는 방식이 안전 — 이후 다른 경로에서 customViewBitmap != null 체크가 있다면 이전엔 "recycle된 비트맵을 non-null로" 오인할 여지가 있었는데, 이번 변경으로 오히려 더 견고해짐
  • Kotlin 변경이므로 dev client 재빌드가 필요하다는 점을 PR 설명·런북 함정 12에 명시해 "패치 전 APK로 재현 안 됨" 오판을 방지
  • 같은 패치 파일에 이미 있던 MSG-445 onLoad 패치와의 병합 이력, 향후 라이브러리 업그레이드 시 두 hunk 모두 재작업해야 한다는 점을 "추가 논의할 사항"에 남겨둔 것도 좋음

이슈 (인라인 코멘트 참고)

  • 🟡 patches/@mj-studio__react-native-naver-map@2.9.0.patchrecycle()을 없애고 GC에 전적으로 맡기는 선택이 크래시 레이스는 해소하지만, 줌 반복처럼 커스텀 뷰 마커(클러스터·경로 번호·미션 이름표)가 빈번히 교체되는 상황에서 비트맵 회수가 GC 타이밍까지 지연돼 메모리 사용량이 늘어날 수 있습니다. docs/spec/MSG-582.md의 리스크 절이 확률적 재현 이슈만 다루고 이 트레이드오프는 다루지 않아, 실기로 지속 줌 반복 시 메모리(dumpsys meminfo)를 한 번 확인해두길 권합니다. (일부 추측 포함 — customViewBitmap이 소프트웨어 비트맵이라는 전제)
  • 🟢 위와 짝을 이루는 리스크 한 줄을 docs/spec/MSG-582.md "3. 리스크"에 추가해두면 향후 함정 12를 보는 사람이 "크래시는 없는데 느려짐/OOM"인 경우도 이 티켓과 바로 연결할 수 있습니다

검증 확인 사항

  • S2(줌·코스 진입/이탈 반복 시 크래시 없음)가 "확률적 재현이라 진행 중"으로 명시돼 있는 점은 정직한 표기로 판단됩니다. 다만 메모리 관점의 후속 관찰도 같이 챙기면 좋겠습니다.

전체적으로 원인 분석과 diff 범위가 타당하고 문서화가 꼼꼼합니다. 위 메모리 트레이드오프는 머지를 막을 정도는 아니라고 보며(🔴 없음), 참고용 권장 사항입니다.

… + 줌 10회 메모리 실측 추가

- 실측: Java 힙 22.0→27.9→22.5MB(3분 후 복귀), 네이티브 407→416→412MB, 크래시 0

Claude-Session: https://claude.ai/code/session_012MtDBFuKP9N4wqmsuoXE7P
@claude

claude Bot commented Sep 7, 2026

Copy link
Copy Markdown

Claude finished @s13121312's task in 1m 59s —— View job


🔍 코드 리뷰 완료 (재검토)

작업 목록

  • 컨텍스트 수집 (git diff origin/develop...HEAD, 최신 커밋 확인)
  • 최신 커밋(10f95e4 — 리뷰 반영 커밋) 변경분 검토
  • 전체 diff 재검토 (patch, lock, docs)
  • 인라인 코멘트 작성 (해당 없음 — 추가 이슈 없음)
  • 최종 요약 코멘트 게시

결과 요약

최신 커밋 10f95e4은 이전 리뷰(🟡 recycle 제거로 인한 GC 지연 메모리 트레이드오프 미문서화)를 실측 데이터(핀치 줌 10회 dumpsys meminfo — Java 힙 22.0→27.9→22.5MB, 네이티브 힙 407→416→412MB, 3분 뒤 원상 복귀)로 충실히 반영했습니다. docs/spec/MSG-582.md "3. 리스크"에 반영 확인. 나머지 diff(patch 1줄, lock 해시 3곳, 런북/STATUS/DECISIONS)도 서로 일관되고 앱 코드 0줄 원칙을 지킵니다.

🔴 반드시 수정할 이슈 없음. 새 인라인 코멘트 없음 — 이전 지적이 해소됐습니다.

전체 요약은 별도 코멘트에 게시했습니다 (마커: claude-review:summary 10f95e42e507b5d714294f8433524c2b2ec0faf6).
· 브랜치

@claude

claude Bot commented Sep 7, 2026

Copy link
Copy Markdown

🔍 코드 리뷰 요약 (재검토) — MSG-582 네이버 지도 마커 비트맵 recycle 크래시 패치

대상: 최신 커밋 10f95e4 — 이전 리뷰(PR #151 첫 리뷰, bad8db5 대상)에서 지적한 🟡 메모리 트레이드오프 미문서화 건에 대한 반영 커밋.

결론: 이전 지적 사항 해소 확인, 추가로 반드시 고칠 이슈 없음 (🔴 0건).

이전 지적 사항 반영 확인

docs/spec/MSG-582.md "3. 리스크"에 아래 내용이 새로 추가됐습니다 (docs/spec/MSG-582.md:39):

  • recycle 제거로 비트맵 회수가 GC 타이밍에 맡겨지는 트레이드오프를 명시
  • 실측 수치로 뒷받침: 에뮬레이터에서 핀치 줌 인/아웃 10회 반복 후 dumpsys meminfo — Java 힙 22.0→27.9MB, 3분 뒤 22.5MB로 복귀(GC 회수 확인) · 네이티브 힙 407→416→412MB(지도 타일 캐시 변동 범위로 설명)
  • 마커 비트맵이 ARGB_4444 수십 px(개당 수 KB)라 누적 OOM 리스크가 낮다는 근거까지 포함
  • "크래시 없이 느려짐·OOM이 보이면 이 티켓과 연결"이라는 향후 진단 가이드도 추가돼, 런북 함정 12와 짝을 이룸

주관적 우려("GC에 맡기면 메모리가 늘 수 있다")를 추측으로 남기지 않고 실측으로 검증한 점이 좋습니다. 특히 3분 뒤 Java 힙이 원상 복귀했다는 수치가 "GC가 실제로 수거한다"는 가장 중요한 주장을 직접 뒷받침합니다.

나머지 diff 재확인

  • patches/@mj-studio__react-native-naver-map@2.9.0.patch: RNCNaverMapMarker.ktremoveCustomView()에서 recycle() 호출 제거 → customViewBitmap = null. 기존 코드는 recycle 후에도 customViewBitmap이 recycle된 비트맵을 계속 참조했는데, 이번 변경으로 참조 자체가 끊겨 오히려 더 안전해졌습니다.
  • pnpm-lock.yaml: 패치 해시(b41f33e5…)가 patchedDependencies·importers·snapshots 3곳 모두 일치.
  • docs/MOBILE_RUNBOOK.md 함정 12, docs/STATUS.md, docs/decisions/DECISIONS.md: 모두 같은 근거(크래시 시그니처, D1 결정, 재빌드 필요성)를 일관되게 기록하고 있고 서로 모순 없음.
  • 앱 코드(apps/mobile/src)·웹·packages 변경 0줄이라는 설명과 diff가 일치.

참고 (추측 포함, 확정적 이슈 아님)

  • 네이티브 힙 변동(407→416→412MB)을 "지도 타일 캐시 변동 범위"로 돌린 해석은 합리적이지만, 순수 재현(마커 없는 화면에서 동일 줌 반복)과 비교한 대조군 실측은 아닙니다. 다만 크래시 원인 자체와는 무관한 부가 리스크 항목이라 머지를 막을 사안은 아니라고 판단합니다.

전체적으로 이번 커밋은 이전 리뷰의 유일한 권장 사항을 실측 데이터로 충실히 반영했습니다. 승인 가능하다고 봅니다.

@s13121312
s13121312 merged commit 9c0aae4 into develop Sep 7, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant