개요

하나의 프로젝트에서 모바일 앱Wear OS 앱을 함께 개발할 수 있습니다.

프로젝트 구조

기본 구조

MyApp/
├── mobile/              # 스마트폰 앱 모듈
│   ├── src/
│   │   └── main/
│   │       ├── kotlin/
│   │       ├── res/
│   │       └── AndroidManifest.xml
│   └── build.gradle.kts
│
├── wear/                # Wear OS 앱 모듈
│   ├── src/
│   │   └── main/
│   │       ├── kotlin/
│   │       ├── res/
│   │       └── AndroidManifest.xml
│   └── build.gradle.kts
│
├── shared/              # 공통 코드 모듈 (선택사항)
│   ├── src/
│   │   └── main/
│   │       └── kotlin/
│   └── build.gradle.kts
│
├── build.gradle.kts     # 프로젝트 레벨 빌드 파일
└── settings.gradle.kts  # 모듈 설정

프로젝트 생성 방법

방법 1: 처음부터 함께 생성

1. Android Studio 실행
2. File → New → New Project
3. Phone and Tablet 선택
4. Empty Activity 선택
5. 프로젝트 생성 완료 후
6. File → New → New Module
7. Wear OS Module 선택
8. Finish

방법 2: 기존 프로젝트에 Wear OS 추가

1. 기존 Android 프로젝트 열기
2. File → New → New Module
3. Wear OS Module 선택
4. Module name: "wear" 입력
5. Finish

설정 파일 구성

settings.gradle.kts

pluginManagement {
    repositories {
        google()
        mavenCentral()
        gradlePluginPortal()
    }
}
 
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
    }
}
 
rootProject.name = "MyApp"
include(":mobile")      // 스마트폰 앱
include(":wear")        // Wear OS 앱
include(":shared")      // 공통 코드 (선택)

build.gradle.kts (프로젝트 루트)

plugins {
    id("com.android.application") version "8.2.0" apply false
    id("com.android.library") version "8.2.0" apply false
    id("org.jetbrains.kotlin.android") version "1.9.22" apply false
}

모듈별 설정

mobile/build.gradle.kts (스마트폰 앱)

plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
}
 
android {
    namespace = "com.example.myapp"
    compileSdk = 34
 
    defaultConfig {
        applicationId = "com.example.myapp"
        minSdk = 26
        targetSdk = 34
        versionCode = 1
        versionName = "1.0"
    }
 
    buildFeatures {
        compose = true
    }
 
    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.8"
    }
}
 
dependencies {
    // 공통 모듈
    implementation(project(":shared"))
 
    // Wear OS 통신
    implementation("com.google.android.gms:play-services-wearable:18.1.0")
 
    // Jetpack Compose
    implementation("androidx.compose.ui:ui:1.6.0")
    implementation("androidx.compose.material3:material3:1.2.0")
    implementation("androidx.activity:activity-compose:1.8.2")
 
    // 기타 의존성
    implementation("androidx.core:core-ktx:1.12.0")
    implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
}

wear/build.gradle.kts (Wear OS 앱)

plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
}
 
android {
    namespace = "com.example.myapp.wear"
    compileSdk = 34
 
    defaultConfig {
        applicationId = "com.example.myapp"  // 모바일과 동일한 패키지명
        minSdk = 30  // Wear OS 3.0 이상
        targetSdk = 34
        versionCode = 1
        versionName = "1.0"
    }
 
    buildFeatures {
        compose = true
    }
 
    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.8"
    }
}
 
dependencies {
    // 공통 모듈
    implementation(project(":shared"))
 
    // Wear OS
    implementation("androidx.wear:wear:1.3.0")
    implementation("com.google.android.gms:play-services-wearable:18.1.0")
 
    // Jetpack Compose for Wear OS
    implementation("androidx.wear.compose:compose-material:1.3.0")
    implementation("androidx.wear.compose:compose-foundation:1.3.0")
    implementation("androidx.wear.compose:compose-navigation:1.3.0")
 
    // Activity
    implementation("androidx.activity:activity-compose:1.8.2")
}

shared/build.gradle.kts (공통 코드)

plugins {
    id("com.android.library")
    id("org.jetbrains.kotlin.android")
}
 
android {
    namespace = "com.example.myapp.shared"
    compileSdk = 34
 
    defaultConfig {
        minSdk = 26
    }
 
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }
 
    kotlinOptions {
        jvmTarget = "17"
    }
}
 
dependencies {
    implementation("org.jetbrains.kotlin:kotlin-stdlib:1.9.22")
 
    // Kotlinx Serialization (데이터 직렬화)
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
}

모듈별 역할

1. mobile 모듈 (스마트폰 앱)

// 일반적인 Android 앱 기능
- 복잡한 UI
- 상세한 데이터 표시
- 설정 화면
- Wear OS 앱과 데이터 동기화
- 서버 통신

예제: mobile/MainActivity.kt

package com.example.myapp
 
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.google.android.gms.wearable.*
 
class MainActivity : ComponentActivity() {
    private lateinit var dataClient: DataClient
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
 
        // Wearable Data Client 초기화
        dataClient = Wearable.getDataClient(this)
 
        setContent {
            MaterialTheme {
                MobileApp()
            }
        }
    }
 
    // Wear OS로 데이터 전송
    private fun sendDataToWear(steps: Int) {
        val putDataReq = PutDataMapRequest.create("/step_count").run {
            dataMap.putInt("steps", steps)
            dataMap.putLong("timestamp", System.currentTimeMillis())
            asPutDataRequest()
        }
        dataClient.putDataItem(putDataReq)
    }
}
 
@Composable
fun MobileApp() {
    var steps by remember { mutableStateOf(0) }
 
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {
        Text(
            text = "걸음 수: $steps",
            style = MaterialTheme.typography.headlineMedium
        )
 
        Spacer(modifier = Modifier.height(16.dp))
 
        Button(onClick = { steps++ }) {
            Text("걸음 증가")
        }
    }
}

2. wear 모듈 (Wear OS 앱)

// Wear OS 전용 기능
- 워치 최적화 UI (원형 디스플레이)
- 간단한 인터랙션
- 센서 데이터 수집 (심박수, 가속도계)
- 모바일 앱과 데이터 동기화
- 컴플리케이션

예제: wear/MainActivity.kt

package com.example.myapp.wear
 
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.wear.compose.material.*
import com.google.android.gms.wearable.*
 
class MainActivity : ComponentActivity(), DataClient.OnDataChangedListener {
    private lateinit var dataClient: DataClient
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
 
        dataClient = Wearable.getDataClient(this)
 
        setContent {
            WearApp()
        }
    }
 
    override fun onResume() {
        super.onResume()
        dataClient.addListener(this)
    }
 
    override fun onPause() {
        super.onPause()
        dataClient.removeListener(this)
    }
 
    override fun onDataChanged(dataEvents: DataEventBuffer) {
        dataEvents.forEach { event ->
            if (event.type == DataEvent.TYPE_CHANGED) {
                val path = event.dataItem.uri.path
                if (path == "/step_count") {
                    val dataMap = DataMapItem.fromDataItem(event.dataItem).dataMap
                    val steps = dataMap.getInt("steps")
                    // UI 업데이트
                }
            }
        }
    }
}
 
@Composable
fun WearApp() {
    var steps by remember { mutableStateOf(0) }
 
    MaterialTheme {
        Scaffold(
            timeText = { TimeText() }
        ) {
            Column(
                modifier = Modifier.fillMaxSize(),
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Text(
                    text = "걸음",
                    style = MaterialTheme.typography.caption1
                )
                Text(
                    text = "$steps",
                    style = MaterialTheme.typography.title1
                )
            }
        }
    }
}

3. shared 모듈 (공통 코드)

// 모바일과 Wear OS에서 공통으로 사용
- 데이터 모델 (Data Class)
- 비즈니스 로직
- 네트워크 통신 코드
- 유틸리티 함수
- 상수 정의

예제: shared/src/main/kotlin/models/StepData.kt

package com.example.myapp.shared.models
 
import kotlinx.serialization.Serializable
 
@Serializable
data class StepData(
    val count: Int,
    val timestamp: Long,
    val calories: Double = count * 0.04
)
 
@Serializable
data class HeartRateData(
    val bpm: Int,
    val timestamp: Long
)
 
@Serializable
data class WorkoutSession(
    val id: String,
    val type: WorkoutType,
    val duration: Long,
    val steps: Int,
    val heartRate: List<HeartRateData>
)
 
enum class WorkoutType {
    RUNNING,
    WALKING,
    CYCLING,
    HIKING
}

예제: shared/src/main/kotlin/utils/DateUtils.kt

package com.example.myapp.shared.utils
 
import java.text.SimpleDateFormat
import java.util.*
 
object DateUtils {
    fun formatTimestamp(timestamp: Long): String {
        val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
        return sdf.format(Date(timestamp))
    }
 
    fun isToday(timestamp: Long): Boolean {
        val today = Calendar.getInstance()
        val target = Calendar.getInstance().apply { timeInMillis = timestamp }
 
        return today.get(Calendar.YEAR) == target.get(Calendar.YEAR) &&
               today.get(Calendar.DAY_OF_YEAR) == target.get(Calendar.DAY_OF_YEAR)
    }
}

데이터 공유 방법

1. Wearable Data Layer API (권장)

의존성 추가

// mobile과 wear 모듈 모두
dependencies {
    implementation("com.google.android.gms:play-services-wearable:18.1.0")
}

데이터 전송 (Mobile → Wear)

// mobile/DataSender.kt
class DataSender(private val context: Context) {
    private val dataClient = Wearable.getDataClient(context)
 
    fun sendStepCount(steps: Int) {
        val putDataReq = PutDataMapRequest.create("/step_count").run {
            dataMap.putInt("steps", steps)
            dataMap.putLong("timestamp", System.currentTimeMillis())
            asPutDataRequest()
        }
 
        dataClient.putDataItem(putDataReq).addOnSuccessListener {
            Log.d("DataSender", "데이터 전송 성공")
        }.addOnFailureListener {
            Log.e("DataSender", "데이터 전송 실패", it)
        }
    }
 
    fun sendMessage(message: String) {
        val nodeClient = Wearable.getNodeClient(context)
 
        nodeClient.connectedNodes.addOnSuccessListener { nodes ->
            nodes.forEach { node ->
                Wearable.getMessageClient(context).sendMessage(
                    node.id,
                    "/message_path",
                    message.toByteArray()
                )
            }
        }
    }
}

데이터 수신 (Wear)

// wear/DataListenerService.kt
class DataListenerService : WearableListenerService() {
 
    override fun onDataChanged(dataEvents: DataEventBuffer) {
        dataEvents.forEach { event ->
            if (event.type == DataEvent.TYPE_CHANGED) {
                when (event.dataItem.uri.path) {
                    "/step_count" -> {
                        val dataMap = DataMapItem.fromDataItem(event.dataItem).dataMap
                        val steps = dataMap.getInt("steps")
                        val timestamp = dataMap.getLong("timestamp")
 
                        // UI 업데이트 또는 로컬 저장
                        updateStepCount(steps, timestamp)
                    }
                }
            }
        }
    }
 
    override fun onMessageReceived(messageEvent: MessageEvent) {
        when (messageEvent.path) {
            "/message_path" -> {
                val message = String(messageEvent.data)
                // 메시지 처리
            }
        }
    }
 
    private fun updateStepCount(steps: Int, timestamp: Long) {
        // LocalBroadcast 또는 ViewModel 사용하여 UI 업데이트
    }
}

AndroidManifest.xml에 서비스 등록

<!-- wear/src/main/AndroidManifest.xml -->
<manifest>
    <application>
        <service
            android:name=".DataListenerService"
            android:exported="true">
            <intent-filter>
                <action android:name="com.google.android.gms.wearable.DATA_CHANGED" />
                <action android:name="com.google.android.gms.wearable.MESSAGE_RECEIVED" />
                <data android:scheme="wear" android:host="*" />
            </intent-filter>
        </service>
    </application>
</manifest>

2. 공통 모듈 사용

shared 모듈의 코드를 mobile과 wear에서 사용

// mobile/build.gradle.kts
dependencies {
    implementation(project(":shared"))
}
 
// wear/build.gradle.kts
dependencies {
    implementation(project(":shared"))
}

사용 예제

// mobile/ViewModel.kt
import com.example.myapp.shared.models.StepData
 
class StepViewModel : ViewModel() {
    private val _stepData = MutableStateFlow(StepData(0, System.currentTimeMillis()))
    val stepData: StateFlow<StepData> = _stepData.asStateFlow()
 
    fun updateSteps(steps: Int) {
        _stepData.value = StepData(steps, System.currentTimeMillis())
    }
}
// wear/ViewModel.kt
import com.example.myapp.shared.models.StepData
 
class WearStepViewModel : ViewModel() {
    private val _stepData = MutableStateFlow(StepData(0, System.currentTimeMillis()))
    val stepData: StateFlow<StepData> = _stepData.asStateFlow()
}

실행 및 디버깅

각 모듈 개별 실행

1. Run Configuration 생성

1. Run → Edit Configurations
2. + 버튼 클릭 → Android App
3. Name: "Mobile App" 입력
4. Module: "MyApp.mobile.main" 선택
5. Apply → OK

같은 방법으로 Wear OS도 생성:
1. + 버튼 클릭 → Android App
2. Name: "Wear App"
3. Module: "MyApp.wear.main" 선택

2. 실행

- Mobile App 실행: 스마트폰 에뮬레이터 또는 실제 기기 선택
- Wear App 실행: Wear OS 에뮬레이터 또는 실제 워치 선택

동시 디버깅

1. 에뮬레이터 페어링

1. Android 에뮬레이터 실행
2. Wear OS 에뮬레이터 실행
3. Wear OS 앱에서 "Google Play 서비스" 설치 확인
4. Android 에뮬레이터의 "Wear OS" 앱에서 페어링

2. 실제 기기 페어링

1. 스마트폰에 "Galaxy Wearable" 앱 설치
2. 갤럭시 워치와 블루투스 페어링
3. 개발자 옵션 활성화 (두 기기 모두)
4. ADB 디버깅 활성화

배포

Google Play Store 배포

1. Wear OS 앱을 모바일 앱에 포함

// mobile/build.gradle.kts
android {
    defaultConfig {
        // Wear OS 앱을 번들에 포함
        wearAppUnbundled = false
    }
}
 
dependencies {
    // Wear OS 앱을 임베드
    wearApp(project(":wear"))
}

2. App Bundle 생성

1. Build → Generate Signed Bundle / APK
2. Android App Bundle 선택
3. 키스토어 설정
4. Release 빌드
5. .aab 파일 생성됨

3. Google Play Console 업로드

1. Google Play Console 접속
2. 앱 만들기
3. App Bundle 업로드
4. 사용자가 모바일 앱 설치 시 자동으로 Wear OS 앱도 설치됨

독립 배포 (Wear OS만)

// wear/build.gradle.kts
android {
    defaultConfig {
        // 독립 실행 가능
    }
}
 
// wear/AndroidManifest.xml
<application>
    <meta-data
        android:name="com.google.android.wearable.standalone"
        android:value="true" />
</application>

통합 프로젝트의 장점

코드 관리

  • ✅ 하나의 저장소에서 관리
  • ✅ 버전 관리 용이
  • ✅ 공통 코드 재사용

개발 효율

  • ✅ 동시 개발 가능
  • ✅ 일관된 빌드 설정
  • ✅ 의존성 관리 통일

데이터 동기화

  • ✅ Wearable Data Layer로 실시간 통신
  • ✅ 공통 데이터 모델 사용
  • ✅ 일관된 사용자 경험

배포

  • ✅ 하나의 앱으로 배포
  • ✅ 사용자는 하나만 설치
  • ✅ 자동 동기화

실전 팁

1. 패키지명 관리

// mobile
applicationId = "com.example.myapp"
 
// wear (같은 패키지명 사용)
applicationId = "com.example.myapp"

2. 리소스 공유

shared/src/main/res/
├── values/
│   ├── strings.xml  # 공통 문자열
│   └── colors.xml   # 공통 색상

3. BuildConfig 활용

// shared/build.gradle.kts
android {
    buildFeatures {
        buildConfig = true
    }
 
    defaultConfig {
        buildConfigField("String", "API_URL", "\"https://api.example.com\"")
    }
}

4. 로깅 유틸

// shared/Logger.kt
object Logger {
    private const val TAG = "MyApp"
 
    fun d(message: String) {
        Log.d(TAG, message)
    }
 
    fun e(message: String, throwable: Throwable? = null) {
        Log.e(TAG, message, throwable)
    }
}

참고 자료