민프

[Frontend][Flutter]iOS와 Android 스모크 테스트, 동시에 돌리면 안 되나?— 한 폴더의 한계, 디바이스 팜, 그리고 다른 회사들의 답 본문

Frontend/[Flutter]

[Frontend][Flutter]iOS와 Android 스모크 테스트, 동시에 돌리면 안 되나?— 한 폴더의 한계, 디바이스 팜, 그리고 다른 회사들의 답

민프야 2026. 10. 15. 21:34

테스트 자동화 삽질기

Flutter 통합 테스트를 Android 에뮬레이터로 옮기고 나서 든 당연한 질문. "터미널 두 개 켜서 같이 돌리면 되는 거 아냐?" 안 됐다. 그 이유와, 그래서 업계가 디바이스 팜이라는 걸 만든 이야기.

2026  ·  Frontend › [Flutter]

#Flutter#integration_test#디바이스팜#FirebaseTestLab#Appium#스모크테스트

iOS 시뮬레이터에서 돌던 스모크 테스트를 Android 에뮬레이터에서도 같은 시나리오로 돌리게 만들었다.
둘을 합치면 30분이 넘어서 동시에 돌리고 싶었는데, 한 작업 폴더에서는 그게 안 된다.

이 글은 그 이유에서 출발해 디바이스 팜이 무엇인지, Flutter 공식 문서가 권하는 방법이 무엇인지, 국내외 회사들은 어떻게 하는지를 링크와 함께 정리한 것이다.

01배경 — 같은 시험지를 Android에서도

우리 앱의 스모크 테스트는 세 묶음이다.
로그인부터 채팅 한 턴과 코인 차감까지 보는 core, 캐릭터와 스토리를 만들고 고치고 지우는 create, 가입부터 탈퇴까지 가는 lifecycle.

전부 Flutter의 integration_test로 쓰여 있고, 실제 백엔드에 실제 OTP 메일로 로그인한다.
지금까지는 iOS 시뮬레이터에서만 돌렸다.

Android 출시 준비가 끝나면서 조건이 하나 붙었다.
Android도 iOS와 똑같은 단계, 똑같은 판정으로 돌아야 한다.

테스트 코드 자체는 거의 플랫폼 중립이었다.
위젯 키로 찾고, 서버 응답으로 판정한다.
플랫폼에 묶인 건 기기를 띄우고 앱을 올리는 러너 스크립트뿐이라, 그 부분만 iOS용과 Android용으로 갈라 같은 함수 이름을 쓰게 했다.

결과

같은 날 로컬 스택에서 iOS 35단계 중 34단계 통과, Android도 같은 35단계를 같은 판정으로 통과했다.
차이는 단 하나, 개발용 패키지가 스토어에 없어 결제 상품 조회 단계가 Android에서 skip으로 찍히는 것뿐이다.

그리고 바로 다음 질문이 나왔다.
iOS 16분, Android 17분. 동시에 돌리면 17분에 끝나는 거 아닌가?

02한 폴더에서는 안 되는 이유

스모크는 테스트를 돌리기 전에 앱을 먼저 빌드한다.
iOS는 시뮬레이터용 앱 번들을, Android는 APK를 만든다.

문제는 그 빌드가 전부 프로젝트 폴더 안의 build/와 .dart_tool/에 중간 파일을 쓴다는 것이다.
그 폴더가 하나뿐이다.

두 빌드를 같은 순간에 시작하면 한쪽이 쓰다 만 파일을 다른 쪽이 읽거나, 한쪽이 지운 파일을 다른 쪽이 찾는다.
그러면 둘 중 하나가 이유를 알기 어려운 오류로 죽는다.

한 작업대에서 두 사람이 동시에 다른 물건을 조립하는 것과 같다.
그래서 러너의 all 옵션은 iOS가 끝난 뒤 Android를 시작하도록 만들었다.

한 줄 요약

동시에 돌리려면 작업 폴더가 둘이어야 한다.
이건 우리 러너의 한계가 아니라 빌드 도구의 성질이고, 뒤에서 보겠지만 다른 회사들이 CI에서 플랫폼마다 머신을 따로 쓰는 이유와 같다.

로컬에서 당장 쓰는 해법은 git worktree다.
같은 브랜치를 다른 폴더에 한 벌 더 꺼내면 빌드 폴더가 분리된다.
터미널 두 개에서 각자 돌리면 끝이고, 로그인 코드를 읽어 주는 로컬 메일박스 포트만 겹치지 않게 바꾸면 된다.

# 같은 브랜치를 폴더 하나 더 꺼낸다
git worktree add ../app-android feature/smoke-android

# 터미널 1
scripts/smoke/smoke.sh ios

# 터미널 2 (메일박스 포트만 다르게)
cd ../app-android
OTP_MAILBOX_PORT=8766 scripts/smoke/smoke.sh android

런 계정은 실행마다 새로 만들고 끝나면 각자 지우므로 두 실행이 섞이지 않는다.
소요 시간은 둘 중 긴 쪽, 약 17분이 된다.

03디바이스 팜이란 무엇인가

"폴더 둘"의 다음 단계는 "머신 둘"이고, 그 끝에 디바이스 팜이 있다.

디바이스 팜은 말 그대로 테스트용 폰과 태블릿을 여러 대 모아 두고 네트워크로 빌려 쓰는 설비다.
앱과 테스트를 올리면 여러 기기에서 동시에 돌리고 결과·로그·영상을 돌려준다.

공식 문서 인용

“Device Farm is an app testing service that you can use to test and interact with your Android, iOS, and web apps on real, physical phones and tablets that are hosted by Amazon Web Services (AWS).”

“Device Farm allows you to run automated tests on multiple devices in parallel by uploading your app and tests.”

→ AWS가 호스팅하는 실물 폰과 태블릿에서 Android·iOS·웹 앱을 테스트하는 서비스이고, 앱과 테스트를 올리면 여러 기기에서 병렬로 돌려 준다.

— AWS Device Farm Developer Guide, What is AWS Device Farm? →

종류는 둘로 나뉜다.

— 클라우드형. Firebase Test Lab, AWS Device Farm, BrowserStack처럼 남이 운영하는 기기를 시간 단위로 빌린다.
기기 관리가 없고 바로 시작할 수 있지만, 테스트가 외부 서버에 닿아야 하고 비용이 실행 시간에 비례한다.

— 자체 운영형. Mac mini를 랙에 꽂고 실기기를 USB로 연결해 회사 안에서 돌린다.
초기 비용과 운영 인력이 들지만 사내망의 서버와 바로 붙고, 기기 수가 많을수록 시간당 비용이 싸진다.

가상 기기와 실기기의 차이도 짚고 가자.
에뮬레이터와 시뮬레이터는 싸고 빠르지만 메모리 제약, 발열, 실제 모뎀, 제조사 커스텀 UI를 흉내 내지 못한다.
그래서 많은 팀이 PR마다는 가상 기기, 출시 전에는 실기기로 층을 나눈다.

04Flutter 공식 문서가 권하는 방법

Flutter의 통합 테스트는 integration_test 패키지로 쓰고, 기기 하나에서는 flutter test나 flutter drive로 돌린다.
여러 기기로 넓힐 때 공식 문서가 이름을 대는 곳은 하나, Firebase Test Lab이다.

공식 문서 인용

“Use the flutter drive command to run tests on a physical device or emulator.”

“To test a mobile platform, use the command line or Firebase Test Lab.”

“Run on Firebase Test Lab, to automate testing on a variety of devices.”

→ 실기기나 에뮬레이터에서는 flutter drive로 돌리고, 모바일은 명령줄 또는 Firebase Test Lab으로 테스트하며, 여러 기기에서의 자동화는 Firebase Test Lab에서 한다.

— Flutter docs, Check app functionality with an integration test →

Firebase 쪽 문서는 그 흐름을 한 문장으로 설명한다.
우리가 이미 쓰는 테스트 형식을 그대로 올릴 수 있다는 뜻이다.

공식 문서 인용

“To test Flutter apps with Firebase Test Lab, you can write Flutter integration tests, build Android APKs or iOS test zip files, and run as regular Android instrumentation tests or iOS XCTests.”

→ Flutter 통합 테스트를 쓰고, Android는 APK, iOS는 테스트 zip으로 빌드해 각각 일반 instrumentation 테스트와 XCTest로 돌린다.

— Firebase Test Lab, Integration Testing with Flutter →

그래서 유료인가?

무료 플랜에도 하루 할당이 있고, 넘어가면 시간 단위로 낸다.
가격 페이지의 문장 그대로다.

공식 문서 인용

“10 test runs per day on virtual devices” · “5 test runs per day on physical devices”

“$1 per hour for each virtual device” · “$5 per hour for each physical device”

“Charges are calculated on a per-minute basis, rounded up to the nearest minute.”

→ 무료 플랜은 가상 기기 하루 10회, 실기기 하루 5회. 종량제는 가상 기기 시간당 $1, 실기기 시간당 $5이고 분 단위로 올림해 계산한다.

— Firebase Test Lab, Usage, Quotas, and Pricing →

우리 스모크로 환산하면 이렇다.
세 묶음 한 바퀴가 약 17분이니, 실기기 한 대에 올리면 약 $1.5, 가상 기기면 약 $0.3이다.

하루에 몇 번 돌리는 수준이면 무료 할당 안에서 끝나고, 기기 세 대 매트릭스를 매 PR마다 돌려야 비용이 눈에 띈다.

사용 방식 한 바퀴 17분 기준 비고
가상 기기 1대 약 $0.3 하루 10회까지 무료
실기기 1대 약 $1.5 하루 5회까지 무료
실기기 3대 매트릭스 약 $4.5 동시에 돌아 17분에 끝남

숨은 의존성

디바이스 팜의 기기는 구글의 서버에서 돈다.
우리 스모크처럼 로컬 스택이나 개발자 Mac의 메일박스에 의존하는 테스트는 그대로 올릴 수 없다.
외부에서 닿는 개발 서버와, 기기 밖에서 OTP를 읽어 줄 경로가 먼저 있어야 한다.

05해외는 어떻게 하나 — 플랫폼마다 머신 하나

찾아본 사례는 전부 같은 모양이었다.
iOS 잡과 Android 잡을 따로 두고, 각자 자기 머신에서 자기 체크아웃으로 빌드한다.
한 폴더에서 두 플랫폼을 동시에 빌드하는 곳은 없었다.

— Dart 팀의 http 패키지. iOS 통합 테스트를 macOS 러너에 시뮬레이터를 띄워 flutter test integration_test로 돌린다.
플랫폼마다 잡이 따로다. 워크플로 파일 →

— Codemagic. Flutter CI 서비스인데, Apple Silicon 맥 빌드 머신에서는 Android 에뮬레이터가 돌지 않는다고 못 박는다.
그래서 iOS는 맥, Android는 리눅스나 외부 팜으로 자연스럽게 갈린다.

공식 문서 인용

“Due to limitations by Apple silicon, the Apple Virtualization API doesn't support nested virtualization” … “required for Android emulators.”

“To use Android emulators in macOS workflows, please use third-party service integrations such as emulator.wtf, Katalon, Firebase, or AWS Device Farm.”

→ Apple Silicon의 가상화 API는 Android 에뮬레이터에 필요한 중첩 가상화를 지원하지 않는다. 맥 워크플로에서 에뮬레이터가 필요하면 외부 팜 서비스를 붙이라는 안내다.

— Codemagic docs, Running tests →

— Patrol. 폴란드 LeanCode가 만든 Flutter 전용 프레임워크.
권한 다이얼로그 같은 네이티브 UI를 Dart에서 조작하고, 테스트를 쪼개 여러 기기에 나눠 돌리는 샤딩을 지원한다.
Firebase Test Lab, BrowserStack, AWS Device Farm 등에 올릴 수 있다. patrol.leancode.co → · Codemagic 연동 문서 →

— Maestro. 2022년에 나온 YAML 기반 도구.
앱을 블랙박스로 보고 화면의 글자와 요소로 조작하므로 네이티브든 Flutter든 가리지 않는다. 소개 글 →

— BrowserStack. 클라우드 실기기 서비스로, Flutter 통합 테스트를 iOS와 Android에 동시에 돌리는 것을 상품으로 판다.
벤더 자료라는 점은 감안하자. 가이드 →

06국내는 왜 Appium이었나

국내 기술 블로그를 뒤지면 한 단어가 반복된다. Appium.
Flutter 통합 테스트 이야기는 거의 없다.
이유를 사례에서 거꾸로 읽어 봤다.

— 카카오페이. 실제 단말을 디바이스 팜에 연결하고 Appium으로 자동화 테스트를 돌리며, 빌드 산출물에서 최신 앱을 받아 자동 설치한다.
테스트라기보다 운영 모니터링에 가깝게 쓴다. if kakao 2022 발표 글 →

— 우아한형제들. QA 담당자가 Cucumber와 Appium으로 Android 앱을 자동화했다.
사용자 영향이 큰 기능부터 케이스를 골랐다는 점이 눈에 띈다. 약 5년 전 글이다. 기술블로그 →

— SKT 에이닷. Appium, XCTest, Espresso를 비교한 뒤 Appium을 골랐다.
장점은 크로스 플랫폼과 언어 자유, 단점은 초기 설정의 복잡함과 성능으로 적었다. Devocean 글 →

— 스포카. Appium이 Android에서는 UIAutomator2, iOS에서는 XCUITest를 중계하는 구조를 설명한다. 기술 블로그 →

— 토스. 기술 글은 못 찾았지만, iOS·Android 기기를 MDM으로 관리하고 Mac mini 랙으로 물리 디바이스 팜을 운영할 사람을 뽑고 있었다.
자체 팜을 돌린다는 뜻이다. 채용 공고 →

사례를 겹쳐 보면 Appium을 고른 이유가 네 가지로 모인다.

하나 — 앱이 네이티브였다.

Kotlin과 Swift로 만든 앱에는 Flutter의 integration_test 같은 것이 없다.
Espresso와 XCTest는 플랫폼마다 따로라, 두 플랫폼을 한 팀이 보려면 위에서 둘을 묶는 도구가 필요했다.

둘 — QA 조직이 개발팀과 분리돼 있었다.

앱 소스 없이 출시 빌드를 블랙박스로 돌릴 수 있어야 했고, 테스트는 Python이나 Java로 쓰고 싶었다.
Appium은 정확히 그 자리를 노린 도구다.

공식 문서 인용

“Appium aims to support UI automation of many different platforms (mobile, web, desktop, etc.).”

“it also aims to support automation code written in different languages (JS, Java, Python, etc.).”

→ 여러 플랫폼의 UI 자동화를, 여러 언어로 쓴 코드로 지원하는 것이 목표다. 플랫폼별 연결은 드라이버가 맡는다.

— Appium Documentation, Introduction →

셋 — 디바이스 팜이 Appium을 기본으로 받았다.

AWS Device Farm의 원격 접속 설명에도 Appium 엔드포인트가 그대로 등장한다.
자체 팜을 만들어도 기기와 테스트를 잇는 공통 언어가 Appium의 WebDriver 프로토콜이었다.

넷 — 시기.

위 글들은 2018년에서 2022년 사이에 쓰였다.
Maestro가 2022년, Patrol도 같은 해에 나왔으니 그때는 선택지가 사실상 Appium뿐이었다.

우리와의 차이

우리는 앱이 Flutter이고 테스트를 개발자가 앱 저장소 안에서 쓴다.
그래서 Appium 없이 integration_test가 위젯 트리를 직접 잡고, 그 결과물이 그대로 디바이스 팜에 올라가는 형식이다.
국내 사례와 다른 길을 가는 게 아니라, 같은 목적지를 Flutter가 준 지름길로 가는 것에 가깝다.

07그래서 우리는 이렇게 간다

한 번에 디바이스 팜으로 가지 않는다.
지금 단계에서 필요한 만큼만 올린다.

단계 어디서 도나 얻는 것
1. 지금 개발자 Mac, worktree 둘, 터미널 둘 33분 → 17분, 설정 변경 없음
2. 다음 CI. iOS는 macOS 러너에 시뮬레이터, Android는 Linux 러너에 에뮬레이터 사람 손 없이 배포마다. 외부에서 닿는 개발 서버와 OTP 경로가 선행
3. 그다음 Firebase Test Lab 같은 디바이스 팜 실기기 여러 대를 동시에, 출시 전 매트릭스

2단계로 가면 "폴더 둘"이 "머신 둘"로 바뀌면서 동시 실행 문제는 저절로 사라진다.
3단계는 테스트 형식이 이미 맞으니 빌드한 테스트 앱을 올리는 일만 남는다.

순서를 이렇게 두는 이유는 하나다.
각 단계에서 막히는 것이 다르고, 앞 단계가 뒷 단계의 전제가 되기 때문이다.

💡배운 것

동시 실행은 테스트 코드의 문제가 아니라 어디서 빌드하느냐의 문제였다.

하나 — 빌드 폴더는 하나다.

같은 폴더에서 두 플랫폼을 동시에 빌드하는 곳은 어디에도 없었다.
로컬에서는 worktree, CI에서는 러너, 그 끝이 디바이스 팜이다. 전부 같은 원리의 다른 규모다.

둘 — 디바이스 팜은 비싸지 않다. 거기에 닿게 만드는 게 비싸다.

실기기 한 바퀴가 $1.5 남짓이다.
진짜 작업은 로컬 스택과 개발자 Mac에 묶인 의존성을 외부에서도 닿게 푸는 일이다.

셋 — 도구 선택은 앱의 종류와 조직의 모양이 정한다.

네이티브 앱과 분리된 QA 조직에는 Appium이 답이었고, Flutter 앱과 테스트를 쓰는 개발자에게는 integration_test가 답이다.
남의 선택을 그대로 가져오기 전에 그 선택의 전제를 먼저 봐야 한다.


참고 — 공식 문서

→ Flutter: Check app functionality with an integration test
→ Flutter: Integration testing concepts
→ Firebase Test Lab: Integration Testing with Flutter
→ Firebase Test Lab: Usage, Quotas, and Pricing
→ AWS Device Farm: What is AWS Device Farm?
→ Appium: Introduction
→ Codemagic: Running tests
→ Patrol: Flutter UI testing framework

다음 글은 2단계, CI 두 러너에서 같은 스모크를 돌리기까지의 기록이 될 것 같다.

Comments