Articles

스마트폰을 PC의 모션 컨트롤러로 만들기

멀티 디바이스 앱을 위한 라이브러리, Zap

Table of Contents

한 명의 사용자가 보유한 기기 수가 꾸준히 늘어남에 따라 이들을 유기적으로 결합하고자 하는 사용자 요구도 증가하고 있다. 나는 맥북과 2개의 스마트폰, 아이패드, 우분투 데스크탑을 일상적으로 사용하는데, 애플의 유니버설 컨트롤을 벗어나면 서로 다른 플랫폼의 기기를 연동하기가 쉽지 않다. 특히 모바일 기기에는 많은 센서가 탑재되어 있지만, PC를 사용할 때는 이를 전혀 사용하지 못한다. 모바일 기기의 각종 센서를 하나의 모바일 기기에만 남겨두기에는 너무 값지다. 만약 모바일 기기의 각종 센서를 다른 기기에서 사용할 수 있다면 보다 입체적인 사용자 경험을 만들 수 있을 것이다. 스마트폰을 PC의 모션 컨트롤러로 사용할 수 있다면 어떨까?

위 비디오에서 스마트폰은 랩탑에서 구동되는 게임의 컨트롤러로서 동작한다. 스마트폰의 가속도 센서를 이용해 기기를 기울이면 자동차의 방향을 조작할 수 있고, 버튼을 눌러서 가속/감속할 수 있다. 이처럼 여러 기기가 밀접히 결합된 멀티 디바이스 애플리케이션을 개발자가 쉽게 만들 수 있으면 좋겠다는 생각에서 애플리케이션 프로그래밍 라이브러리 Zap을 만들었다.

Zap은 학교 캡스톤 프로젝트의 결과물이다. 요즘 캡스톤 프로젝트를 한다고 하면 보통 완결된 형태의 웹 서비스를 만드는데, 대부분 프론트엔드는 리액트로 작성하고, 백엔드는 자바로 작성한 스프링부트 서버를 AWS에 배포하곤 한다. 하지만 웹은 내가 지난 수년간 지겨울 정도로 해온 일이기도 했고, 창업으로 이어질 만한 획기적인 서비스 아이디어가 있던 것도 아니었기에 지금까지 공부해 온 것과 완전히 다른 주제를 선택해 보고 싶었다. 그런 점에서 Zap은 나에게 새로운 영역을 공부해볼 기회이기도 했다.

Zap 라이브러리

Zap의 기본적인 컨셉은 개발자로 하여금 모바일 기기의 데이터소스로부터 얻은 데이터를 원격 기기로 쉽게 스트리밍할 수 있도록 돕는 라이브러리다. 연구실에 들어온 뒤 첫 번째로 읽은 논문이 Tap: An App Framework for Dynamically Composable Mobile Systems였는데, 이 연구에서 소개하는 Tap 프레임워크는 모바일 기기간 데이터소스 공유를 목표로 한다. Tap이 레퍼런스하는 각종 멀티 디바이스 컴퓨팅 프레임워크 연구들을 보며 아쉬웠던 부분은 ‘즉시 사용 가능한’ 솔루션이 공개된 경우가 없다는 점이었다. 좋은 아이디어가 연구실과 문서에만 남아있는 것은 연구자 입장에서도, 개발자 입장에서도, 그리고 사용자 입장에서도 비극일 것이다. 따라서 Zap은 Tap을 개선, 확장하여 실제로 누구나 사용 가능한 솔루션을 제공하고자 했다. 이를 위해 크게 두 가지 요구사항을 상정했다.

첫 번째, 애플리케이션 레벨에서 한 기기의 리소스를 원격 기기와 쉽게 공유할 수 있도록 추상화된 인터페이스를 제공해야 한다. 다른 기기와 데이터를 공유하기 위해서는 애플리케이션 개발자가 네트워크 통신과 멀티스레딩을 직접 신경써야 하는데, 이 부분을 추상화하여 단순한 API로 제공하는 것이 핵심이다. 이를 통해 개발자는 로컬 기기에서 데이터를 다루는 것과 같이 원격 기기의 데이터를 다룰 수 있다.

두 번째, 모바일 기기 뿐 아니라 PC나 TV, 키오스크와 같은 기기가 네트워크에 참여할 수 있어야 한다. 대부분의 PC, TV, 키오스크 기기에는 모바일 기기에 탑재된 것과 같은 장치(센서, 터치스크린 등)가 없거나 부족하다. 따라서 정말 유용한 멀티 디바이스 사용자 경험을 제공하려면 그런 장치를 갖춘 기기와, 장치가 결핍된 기기를 함께 결합할 필요가 있다.

다음은 안드로이드 기기의 가속도 센서로부터 얻은 데이터를 원격 서버로 전송하는 코틀린 코드다. 특정 서버 기기의 IP 주소를 바라보는 클라이언트를 만들고, 센서값이 변경될 때마다 호출되는 콜백 메서드를 정의해 서버로 가속도계 데이터를 전송한다.

class MainActivity: AppCompatActivity(), SensorEventListener {
  private lateinit var zap: ZapClient

  override fun onCreate(state: Bundle?) {
    zap = ZapClient(InetAddress.getByName(...))
  }

  override fun onSensorChanged(event: SensorEvent) {
    if (event.sensor.type == Sensor.TYPE_ACCELEROMETER) {
      val (x, y, z) = event.values
      zap.send(ZapAccelerometer(x, y, z))
    }
  }
}

클라이언트(안드로이드 기기)가 보낸 데이터를 수신하는 서버는 단순한 데스크탑 프로그램이다. 아래 Node.js 애플리케이션은 가속도계 데이터를 수신하고 로그를 출력한다. 이렇게 통신 절차를 추상화함으로써 개발자가 적은 양의 코드만으로 원격 기기와 데이터를 주고받을 수 있도록 했다.

(new class extends ZapServer {
  onAccelerometerReceived(info: MetaInfo, data: ZapAccelerometer) {
    console.log(`Data received from ${info.dgram.address}:
      (${data.x}, ${data.y}, ${data.z})`);
  }
}).listen();

Zap 네트워크는 기본적으로 클라이언트-서버 모델이기 때문에 애플리케이션의 전체적인 구조를 단순한 형태로 유지할 수 있다. 모델의 구조적인 특성에 따라 하나의 기기에 여러 기기를 연동하는 1:N 연결도 자연스럽게 지원할 수 있으며, 기기 연결에 클라우드 인프라도 필요하지 않다.

Zap 클라이언트-서버 모델.

위 그림은 클라이언트가 자신의 가속도 센서값을 실시간으로 서버에 전송하는 모습을 보여준다. Zap은 IP 기반 통신으로 높은 대역폭의 이점을 누릴 수 있게 설계되었다. 따라서 클라이언트가 서버에 연결하기 위해서는 우선 서버 기기의 IP 주소를 얻어야 한다. Tap 프레임워크의 경우 모바일 기기간 통신을 목표로 하기 때문에 NFC를 통해 IP 주소를 교환한 뒤, IP 통신을 수행하도록 만들어졌다. 하지만 Zap의 대상 기기는 NFC를 지원하지 않을 가능성이 높으므로 다른 방법을 취해야 했다. 연결 매커니즘이 라이브러리 차원에서 지원할 영역은 아니지만, Zap을 실제 애플리케이션 개발 환경에서 유용하게 사용될 수 있음을 보이려면 고민해볼 필요는 있었다.

여기에는 다양한 방법이 있겠지만, 일반적으로는 서버 기기의 디스플레이에 서버 IP 주소를 담은 QR 코드를 띄워서 클라이언트가 이를 스캔하도록 하는 방식을 선택할 수 있을 것이다. (Zap을 이용한 예시 애플리케이션들은 모두 이 방식으로 구현했다.) 아니면 상황에 따라 Tap처럼 NFC를 사용해 물리적 접촉으로 기기를 연결할 수도 있을 것이고, BLE를 사용해 주변 기기에 연결 요청을 브로드캐스팅할 수도 있을 것이다. 기기가 연결된 뒤에는 클라이언트의 데이터를 UDP 소켓을 통해 서버로 전송할 수 있댜. 수신 데이터는 그 유형에 따라 서버 측에 정의한 콜백 메서드로 전달된다.

ZAPP 통신 프로토콜

안드로이드 기기 간 통신하는 멀티 디바이스 앱을 만들 때는 IPC를 확장하는 개념으로써 원격 기기와 바인더를 공유하는 접근이 일반적이다. 하지만 Zap의 원격 기기는 안드로이드가 아닌 macOS가 구동되는 맥북이나 리눅스 데스크탑, 윈도우즈 키오스크가 될 수 있으므로 보다 범용적인 방식이 필요했다. 시시각각 변경되는 센서 데이터를 빠르게 전송해야 하므로 UDP 소켓을 활용하는 것은 쉽게 생각할 수 있었다.

하지만 데이터그램에 데이터를 어떻게 담을 것인지가 문제였다. HTTP에 익숙한 나는 큰 고민 없이 JSON을 담아봤다. 그러나 센서에서 얻은 데이터 객체를 JSON으로 인코딩하고, 이를 전송한 뒤, 다시 객체로 디코딩하는 방식은 수십 밀리초 정도로 너무 느렸다. 구분자로 연결된 단순 텍스트를 담는다고 해도 글자 하나당 최소 1바이트를 차지하게 되므로 데이터그램의 크기가 너무 커지는 것이 문제였다. BSON, Protobuf 등의 방식은 목적에 맞지 않는 것처럼 느껴졌다. 결국에는 높은 성능을 달성하기 위해 UDP 위에 자체적인 네트워크 프로토콜 ZAPP(Zap Protocol)을 정의했다.

ZAPP 오브젝트의 헤더 파트 구성.

ZAPP 오브젝트의 첫 9바이트는 헤더 파트로 구성된다. 순서가 중요한 데이터라면 헤더의 8바이트 타임스탬프 필드를 기반으로 애플리케이션에서 순서를 보장할 수 있다. 이어지는 페이로드 파트에는 실제 값이 담겨있으며, 리소스의 종류에 따라 그 인코딩 형식이 다르게 구성된다. 페이로드의 형식은 헤더 파트의 1바이트 리소스 필드를 통해 판별할 수 있다.

ZAPP 오브젝트의 가속도계 데이터 페이로드 파트 구성.

가령 헤더 파트의 리소스 필드 값이 ACC라면 가속도계 데이터를 의미하는 것으로, 페이로드가 x, y, z 축 각각에 대한 값이 4바이트씩, 총 12바이트로 인코딩되어 있음을 알 수 있다. Zap은 가속도 센서와 같은 동작 센서, 조도 센서를 비롯한 환경 센서, 그리고 UI 이벤트, 텍스트, 지오포인트 등의 리소스를 지원한다.

데이터 형식에 대한 유연성은 다소 떨어졌지만, ZAPP를 적용해보니 클라이언트에서 서버로 10만개의 ZAPP 오브젝트를 전송했을 때 하나의 오브젝트 당 인코딩부터 디코딩까지 평균 0.03ms 이내의 성능을 얻을 수 있었다.

멀티 디바이스 애플리케이션

Zap 구현체를 실제로 유용하게 사용할 수 있음을 보이기 위해, 앞서 제시한 모션 컨트롤러를 비롯한 멀티 디바이스 애플리케이션을 몇 가지 만들었다. 첫 번째는 스마트폰을 프레젠테이션 리모컨으로 사용하는 예시다.

화면상 버튼 UI 뿐만 아니라 측면 음량 버튼을 눌러서 슬라이드를 넘기는 모습도 볼 수 있다.

또 다른 예시는 태블릿에서 펜으로 작성한 글자를 랩탑에 공유하는 애플리케이션이다. 일반적인 랩탑에는 터치스크린이 없기 때문에 모바일 기기를 결합하면 높은 시너지 효과를 낼 수 있다. 특히 한자나 카나 문자를 작성해야 하는 상황에 유용하다.

이 예시는 ML Kit Digital Ink를 수정한 것이기 때문에 태블릿 기기 내에서 글자 인식이 이루어진다. 하지만 이를 역으로 확장하면 GPU 연산은 고성능 기기에서 수행하고, 모바일 기기에서는 그 연산 결과만을 전달받아 응용하는 분산 컴퓨팅도 가능하다. 또한 Zap의 주요 목표는 모바일 기기와 PC의 결합이지만, 모바일-모바일 연동이나 PC-PC 연동도 문제없다.

Zap 라이브러리와 예시 애플리케이션은 아직 프로토타입 수준이기 때문에 더 고민해볼 지점이 많이 남아있다. 우선 UDP 데이터그램을 암호화하고, 클라이언트 기기에 대한 권한 인증 절차가 필요하다. 또한 처리량을 높이기 위해 서버가 접속 클라이언트 수에 따라 스레드를 유동적으로 관리하도록 개선해야 한다. 추가로 서버가 클라이언트로부터 수신한 데이터를 더 이상 처리하지 못하는 상황에 대응할 필요도 있다. 현재 Zap 구현체는 푸시 모델로 구현되어 있으며, 버퍼가 꽉 차서 서버가 더 이상 수신 데이터를 처리할 수 없는 상황에는 추가로 수신하는 데이터를 버리게 된다. 성능이 좋지 않는 랩탑에서 앞서 구현한 모션 컨트롤러를 사용해봤을 때 딜레이가 체감될 정도였는데, 아예 처리율을 서버 측에서 설정하는 방식으로 개선할 수도 있을 것 같다.

나에게 Zap은 나름 도전적인 과제였는데, 우수상을 수상하며 캡스톤 프로젝트를 잘 마무리 지었다. Zap의 구현체와 모든 예시는 github.com/zap-lib에서, 문서는 zap-lib.github.io에서 확인할 수 있다.

아치 리눅스로 15년차 넷북 되살리기

Eee PC 1000HE 위에 올린 아치 리눅스 32

Articles