Shopify가 엄격한 테스트 API와 컴퓨터 비전을 통해 모바일 엔드투엔드 테스트 안정성을 98%까지 높인 과정입니다.
Shopify의 DNA에서 핵심적인 부분은 QA 문화를 반드시 조성한다는 것입니다. 모바일 앱에서는 이 문화가 테스트 피라미드 최상단의 작은 엔드투엔드(E2E) 테스트 계층에 의존합니다. 즉, 판매자가 하듯 실제 앱을 구동하고 모든 풀 리퀘스트에서 차단형 CI로 실행되는 흐름입니다. 테스트 스위트가 신뢰할 수 있기만 하다면 이 피라미드는 매우 잘 작동합니다.
Shopify 모바일 앱(가장 큰 앱)은 좋은 PR보다 나쁜 PR을 더 많이 차단하는 지점에 이르렀습니다. 화면을 불러오는 데 1초가 더 걸릴 수 있어 테스트가 불안정해졌고, 상황이 너무 악화되어 E2E 스위트를 PR 검사에서 완전히 제외해야 했습니다.
이 글은 이를 해결해 **테스트 안정성 98%**를 달성한 이야기입니다. 이전 API를 사용했을 때의 50%에서 크게 올랐습니다.
2023년부터 Shopify 앱의 E2E 테스트는 React Native 테스트 ID를 사용해 요소를 찾으며 WebdriverIO를 통해 Appium에서 실행되었습니다. 이러한 유연성은 오히려 약점이 되었습니다. Appium은 저수준 제어를 제공했지만, 올바른 테스트 패턴을 강제하는 장치는 없었습니다. 한 요소를 탭한 뒤 새 화면이 렌더링되기도 전에 즉시 다음 요소를 탭하려 할 수 있었고, 이로 인해 “요소를 찾을 수 없음” 오류가 발생했습니다.
해결책은 요소가 나타날 때까지 명시적으로 기다리는 것이었지만, 로컬에서는 작동하는 듯 보이고 CI에서도 대체로 통과하는 pause(1000)을 넣는 일도 마찬가지로 쉬웠습니다(화면 로딩이 아주 조금만 길어져도 실패할 때까지는 말입니다!). 시간이 지나며 이런 지름길은 불안정한 테스트로 쌓였습니다. 앱은 문제가 없었지만 테스트 스위트는 계속 실패했습니다.
테스트가 통과하더라도 종종 잘못된 것을 단언하고 있었습니다. 판매자가 실제로 보고 사용할 수 있는지가 아니라 컴포넌트 트리에 노드가 존재하는지를 확인했던 것입니다. 사용자 경험 대신 구현 세부 사항을 테스트하고 있었습니다.

하단 여백이 잘못되었고 마지막 셀이 가려져 있습니다. 이전 API는 이 셀을 클릭할 수 있었고, 잘못되게 통과 처리했을 것입니다.
이 패턴은 이미 본 적이 있었습니다. 불안정성이 커지고, 스위트를 통과 상태로 유지하는 데 막대한 자원을 투입하게 됩니다. 문제는 스위트를 포기하는 데 서툴렀다는 것이 아니라, 프레임워크 자체가 계속 같은 실패를 만들어 냈다는 점이었습니다. 아무리 정리해도 해결되지 않았습니다. 근본 시스템을 고쳐야 했습니다.
Appium을 계속 덧대어 고치려는 시도를 멈추고, 이를 감싼 규범적 래퍼를 만들었습니다. 두 부분으로 이루어집니다. 불안정한 테스트를 작성하기 어렵게 만드는 엄격한 빌더 스타일 API와, 뷰 계층 구조를 훑는 대신 사용자가 하는 방식으로 요소를 찾는 컴퓨터 비전입니다. 내부적으로는 여전히 Appium이 기기를 구동합니다. 다만 개발자는 더 이상 이를 보지 못하며, 이전 스위트를 망가뜨린 작업을 하기 위해 래퍼를 우회할 수도 없습니다.
불안정하지 않을 것이라고 확신하는 작업만 노출하는 빌더를 대상으로 테스트를 작성합니다.
여기에는 몇 가지 의도적인 설계가 있습니다.
logIntoApp은 앱의 어떤 테스트에서도 가져다 쓸 수 있는 이름 있는 단계 시퀀스입니다.UNSAFE_ 접두사를 붙입니다. 사용자 지정 타임아웃이나 스크립트 삽입처럼 보호 장치를 우회하는 옵션은 존재하지만, 쉽게 사용하지 않도록 이름을 붙였습니다. 테스트에서 UNSAFE_timeoutInSeconds는 검토가 필요하다는 신호입니다.더 큰 변화는 한 계층 아래에 있습니다. 모든 단계는 스크린샷을 찍고 판매자가 하듯 시각적으로 대상을 찾습니다. 화면에서 “저장”이나 더하기 아이콘을 찾은 다음 탭하는 방식입니다. PaddleOCR는 텍스트를 처리하고, OpenCV는 Polaris 디자인 시스템의 SVG와 스크린샷을 대조합니다. 생성된 콘텐츠 때문에 시각적 매칭이 신뢰하기 어려운 화면에서는 테스트 ID가 여전히 대안으로 작동하지만, UNSAFE_testID 필드를 통해 명시적으로 선택해야 합니다. 이 이름 자체가 사용을 억제합니다.
진정한 이점은 작성 속도입니다. 테스트 ID를 사용할 때는 단계를 추가하려면 검사기를 열고, 컴포넌트 트리를 파고들어 testID를 찾거나 추가한 뒤 테스트에 연결해야 했습니다. 컴퓨터 비전에서는 시뮬레이터를 보고 “저장”을 확인한 다음 touch({ text: 'Save' })를 작성합니다. 이것이 전체 과정입니다. AI 에이전트도 같은 이점을 얻습니다. 문법이 화면에 있는 것과 일대일로 대응하므로, _“제품을 만드는 테스트를 작성해 줘”_는 코드베이스 지식 없이도 첫 시도에 올바른 코드가 됩니다.
실행할 때마다 각 단계가 무엇을 찾고 어디를 살폈는지 주석을 단 동영상이 생성됩니다. 테스트가 실패하면 이유를 정확히 볼 수 있습니다. OCR이 어떤 텍스트를 찾고 있었는지, 대신 무엇을 찾았는지, 어디를 탭했는지 확인할 수 있습니다. 대부분의 실패는 재실행 없이 몇 초짜리 동영상만으로 스스로 진단됩니다.

실행기는 노트북, CI 에뮬레이터, 원격 기기 팜의 실제 기기 호출 환경에서 모두 동일하게 작동하는 단일 명령입니다.
이 명령은 RemoteDeviceFarm 설정에 선언된 iOS 기기에서 logout과 일치하는 모든 테스트 파일을 실행합니다. --runner remote-device-farm을 --runner local로 바꾸면 동일한 명령이 내 컴퓨터의 시뮬레이터에서 실행됩니다.
새 API를 Shopify 앱의 차단형 CI에 적용한 지 몇 주 후, 개별 테스트 성공 횟수를 전체 실행 횟수로 나누어 측정한 결과 **테스트 안정성 98%**를 달성했습니다. 이전 API의 50%에서 상승한 수치입니다. 남은 테스트 실패는 대체로 예상 가능한 원인에서 발생합니다. 간헐적인 네트워크 실패와 시뮬레이터 부팅 실패입니다.
또한 적용 전 불안정성 관문을 만들었습니다. 새 테스트가 차단형 스위트에 들어가기 전에 전용 파이프라인이 여러 번 실행하며, 정해진 임계값을 넘겨 실패하면 거부합니다.
가장 큰 앱에서 이 프레임워크를 검증했고, 이제 다른 앱에도 도입하는 방안을 살펴보고 있습니다.
수년 동안 모바일 E2E 테스트는 본질적으로 불안정하며, 우리가 할 수 있는 최선은 불안정성을 관리하는 것이라고 믿었습니다. 그러나 그 상당 부분은 테스트 자체가 아니라 API에 있었습니다. 모든 단계에서 단언하고, 사용자가 하듯 요소를 찾으며, 위험한 도구를 거부한다는 원칙으로 API를 교체했을 때, 차단형 CI에서 유지될 수 없던 스위트가 두 플랫폼에서 98% 안정성으로 실행되는 것을 확인했습니다.
AI가 엔지니어링 속도를 높일수록 이런 프레임워크는 더욱 가치 있어집니다. 팀이 출시하는 것에 대한 확신을 잃지 않으면서 더 빠르게 움직일 수 있게 해 주기 때문입니다.
여러분의 CI도 불안정하고 사용하기 어렵고 해석하기 어려운 엔드투엔드 테스트 프레임워크로 어려움을 겪고 있을지 모릅니다. 더 나은 상태에 도달하기 위해 저희가 한 일은 다음과 같습니다.
이제 프레임워크가 신뢰할 수 있는 테스트를 가장 쉽게 작성할 수 있는 테스트로 만들었으므로, E2E 테스트는 본래 만들어진 목적을 수행합니다. 좋은 변경을 방해하지 않으면서 나쁜 변경을 포착하는 일입니다.