2주차_ "나만의 MVP 앱 구현해 보기"

# [Claude Code] Galaxy Watch 5에 러닝 코치 앱 빌드·배포까지 완성한 하루

---

## 소개

### 시도하고자 했던 것과 그 이유

저는 매주 일요일 러닝을 시작하고, 1년 뒤 하프마라톤(21.0975km) 완주를 목표로 하고 있습니다.

러닝할 때 Galaxy Watch 5에서 실시간 심박수·거리·페이스를 확인하고, 구간마다 알람(거리 도달 / 심박수 초과 / 페이스 저하 / 스피드업 존)을 받을 수 있는 앱이 필요했습니다. 기성 앱은 너무 복잡하거나 커스터마이징이 안 되어서 **Claude Code로 직접 Wear OS 앱을 만들어보기로** 했습니다.

어제(3/21) 기획·코드 뼈대를 완성한 뒤, 오늘은 **Android Studio에서 빌드 가능한 상태로 만들고 실기기에 APK 배포까지** 완료하는 것이 목표였습니다.

---

## 진행 방법

### 사용 도구

- **Claude Code** — 코드 분석, 오류 원인 파악, 수정, 문서 작성
- **Android Studio Meerkat** — Gradle Sync 및 빌드 실행
- **Galaxy Watch 5 (SM-R935N)** — Wi-Fi ADB로 실기기 배포 및 동작 확인

### 프롬프트 예시

> "에러가 나왔어 원인분석하고 조치해줘" + 빌드 에러 스크린샷

Claude Code에게 에러 화면을 첨부하면 원인을 분석하고 수정 코드까지 바로 제시해줍니다. 저는 이 방식으로 빌드 에러를 하나씩 해결했습니다.

---

### Step 1. Gradle Sync

어제 작성한 프로젝트 구조로 첫 Gradle Sync 실행.

```
BUILD SUCCESSFUL in 6m 57s
```

app / wear 두 모듈 모두 인식 확인.

---

### Step 2. AndroidManifest.xml 권한 보완

```xml
<!-- 백그라운드 심박수 수신 -->
<uses-permission android:name="android.permission.BODY_SENSORS_BACKGROUND" />
<!-- 러닝 중 화면 꺼짐 방지 -->
<uses-permission android:name="android.permission.WAKE_LOCK" />

<!-- 메시지 경로 통합 (/goal, /records 등 분산 → / 하나로) -->
<data android:scheme="wear" android:host="*" android:pathPrefix="/" />
```

---

### Step 3. RunningRepository — Service ↔ ViewModel 브릿지 패턴

Android에서 Service는 Hilt ViewModel에 직접 주입할 수 없습니다.
`@Singleton Repository`를 중간 브릿지로 만들어 해결했습니다.

```
[WearRunningService] → repository.updateSensorData()
                              ↓
[RunningViewModel]  ← repository.sensorData.collect()
[PostRunViewModel]  ← repository.completedResult.collect()
```

```kotlin
@Singleton
class RunningRepository @Inject constructor() {
    private val _sensorData = MutableStateFlow(SensorData())
    val sensorData: StateFlow<SensorData> = _sensorData.asStateFlow()

    private val _completedResult = MutableStateFlow<RunResult?>(null)
    val completedResult: StateFlow<RunResult?> = _completedResult.asStateFlow()

    fun updateSensorData(data: SensorData) { _sensorData.value = data }
    fun updateResult(result: RunResult)    { _completedResult.value = result }
    fun reset() { _sensorData.value = SensorData(); _completedResult.value = null }
}
```

---

### Step 4. Wear Compose BOM → 명시적 버전으로 변경

Wear Compose는 일반 Compose와 달리 BOM을 제공하지 않습니다.

```kotlin
// 오류 (BOM 미존재)
implementation(platform(libs.androidx.wear.compose.bom))

// 수정
implementation("androidx.wear.compose:compose-material:1.3.1")
implementation("androidx.wear.compose:compose-foundation:1.3.1")
implementation("androidx.wear.compose:compose-navigation:1.3.1")
```

---

### Step 5. gradle.properties 신규 생성

```properties
android.useAndroidX=true
android.enableJetifier=true
org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m
org.gradle.parallel=true
org.gradle.caching=true
```

이 파일이 없으면 `OutOfMemoryError: Java heap space` 및 AndroidX 의존성 오류가 발생합니다.

---

### Step 6. Health Services API 버전 불일치 수정 (15 errors → 0)

`health-services-client:1.1.0-alpha03`에서 인터페이스 이름이 변경되어 15개 에러가 발생했습니다.

```kotlin
// Before — alpha02 이하 API (오류)
import androidx.health.services.client.ExerciseUpdateListener

private val exerciseListener = object : ExerciseUpdateListener {
    override fun onExerciseUpdateReceived(update: ExerciseUpdate) { ... }
    override fun onLapSummaryReceived(lapSummary: ExerciseLapSummary) {}
    override fun onAvailabilityChanged(dataType: DataType<*, *>, availability: Availability) {}
}
exerciseClient.setUpdateListenerAsync(MoreExecutors.directExecutor(), exerciseListener).await()

// 칼로리 (오류 — AggregateDataType에는 lastOrNull() 없음)
val calorie = metrics.getData(DataType.CALORIES_TOTAL).lastOrNull()?.total?.toInt() ?: 0
```

```kotlin
// After — alpha03+ 정상 API
import androidx.health.services.client.ExerciseUpdateCallback

private val exerciseCallback = object : ExerciseUpdateCallback {
    override fun onRegistered() {}
    override fun onRegistrationFailed(throwable: Throwable) {}
    override fun onExerciseUpdateReceived(update: ExerciseUpdate) { ... }
    override fun onLapSummaryReceived(lapSummary: ExerciseLapSummary) {}
    override fun onAvailabilityChanged(dataType: DataType<*, *>, availability: Availability) {}
}
exerciseClient.setUpdateCallback(exerciseCallback)

// 칼로리 수정 (단일값 직접 접근)
val calorie = metrics.getData(DataType.CALORIES_TOTAL)?.total?.toInt() ?: 0
```

---

### Step 7. Galaxy Watch Wi-Fi ADB 연결 및 APK 배포

```bash
# 페어링 (최초 1회)
adb pair 192.168.219.108:36911 874867

# 연결
adb connect 192.168.219.108:34371

# 확인
adb devices
```

Android Studio 상단 드롭다운에서 **samsung SM-R935N** 선택 후 ▶ Run

> **[이미지 삽입] Galaxy Watch 5 앱 실행 화면 — 홈 대시보드**

> **[이미지 삽입] Galaxy Watch 5 앱 실행 화면 — 러닝 중 화면**

> **[이미지 삽입] Galaxy Watch 5 앱 실행 화면 — 결과 화면**

---

## 결과와 배운 점

### 배운 점

**1. Wear Compose BOM은 존재하지 않는다**
일반 Compose BOM처럼 `platform(...)` 방식으로 묶으려 하면 빌드 오류 발생. 각 라이브러리에 명시적 버전 지정 필요.

**2. Service는 Hilt ViewModel에 직접 주입 불가**
`@Singleton Repository`를 중간 브릿지로 사용하는 패턴이 정석.

**3. gradle.properties는 수동 프로젝트 설정 시 필수**
자동 생성 시에는 포함되지만, 수동 구성할 때 빠뜨리면 AndroidX 오류 + OOM 발생.

**4. Health Services alpha 버전마다 API가 크게 바뀐다**
`ExerciseUpdateListener` → `ExerciseUpdateCallback`, `setUpdateListenerAsync` → `setUpdateCallback`. alpha 버전 업그레이드 시 반드시 릴리즈 노트 확인 필요.

**5. AggregateDataType vs DeltaDataType 구분**
- `DeltaDataType` (HEART_RATE_BPM, DISTANCE, SPEED): 리스트 → `.lastOrNull()?.value`
- `AggregateDataType` (CALORIES_TOTAL): 단일 누적값 → `?.total`

**6. Wear OS Wi-Fi ADB는 페어링과 연결이 분리되어 있다**
`adb pair`(페어링, 최초 1회)와 `adb connect`(실제 연결)는 포트 번호도 다름.

---

### 시행착오

| 문제 | 원인 | 해결 |
|------|------|------|
| Wear Compose BOM not found | BOM 버전 미존재 | 명시적 버전 1.3.1 지정 |
| OutOfMemoryError | JVM 힙 부족 | gradle.properties -Xmx4096m |
| AndroidX 오류 | gradle.properties 누락 | 파일 신규 생성 |
| Service Hilt 주입 오류 | Service는 Hilt 의존성 불가 | RunningRepository 브릿지 패턴 |
| pacePer km 오타 | 변수명 중간 공백 | pacePerKm으로 수정 |
| ExerciseUpdateListener 미존재 (15 errors) | alpha03에서 API 이름 변경 | ExerciseUpdateCallback으로 교체 |
| lastOrNull() 컴파일 오류 | CALORIES_TOTAL은 단일값 반환 | ?.total 직접 접근 |
| setUpdateListenerAsync 미존재 | alpha03에서 메서드 변경 | setUpdateCallback으로 교체 |

---

### 앞으로의 계획

1. **Samsung Health SDK 연동** — 별도 .aar 다운로드 후 app/libs 추가
2. **폰 ↔ 워치 데이터 동기화 테스트** — 러닝 완료 데이터 Wearable Data Layer 전송 확인
3. **온보딩 화면** — 첫 실행 시 초기 목표 설정 UI
4. **실제 러닝 테스트** — 야외 GPS 환경에서 심박수·거리·페이스 정확도 확인

---

## 도움 받은 글 (옵션)

- [Android Health Services 공식 문서](https://developer.android.com/health-and-fitness/guides/health-services)
- [Wear Compose 공식 샘플 — health-services-walkthrough](https://github.com/android/health-samples)
- [Wearable Data Layer API 가이드](https://developer.android.com/training/wearables/data/messages)
2
3개의 답글

뉴스레터 무료 구독

👉 이 게시글도 읽어보세요