Bldev's Blog

[테스트 자동화] Appium으로 iOS 앱 화면을 Python에서 눌러 보기

2026. 10. 30.

앱을 고칠 때마다 사람이 시뮬레이터를 켜고 버튼을 눌러 보는 일은 금방 감당이 안 된다. Appium은 이 일을 코드로 옮긴다. 어떤 버튼을 누르고 무엇을 입력하고 결과가 맞는지 판정하는 일을 Python 같은 일반 언어로 쓰고, 그 명령을 기기 안에서 실제 탭과 입력으로 바꿔 준다. 앱 소스를 고치지 않고, 빌드된 앱을 바깥에서 조작한다.

Appium은 컴퓨터에서 돌고 기기 안의 도우미 앱이 화면을 누른다

처음 보면 "기기에 Appium을 설치하는 것인가"가 헷갈린다. Appium 서버와 드라이버는 컴퓨터에 설치한다. 기기에는 드라이버가 도우미 앱을 자동으로 올린다.

  • Appium 서버: 테스트 코드의 명령을 HTTP로 받는 중계 프로그램이다. API는 브라우저 자동화 표준인 W3C WebDriver 명세를 따르고, 이 명세는 HTTP 기반 프로토콜이다(Appium 소개).
  • 드라이버: 플랫폼마다 붙이는 모듈이다. iOS는 XCUITest 드라이버, Android는 UiAutomator2 드라이버를 쓴다. 공식 소개는 Appium을 코어, 드라이버, 클라이언트, 플러그인 네 부분으로 나눈다.
  • WebDriverAgent: iOS에서 실제로 화면을 누르는 앱이다. iOS는 다른 앱이 화면을 조작하지 못하게 막는다. Apple이 열어 둔 통로가 UI 테스트 프레임워크 XCTest이고, 그 위에서 동작하는 이 앱이 Mac의 명령을 대신 수행한다.

iOS 기기를 다루는 도구(Xcode, XCTest)가 macOS에만 있어서 iOS 테스트에는 Mac이 필요하다. 실제 아이폰은 USB로 Mac에 연결하고, 시뮬레이터는 Mac 안에서 돈다. 실기기에서는 WebDriverAgent도 서명해야 하므로 Apple 개발자 계정과 프로비저닝 프로파일이 필요하다. 시뮬레이터는 서명이 필요 없어 처음 써 보기에 알맞다.

준비물은 Node, Xcode, 드라이버, 테스트 코드다

항목 내용
Node.js ^20.19.0 || ^22.12.0 || >=24.0.0, npm 10 이상 (요구사항)
Xcode macOS 호스트에 설치. 드라이버 버전마다 지원 Xcode가 다르다 (XCUITest 요구사항)
Appium 서버 본체와 XCUITest 드라이버
클라이언트 테스트를 쓸 언어의 라이브러리. Python은 Appium-Python-Client
앱 시뮬레이터에 설치된 앱의 bundle id, 또는 .app 경로

공식 문서의 설치법은 npm install -g appium으로 전역 설치하는 것이다. 시스템을 더럽히고 싶지 않으면 작업 폴더에 로컬로 설치하고, 드라이버가 들어갈 자리를 APPIUM_HOME으로 정해 둘 수 있다. 아래는 Node 26.0.0, Xcode 27.0, macOS에서 실행한 순서다.

bash
mkdir appium-trial && cd appium-trial
npm init -y
npm install appium
export APPIUM_HOME=$PWD/home        # 드라이버를 이 폴더에 둔다
npx appium -v                        # 3.9.0
npx appium driver install xcuitest
ℹ Driver xcuitest@12.22.7 successfully installed
- automationName: XCUITest
- platformNames: ["iOS","tvOS","watchOS"]

Python 쪽은 가상환경에 클라이언트만 설치한다(이 글에서는 6.0.9).

bash
python3 -m venv venv
venv/bin/pip install Appium-Python-Client

서버는 별도 터미널에서 띄운다. 기본 포트는 4723이다.

bash
APPIUM_HOME=$PWD/home npx appium --address 127.0.0.1 --port 4723 --log server.log

누를 버튼과 판정은 모두 Python 코드가 정한다

Appium은 명령을 전달하고 화면 상태를 돌려줄 뿐이다. 무엇을 누를지, 결과가 맞는지는 테스트 코드가 정한다. 세션을 열 때 보내는 설정 묶음을 capabilities라고 하고, 여기에 어느 기기에서 어떤 앱을 띄울지 적는다.

예제 앱은 iPhone용 마크다운 편집기다. 시나리오는 새 문서를 만들고 글을 입력한 뒤 목록으로 돌아가 문서가 생겼는지 확인하는 것이다.

python
import sys, time
from appium import webdriver
from appium.options.ios import XCUITestOptions
from appium.webdriver.common.appiumby import AppiumBy

opts = XCUITestOptions()
opts.udid = sys.argv[1]                 # 시뮬레이터 UDID (xcrun simctl list devices)
opts.bundle_id = "com.example.editor"   # 이미 설치된 앱
opts.new_command_timeout = 120

driver = webdriver.Remote("http://127.0.0.1:4723", options=opts)
driver.implicitly_wait(5)               # 요소가 나타날 때까지 최대 5초 기다린다
try:
    # 1. 버튼을 접근성 이름으로 찾아 누른다
    driver.find_elements(AppiumBy.ACCESSIBILITY_ID, "새 문서")[0].click()

    # 2. 편집기에 입력하고 값을 다시 읽어 판정한다
    editor = driver.find_element(AppiumBy.CLASS_NAME, "XCUIElementTypeTextView")
    editor.click()
    editor.send_keys("Appium hello")
    assert "Appium hello" in (editor.get_attribute("value") or "")

    # 3. 내비게이션 바의 뒤로 버튼
    driver.find_element(AppiumBy.IOS_CLASS_CHAIN,
                        "**/XCUIElementTypeNavigationBar/**/XCUIElementTypeButton").click()

    # 4. 목록에 셀이 생겼는지 판정한다
    cells = driver.find_elements(AppiumBy.CLASS_NAME, "XCUIElementTypeCell")
    assert cells, "목록에 문서가 없음"
    driver.save_screenshot("after.png")
    print("통과")
except Exception:
    driver.save_screenshot("failure.png")   # 실패한 순간의 화면
    raise
finally:
    driver.quit()

find_element, click 같은 호출은 서버로 가는 HTTP 요청이 된다. 서버가 그 요청을 드라이버에 넘기고, 기기 안의 WebDriverAgent가 접근성 트리에서 요소를 찾아 탭한다. "결과를 받는다"는 화면에서 요소를 다시 찾아 값, 표시 여부, 개수를 읽어 오는 것이다. 앱 내부 변수는 보이지 않고 화면에 드러난 것만 판정할 수 있다.

iOS 26.1 시뮬레이터에서 이 시나리오는 4단계를 모두 지나 5.3초에 끝났다. 스크린샷에는 입력한 글이 든 편집기와, 목록에 생긴 문서 한 줄이 남았다.

새 시뮬레이터의 첫 세션은 이 환경에서 50초 걸렸다

방금 복제한 시뮬레이터에서 첫 세션이 열리기까지 50.0초가 걸렸다. 한 번 잰 값이다. 드라이버가 WebDriverAgent를 빌드해 시뮬레이터에 설치하는 시간이 대부분으로 보인다. 그 뒤 시나리오 본체는 몇 초 안에 끝났다. WebDriverAgent가 뜨기를 기다리다 세션이 실패하면 capabilities의 appium:wdaLaunchTimeout을 늘린다. 드라이버 소스의 오류 문구가 WebDriverAgent의 /status 응답을 못 받았을 때 이 값을 늘리라고 안내한다. CI에서 매번 새 기기를 쓰면 이 비용을 매번 치를 수 있다.

셀 제목이 None으로 읽혀 개수로만 판정했다

목록 화면에는 문서 이름 "제목 없음"이 보였는데, 셀의 label 속성은 None으로 읽혔다. 글자가 셀 자신이 아니라 셀 안의 다른 요소에만 접근성 속성으로 노출된 것으로 보인다. 그래서 위 코드는 셀이 하나 이상 있는지만 확인한다. "방금 만든 그 문서"인지까지 판정하려면 셀 안의 글자 요소를 찾거나, 앱 쪽에서 셀에 accessibility identifier나 label을 달아야 한다.

요소를 찾는 방법마다 깨지기 쉬운 정도가 일반적으로 다르다.

찾는 방법 깨지는 경우
accessibility identifier 개발자가 이름을 바꿀 때만
화면 글자(label) 문구 수정, 다국어
클래스 이름, class chain 화면 구조가 바뀔 때
XPath, 좌표 레이아웃이 조금만 바뀌어도

테스트를 오래 유지하려면 앱 개발 단계에서 주요 버튼과 셀에 식별자를 달아 두는 편이 낫다.

리포트와 CI는 Appium 바깥의 도구가 맡는다

Appium은 리포트를 만들지 않는다. 통과와 실패는 테스트 러너(pytest, JUnit 등)가 정하고 HTML 리포트는 그 러너의 리포터(Allure, pytest-html 등)가 만든다. Appium이 내놓는 것은 스크린샷(save_screenshot), 화면 구조 XML(page_source), 서버 로그, 화면 녹화(start_recording_screen, ffmpeg 필요) 같은 증거 자료다.

CI에서는 Appium이 명령줄 서버라서 파이프라인 단계 하나로 띄울 수 있다. iOS는 macOS 러너가 필요하다. GitHub Actions의 macOS 러너에서 시뮬레이터를 띄우고 로그와 스크린샷을 actions/upload-artifact로 올리는 예가 있다. Mac을 직접 둘 수 없으면 BrowserStack, Sauce Labs, AWS Device Farm 같은 클라우드 기기 팜의 원격 Appium 서버에 접속한다. BrowserStack은 Jenkins 플러그인으로 결과를 빌드 화면에 붙인다.