반응형
<!doctype html>
🧭 Android | 위치 수신·히스토리 저장 및 Vico 그래프 NaN 크래시 대응

개요 (Intro)
- 오늘의 목표 / 배경: 폰앱에서 위치(및 걸음수)를 영구 저장(히스토리)하고, Setting 화면에는 마지막 정보만 노출, 메인 화면의 Vico 그래프에 시간축으로 X/Y/Z와 걸음수 시리즈를 표기(색상 구분) — 동시에 발생한 런타임 크래시(NaN 관련)를 해결한다.
- 해결하려던 문제: Vico 라인 차트에서 마커/툴팁 계산 중 NaN 값이 투입되어 앱이 크래시 나는 문제. 또한 걸음수가 그래프에 보이지 않는 문제와 설정 화면의 측정 상태가 앱 재진입 시 유지되지 않는 문제를 정리.
- 사용한 기술 스택: Kotlin, Jetpack Compose, Vico chart, Health Connect API, Coroutines, Android Studio
📅 날짜: 2025.12.07
🎯 목표: 위치 히스토리 영구 저장, Setting 화면 최근값 표시, Vico 그래프에 X/Y/Z/Steps 표시(시간 축), NaN 크래시 방지
🧰 기술: Kotlin, Compose, Vico, Coroutines
문제 정의 (Problem / Motivation)
작업 중에 다음과 같은 런타임 예외가 발생했습니다:
java.lang.IllegalArgumentException: Cannot round NaN value.
at kotlin.math.MathKt__MathJVMKt.roundToInt(MathJVM.kt:1192)
at com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianLayer.updateMarkerTargets(LineCartesianLayer.kt:471)
... (생략)
원인으로 의심되는 상황:
- 시간별 배열을 FloatArray(24)로 만들고 기본값을 Float.NaN으로 두었음 — 일부 시간대에 값이 없으면 NaN이 남아 있음.
- Vico 차트에 여러 시리즈(예: X, Y, Z, Steps)를 정의하는 레이어는 고정된 시리즈 수를 기대하거나, 마커 계산 시 시리즈 내부 포인트가 없는 경우를 안전하게 처리하지 못함.
- lineSeries 빌더에서 조건부로 시리즈를 추가하면 레이어 쪽 series 컬러 매핑과 시리즈 개수가 맞지 않아 인덱스 문제/NaN 전파가 발생할 수 있음.
// 문제를 일으킨 코드 개요 (발생 사례 일부)
val locXHourly = FloatArray(24) { Float.NaN }
// ... 최근 위치를 시간별로 채움
// 일부 시간대는 NaN으로 남아 있을 수 있음
// 이후 Vico에 시리즈를 만들 때 NaN이 남아 있는 값 때문에 마커 계산이 NaN을 전달받아 roundToInt 에러 발생
해결 과정 (How I Solved It)
접근한 순서:
- 원인 분석 — 로그와 스택트레이스에서 NaN이 roundToInt로 전달된 것을 확인.
- 데이터 포맷/시리즈 구성 점검 — Vico 레이어가 기대하는 시리즈 수와 순서를 맞추도록 수정 방향 결정.
- 안전성 확보 — NaN이 마커 계산으로 전달되지 않도록 시리즈를 일관된 길이로 만들거나, 마커를 비활성화/무시하도록 보호 코드 추가.
- 걸음수 표시 문제 해결 — 시간 축 기준으로 시간별 합계를 구하고 그래프 범위/스케일을 조정하여 눈에 띄게 함.
- Setting 화면 상태 영속화는 DataStore/SharedPreferences로 저장하도록 계획(이번 변경에서는 로그와 그래프 중심 수정에 초점).
핵심 수정 아이디어(예시 코드)
아래 코드는 Vico 시리즈를 생성할 때 안전하게 NaN을 처리하고, 항상 레이어에 정의된 시리즈 개수를 맞추기 위한 예시입니다. 초보자도 이해하기 쉽도록 주석을 상세히 달았습니다.
// 안전한 시리즈 생성 예시 (MainScreen.kt의 modelProducer 구성부에서 사용)
// 1) 공통 X축: hours (0..23)
val hours = (0..23).toList()
// 2) 시간별 값들을 미리 계산 (값이 없으면 null로 둠)
val xValuesNullable: List<Float?> = hours.map { h -> /* 값이 있으면 실수, 없으면 null */ null }
// 실제 코드에서는 recentLocations를 순회해서 채움
// 3) Vico에 넣을 때는 레이어가 기대하는 시리즈 개수(예: 4)에 맞춰 항상 series를 추가
// 값이 없는 포인트는 0f로 대체하거나, 이전 값으로 보간하거나, 마커를 끄는 방식 중 선택
val xValuesForChart = xValuesNullable.map { it ?: 0f } // 비어있을 때 0으로 대체 (안전한 방법)
val yValuesForChart = /* 동일 처리 */ hours.map { 0f }
val zValuesForChart = /* 동일 처리 */ hours.map { 0f }
val stepsValuesForChart = /* 시간별 steps, 스케일 조정 */ hours.map { h -> (stepsHourly[h] / 10f) }
// 4) lineSeries에 항상 4개의 시리즈를 추가 (레이어와 일치)
lineSeries {
series(hours, xValuesForChart)
series(hours, yValuesForChart)
series(hours, zValuesForChart)
series(hours, stepsValuesForChart)
}
// 주석: 0으로 대체하면 그래프 수치 왜곡 가능성이 있으므로, 시각적으로는
// 값이 없음을 별도로 표시(예: 투명도 낮게 그리거나 범례에 설명 추가)하는 것이 좋습니다.
또한 마커 계산 시점에 안전 장치를 추가합니다:
// 마커 사용 시 안전 체크 예시
val marker = rememberMarker()
// modelProducer에 데이터가 아예 없을 땐 marker를 null처럼 취급하거나
// marker가 포인트 좌표를 계산할 때 NaN을 만나면 아무것도 그리지 않도록 구현
// (이 부분은 vico의 marker 콜백 구현 시점에 방어 코드 추가 필요)
결과 (Result)
✅ NaN으로 인한 라인 차트 마커 크래시의 원인을 파악했습니다. (NaN이 roundToInt로 전달되어 예외 발생)
적용한 전략의 장점:
- 시리즈 개수와 레이어 정의를 일치시켜 인덱스 불일치 문제를 제거했습니다.
- 빈 시간대 처리(0 또는 보간)는 크래시를 방지합니다. 시각적 정확성은 추가 보완 필요(예: 투명도/점선 처리).
- 걸음수는 시간별로 합산해 별도 시리즈로 추가했으며, 그래프 상에서 보이도록 스케일을 조정했습니다(예: /10 표기).
느낀 점 / 회고 (Reflection)
- 데이터 시각화 라이브러리는 데이터 포맷에 민감합니다. 특히 여러 시리즈를 그릴 때 길이와 인덱스 정합성을 반드시 맞춰야 합니다.
- 디버깅 시 스택트레이스가 가리키는 함수 내부에서 어떤 입력값이 NaN/무효값인지 역추적하는 것이 중요했습니다. 로그와 작은 재현 데이터셋을 만들어 원인을 좁혔습니다.
- 완벽한 해결을 위해선 다음 작업이 필요합니다: (1) SettingScreen의 측정 시작 상태 영속화(DataStore), (2) 빈값 처리 시 시각적 표시 개선(투명도/점선/툴팁에서 ‘데이터 없음’ 표기), (3) 마커가 NaN을 만나면 안전하게 무시하도록 vico 콜백 방어 코드 강화.
참고자료 (References)
- Vico 공식 문서 및 예제 (GitHub) - https://github.com/patrykandpatrick/vico
- Kotlin stdlib - Float.isNaN(), roundToInt() 동작 참고
- Android Developers - Data persistence (DataStore) 가이드
반응형
'모바일 앱(안드로이드)' 카테고리의 다른 글
| 🦾 Android | 워치앱 빌드 오류 수정과 UI/국제화 개선 정리 (1) | 2025.12.11 |
|---|---|
| 🐾 Android | Kalman vs EMA — 고도/센서 데이터 필터링 비교와 적용기 (2) | 2025.12.09 |
| ⌚ Wear OS | 센서 수명주기·권한·동기화 연결로 기본 데이터 파이프 완성 (2) | 2025.12.05 |
| 🛰️ Android | 워치-폰 동기화 + 위치(x,y,z)+고도 융합(칼만/EMA) 기능 통합 작업 기록 (0) | 2025.12.03 |
| 🛠 Wear + Phone Altitude 동기화 & 설정화면 컴파일 오류 해결 요약 (1) | 2025.12.01 |