클로드 코드에서 iOS 시뮬레이터 조작을 최적화할 수 있을까? - 2
1편은 이렇게 끝냈다.
다음에는
idb같은 도구를 이용해 이 부분을 줄여보려고 한다.
이후 방법을 더 확인해 본 결과, idb 대신 Xcode 내부의 시뮬레이터 입력 경로를 직접 이용하기로 했다.
shell에서 탭을 실행할 수 있게 되면서 첫 실행 기준 툴 호출은 48회에서 14회로 줄었다.
그런데 전체 경과 시간은 기대만큼 줄지 않았다.
그 이유를 찾는 과정이 이 편의 내용이다. 1편에서 세운 수식이 여기서 깨진다.
먼저 결과
같은 과제(검색 → 첫 상품 상세 → 색상 필터 적용)를 도구 있는 조건과 없는 조건으로 각각 돌렸다. 하나의 세션 안에서 순서의 영향을 보려고 순서를 바꿔 두 세션에서 총 네 번 실행했다.
| 세션 | 조건 | 순서 | 툴 호출 | 그중 시뮬레이터 MCP 호출 | 이미지 읽기 |
|---|---|---|---|---|---|
| 1 | 없음 | 먼저 | 48회 | 29회 | 14회 |
| 1 | 있음 | 나중 | 14회 | 0회 | 0회 |
| 2 | 있음 | 먼저 | 14회 | 0회 | 1회 |
| 2 | 없음 | 나중 | 20회 | 18회 | 9회 |
빗나간 탭은 4 시도 모두 0회였다.
시뮬레이터 MCP 도구 호출을 0으로 만들어 그 도구가 왕복을 강제하던 부분을 없앤 것이 이번 작업의 성과다.
시간은 이 표에 넣지 않았다. 세션마다 모델의 추론 속도가 달라서 표 하나로는 비교가 안 된다.
1. idb를 안 쓴 이유
idb를 쓰려면 다음 도구를 설치해야 한다.
brew tap facebook/fb
brew install idb-companion
pipx install fb-idb --python python3.11
서드파티 바이너리와 Python 실행 환경이 추가로 필요했다. 다른 개발자가 사용할 때 설치해야 하는 의존성이 늘어난다는 점이 부담이었다.
기성 시뮬레이터 MCP 서버들도 알아봤다. whitesmith/ios-simulator-mcp, joshuayoes/ios-simulator-mcp 같은 것들이다.
확인한 두 서버는 모두 idb 래퍼였고, 동작 하나당 도구 호출 하나다.
따라서 지금 쓰는 MCP를 다른 MCP로 바꾸는 것에 불과하다.
2. simctl 밖의 입력 경로
shell에서 탭을 실행하려면 시뮬레이터에 좌표 입력을 전달하는 경로가 필요하다. 하지만 simctl에는 탭을 실행하는 명령이 없다.
Simulator.app에서 마우스를 클릭하면 Indigo HID 메시지가 만들어져 시뮬레이터로 전달된다.
simctl은 이 기능을 제공하지 않지만, Xcode의 SimulatorKit.framework에는 같은 입력을 보내는 함수와 클래스가 있었다.
IndigoHIDMessageForMouseNSEvent (C 함수)
SimDeviceLegacyHIDClient initWithDevice:error:
sendWithMessage:freeWhenDone:completionQueue:completion:
공개 헤더나 .swiftinterface는 없었지만 Objective-C 런타임을 통해 호출할 수 있었다.
Swift 클래스의 이니셜라이저도 initWithDevice:error:라는 이름으로 노출돼 있었다.
메시지 형식은 idb가 공개한 헤더를 참고했다.
좌표는 0에서 1 사이의 비율이고, 타깃 값은 0x32, 단일 터치 메시지의 크기는 320바이트다.
이를 약 250줄의 Objective-C 파일로 만들고 clang으로 컴파일했다.
./simui tap $UDID 340 425
첫 실행에서 지정한 좌표의 항목이 정상적으로 열렸다. 별도의 런타임 의존성은 추가하지 않았지만, Xcode 내부 구현을 사용하므로 Xcode 업데이트에 따라 동작이 깨질 수 있다.
3. 여러 탭을 한 번의 호출로 묶기
시뮬레이터 MCP는 동작 하나마다 도구를 한 번 호출한다. 탭과 화면 확인을 반복하면 그만큼 모델 왕복도 늘어난다.
반면 Claude Code의 Bash 도구는 한 번의 호출 안에서 여러 작업을 이어서 실행할 수 있다.
simui로 탭을 실행할 수 있게 되면서 탭과 화면 확인을 하나로 묶었다.
./simui tap $U 367 576; sleep 1; xcrun simctl io $U screenshot a.png
./simui tap $U 248 225; sleep 1; xcrun simctl io $U screenshot b.png
./simui tap $U 46 325; sleep 1; xcrun simctl io $U screenshot c.png
bash sim-observe.sh $U
탭 3회, 중간 스크린샷 3장, 마지막 화면 확인까지 Bash 도구 호출 한 번으로 실행했고 7초가 걸렸다.
MCP 방식에서는 탭과 화면 확인을 각각 호출해야 하므로 모두 7회가 필요하다.
1편에서 측정한 호출당 약 10초를 적용하면 약 70초다.
사람의 승인 없이 실행할 수 있다는 차이도 있었다.
측정 중 새 시뮬레이터에서 MCP 접근 승인 프롬프트가 나타나 121초 동안 작업이 멈춘 적이 있었다.
하지만 simui는 MCP 접근 승인을 사용하지 않으므로 이 프롬프트의 영향을 받지 않았다.
4. 좌표를 미리 몰라도 조작하기
simui로 탭하려면 좌표가 필요하다. 좌표를 미리 찾아 문서에 기록하는 대신, 1편에서 만든 로컬 OCR로 실행할 때마다 텍스트의 위치를 찾도록 했다.
tap-text.sh는 스크린샷에서 지정한 텍스트를 찾고, 인식된 영역의 중심 좌표를 simui에 전달한다.
bash tap-text.sh $U "색상"
bash tap-text.sh $U "블랙"
bash tap-text.sh $U "개 상품보기"
세 번의 실행에는 좌표가 직접 들어가지 않지만, 색상 탭과 블랙 항목을 차례로 눌러 필터를 적용할 수 있었다.
Vision이 인접한 탭 라벨을 하나의 문자열로 합쳐서 인식하는 문제도 있었다.
종류 색상 가격 전체 영역의 중심을 누르면 원하는 탭이 아니라 다른 위치가 선택됐다.
인식된 행 전체의 좌표를 사용하는 대신 boundingBox(for:)로 일치한 문자열 범위의 좌표를 받도록 수정했다.
행 단위: 235,225 종류 색상 가격
--find 색상: 250,225 색상
--find 종류: 193,225 종류
OCR로 찾은 색상의 좌표는 (250,225)였고, 직접 측정한 좌표 (248,225)와 2pt 차이였다.
이를 통해 화면별 좌표를 미리 기록하지 않고도 텍스트가 있는 요소를 조작할 수 있었다.
5. 전체 경과 시간을 다시 측정하지 않았다
simui를 만든 뒤 네 차례의 개선 작업을 진행했다. 이 과정에서 기록한 값은 명령 실행 시간과 툴 호출 수였다.
지시를 받은 순간부터 완료 보고가 끝날 때까지의 전체 경과 시간은 다시 측정하지 않았다.
1편에서는 전체 시간을 235.8초, 533초, 163초로 기록했지만 2편에 들어와서는 그 기준을 놓쳤다. 툴 호출 수는 바로 셀 수 있지만, 전체 경과 시간은 세션 기록을 다시 확인해야 알 수 있었다. 쉬운 것만 재고 있었다.
세션 기록에서 전체 시간을 다시 계산하니 237초였다. 1편의 출발점은 235.8초였다. 서로 다른 세션의 값이라 직접 비교할 수는 없지만, 수치만 보면 툴 호출을 줄이고도 전체 시간은 거의 달라지지 않은 것처럼 보였다.
6. 1편의 수식이 깨졌다
1편에서는 전체 경과 시간을 다음과 같이 설명했다.
경과 시간 ≈ 툴 호출 수 × 약 10초
237초를 다시 나눠보니 명령이 실제로 실행된 시간은 29초였다. 나머지 208초는 문서를 읽고 명령을 조립하거나, 결과를 판단하고 답변을 작성하는 데 쓰였다.
| 구간 | 초 |
|---|---|
| 문서 읽고 명령 조립 | 112 |
| 완료 보고 작성 | 37 |
| 호출 사이 판단 | 37 |
| 지시 받고 첫 명령까지 | 8 |
Bash 도구 호출 오버헤드(6회 × 2.3초) |
14 |
이 구분에서 툴 호출 수에 직접 비례한다고 본 시간은 마지막 14초였다. 나머지 시간은 호출 횟수보다 작업을 위해 알아내고 판단해야 하는 정보의 양에 영향을 받았다.
여러 동작을 하나의 호출로 묶어도 그 동작에 필요한 판단이 사라지지는 않는다. 첫 호출을 실행하기 전에 문서를 읽고 실행할 내용을 조립해야 하므로, 호출 사이에 있던 시간이 첫 호출 앞으로 이동한다.
명령 실행 시간은 전체의 12%였다. 네 차례의 개선 작업에서는 주로 이 12%를 줄이고 있었다.
7. 같은 세션에서 순서를 바꿔 측정하기
1편의 235.8초와 이번에 측정한 237초는 서로 다른 세션의 값이다. 세션에 따라 모델의 응답 시간이 달라지기 때문에 두 값을 직접 비교할 수 없다. 실제 측정에서도 툴 호출 한 번당 평균 경과 시간이 한 세션에서는 10초, 다른 세션에서는 34.7초로 3.5배 차이가 났다.
세션에 따른 영향을 줄이기 위해 하나의 세션 안에서 도구가 있는 조건과 없는 조건을 모두 실행했다. 같은 과제를 두 번째로 실행하면 화면과 좌표를 이미 알고 있어 유리해지므로, 두 세션의 실행 순서도 반대로 배치했다.
| 세션 | 먼저 실행 | 시간 | 나중에 실행 | 시간 |
|---|---|---|---|---|
| 1 | 도구 없음 | 608초 | 도구 있음 | 286초 |
| 2 | 도구 있음 | 269초 | 도구 없음 | 328초 |
시간에 조건, 실행 순서, 세션의 효과가 각각 곱해진다고 가정하고 네 값을 분리했다.
| 요인 | 계산 결과 |
|---|---|
| 도구 조건 | 도구가 있을 때 1.61배 빠름 |
| 실행 순서 | 두 번째 실행에서 24% 단축 |
| 세션 | 두 세션 사이에 29% 차이 |
모형에서 계산한 도구 조건의 차이는 1.61배였다. 처음 기대했던 6배에는 미치지 못했다.
다만 관측값 네 개로 기준값과 세 요인을 계산한 포화 모형이므로 오차를 확인할 잔차가 남지 않는다. 조건과 실행 순서 사이에 상호작용이 없다고 가정한 결과이기도 하다. 따라서 1.61배는 이번 두 세션을 설명하는 값이며, 일반적인 성능 개선 수치로 해석할 수는 없다.
8. 도구가 줄인 것은 초기 탐색 비용이었다
두 조건의 관측값을 실행 순서에 따라 나누면 다음과 같다.
도구 없음: 608초(먼저), 328초(나중)
도구 있음: 269초(먼저), 286초(나중)
이번 두 세션에서는 도구가 있을 때 실행 순서에 따른 차이가 작았다. 반면 도구가 없는 첫 실행에서는 딥링크 형식과 좌표계를 확인하는 데 시간이 추가로 필요했다.
조건별 관측값이 두 개뿐이고 각 값에는 세션과 실행 순서의 영향이 함께 들어 있다. 따라서 이 결과만으로 도구가 실행 시간의 변동성을 줄였다고 일반화할 수는 없다. 다만 첫 실행에서 어떤 탐색 작업에 시간이 쓰였는지는 세션 기록으로 확인할 수 있었다.
먼저 MCP 도구의 text 액션은 한글을 입력하지 못했다. 테스트를 입력하면 다음 결과가 돌아왔다.
Typed 0 characters (3 unsupported characters dropped;
only printable ASCII and newline can be typed)
한글을 입력하기 위해 딥링크로 우회해야 했다.
도구가 없는 조건에서는 Info.plist의 URL scheme부터 딥링크 타입과 URL 파라미터 형식까지 관련 소스를 차례로 확인했고, 이 과정에 84초가 걸렸다.
스크린샷과 탭 좌표의 단위도 달랐다. MCP가 반환한 스크린샷의 너비는 919px이고 시뮬레이터의 화면 너비는 402pt였다. 이미지에서 읽은 좌표를 그대로 사용하면 탭 위치가 어긋나므로 배율을 계산해야 했다.
두 번째 실행에서는 앞서 확인한 정보를 다시 사용할 수 있어 추가 탐색이 필요하지 않았다. 도구가 있는 조건에서는 딥링크 형식을 문서에서 읽고, 탭 좌표는 OCR로 찾기 때문에 처음부터 이 과정을 생략할 수 있었다.
이번에 만든 도구의 역할은 최고 속도를 높이는 것보다 처음 실행할 때 필요한 탐색 비용을 줄이는 데 가까웠다. 다른 개발자가 이 도구로 처음 보는 화면을 조작하는 상황에서 이 차이가 의미가 있다.
9. 탭 전송 성공과 화면 변화는 다르다
simui가 TAP=ok를 반환하는 것은 탭 메시지를 정상적으로 전송했다는 뜻이다. 실제 화면이 바뀌었다는 뜻은 아니다.
측정 과정에서 메시지는 전송됐지만 화면이 바뀌지 않은 경우를 네 종류 확인했다.
| 원인 | 확인한 상황 |
|---|---|
| 화면이 자동으로 스크롤해 기존 좌표가 달라짐 | 필터 시트에서 대상 항목이 200pt 이동 |
| 앱이 짧은 간격의 두 번째 탭을 처리하지 않음 | 토글 버튼 |
| 탭해도 동작하지 않는 컨트롤 | 동작이 구현되지 않은 탭을 28회 눌러도 반응 없음 |
| 스크립트의 버그 | pt 값이 subshell을 통과하지 못해 잘못된 좌표로 전송 |
각 원인에 맞는 예외 처리를 추가하는 대신, 탭을 실행한 뒤 기대한 결과가 나타났는지 확인하도록 했다.
bash tap-text.sh $U "상세" --expect "상세 화면"
스크립트는 기대한 텍스트가 나타나는지 최대 6초 동안 확인한다.
나타나지 않으면 좌표를 다시 찾아 탭하고, 기본 설정으로 최대 세 번까지 시도한다.
그래도 화면이 바뀌지 않으면 TAP=noeffect와 기다리던 텍스트를 출력하고 종료한다.
동작하지 않는 컨트롤을 실제로 누를 수 있게 만드는 방법은 아니다. 대신 탭 메시지를 보냈다는 이유만으로 작업을 성공 처리하지 않는다. 실패 원인을 미리 구분하지 않아도 같은 방식으로 결과를 확인할 수 있다.
이 과정에서는 검증 방법 자체의 문제도 발견했다. 스크립트의 기본값 경로에 버그가 있었지만 테스트에서는 좌표를 항상 인자로 전달해 해당 코드가 한 번도 실행되지 않았다. 직접 사용한 경로뿐 아니라 다른 개발자가 처음 사용할 기본값 경로도 따로 검증해야 했다.
그래서 최적화가 됐나
최적화는 됐다. 첫 실행 기준으로 같은 과제의 툴 호출은 48회에서 14회로 줄었고, 시뮬레이터 MCP 호출은 29회에서 0회가 됐다.
탭 3회와 화면 확인 4회를 MCP로 각각 호출하면 약 70초로 예상되는 구간을 Bash 도구 호출 한 번, 7초에 실행했다.
하지만 이 결과가 전체 작업 시간을 10분의 1로 만들지는 않았다. 두 세션에서 조건, 실행 순서, 세션 차이를 나눠 계산한 결과는 1.61배였다.
이번 작업으로 줄인 것은 탭을 실행하고 확인하기 위해 반복하던 모델 왕복과, 처음 보는 화면에서 딥링크와 좌표를 찾아내는 비용이었다. 반면 문서를 읽고 실행할 내용을 조립하는 시간은 그대로 남았다. 결과적으로 시뮬레이터 조작은 빨라졌지만, 전체 작업의 병목은 다른 곳으로 이동했다.
10. 다음 병목은 에이전트가 읽는 문서다
작업이 끝났을 때 사용 방법을 정리한 문서는 889줄이었다. 이번 측정에서는 전체 시간의 47%가 문서를 읽고 실행할 내용을 조립하는 데 쓰였다. 측정한 구간 중 가장 큰 비중이었다.
문서 앞부분에 실행 내용을 조립하기 위한 양식을 추가했지만 뚜렷한 효과는 확인하지 못했다. 문서를 읽고 조립하는 데 70초, 문서 없이 소스에서 같은 정보를 찾는 데 84초가 걸렸다. 한 번씩 측정한 값이므로 두 방식의 차이라고 단정할 수는 없다.
문서는 딥링크 형식과 입력 방법을 찾는 탐색을 줄여줬다. 하지만 문서의 모든 내용이 매번 필요한 것은 아니었다. 좌표를 정리한 표를 읽지 않은 실행에서도 전체 시간은 11%만 늘었다.
이번 시뮬레이터 최적화를 진행하면서 문제의 범위가 도구를 벗어날 수 있다는 것을 다시 느꼈다. LLM 모델이 계속 발전하면서 이전에는 모델의 능력에 가려졌던 하네스와 스킬의 문서가 오히려 에이전트 동작의 병목으로 드러날 수 있다. 에이전트는 작업을 시작하기 전에 이 문서들을 읽고, 필요한 정보를 고르고, 실행할 행동으로 바꿔야 하기 때문이다.
문서가 없으면 에이전트는 소스를 다시 탐색하고 이미 해결한 문제를 반복한다. 반대로 문서가 계속 길어지면 현재 작업과 관계없는 정보까지 읽고 조립하는 비용이 늘어난다. 모델이 발전하더라도 모델이 읽는 문서가 함께 바뀌지 않으면 에이전트 전체가 계속 나아지는 것은 아니다.
하네스와 스킬의 문서도 코드처럼 한 번 만들면 끝나는 것이 아니라는 사실은 이미 알고 있었다. 그래도 이번 측정에서 889줄의 문서가 가장 오래 걸리는 구간이 되는 것을 보면서 그 사실을 다시 확인했다. 실제 실행을 관찰하면서 필요 없는 내용을 지우고, 자주 빠지는 규칙은 도구로 옮기고, 필요한 정보만 제때 읽을 수 있도록 구조를 계속 바꿔야 한다.
에이전트를 위한 문서도 코드와 마찬가지로 계속 유지보수하고 개선해야 하는 시스템의 일부이다.
댓글 남기기