Today's

길을 나서지 않으면 그 길에서 만날 수 있는 사람을 만날 수 없다

모바일 앱(안드로이드)

🧭 Android | 위치 수신·히스토리 저장 및 Vico 그래프 NaN 크래시 대응

Billcorea 2025. 12. 7. 15:32
반응형

<!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)

접근한 순서:

  1. 원인 분석 — 로그와 스택트레이스에서 NaN이 roundToInt로 전달된 것을 확인.
  2. 데이터 포맷/시리즈 구성 점검 — Vico 레이어가 기대하는 시리즈 수와 순서를 맞추도록 수정 방향 결정.
  3. 안전성 확보 — NaN이 마커 계산으로 전달되지 않도록 시리즈를 일관된 길이로 만들거나, 마커를 비활성화/무시하도록 보호 코드 추가.
  4. 걸음수 표시 문제 해결 — 시간 축 기준으로 시간별 합계를 구하고 그래프 범위/스케일을 조정하여 눈에 띄게 함.
  5. 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) 가이드
반응형