Kotlin

[Kotlin 실전 (8)] 손목 위의 앱: 모바일과 워치의 데이터 브릿지 아키텍처

smileDeveloper 2026. 9. 26. 10:00
반응형
SMALL

스마트폰 주머니에서 손을 뺄 필요 없이, 손목 위에서 즉각적인 데이터를 주고받는 워치 OS 통신 구조를 해부합니다.

1. 왜 워치는 네이티브로 가야 하는가?

* 배터리와 리소스 최적화: 손목이라는 제한된 환경에서 화면을 누른 상태를 유지하며 진동을 지속적으로 울리려면, 네이티브 환경(Kotlin/Swift)에서 라이프사이클을 직접 관리해야 햅틱 지연과 앱 강제 종료를 막을 수 있습니다.

2. 통신 아키텍처 설계 (하이브리드 vs 풀 네이티브)

A. 하이브리드 브릿지 (Flutter Mobile ↔ Kotlin Wear OS)

크로스 플랫폼의 효율을 극대화하는 현실적인 타협안입니다.
* 역할 분담: UI 레이아웃과 결제, 통계 로직 등 모바일 앱 본체는 Flutter로 단일화하여 생산성을 높이고, Wear OS 앱은 Kotlin과 Jetpack Compose for Wear로 네이티브 개발합니다.
* 데이터 동기화: 사용자가 모바일에서 설정한 목표 시간이나 정보는 Flutter의 채널 패키지(예: watch_connectivity)를 브릿지로 활용해 네이티브 워치 앱으로 동기화합니다.

B. 풀 네이티브 다이렉트 (Kotlin Mobile ↔ Kotlin Wear OS)

안드로이드 생태계의 공용어인 코틀린을 양쪽에 모두 사용하여 가장 빠르고 안정적인 통신 파이프라인을 구축합니다. 안드로이드 공식 Wearable Data Layer API를 활용합니다.

* MessageClient (단발성 명령): "지금 진동 울려!", "타이머 정지!"처럼 한 기기에서 다른 기기로 즉각적인 이벤트(RPC)를 쏠 때 사용합니다. 네트워크 유실 가능성이 있지만 속도가 빠릅니다.
* DataClient (상태 동기화): 사용자의 설정값, 현재 걸음 수 등 지속적으로 유지되어야 하는 '상태(State)'를 동기화합니다. 연결이 끊겨도 OS가 캐싱해 두었다가 재연결 시 자동으로 동기화(Sync)해 줍니다.

3. 범용 MessageClient 통신 코드 (Mobile 발신 ↔ Watch 수신)

모바일 기기의 버튼을 눌렀을 때, 연결된 스마트워치로 특정 액션(예: 알림 진동)을 트리거하는 가장 표준적인 로직입니다.

모바일 (발신 측: 명령 쏘기)

import com.google.android.gms.wearable.Wearable

// 연결된 모든 워치 노드(Node)를 찾아 특정 경로(path)로 메시지 전송
fun sendCommandToWatch(context: Context, command: String) {
    Wearable.getNodeClient(context).connectedNodes.addOnSuccessListener { nodes ->
        for (node in nodes) {
            Wearable.getMessageClient(context).sendMessage(
                node.id, 
                "/app_action", // 통신을 위한 고유 경로
                command.toByteArray() // 전송할 데이터 페이로드
            ).addOnSuccessListener {
                // 전송 성공 로그 처리
            }.addOnFailureListener {
                // 전송 실패 예외 처리
            }
        }
    }
}

// 사용 예시: 버튼 클릭 시 호출
// sendCommandToWatch(context, "TRIGGER_VIBRATION")


Wear OS (수신 측: 백그라운드 대기 및 네이티브 하드웨어 제어)

import android.os.VibrationEffect
import android.os.Vibrator
import com.google.android.gms.wearable.MessageEvent
import com.google.android.gms.wearable.WearableListenerService

// 매니페스트에 등록되어 백그라운드에서 메시지를 수신하는 리시버
class WatchCommandService : WearableListenerService() {
    
    override fun onMessageReceived(messageEvent: MessageEvent) {
        super.onMessageReceived(messageEvent)

        // 설정한 경로(/app_action)가 맞는지 확인
        if (messageEvent.path == "/app_action") {
            val payload = String(messageEvent.data)
            
            when (payload) {
                "TRIGGER_VIBRATION" -> executeNativeVibration()
                "OTHER_COMMAND" -> { /* 다른 범용 비즈니스 로직 처리 */ }
            }
        }
    }

    // Wear OS 네이티브 햅틱(진동) 제어
    private fun executeNativeVibration() {
        val vibrator = getSystemService(VIBRATOR_SERVICE) as Vibrator
        if (vibrator.hasVibrator()) {
            // 0.5초 동안 기본 강도로 진동
            val effect = VibrationEffect.createOneShot(500L, VibrationEffect.DEFAULT_AMPLITUDE)
            vibrator.vibrate(effect)
        }
    }
}

이 범용 브릿지 코드를 통해 모바일 앱에서 워치의 하드웨어(진동, 화면 켜짐 등)를 원격으로 제어할 수 있는 통로가 뚫렸습니다.


단발성 명령(RPC)을 쏘는 ⁠MessageClient⁠와 달리, 안드로이드의 ⁠DataClient⁠는 모바일과 워치 간의 '상태(State)'를 영구적으로 동일하게 맞추는 거울 역할을 합니다. 유저가 스마트폰을 집에 두고 워치만 찬 채로 외출하여 연결이 끊어지더라도, OS 시스템 단에서 변경된 데이터를 캐싱해 두었다가 블루투스가 재연결되는 순간 완벽하게 상태를 동기화(Sync)해 줍니다. 앱의 설정값, 테마, 누적 통계치 등을 다룰 때 필수적인 범용 아키텍처입니다.

1. 상태 밀어넣기 (모바일 ➡️ 워치)

모바일에서 유저가 앱의 테마 설정이나 프로필 이름을 변경했을 때, 이를 워치 데이터 레이어에 등록합니다. ⁠PutDataMapRequest⁠를 사용해 Key-Value 형태로 데이터를 묶어 던집니다.

import com.google.android.gms.wearable.PutDataMapRequest
import com.google.android.gms.wearable.Wearable

fun syncUserSettingsToWatch(context: Context, isDarkMode: Boolean, userName: String) {
    val putDataReq = PutDataMapRequest.create("/user_settings").run {
        dataMap.putBoolean("DARK_MODE", isDarkMode)
        dataMap.putString("USER_NAME", userName)
        // DataClient는 데이터가 '변경'될 때만 이벤트를 발생시키므로, 
        // 덮어쓰기 강제 트리거를 위해 타임스탬프를 함께 보내는 것이 정석입니다.
        dataMap.putLong("TIMESTAMP", System.currentTimeMillis()) 
        asPutDataRequest()
    }
    
    // 백그라운드 데이터 레이어에 저장
    Wearable.getDataClient(context).putDataItem(putDataReq)
}


2. 상태 수신 및 UI 반영 (워치 백그라운드)

워치 앱에서는 ⁠WearableListenerService⁠를 통해 데이터 변경을 감지합니다. 이 데이터가 들어오면 로컬 DB(Room)나 DataStore를 업데이트하여 Compose UI가 자연스럽게 리렌더링 되도록 연결합니다.

import com.google.android.gms.wearable.DataEvent
import com.google.android.gms.wearable.DataEventBuffer
import com.google.android.gms.wearable.DataMapItem
import com.google.android.gms.wearable.WearableListenerService

class WatchDataSyncService : WearableListenerService() {
    
    override fun onDataChanged(dataEvents: DataEventBuffer) {
        for (event in dataEvents) {
            // 데이터가 변경되었고, 지정한 경로(/user_settings)가 맞는지 확인
            if (event.type == DataEvent.TYPE_CHANGED && event.dataItem.uri.path == "/user_settings") {
                
                val dataMap = DataMapItem.fromDataItem(event.dataItem).dataMap
                val isDarkMode = dataMap.getBoolean("DARK_MODE")
                val userName = dataMap.getString("USER_NAME")
                
                // TODO: 워치의 로컬 상태 관리자(ViewModel, DataStore)에 값 업데이트
            }
        }
    }
}
반응형
LIST