본문으로 건너뛰기
중급약 45분

Appium 심화 — 로케이터 전략, 하이브리드 앱 컨텍스트 전환, W3C Actions 제스처

모듈 13에서 Appium이 무엇인지 배웠다면, 여기서는 깨지지 않는 모바일 자동화를 쓰는 법을 다룬다. 접근성 id를 1순위로 두는 로케이터 우선순위와 Android/iOS 전략 차이, 하이브리드 앱의 NATIVE_APP↔WEBVIEW 컨텍스트 전환, W3C Actions API로 스와이프·롱프레스·핀치를 조립하는 법, 암묵적·명시적 대기와 모바일 특유의 불안정 요인을 정리한다. Appium 2의 드라이버 분리 구조도 짚는다.

  • #Appium
  • #로케이터전략
  • #하이브리드앱
  • #W3CActions
  • #모바일자동화

개념

이 레슨의 출발점

모듈 13 레슨 7에서 Appium이 무엇이고 웹 자동화와 무엇이 다른지 배웠고, 레슨 3·5·6에서 견고한 로케이터·대기 전략·불안정 관리 원칙을 웹 기준으로 익혔다. 이 레슨은 그 원칙을 모바일의 실제 API 위에서 다시 쓴다 — 같은 단어를 쓰지만 구현과 함정이 전부 다르기 때문이다.

Appium 2 이후의 구조 — 드라이버가 분리되어 있다

Appium은 네 덩어리로 나뉜다. 코어(API 정의), 드라이버(플랫폼에 붙는 부분), 클라이언트(각 언어 라이브러리), 플러그인(기능 확장). 예전에는 서버 하나에 모든 플랫폼 지원이 들어 있었지만, 지금은 서버를 설치한 뒤 필요한 드라이버를 따로 설치 한다.

npm install -g appium                    # 서버(코어)
appium driver install uiautomator2       # Android
appium driver install xcuitest           # iOS
appium driver list --installed           # 설치 상태 확인

QA가 이 구조에서 얻는 것은 세 가지다. 버전 문제를 분리해서 말할 수 있고("Appium이 안 돼요"가 아니라 "서버는 정상인데 UiAutomator2 드라이버가 특정 Android 버전에서 실패한다"), 플랫폼별 업그레이드를 따로 하며(iOS 새 버전 대응은 XCUITest 드라이버만 올리면 되는 경우가 많다), 드라이버가 곧 기능 범위다(mobile: 확장 명령은 드라이버가 제공하므로 플랫폼마다 목록이 다르다).

Appium은 W3C WebDriver 프로토콜을 기반으로 한다. 오래된 예제의 TouchAction·MultiTouchAction 은 더 이상 권장되지 않으며, 제스처는 아래의 W3C Actions 또는 드라이버의 mobile: 확장 명령으로 쓴다. 서버 기본 경로와 각 API의 지원 중단 시점 등 버전에 얽힌 세부는 바뀌므로 Appium 공식 문서에서 원문 확인 한다.

로케이터 전략 — 우선순위를 고정한다

모바일 UI는 웹보다 구조가 자주 바뀌고, 같은 화면이라도 플랫폼마다 트리가 완전히 다르다. 로케이터는 취향이 아니라 팀 규칙 이어야 한다.

순위전략왜 이 순위인가
1접근성 id (accessibility id)양 플랫폼 공통. 의도적으로 붙인 값이라 UI가 바뀌어도 잘 안 변한다. 접근성 검증(레슨 6)과 같은 자산을 쓴다
2플랫폼 고유 id — Android resource-id, iOS accessibility identifier빠르고 안정적이지만 플랫폼별로 코드가 갈린다
3플랫폼 고유 질의 — Android -android uiautomator, iOS -ios predicate string / -ios class chain네이티브 엔진이 직접 평가해 XPath보다 빠르고 표현력이 좋다
4화면에 보이는 텍스트문구·번역이 바뀌면 깨진다. 다국어 앱에서는 사실상 금지
5XPath최후의 수단. 느리고 트리 구조 변화에 극도로 취약하다

1순위를 강조하는 이유가 중요하다. 접근성 id는 자동화를 위한 별도 비용이 아니라 접근성 레이블과 같은 기반 이다. "자동화 때문에 id를 붙여 달라"가 아니라 "접근성과 자동화가 같이 요구하는 것"이라는 프레이밍이 개발팀 설득에 훨씬 잘 먹힌다.

Android와 iOS는 같은 이름이 다른 속성에 매핑된다. 이걸 모르면 "왜 Android에서만 되지?"가 반복된다.

개념Android(UiAutomator2)iOS(XCUITest)
접근성 idcontent-descaccessibilityIdentifier(없으면 label로 대체되기도)
고유 idresource-id (com.example:id/btn_pay)accessibility identifier
화면 텍스트 · 요소 종류text · class(android.widget.Button)label/value · type(XCUIElementTypeButton)
고유 질의-android uiautomator — new UiSelector().textContains("결제")-ios predicate string — label CONTAINS '결제', -ios class chain

그래서 실무에서는 플랫폼별 로케이터를 한 곳에 모으고 페이지 오브젝트는 공통으로 쓰는 구조를 택한다(모듈 13 레슨 4의 패턴을 모바일로 옮긴 형태다).

PAY_BUTTON = {   # 접근성 id 를 양쪽 같은 문자열로 맞추면 분기 자체가 사라진다
    "android": (AppiumBy.ACCESSIBILITY_ID, "pay_button"),
    "ios":     (AppiumBy.ACCESSIBILITY_ID, "pay_button"),
}
AMOUNT_FIELD = {
    "android": (AppiumBy.ID, "com.example:id/amount"),
    "ios":     (AppiumBy.IOS_PREDICATE, "type == 'XCUIElementTypeTextField' AND name == 'amount'"),
}

XPath를 금지하지는 않되, 쓰는 순간을 기록한다. id가 없는 화면은 반드시 남는다. XPath로 임시로 막되 "id 요청" 티켓을 같이 만든다. 절대 하지 말 것은 도구가 자동 생성한 절대 경로 XPath(/hierarchy/android.widget.FrameLayout[1]/…)를 그대로 쓰는 것이다 — 한 번의 레이아웃 변경으로 전부 깨진다.

로케이터를 고르려면 실제 트리를 봐야 한다. Appium Inspector 가 표준 도구이고, Android는 Layout Inspector, iOS는 Xcode의 Accessibility Inspector(레슨 6)로 교차 확인한다. 세션 중에는 driver.page_source 로 현재 화면의 XML 트리를, driver.get_window_size() 로 제스처 좌표 계산의 기준이 되는 논리 좌표계 크기를 얻는다. page_source 는 현재 컨텍스트 의 트리만 준다 — 네이티브 컨텍스트에서 웹뷰 안을 찾으면 아무것도 안 나온다.

하이브리드 앱 — NATIVE_APP과 WEBVIEW를 오간다

하이브리드 앱은 네이티브 껍데기 안에 웹뷰가 있다. Appium은 둘을 컨텍스트 로 구분하고 한 번에 하나만 본다.

contexts = driver.contexts          # ['NATIVE_APP', 'WEBVIEW_com.example.app', ...]
driver.switch_to.context(next(c for c in contexts if c.startswith("WEBVIEW")))
driver.find_element(AppiumBy.CSS_SELECTOR, "#submit").click()   # 웹 로케이터가 동작한다
driver.switch_to.context("NATIVE_APP")   # 반드시 되돌아온다

실무에서 걸리는 지점은 API가 아니라 환경 이다.

  • 웹뷰가 목록에 안 나온다. 대개 앱의 웹뷰가 디버깅 가능 상태가 아니어서다. Android는 앱 코드에서 웹뷰 디버깅을 켜 줘야 하고(디버그 빌드에서만 켜는 팀이 많다), iOS는 웹 인스펙터 계열 설정이 필요하다. "테스트 빌드에서는 웹뷰 디버깅을 켠다"를 팀 합의로 만드는 것 이 이 레슨의 실무 조치다
  • Android는 Chromedriver가 따로 필요하다. 기기의 시스템 WebView·Chrome 버전과 맞는 것이 있어야 하고, 어긋나면 전환이 실패한다. 기기마다 WebView 버전이 다르므로 디바이스 팜에서 특정 기기만 실패하는 전형적 원인 이다(레슨 4의 "축으로 가르기")
  • 컨텍스트 이름이 여러 개다. 첫 번째를 무조건 집는 코드는 언젠가 엉뚱한 웹뷰를 잡는다. 제목이나 URL로 식별하는 보조 로직을 둔다
  • 전환 직후가 비어 있다. 문서가 아직 로드되지 않은 상태가 흔하다. 전환 후 명시적 대기 를 건다
  • 되돌아오지 않는다. 웹뷰에 머문 채 네이티브 요소를 찾으면 전부 실패한다. try/finally 로 원복을 보장한다
@contextmanager                       # 전환·대기·원복을 한 군데로 모은다
def webview(driver, timeout=10):
    original = driver.current_context
    handle = WebDriverWait(driver, timeout).until(
        lambda d: next((c for c in d.contexts if c.startswith("WEBVIEW")), None))
    driver.switch_to.context(handle)
    try:
        yield
    finally:
        driver.switch_to.context(original)

"웹뷰가 보이는데 클릭이 안 된다"의 상당수는 네이티브 좌표와 웹 좌표가 어긋난 문제 다. 키보드나 네이티브 바가 웹뷰를 가리면 웹 로케이터는 요소를 찾지만 실제 탭은 다른 곳에 떨어진다. 네이티브 컨텍스트로 돌아가 스크롤하거나 키보드를 내린 뒤 다시 시도한다.

W3C Actions API — 제스처를 직접 조립한다

스와이프·롱프레스·핀치에는 전용 명령이 없다. 입력 장치를 정의하고 동작을 시간순으로 나열해 보내는 것이 W3C WebDriver의 방식이다. 구조는 네 가지 입력 소스(none, key, pointer, wheel)와 동작 항목(pointerMove, pointerDown, pointerUp, pause)으로 이루어진다. 포인터는 mouse·pen·touch 중 하나이고 모바일에서는 touch를 쓴다. pointerMove 의 기준점(origin)은 viewport·pointer·element 중에서 고른다. 서버 엔드포인트는 POST /session/{id}/actions(수행)와 DELETE /session/{id}/actions(입력 상태 해제)다.

스와이프 — 손가락 하나가 내려가고, 움직이고, 떼는 세 동작이다.

def swipe(driver, start, end):
    finger = PointerInput(interaction.POINTER_TOUCH, "finger")
    actions = ActionBuilder(driver, mouse=finger)
    actions.pointer_action.move_to_location(*start)
    actions.pointer_action.pointer_down()
    actions.pointer_action.pause(0.1)      # 관성 대신 안정적인 드래그
    actions.pointer_action.move_to_location(*end)
    actions.pointer_action.pointer_up()
    actions.perform()
 
size = driver.get_window_size(); cx = size["width"] // 2     # 항상 비율로 계산한다
swipe(driver, (cx, int(size["height"] * 0.8)), (cx, int(size["height"] * 0.2)))

좌표를 화면 크기의 비율로 계산 하는 것이 핵심이다. 절대 픽셀을 박으면 해상도가 다른 기기에서 전부 깨진다. 시작점은 가장자리에서 충분히 떨어뜨린다 — 가장자리는 OS의 뒤로 가기·알림창 제스처 영역이라 시스템이 먼저 먹는다.

롱프레스 는 같은 뼈대에서 pointer_down() 뒤에 pause(1.2) 를 넣고 떼면 된다. 핀치(확대/축소) 는 손가락이 둘이므로 포인터 입력 소스를 두 개 만들어 add_input() 으로 등록하고 서로 반대 방향으로 움직인다 — 두 시퀀스의 동작 개수를 맞추면 스텝 단위로 동기화된다.

실무 조언이 하나 붙는다. 드라이버가 mobile: 확장 명령을 제공하면 그것을 먼저 쓴다. UiAutomator2와 XCUITest 드라이버는 스크롤·스와이프·핀치에 해당하는 확장 명령을 제공하고, 플랫폼 네이티브 구현이라 직접 조립한 것보다 안정적인 경우가 많다. Actions API는 확장 명령으로 표현되지 않는 제스처 를 만들 때의 무기다. 지원 목록은 드라이버 문서에서 원문 확인 한다.

대기 — 암묵적과 명시적을 섞지 않는다

암묵적 대기(implicit wait) 는 세션 전역 설정으로 요소를 못 찾으면 지정 시간까지 재시도하고, 명시적 대기(explicit wait) 는 "이 조건이 참이 될 때까지"를 매번 지정한다.

driver.implicitly_wait(0)   # 암묵적 대기는 끄고 시작한다
WebDriverWait(driver, 15).until(
    EC.element_to_be_clickable((AppiumBy.ACCESSIBILITY_ID, "pay_button"))
).click()

둘을 같이 쓰면 대기 시간이 예측 불가능해진다. 암묵적 대기가 켜진 상태에서 "요소가 없어야 한다"를 확인하면 매번 전체 시간을 기다려 느려지고, 실패까지의 시간도 불규칙해진다. 암묵적 대기 0, 명시적 대기만 사용 으로 팀 규칙을 고정하는 편이 관리하기 쉽다.

모바일에서는 조건이 한 겹 더 필요하다. "존재한다"와 "탭할 수 있다"가 다르기 때문이다 — 애니메이션 중인 요소는 트리에 있지만 탭이 엉뚱한 좌표에 떨어진다. element_to_be_clickable 처럼 상호작용 가능성까지 보는 조건 을 쓰고, 그래도 불안정하면 "요소 좌표가 두 번 연속 같을 때까지" 같은 사용자 정의 조건을 만든다.

모바일 특유의 불안정 요인

모듈 13 레슨 6의 불안정 관리 원칙 위에, 모바일에만 있는 원인을 얹는다.

원인증상대응
화면 전환 애니메이션탭이 이전 화면에 떨어진다상호작용 가능 조건으로 대기. 테스트 환경에서 애니메이션 배율 0 설정
권한 팝업(레슨 2)엉뚱한 시점에 떠서 탭을 가로챈다세션 시작 전 adb pm grant 로 권한 선부여, 또는 자동 수락 옵션
키보드·시스템 대화상자요소를 가리거나 탭을 가로챈다입력 후 키보드를 닫고 진행. 기기 설정을 고정
웹뷰 로딩컨텍스트 전환 직후 빈 문서전환 후 문서 준비 상태를 대기
앱 콜드 스타트첫 실행만 느려 타임아웃앱 시작 대기를 별도 상수로 분리
기기 성능 편차저사양 기기에서만 실패기기별로 시간을 늘리지 말고 조건 기반 대기 로 전환
네트워크목록이 비어 단언 실패목을 쓰거나 데이터 준비를 API로(모듈 13 레슨 10)

저사양 기기에서 깨진다고 대기 시간을 늘리는 것은 해결이 아니다. 전체 실행이 느려지고 원인은 남는다. 조건 기반 대기로 바꾸면 빠른 기기에서는 빠르게, 느린 기기에서는 필요한 만큼만 기다린다.

실무에서 왜 필요한가

모바일 자동화가 실패하는 팀의 패턴은 거의 같다. 도구를 못 다뤄서가 아니라 XPath로 시작해 불안정을 시간 늘리기로 덮다가 아무도 결과를 안 믿게 되는 경로다. 로케이터 우선순위를 규칙으로 못 박고, 암묵적 대기를 끄고, 제스처를 비율 좌표로 쓰는 세 가지만 초기에 잡아도 유지보수 비용이 크게 달라진다.

하이브리드 컨텍스트 전환은 특히 국내 앱에서 자주 필요하다. 결제·본인확인·약관·이벤트 페이지가 웹뷰로 들어오는 구조가 흔해서, 컨텍스트를 못 넘으면 핵심 플로우 자동화가 통째로 막힌다. "웹뷰 디버깅을 테스트 빌드에서 켜 달라", "Chromedriver 버전 매칭을 CI에서 관리하자"를 제때 요청하는 것이 QA의 실질적 기여다.

채용시장에서의 위치

모바일 자동화 경력 면접에서 변별력이 큰 질문은 "로케이터를 어떻게 고르시나요"와 "하이브리드 앱은 어떻게 다루시나요" 두 개다. 전자는 접근성 id를 1순위로 두는 이유를 접근성과 자동화가 같은 자산을 쓴다 는 맥락으로 설명할 수 있는지가 갈림길이고, 후자는 컨텍스트 목록·Chromedriver 매칭·전환 후 대기까지 말할 수 있으면 해 본 사람으로 읽힌다. W3C Actions로 제스처를 조립해 본 경험은 그 위의 가점이다.

실습 과제

과제 1 — 로케이터 감사와 교체 (30분)

기존 모바일 자동화 코드(없다면 임의의 앱으로 새로 작성)에서 로케이터를 전부 뽑아 본문의 5단계 우선순위로 분류한다.

  1. 전략별 개수를 세어 표로 만든다 (접근성 id / 플랫폼 id / 고유 질의 / 텍스트 / XPath)
  2. XPath와 텍스트 로케이터 각각이 한 단계 위로 올라갈 수 있는지 판정한다
  3. 올릴 수 없는 것은 "접근성 id 요청" 목록으로 만들고, 사유에 접근성 검증에도 필요하다 는 근거를 덧붙인다
  4. Appium Inspector로 해당 화면의 트리를 열어 요청한 id가 붙으면 어떤 값이 될지 확인한다

과제 2 — 컨텍스트 전환과 제스처 구현 (40분)

웹뷰를 포함한 앱(자사 앱의 약관·이벤트 페이지가 가장 좋다)을 대상으로 아래를 구현한다.

print(driver.contexts)     # 먼저 목록부터 확인한다
  1. 컨텍스트 목록을 출력하고, 웹뷰가 보이지 않으면 원인을 조사해 기록 한다(디버깅 미활성, Chromedriver 불일치 등)
  2. 본문의 webview() 컨텍스트 매니저를 구현해 웹뷰 안 요소를 하나 조작하고 네이티브로 복귀한다
  3. W3C Actions로 비율 좌표 기반 스와이프 와 롱프레스 를 구현한다. 서로 다른 해상도의 기기 2대에서 같은 코드가 동작하는지 확인한다
  4. 같은 동작을 드라이버의 mobile: 확장 명령으로도 시도해 안정성과 코드 길이를 비교한다
  5. 실패가 났다면 본문의 불안정 요인 표 중 어디에 해당하는지 분류하고, 대기 시간을 늘려 덮은 곳 을 조건 기반 대기 후보로 목록화한다

자가 체크리스트

  • Appium의 코어·드라이버·클라이언트·플러그인 구분과 드라이버 설치 방식을 설명할 수 있다
  • 로케이터 5단계 우선순위를 말하고 각 순위의 근거를 댈 수 있다
  • 접근성 id가 Android·iOS에서 각각 어떤 속성에 매핑되는지 안다
  • -android uiautomator 와 -ios predicate string/-ios class chain 의 쓰임을 구분할 수 있다
  • 플랫폼별 로케이터를 분리하고 페이지 오브젝트는 공통으로 유지할 수 있다
  • 컨텍스트 목록을 확인하고 웹뷰로 전환·복귀하는 코드를 안전하게 작성할 수 있다
  • 웹뷰가 목록에 안 나올 때의 대표 원인 두 가지 이상을 말할 수 있다
  • W3C Actions 구조를 설명하고 비율 좌표로 스와이프·롱프레스를 조립할 수 있다
  • 암묵적·명시적 대기를 섞으면 안 되는 이유와 조건 기반 대기 전환을 설명할 수 있다

흔한 실수

  • 자동 생성된 절대 경로 XPath를 그대로 쓴다. 레이아웃이 한 번만 바뀌어도 전부 깨진다. 임시로 쓰더라도 상대 경로와 속성 조건으로 줄이고 id 요청 티켓을 남긴다.
  • 암묵적 대기와 명시적 대기를 함께 켠다. 대기 시간이 예측 불가능해지고 "없음"을 확인하는 테스트가 극단적으로 느려진다.
  • 제스처 좌표를 픽셀로 박는다. 해상도가 다른 기기에서 전부 실패한다. 창 크기 기준 비율로 계산한다.
  • 화면 가장자리에서 스와이프를 시작한다. OS의 뒤로 가기·알림창 제스처가 먼저 먹는다. 안쪽에서 시작한다.
  • 웹뷰에서 네이티브로 복귀하지 않는다. 이후 테스트가 원인 불명으로 줄줄이 실패한다. finally 로 원복을 보장한다.
  • 저사양 기기 실패를 대기 시간 증가로 덮는다. 전체 실행이 느려지고 원인은 그대로 남는다. 조건 기반 대기로 바꾼다.

참고 자료

낯선 말 풀이

이 레슨에 나온 약어입니다. 본문에서도 처음 나올 때 점선으로 표시해 두었습니다 (제목과 링크 글자는 그대로 둡니다).

API — 응용 프로그램 인터페이스
프로그램끼리 주고받는 창구. 사람이 화면 버튼을 누르는 대신, 프로그램이 정해진 주소로 요청을 보내 답을 받는다.
QA — 품질보증
만든 것이 쓸 만한지 확인하고, 애초에 덜 망가지게 만드는 방법을 정하는 일. "테스트"는 그 일의 일부다.
UI — 사용자 인터페이스
사람이 직접 보고 누르는 부분 — 화면, 버튼, 입력창.
XPath — 엑스패스
문서의 나무 구조에서 "몇 번째 줄의 어느 칸" 처럼 위치를 적어 원하는 조각을 집어내는 표기법.
XML — 확장 표시 언어
제이슨과 목적이 같은, 더 오래된 글자 형식. 꺾쇠 괄호로 이름을 감싼다.
URL — 인터넷 주소
웹에서 무언가의 위치를 가리키는 주소 한 줄.
OS — 운영체제
기기를 돌리는 바탕 프로그램 — 윈도우, 맥, 안드로이드 같은 것.
CI — 지속적 통합
코드를 올릴 때마다 자동으로 합쳐서 빌드하고 시험까지 돌리는 방식.

용어집 전체 보기