이 글은 Hacktoberfest 오픈소스 AI 챌린지: 3주차 — 느리지만 꾸준하게에 제출하는 글입니다.
내가 만든 것
이번 주에는 일부러 게으른 상태였습니다. 과제나 식료품 쇼핑을 건너뛰고, 저 대신 터치할 수 있는 무언가를 만들어보려고 했습니다. 그 이유는 며칠 전만 해도 Zepto에서 약 28개의 품목을 예약하려고 할 때, 제가 직접 검색하고 항목을 추가하며 모든 행동을 해야 했기 때문입니다. 정말 싫었습니다. 저는 그런 게으른 사람입니다. 그래서 생각했습니다. 내가 품목 목록과 수량(타이핑은 또 너무 지루해서 음성 기반으로 하는 것이 목표였습니다)만 주면, 그것을 장바구니에 담아주는 자동화 에이전트를 만들지 않을까? 제가 확인하고 결제하면 펑! 주문이 완료되고 가장 지루한 부분이 사라집니다🤩
SlothAssist는 사용자가 열어둔 어떤 Android 앱 위에도 떠 있는 🦥 모양입니다. 이것을 터치하고 다음과 같은 명령어를 입력합니다:
쌀 1kg 검색 후, 첫 번째 옵션 2개 수량을 장바구니에 담아줘
이 아이디어는 화면을 읽고, 단계를 파악하여 Zepto, Blinkit, Chrome 또는 다른 어떤 앱에서든 터치하고, 타이핑하고, 스크롤하는 것입니다. 특정 앱 통합이나 API가 필요하지 않습니다. Android의 접근성 트리(accessibility tree)를 통해 사용자가 보는 것과 동일한 화면을 인식합니다.
플래너는 Gemma 4 E2B이며, 휴대폰 자체에서 실행됩니다 (Google의 LiteRT-LM runtime을 GPU에서 사용). 사용자님의 화면, 장바구니, 그리고 명령어는 절대 기기를 벗어나지 않습니다.
저는 Android 개발자가 아닙니다. 웹 및 백엔드 쪽 출신입니다. 그래서 이 게시물은 솔직한 수치를 담은 진행 보고서이며, 마지막에는 저보다 Android에 대해 더 잘 아는 분들을 위한 구체적인 질문 목록도 포함하고 있습니다.
솔직한 현황
| Status | Evidence |
|---|
| 모든 앱 위에 떠 있는 버블 및 명령 패널 | ✅ 작동함 |
| ... | |
| 오늘 비기술 사용자에게 이것을 넘겨줄 수는 없습니다. 목록이 렌더링되기를 기다리는 최신 빌드는 제품 카드에 있는 카운터를 읽고 가장 상단의 팝업만 나열하며, 컴파일되고 푸시됩니다. 하지만 마감일 전에 휴대폰에서 엔드투엔드로 재테스트할 시간이 부족했습니다. | |
데모 (Demo)
게시 전: 여기에 화면 녹화 또는 스크린샷을 추가하세요.
Pixel 9a에서 측정:
| 단계 | 시간 |
|---|
| Gemma 4 E2B 로드 (GPU, 첫 실행 후 캐시가 구축됨) | ~3–5초 (최초 로드: 38초) |
| ... | |
솔직히 말해서 게으른 보상
현재로서는 수동으로 하는 것이 더 빠릅니다. 전체 '검색, 추가, 수량 설정' 과정은 각 7~20초가 걸리는 여러 Gemma 호출을 필요로 하며, 이는 제가 엄지손가락으로 직접 하는 것보다 오래 걸립니다. 검색 기능만 저보다 빠릅니다. 목표는 시간 절약이며, 현재 상태를 개선하고 그 격차를 줄이는 것은 주로 아래 질문들과 관련이 있습니다.
코드 (Code)
SlothAssist on GitHub
설정 화면에는 React Native (TypeScript)를 사용했습니다. JS 런타임보다 오래 지속되어야 하는 모든 것(오버레이 서비스, 접근성 서비스, 에이전트 하네스 및 온디바이스 모델)에는 Kotlin을 사용했습니다.
구축 방법 (How I Built It)
1. 스크린샷 없이 화면 읽기
AccessibilityService는 사용 중인 앱의 창을 텍스트, 버튼, 입력 필드, 경계(bounds), 클릭 가능한 요소 등으로 구성된 노드의 트리 구조로 읽을 수 있습니다. 저는 이를 읽기 순서에 따라 짧은 번호가 매겨진 목록으로 평탄화합니다. 제품 카드 내부의 텍스트는 해당 카드 안에 포함되고 각 버튼은 자신의 카드를 가리킵니다:
[4] item "₹189, India Gate Basmati Rice Classic, 1 kg"
[5] button "Add to Cart" in [4]
2. 탭당 하나의 모델 호출은 잘못된 방식이었습니다
초기 버전에서는 탭할 때마다 "다음 행동은 무엇인가요?"라고 물었습니다. 당시 휴대폰에서 구동되는 2B 모델은 느렸고(단계당 10~30초 소요), 특히 작업이 끝났다는 것을 인지하는 능력이 떨어졌습니다. 올바른 Google 검색 결과를 열어놓고는 새로운 페이지의 링크들을 계속해서 탭했습니다. Zepto에서는 "장바구니에 추가"를 완료된 것으로 표시하지 않아, 계속해서 쌀을 담았습니다. 제 장바구니가 이에 대해 의견이 있습니다.
그래서 저는 이를 작은 하네스(harness) 형태로 재구축했습니다:
- 한 번 만드는 체크리스트. Gemma는 명령을 미리 분해합니다. 이 부분이 가장 잘 작동하는 부분입니다.
- 단계가 아닌 청크(Chunks). 각 검사 지점마다, 하나의 호출이 진행 상황을 판단하고 다음 페이지 로드까지 예측 가능한 행동의 실행 흐름을 반환합니다. 단계들은 **라벨(label)**에 의해 대상을 지정하며("ADD", "+"), 이는 실시간 화면과 비교되어
tap ADD → tap +와 같이 다른 모델 호출 없이 실행됩니다.
- 알려진 형태를 위한 스킬(Skills). "X 검색하기", "N번째 옵션 추가하기", "수량을 N으로 설정하기" 같은 기능들은 모델 없이 결정론적으로 실행되며 화면에서 스스로 검증합니다: 수량 카운터가 실제로 2로 표시되어야 합니다. 모델은 판단자가 아니라 폴백(fallback) 역할을 합니다.
3. 소형 모델은 작고 엄격한 출력이 필요하다
일반 JSON 모드에서는 Gemma가 긴 "생각 과정(thought)"을 작성한 후, 토큰 제한에 도달할 때까지 수백 개의 탭 문자(유효한 JSON 공백!)로 패딩했습니다. LiteRT-LM은 정규표현식(regex) 제약 디코딩을 지원하므로, 온디바이스 경로는 간결한 한 줄 형식의 코드를 사용합니다:
private val STEP_REGEX =
"\{"\"do\":\"(tap|type|search|scroll|back|open_url)\",\"target\":\"[^\"\
]{0,60}\",\"nth\":\[1-9\]\",\"text\":\"[^\"\
]{0,80}\"\}"
응답은 "사용 불가능"에서 "항상 유효함"으로 바뀌었습니다. 하지만 유효하다는 것이 똑똑하다는 것을 의미하지는 않았습니다.
4. 접근성 이벤트를 통한 실시간 컨텍스트
가장 좌절스러웠던 버그는 _"화면에 분명히 보이는 요소를 찾을 수 없음"_이었습니다. 실제 원인은 사소했습니다:
Zepto의 결과 목록은 점진적으로 렌더링되므로, 내 코드는 제품이 존재하기 전에, 검색 후 28ms 후에 화면을 읽었습니다.
묶음 크기(pack-size)의 하단 시트가 열렸는데, 모델은 그 뒤에 있는 것들을 계속해서 터치했습니다.
모델이 나 자신의 서식((hint: ))을 대상으로 복사했습니다.
화면이 변경된 후에도, 나는 이전 화면의 요소 번호를 사용하고 있었습니다.
더 강하게 폴링(polling)하는 대신, 저는 듣기 시작했습니다. 안드로이드는 접근성 이벤트(accessibility events): 토스트(toasts), 알림(announcements), 패널의 나타남과 사라짐, 텍스트 변경 등을 푸시합니다. EventJournal은 어떤 동작이 무엇을 유발했는지 설명하는 기록들을 유지하며, 각 로그 항목에는 이 정보와 함께 트리의 이전/이후 차이(before/after diff)가 담겨 있습니다:
✓ tap "Add to Cart": the screen changed | new: button "decrease quantity",
button "increase quantity", item "Cart, 28"
이 한 줄만으로도 터치가 성공했고, 스텝퍼(stepper)가 나타났으며, 장바구니가 27개에서 28개로 변했다는 것을 알 수 있습니다. 모두 스크린샷 없이 말입니다.
오픈 혁신(Open Innovation)은 왜 중요한가?
이 비서는 사용자의 화면에 있는 모든 것: 장바구니, 메시지, 어떤 앱이든 읽습니다. 바로 이것이 다음으로 어디를 터치할지 결정하기 위해 호스팅된 API로 스트리밍하고 싶지 않은 데이터입니다.
- 휴대폰에서 실행됩니다. 오픈 LiteRT-LM 런타임(runtime)을 사용하여 Pixel의 GPU 위에서 Gemma 4 E2B가 작동합니다. 서버도 없고, 계정도 필요 없으며, 오프라인으로 작동합니다.
- 하나의 모델 파일, 재사용됩니다. 저는 이미 다른 프로젝트를 위해 이 Gemma 파일을 다운로드했기 때문에, 이를 이 앱의 저장소에 복사하고 게시된 체크섬(checksum)과 비교하여 SHA-256을 확인했습니다.
- 교체 가능합니다. 더 크고 빠른 모델이 필요할 때 동일한 하네스(harness)는 제 노트북의 Ollama(Qwen 3.5)와 통신합니다. 단지 설정 하나만 변경하면 됩니다.
- 오픈 런타임, 열린 조절 장치(knobs). 제약 디코딩(Constrained decoding)은 단일 최대 신뢰성 수정 사항이었으며, 이 런타임이 오픈 상태였기 때문에 제가 이를 사용할 수 있었습니다.
다른 연구자들도 같은 문제에 대해 작업하고 있습니다: Open-AutoGLM은 오픈폰 에이전트 프레임워크이며, local-first Android automation에 대한 이 게시물은 이러한 에이전트를 온디바이스(on-device)로 유지해야 한다고 주장하며, 15개 앱의 접근성 감사에 대한 이 글에서 제가 발견한 것과 일치합니다. 즉, 에이전트의 성능은 앱의 접근성 트리(accessibility tree)에 달려 있습니다.
안드로이드 개발자 여러분께: 조언을 구합니다
제가 플랫폼과 협력하기보다 그에 역행하고 있다고 의심되는 부분들입니다.
가장 중요한 것은 무엇일까요? 앱 자체 창 내에 그려지는 바텀 시트(bottom sheets)와 팝업은 항상 paneTitle이나 인식 가능한 클래스 이름을 설정하지 못하며, 그 뒤의 콘텐츠는 여전히 isVisibleToUser = true를 보고합니다. 실제로 눈에 보이고 위에 있는 것이 무엇인지 어떻게 신뢰성 있게 알 수 있을까요?
-
재렌더링 전반에 걸친 안정적인 식별자(Stable identity across re-renders). Zepto의 ID는 product-card-add-button<uuid>와 같아 훌륭하지만, 많은 앱(특히 Compose)은 아예 ID가 없습니다. 리스트가 재렌더링된 후에도 "같은 제품 카드에 있는 같은 버튼"을 지칭할 수 있는 견고한 방법이 있을까요?
-
입력 및 제출. 저는 ACTION_SET_TEXT를 사용한 다음 ACTION_IME_ENTER를 사용하고, 근처의 "검색(Search)" 버튼으로 대체합니다. 일부 앱은 실제 키 이벤트를 받거나 디바운스된 텍스트 감지기(debounced text watcher)에만 반응합니다. 임의의 앱 검색창에 가장 신뢰성 있게 타이핑하고 제출하는 방법은 무엇일까요?
-
오버레이 및 포커스. 제가 사용하는 플로팅 패널은 타이핑을 위해 포커스를 가져와야 하고, 그 다음 타겟 앱이 검색어와 Enter 키를 받을 수 있도록 다시 포커스를 반환해야 합니다. 저는 FLAG_NOT_FOCUSABLE을 토글하고 키보드를 숨깁니다. 더 깔끔한 패턴이 있을까요? 그리고 Android 14+ 및 Play의 accessibility-service 정책과 관련하여 무엇에 주의해야 할까요?
-
온디바이스 모델 속도. Gemma의 경우, 호출당 7~20초가 걸리는 대부분의 시간은 프롬프트(시스템 명령어와 약 50개의 화면 요소 포함)를 읽는 데 사용됩니다. LiteRT-LM을 사용하여 고정된 시스템 프롬프트에 대한 KV 캐시(KV cache)를 재사용할 수 있게 해줄까요, 아니면 2B 모델의 프롬프트를 작게 유지하는 더 나은 방법이 있을까요?
만약 AccessibilityService Android 자동화나 LiteRT-LM을 사용하여 무언가를 구축한 경험이 있다면 댓글로 알려주시면 정말 감사하겠습니다.
다음 계획 (What's next)
(원래 이 챌린지를 위해 많은 것을 계획했지만, 아쉽게도 시간이 충분하지 않았습니다💔
- 최신 빌드(셀프 검증 기술, wait-for-render, 최상위 레이어 전용)를 Zepto와 Blinkit에서 엔드투엔드로 재테스트.
- 위에 제시된 질문들에서 나온 조언들을 통합하기.
- Android의 온디바이스 음성 인식기를 사용한 음성 입력 기능 추가.
경품 부문 (Prize Categories)
- Gemma의 최적 활용: Gemma 4 E2B는 플래너(planner)로서 LiteRT-LM을 통해 온디바이스에서 실행되며, 정규표현식 제약 디코딩(regex-constrained decoding)과 함께 명령어(command) → 체크리스트(checklist) → 행동 청크(action chunks) 순서로 작동합니다.