Command Palette

Search for a command to run...

LazyPad 1편에서는 처음 다루는 Swift/Xcode 환경에서 iPhone-Mac 입력 앱을 배포 후보까지 끌고 간 전체 흐름을 정리했습니다. 2편에서는 touch sample, coalescedTouches, jitter, lag, gain, transfer function 같은 입력감 용어를 먼저 풀었습니다.

이번 글은 그중 가장 오래 남았던 입력감 문제를 다룹니다. iPhone surface에서 손을 뗐다가 다시 올린 직후, 아주 작은 움직임이 Mac cursor에서는 큰 jump처럼 보이던 문제입니다.

처음에는 네트워크 지연처럼 보일 수 있었습니다. 그러나 build별로 확인해 보니 원인은 네트워크 하나가 아니었습니다. Gesture boundary, 첫 coalescedTouches batch, Mac pointer gain이 겹친 문제로 좁혀졌습니다.

이 글의 핵심은 버그 하나를 고친 기록이 아닙니다. 그럴듯한 원인 후보를 AI로 빠르게 넓히고, 실제 기기 검증으로 하나씩 줄여간 과정입니다.

디버깅 용어를 알아야 build별 판단이 보입니다

이번 글은 2편보다 실제 문제 해결 흐름에 가깝습니다. 그래서 build, clamp, stream, regression 같은 디버깅 용어가 자주 나옵니다.

용어쉬운 설명이 글에서의 의미
Build특정 시점에 만든 앱 버전입니다각 build는 하나의 가설을 확인하는 실험 단위였습니다
Cursor reacquire손을 뗐다가 다시 올렸을 때 cursor 입력을 다시 잡는 순간입니다이번 문제의 핵심 구간이었습니다
Jump / teleport사용자가 조금 움직였는데 cursor가 크게 튄 것처럼 보이는 현상입니다실제 제품 신뢰를 떨어뜨리는 입력감 문제였습니다
Gesture boundary이전 손동작과 새 손동작을 나누는 경계입니다손을 뗐다 다시 올리는 순간을 새 입력으로 다루기 위해 필요했습니다
Activation delta"이제 move로 보겠다"고 판단되는 순간의 첫 이동량입니다build 5-6에서 먼저 제한한 값입니다
Clamp값이 너무 커지지 않도록 상한을 두는 처리입니다첫 output이 과하게 나가지 않도록 사용했습니다
Batch한 번에 묶여 들어온 sample 묶음입니다첫 sample만이 아니라 첫 batch 전체를 봐야 했습니다
Input streamiPhone에서 Mac으로 입력을 계속 보내는 연결 경로입니다이전 입력이 늦게 섞이는지 확인한 후보였습니다
Stale completion이미 지난 연결이나 작업의 완료 신호가 뒤늦게 오는 현상입니다build 7에서 방어한 네트워크 후보였습니다
Regression고쳤던 기능이 다른 수정 뒤 다시 나빠지는 현상입니다build 10에서 drag 동작을 다시 확인한 이유입니다
Physical smoke실제 iPhone과 Mac에서 핵심 동작을 빠르게 확인하는 수동 검증입니다입력감 결론을 내리는 마지막 기준이었습니다

문제는 연속 이동이 아니라 첫 재터치에 있었습니다

증상은 좁았습니다. 손가락을 계속 올려둔 상태에서는 cursor movement가 비교적 부드러웠습니다.

문제는 손을 뗐다가 다시 올리는 첫 순간에 나타났습니다. 사용자는 조금만 움직였다고 느끼는데, Mac cursor는 그보다 크게 움직이는 것처럼 보였습니다.

이 차이가 중요했습니다. 연속 이동 전체가 느렸다면 네트워크나 전송 경로를 먼저 의심해야 합니다. 하지만 특정 gesture boundary에서만 문제가 나온다면 touch 처리와 입력 상태 전환을 따로 봐야 합니다.

관찰의미
연속 cursor movement는 부드럽게 보였습니다전체 전송 경로가 항상 막힌 것은 아니었습니다
click과 scroll은 Mac까지 도달했습니다session/message gate가 전반적으로 깨진 것은 아니었습니다
손을 뗀 뒤 첫 움직임에서 jump가 집중됐습니다gesture 시작 경계와 첫 sample 처리가 주요 후보였습니다
ordinary Wi-Fi에서도 재현됐습니다hotspot 지연 하나로 설명하기 어려웠습니다

이 관찰 덕분에 첫 질문을 바꿀 수 있었습니다. "왜 네트워크가 느린가"가 아니라 "왜 새 gesture의 첫 output이 커지는가"로 좁혔습니다.

감도 배율을 먼저 만지지 않았습니다

입력감 문제가 보이면 가장 쉬운 대응은 sensitivity multiplier를 낮추는 것입니다.

하지만 이 문제에는 맞지 않았습니다. 배율을 낮추면 첫 jump는 줄어 보일 수 있습니다. 대신 정상적인 느린 이동까지 둔해질 수 있습니다.

LazyPad의 입력감 문제는 최소 세 가지가 섞여 있었습니다.

문제배율 하나로 해결하기 어려운 이유
느린 이동이 늦게 따라오는 느낌smoothing, timestamp, send cadence 영향이 섞입니다
빠른 이동이 과하게 앞서는 느낌high-velocity gain cap과 acceleration slope 문제입니다
재터치 첫 jumpgesture boundary와 첫 batch 처리 문제입니다

그래서 2편에서 입력 파이프라인을 계층으로 나누었습니다. Apple coalescedTouchespredictedTouches는 touch sample을 어떻게 다룰지 확인하는 기준이 되었습니다. 1 Euro Filter는 jitter와 lag를 분리해서 생각하는 데 도움이 되었습니다. Control-display gain 연구와 libpointing은 pointer movement를 단순 배율이 아니라 transfer function으로 보는 근거가 되었습니다.

이번 글에서는 그중 gesture boundary, coalesced touch batch, pointer gain, input stream 네 가지를 실제 버그 후보로 다룹니다.

AI는 이 후보들을 빠르게 모으는 데 유용했습니다. 다만 어떤 후보를 실제 제품에 넣을지는 문서, 논문, 테스트 결과로 다시 판단했습니다.

build 4부터 build 10까지 가설을 하나씩 줄였습니다

이 문제는 한 번에 닫히지 않았습니다. 각 build는 다른 가설을 확인하는 실험이었습니다.

빌드가설조치결과
build 4Mac pointer engine이 이전 gesture 상태를 끌고 올 수 있습니다touch began 시 zero-delta gesture-start를 전송했습니다buffered catch-up은 줄었지만 tiny jump가 남았습니다
build 5-6첫 activation delta가 너무 클 수 있습니다first activation delta clamp를 낮췄습니다개선 방향은 맞았지만 충분하지 않았습니다
build 7이전 gesture의 pending send가 다음 gesture로 flush될 수 있습니다input stream reset과 stale completion generation guard를 추가했습니다stream 후보는 약해졌지만 jump가 남았습니다
build 8첫 callback 안의 coalesced batch 전체가 커질 수 있습니다첫 emitted reacquire move batch 전체를 clamp했습니다큰 cursor teleport가 재현되지 않았습니다
build 10drag가 자동으로 열리는 regression이 남을 수 있습니다drag hold timer와 forceStart path를 제거했습니다input-feel 기준선으로 채택했습니다

Zero-delta gesture-start는 이동량이 0인 move 신호를 보내 "새 gesture가 시작됐습니다"라고 Mac 쪽 pointer 처리기에 알려주는 방식입니다. Cursor를 움직이려는 목적이 아니라, 이전 gesture 상태를 끊고 새 입력을 시작시키려는 신호였습니다.

Generation guard는 오래된 전송 완료 신호가 현재 gesture에 영향을 주지 않도록 세대를 구분하는 방어 장치입니다. 같은 input stream을 재사용하면 이전 작업의 완료 callback이 늦게 돌아올 수 있으므로, 이 신호가 현재 세대의 것인지 확인해야 했습니다.

이 흐름에서 build 4는 필요한 수정이었습니다. Mac 쪽 pointer state를 새 gesture로 시작시키는 신호가 필요했기 때문입니다.

하지만 build 4만으로는 충분하지 않았습니다. 이것이 중요한 지점이었습니다. 첫 수정이 일부 증상을 줄였다고 해서 원인을 닫았다고 보면 안 됐습니다.

첫 activation delta만 막아서는 충분하지 않았습니다

build 5와 build 6에서는 첫 activation delta를 제한했습니다. 손을 다시 올린 뒤 move intent가 열리는 순간, 첫 이동량이 너무 크게 나가지 않도록 막는 접근이었습니다.

방향은 맞았습니다. 그러나 문제는 그보다 한 단계 안쪽에 있었습니다.

UIKit의 coalescedTouches(for:)는 하나의 touchesMoved callback 안에 여러 sample을 담을 수 있습니다. 첫 sample만 clamp해도, 같은 callback 안에 뒤따라온 sample들이 함께 output으로 나갈 수 있었습니다.

그래서 build 8에서는 기준을 바꿨습니다.

이전 기준바꾼 기준
첫 activation delta를 제한합니다처음 외부로 나가는 reacquire move batch 전체를 제한합니다
threshold를 넘긴 순간만 봅니다같은 callback 안의 activation-following sample까지 봅니다
첫 sample 중심으로 판단합니다첫 output 중심으로 판단합니다

이 변경 뒤 큰 cursor teleport는 재현되지 않았습니다. 현재 가장 설명력 높은 원인은 첫 coalescedTouches batch 전체가 새 gesture의 첫 output으로 과도하게 나가고, Mac pointer gain이 이를 확대했다는 쪽입니다.

여기서 Mac pointer gain은 iPhone에서 보낸 이동량이 Mac 화면에서 얼마나 크게 cursor 움직임으로 나타나는지에 관한 비율입니다. 작은 입력이라도 gain이 적용되면 화면에서는 더 크게 보일 수 있습니다. 그래서 첫 batch가 조금만 커져도 사용자는 큰 jump처럼 느낄 수 있었습니다.

네트워크 hardening은 필요했지만 최종 원인은 아니었습니다

네트워크 후보를 버린 것은 아닙니다. build 7의 input stream reset과 stale completion generation guard는 장기 안정성을 위해 필요한 방어였습니다.

다만 build 7 이후에도 tiny re-touch jump가 남았습니다. 그래서 최종 원인을 reusable input stream 하나로 보기는 어려웠습니다.

이 구분이 중요했습니다. 네트워크를 계속 만지면 문제를 더 넓게 만들 수 있습니다. 반대로 touch processor의 첫 output semantics를 보면 더 좁은 수정으로 갈 수 있었습니다.

후보현재 판단
hotspot 지연별도 transport watch item입니다
ordinary Wi-Fi 지연단일 root cause로 보지 않았습니다
reusable input stream stale completion방어는 필요하지만 최종 원인은 아니었습니다
first coalesced touch batch현재 가장 설명력 높은 원인입니다
Mac pointer gain증상을 확대하는 요인으로 판단했습니다

문제를 닫기 위해서는 원인을 하나로 단정하는 것보다, 각 후보가 설명하는 범위와 설명하지 못하는 범위를 나누는 일이 더 중요했습니다.

build 10 기준선은 완전한 대체가 아니라 검증 가능한 입력면입니다

build 0.1.0 (10)은 input-feel 기준선으로 채택했습니다.

이 표현은 조심해서 써야 합니다. 모든 macOS gesture를 지원한다는 뜻이 아닙니다. Magic Trackpad를 완전히 대체한다는 뜻도 아닙니다.

현재 말할 수 있는 범위는 더 좁고 구체적입니다.

항목build 10 기준 판단
lift/re-touch movementphysical smoke 통과
fast movement feelphysical smoke 통과
tap-hold drag behaviorphysical smoke 통과
automatic-drag-after-wait regression재확인 후 통과
큰 cursor teleport재현되지 않아 해결로 판단
작은 잔여 튐blocker가 아니라 watch item으로 분리

이 기준선이 생기면서 다음 단계로 갈 수 있었습니다. 더 많은 기능을 넣기 전에, 현재 입력면이 외부 검증 후보로 설명 가능한 수준인지 먼저 확인했습니다.

AI는 원인 후보를 넓혔고, 실기기 smoke가 결론을 좁혔습니다

AI는 이 문제에서 빠른 검색 도구이자 가설 정리 도구였습니다.

네트워크 지연, touch sample, smoothing, gain curve, gesture state, stream flush 같은 후보를 빠르게 나열할 수 있었습니다. build별로 어떤 실험을 하면 후보가 줄어드는지도 정리할 수 있었습니다.

하지만 결론은 AI만으로 낼 수 없었습니다. 실제 손가락 움직임, iPhone touch surface, Mac cursor 반응, build별 physical smoke가 필요했습니다.

AI가 도운 일직접 확인한 일
원인 후보를 layer별로 나눴습니다ordinary Wi-Fi와 hotspot에서 증상을 구분했습니다
관련 문서와 알고리즘 후보를 찾았습니다Apple 문서와 HCI 연구를 제품 구조에 맞춰 선별했습니다
build별 실험 순서를 정리했습니다실제 build, install, launch, physical smoke를 반복했습니다
증거 경계를 정리했습니다raw log, device identifier, account 값은 공개 문서에 남기지 않았습니다

AI를 도구로 쓰면 처음 다루는 문제도 빠르게 쪼갤 수 있습니다. 다만 최종 판단은 실제 환경에서 확인해야 했습니다.

이번 글에서 남는 기준

입력감 문제는 숫자 하나로 닫히지 않았습니다.

이 문제를 닫는 데 필요했던 기준은 다음이었습니다.

  1. 증상이 나타나는 구간을 먼저 좁힙니다.
  2. 감도 배율을 바로 만지지 않습니다.
  3. 네트워크, gesture, touch batch, pointer gain을 분리합니다.
  4. build마다 하나의 가설을 약하게 만들거나 강하게 만듭니다.
  5. 마지막에는 사용자가 느끼는 행동 기준으로 다시 확인합니다.

LazyPad에서 첫 재터치 문제는 작은 입력감 문제처럼 보였습니다. 그러나 실제로는 제품이 믿을 만한 입력면인지 확인하는 핵심 gate였습니다.

다음 글은 TestFlight와 Mac Companion 배포 gate를 다룹니다

입력감 기준선이 생겼다고 바로 외부 사용자가 설치할 수 있는 것은 아니었습니다.

다음 글에서는 iOS TestFlight, Mac Companion signing/notarization, support email, privacy/support page가 왜 제품의 일부가 되었는지 정리합니다. build 0.1.0 (10) 입력감 기준선과 build 0.1.0 (11) 외부 TestFlight 심사 제출 상태도 분리해서 설명합니다.

이어 읽기

시리즈는 순서대로, 편집 추천은 맥락대로, 비슷한 주제는 태그 기준으로 정리합니다.

시리즈 전체

LazyPad 개발기: 아이폰을 맥 입력면으로 만들기3/4
  1. 1.LazyPad 개발기 1: 처음 쓰는 Swift와 Xcode로 iPhone-Mac 앱 배포 후보 만들기
  2. 2.LazyPad 개발기 2: 터치 입력감은 왜 배율 하나로 해결되지 않았나
  3. 3.LazyPad 개발기 3: 첫 재터치에서 드러난 커서 입력감 디버깅
  4. 4.LazyPad 개발기 4: TestFlight와 Mac Companion 배포 후보 검증

함께 읽으면 좋은 글

편집 추천

비슷한 주제의 글

태그가 겹치는 글입니다. 시리즈와 편집 추천에 이미 나온 글은 제외합니다.