Android Health Connect로 “오늘 칼로리” 연동 튜토리얼 (React Native Bare)

박정빈·2026년 4월 21일

React Native 사용기

목록 보기
38/40

이 문서는 React Native(bare) 앱에서 Android Health Connect를 통해 “오늘 소모 칼로리(kcal)” 를 가져와 화면에 표시하는 방법을 정리한 튜토리얼입니다.

핵심 요약

  • 칼로리는 readRecords()만 쓰면 0이 나올 수 있어(UI에는 값이 있는데 records가 비는 케이스) aggregate()기본 경로로 사용합니다.
  • iOS(HealthKit)의 activeEnergyBurned와 의미를 맞추기 위해 Android도 Active 칼로리 우선으로 집계합니다.

목표

  • Health Connect에서 오늘(0시~현재) 의 칼로리를 읽어와 RN 화면에 노출
  • 권한이 없으면 권한을 요청하고, 승인 후 재조회

1) 의존성 추가 (Android)

gg24/android/app/build.gradle에 Health Connect client를 추가합니다.

dependencies {
  implementation("androidx.health.connect:connect-client:1.2.0-alpha02")
}

2) AndroidManifest 설정 (권한 + queries)

gg24/android/app/src/main/AndroidManifest.xml에 다음 권한을 선언합니다.

<uses-permission android:name="android.permission.health.READ_ACTIVE_CALORIES_BURNED" />
<uses-permission android:name="android.permission.health.READ_TOTAL_CALORIES_BURNED" />

그리고 Android 11+에서 Health Connect 앱 패키지 가시성을 위해 <queries>도 추가합니다.

<queries>
  <package android:name="com.google.android.apps.healthdata" />
  <intent>
    <action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
  </intent>
</queries>

참고: Health Connect 권한 rationale/usage 화면 intent-filter는 프로젝트의 MainActivity 설정을 따르세요.


3) 권한 요청 (HealthPermission)

Health Connect 권한은 레코드 타입 기반으로 요청합니다.

  • ActiveCaloriesBurnedRecord (활동 칼로리)
  • TotalCaloriesBurnedRecord (활동 + BMR 포함)

예시:

val permissions = setOf(
  HealthPermission.getReadPermission(ActiveCaloriesBurnedRecord::class),
  HealthPermission.getReadPermission(TotalCaloriesBurnedRecord::class),
)

이 프로젝트에서는 권한 요청을 MainActivity.requestHealthConnectPermissions(...) 헬퍼로 처리하고, 네이티브 모듈(HealthConnectModule)에서 이를 호출합니다.


4) “오늘 칼로리” 읽기 구현 (중요: aggregate 사용)

aggregate()가 필요한가?

Health Connect 앱 UI에는 “오늘 칼로리”가 보이는데도, readRecords() 결과는 records.size == 0인 경우가 있습니다.
이럴 때 readRecords()만 쓰면 앱은 0 kcal로 표시됩니다.

따라서 “오늘 칼로리”는 아래 순서로 계산합니다.

  1. aggregate()로 Active 칼로리 집계
  2. Active가 0이면 aggregate()로 Total 칼로리 집계
  3. (선택) 그래도 0이면 readRecords()를 폴백으로 사용

Kotlin 예시 코드

AggregateRequest의 import는 아래를 사용하세요.
import androidx.health.connect.client.request.AggregateRequest

import androidx.health.connect.client.HealthConnectClient
import androidx.health.connect.client.permission.HealthPermission
import androidx.health.connect.client.records.ActiveCaloriesBurnedRecord
import androidx.health.connect.client.records.TotalCaloriesBurnedRecord
import androidx.health.connect.client.request.AggregateRequest
import androidx.health.connect.client.request.ReadRecordsRequest
import androidx.health.connect.client.time.TimeRangeFilter
import java.time.ZoneId
import java.time.ZonedDateTime

suspend fun getTodayCaloriesKcal(client: HealthConnectClient): Double {
  val zoneId = ZoneId.systemDefault()
  val end = ZonedDateTime.now(zoneId)
  val start = end.toLocalDate().atStartOfDay(zoneId)
  val range = TimeRangeFilter.between(start.toInstant(), end.toInstant())

  val activeAgg = client.aggregate(
    AggregateRequest(
      metrics = setOf(ActiveCaloriesBurnedRecord.ACTIVE_CALORIES_TOTAL),
      timeRangeFilter = range
    )
  )
  val activeKcal =
    activeAgg[ActiveCaloriesBurnedRecord.ACTIVE_CALORIES_TOTAL]?.inKilocalories ?: 0.0

  val totalAgg = client.aggregate(
    AggregateRequest(
      metrics = setOf(TotalCaloriesBurnedRecord.ENERGY_TOTAL),
      timeRangeFilter = range
    )
  )
  val totalKcal =
    totalAgg[TotalCaloriesBurnedRecord.ENERGY_TOTAL]?.inKilocalories ?: 0.0

  if (activeKcal > 0) return activeKcal
  if (totalKcal > 0) return totalKcal

  // (선택) 폴백: readRecords로 합산
  val activeRecords = client.readRecords(
    ReadRecordsRequest(
      recordType = ActiveCaloriesBurnedRecord::class,
      timeRangeFilter = range
    )
  ).records
  val activeSum = activeRecords.sumOf { it.energy.inKilocalories }
  if (activeSum > 0) return activeSum

  val totalRecords = client.readRecords(
    ReadRecordsRequest(
      recordType = TotalCaloriesBurnedRecord::class,
      timeRangeFilter = range
    )
  ).records
  return totalRecords.sumOf { it.energy.inKilocalories }
}

5) React Native로 브릿지 연결

이 프로젝트는 RN ↔ 네이티브 통신을 NativeModules로 연결합니다.

  • TypeScript 브릿지: <root>/src/shared/natives/Health.ts
  • 조회 훅: <root>/src/shared/hooks/useTodayCalories.ts
  • 화면: <root>/src/features/home/screens/HomeScreen.tsx

앱 코드에서는 보통 훅으로 감싸서 사용합니다.

const { calories, isLoading, errorMessage } = useTodayCalories();

6) 디버깅 팁

  • 네이티브 코드(Manifest/Kotlin)를 변경했다면 Android 앱 리빌드/재설치가 필요합니다. (Fast Refresh로 반영 안 됨)
  • 권한 추가/변경 후에는 앱 데이터 삭제 또는 재설치 후 다시 권한을 받아야 새 권한이 적용될 때가 많습니다.

부록: Active vs Total 차이

  • ActiveCaloriesBurnedRecord: 활동으로 인한 소모(보통 BMR 제외) → iOS activeEnergyBurned와 의미가 유사
  • TotalCaloriesBurnedRecord: 활동 + BMR 포함한 총 소모

이 프로젝트에서는 플랫폼 지표 의미를 맞추기 위해 Active 우선으로 노출하고, 데이터가 없을 때 Total로 폴백합니다.

0개의 댓글