Android 개발/Jetpack Compose

안드로이드 스크린샷 테스트 3종 비교 — Roborazzi·Paparazzi·Compose Preview 갈라 써본 한 주

stackD 2026. 7. 5. 18:00
반응형

 

코드 리뷰어 4명이 통과시킨 PR에서 패딩 2dp가 슬쩍 깎였습니다. 14개 화면 중 4개가 깨졌는데 머지될 때까지 아무도 몰랐어요.

 

QA가 다음 날 다크 모드 캡처 비교본을 슬랙에 올리고 나서야 알았습니다. Modifier.padding(16.dp)14.dp 로 바뀐 한 줄이었는데, diff 만 보면 그저 숫자 두 자리지요. 렌더링 결과를 보지 않으면 잡히지 않는 회귀였습니다. 이 사건을 계기로 안드로이드 스크린샷 테스트 도구 세 개를 같은 화면에 붙여보고 CI에 뭘 박을지 골랐는데, 그 한 주짜리 비교 기록을 남겨봅니다.

 

코드 리뷰가 못 잡는 1px UI 회귀

CI 한 사이클을 90초 안에 끝내는 게 우리 팀 목표였어요. 그러니 에뮬레이터 띄우는 데만 5분 넘게 잡아먹는 Espresso 계열은 후보에서 빠졌습니다. JVM 위에서 도는 스크린샷 테스트만 비교 대상으로 좁혔어요.

 

핵심은 1픽셀 단위로 시각적 변화를 잡아주냐, 그리고 그게 머지 전에 PR 위에서 빨간 불로 떠주냐였습니다. 후보로 올린 게 페이퍼라치(Paparazzi), 로보라치(Roborazzi), 그리고 컴포즈 프리뷰 스크린샷 테스팅(Compose Preview Screenshot Testing) 세 가지였어요.

 

Paparazzi 속도 우선 — 디자인 시스템 모듈에 박은 지점

붙여놓고 가장 먼저 놀란 게 속도였습니다. :core:designsystem 모듈의 카드·버튼·인풋 컴포넌트 14컷을 11초에 다 뽑아냈어요. Robolectric을 부팅하지 않고 Paparazzi 가 번들한 LayoutLib을 그대로 쓰는 구조라서, 단일 프레임 렌더링은 압도적으로 빠른 편입니다.

 

@Test
fun cardComponent_default() {
    paparazzi.snapshot {
        AppTheme {
            AppCard(title = "결제 내역", subtitle = "₩12,400")
        }
    }
}

 

대신 단일 프레임만 잡힙니다. 다이얼로그가 펼쳐진 상태나 바텀시트가 올라온 상태를 보고 싶으면, 그 상태를 별도 @Preview 로 따로 만들어야 해요. 인터랙션은 못 캡처한다고 보시면 되겠습니다.

 

함정이 하나 있었는데, Robolectric과 같은 모듈에서 같이 돌리면 클래스로더 충돌이 납니다. 페이퍼라치가 LayoutLib을 자기 방식으로 끌어다 쓰다 보니 그래요. 저는 별도 모듈로 격리하거나, 아예 디자인 시스템 라이브러리 모듈에서만 쓰는 쪽으로 갔습니다. 그래야 안전하더라고요.

 

 

Roborazzi 인터랙션 캡처 — feature 모듈 Hilt·ViewModel 화면

:feature:order 모듈은 사정이 달랐어요. 화면 하나를 그리려면 ViewModel 주입이 필요하고, 그 ViewModel 이 들고 있는 상태 따라 빈 화면·로딩·에러·정상이 다 다른 UI거든요.

 

로보라치는 Robolectric 위에서 도니까 @HiltAndroidTest 안에서 ViewModel 을 그대로 주입받아 실제 상태를 렌더할 수 있습니다. performClick() 으로 버튼 누른 다음 상태를 캡처하는 것도 됩니다. 그래서 인터랙션 후 화면 검증이 필요한 구간은 로보라치 쪽이 맞아요. 단, 스크린샷 테스트는 결정적 상태를 보장해야 flaky 를 피할 수 있어서, 저는 ViewModel 대신 상태를 직접 주입하는 stateless Composable 을 호출하는 쪽으로 정리해뒀어요.

 

@Test
fun orderScreen_emptyState_captured() {
    composeTestRule.setContent {
        AppTheme {
            OrderScreen(state = OrderUiState.Empty, onAction = {})
        }
    }
    composeTestRule.onRoot().captureRoboImage()
}

 

속도는 같은 14컷에 28초로, 페이퍼라치의 두세 배쯤 됩니다. Robolectric 부팅 비용이 그대로 얹혀서 그래요. 28초가 어느 정도냐면, 페이퍼라치 한 사이클 돌리고 커피 한 모금 마실 시간이라고 보시면 됩니다. 그래도 기준·실측·diff 3단으로 떨궈주는 HTML 리포트가 깔끔해서, 디자이너에게 링크 던지고 "이거 의도된 변경 맞아요?" 묻기가 편했네요.

 

 

Compose Preview Screenshot Testing 알파, 도입 보류한 이유

마지막 후보가 구글 공식 도구였어요. 이미 우리 팀이 @Preview 를 디자인 시스템 카탈로그처럼 관리하고 있어서, 그 에셋을 재활용한다는 점이 가장 끌렸습니다. @PreviewTest 애노테이션만 붙이면 별도 테스트 코드 거의 없이 스크린샷이 뽑힌다고 해요.

 

문제는 알파 단계인 점입니다. AGP 9.0 이상(또는 8.13 호환)을 요구하고, 알파 단계에서 API 가 크게 바뀔 수 있다고 공식 릴리즈 노트에 명시되어 있어요. 우리 CI에 박아두고 안정적으로 굴리기엔 시점이 일러 보였습니다. 개인적으로는 정식 안정화되면 페이퍼라치 포지션을 이쪽이 가져갈 가능성이 높다고 봅니다. 다만 지금은 도입 보류로 가닥을 잡았어요.

 

안드로이드 CI 도구 모듈별 배치와 한 주 운영 결과

결국 모듈별로 나눠 박았습니다.

 

  1. :core:designsystem 같은 디자인 시스템 모듈은 페이퍼라치. 속도가 최우선이고 인터랙션 검증이 필요 없어서.
  2. :feature:* 모듈은 로보라치. Hilt·ViewModel·인터랙션 후 상태까지 잡아야 해서.
  3. 컴포즈 프리뷰 스크린샷 테스팅은 정식 안정화 시점까지 도입 보류.

 

CI 빌드 시간은 1분 36초 늘었어요. 대신 도입 전 4주 평균 주간 핫픽스가 3.2건이었는데, 도입한 첫 주는 0.8건으로 떨어졌습니다. 패딩·정렬·다크 모드 색상 같은 1px 회귀가 PR 위에서 빨갛게 떠주니까, 머지 전에 잡혀나가는 경우가 많거든요.

 

기준 이미지 운영은 좀 신경 써야 했어요. 로컬 맥OS(macOS)와 CI 리눅스(Linux)의 폰트 렌더링이 미세하게 달라서, 로컬에서 record 한 이미지를 CI에서 비교하면 멀쩡한 테스트도 깨졌습니다. 그래서 기준 이미지는 무조건 CI 위에서 생성하고 픽셀 차이 허용치를 0.1% 로 잡아뒀습니다. 참고로 저는 깃허브 액션(GitHub Actions) 워크플로우에 record-screenshots 잡을 하나 따로 만들어서, 의도된 UI 변경이 있을 때만 라벨 붙여서 돌리고 있어요.

 

한계도 명확합니다. JVM 호스트에서 렌더하니까 실제 기기 OEM 폰트, 시스템 다크 모드 토글, 접근성 글자 크기 같은 건 못 잡아요. 수동 QA를 완전히 대체하지는 못한다는 얘기입니다. 스크린샷 테스트는 "리뷰어 4명이 못 잡는 1픽셀 회귀를 PR 위에서 빨간 불로 띄워주는 알람" 자리에 두고, 마지막 검수는 사람 눈이 하는 분업으로 굴리는 게 맞다고 봅니다.

 

세 도구를 같은 화면에 붙여보고 나서 분명해진 건 하나였어요. 어떤 도구가 1등이냐가 아니라, 모듈마다 풀어야 할 문제가 다르니 도구도 갈라 박는 게 답이라는 것. 다음 분기엔 기준 이미지가 늘어나면서 깃 저장소 용량이 어디까지 부풀어 오르는지, Git LFS 로 빼는 시점을 어떻게 잡았는지 한 번 더 풀어볼 생각입니다.

 

 

반응형
LIST