BookaMac 엔지니어링 기록

클라우드 Mac에서 iOS 시각 회귀 게이트 구축

클라우드 Mac에서 iOS 시각 회귀 게이트 구축

동일한 iOS UI 코드도 개발 환경에서는 정상으로 보이지만, 병합 후에는 버튼이 잘리거나 동적 글꼴 때문에 레이아웃이 밀리고 다크 모드에서 색상이 맞지 않는 문제가 발생할 수 있습니다. 시각 회귀 게이트의 목적은 단위 테스트를 대체하는 것이 아니라, 고정된 렌더링 조건에서 이번 커밋이 사용자가 실제로 보는 픽셀을 변경했는지 확인하는 것입니다.

클라우드 Mac은 이러한 작업을 지속적으로 실행하기에 적합합니다. 단, 시뮬레이터 상태와 테스트 데이터, 비교 규칙까지 모두 버전 관리에 포함해야 합니다. 단순히 “정상으로 보이는” 스크린샷 몇 장을 저장하는 것만으로는 안정적인 게이트를 구축할 수 없습니다.

안정적인 스크린샷 매트릭스부터 정의하기

처음부터 “모든 화면을 포괄하겠다”는 목표를 세우지 마세요. 먼저 로그인 전 홈 화면, 핵심 목록, 상세 화면, 빈 상태, 오류 상태처럼 가치가 높은 UI를 선정한 뒤, 각 화면에 대해 다음 항목을 고정합니다.

항목 권장 고정값 변경 시 처리
기기 명확하게 지정한 Simulator 모델 하나 별도의 기준 이미지 디렉터리 생성
시스템 지정된 사용 가능 런타임 기준 이미지 재검토
화면 모드 라이트 모드와 다크 모드를 각각 실행 서로 다른 화면 모드끼리 비교하지 않음
언어 대상 언어별로 별도 스크린샷 생성 파일명에 언어 코드 포함
글자 크기 기본 글자 크기부터 시작 큰 글자 크기는 별도 테스트 그룹으로 구성
데이터 로컬 픽스처 또는 테스트 API 무작위 콘텐츠 의존 금지

기준 이미지 경로는 Snapshots/<runtime>/<device>/<locale>/<appearance>/ 형식을 사용할 수 있습니다. 디렉터리 계층이 다소 길어지지만, 실패 시 어느 환경에서 비교가 수행되었는지 즉시 확인할 수 있어 서로 다른 시스템의 렌더링 차이를 제품 회귀로 잘못 판단하는 일을 방지할 수 있습니다.

기준 이미지는 영구적으로 정답인 이미지가 아닙니다. 검토를 거친 UI 계약이며, 모든 업데이트는 코드 변경과 함께 검토되어야 합니다.

시뮬레이터 상태를 재사용하지 말고 고정하기

수동으로 조작한 시뮬레이터 하나를 장기간 재사용하면 권한 팝업, 키보드 설정, 캐시, 테스트 계정이 남게 됩니다. 더 안정적인 방법은 시각 테스트 전용 기기를 할당하고, 실행할 때마다 종료한 뒤 초기화하고 다시 부팅하는 것입니다.

#!/usr/bin/env bash
set -euo pipefail

: "${DEVICE_UDID:?DEVICE_UDID is required}"

xcrun simctl shutdown "$DEVICE_UDID" 2>/dev/null || true
xcrun simctl erase "$DEVICE_UDID"
xcrun simctl boot "$DEVICE_UDID"
xcrun simctl bootstatus "$DEVICE_UDID" -b

xcrun simctl status_bar "$DEVICE_UDID" override \
  --time 09:41 \
  --batteryState charged \
  --batteryLevel 100 \
  --wifiBars 3 \
  --cellularBars 4

rm -rf TestResults/Visual.xcresult
xcodebuild test \
  -workspace Example.xcworkspace \
  -scheme ExampleVisualTests \
  -destination "id=$DEVICE_UDID" \
  -resultBundlePath TestResults/Visual.xcresult

rm -rf TestResults/Attachments
xcrun xcresulttool export attachments \
  --path TestResults/Visual.xcresult \
  --output-path TestResults/Attachments

현재 실행 중인 booted 기기에 의존하지 마세요. 병렬 작업이 여러 시뮬레이터를 동시에 실행할 수 있으므로, UDID를 명시적으로 전달해야 상태 표시줄 오버라이드와 앱 설치, 테스트가 모두 동일한 기기에서 수행됩니다.

관찰 가능한 상태를 기준으로 스크린샷 촬영하기

시각 테스트에서 가장 흔한 실수는 앱을 실행한 뒤 무조건 2초 동안 기다리는 것입니다. 머신 부하나 네트워크 상태에 따라 2초가 너무 길 수도, 부족할 수도 있습니다. XCUITest에서는 “페이지를 비교할 준비가 완료되었다”는 사실을 나타내는 접근성 요소가 나타날 때까지 기다려야 합니다.

func capture(_ name: String, readyIdentifier: String) {
    let app = XCUIApplication()
    app.launchArguments = ["-VisualTestMode", "1"]
    app.launch()

    let ready = app.descendants(matching: .any)[readyIdentifier]
    XCTAssertTrue(ready.waitForExistence(timeout: 15))

    let image = XCUIScreen.main.screenshot()
    let attachment = XCTAttachment(screenshot: image)
    attachment.name = name
    attachment.lifetime = .keepAlways
    add(attachment)
}

테스트 모드에서는 캐러셀, 스켈레톤 애니메이션, 깜박이는 커서를 비활성화하고 고정된 날짜, 사용자 이름, 목록 데이터를 주입해야 합니다. 여기서 고정하는 것은 입력값이지, 테스트 대상 UI를 별도의 구현으로 교체하는 것이 아닙니다. 페이지가 요청 완료 여부에 의존한다면 대기 시간을 계속 늘리는 대신, 로딩 완료를 나타내는 안정적인 식별자를 UI에 노출할 수 있습니다.

글꼴과 애니메이션 처리하기

글꼴은 시스템에서 제공되거나 앱과 함께 배포되어야 하며, 특정 시점에 수동으로 설치한 글꼴에 의존해서는 안 됩니다. 애니메이션은 실행 인수로 비활성화할 수 있으며, 스크린샷을 촬영하기 전에 스크롤이 완전히 멈췄는지도 확인해야 합니다. 계속 변하는 동영상, 지도, 타이머는 결정론적인 테스트 픽스처로 대체하는 것이 우선입니다. 제어할 수 없는 매우 작은 영역에만 마스크를 사용하세요.

설명 가능한 픽셀 임계값 설정하기

PNG 파일의 바이트가 다르다고 해서 화면도 다르다는 뜻은 아닙니다. cmp로 직접 판정하면 인코딩 정보의 영향을 받기 쉽습니다. 비교기는 먼저 두 이미지를 동일한 크기와 sRGB 픽셀로 디코딩한 다음, 픽셀별로 채널 차이를 계산해야 합니다.

다음 두 조건을 함께 사용하는 것이 좋습니다.

  1. 채널별 최대 허용 오차. 예를 들어 미세한 앤티앨리어싱 차이를 허용합니다.
  2. 허용 오차를 초과한 픽셀의 비율. 예를 들어 전체 이미지에서 극히 작은 비율을 넘지 않도록 제한합니다.

평균 차이만 사용하면 국소적으로 심각한 문제를 놓칠 수 있습니다. 사라진 버튼 하나가 전체 이미지에서 차지하는 면적은 매우 작을 수 있기 때문입니다. 비교기는 차이를 강조한 이미지도 출력하고, 임계값을 초과한 픽셀 수와 경계 상자, 임계값을 기록해야 합니다. 중요한 확인 화면에는 엄격한 규칙을 적용하고, 그림자나 복잡한 그라데이션이 포함된 화면에는 별도 설정을 사용할 수 있습니다. 다만 테스트를 단지 “통과 상태로 만들기” 위해 전역 임계값을 계속 완화해서는 안 됩니다.

실패를 검토 가능한 증거로 만들기

실패할 때마다 최소한 기준 이미지, 실제 이미지, 차이 이미지, .xcresult를 보관해야 합니다. 이와 함께 커밋 식별자, Xcode 버전, 시스템 런타임, 기기 모델, 언어, 화면 모드도 기록합니다. 파일명은 테스트 스위트, 페이지 상태, 환경을 조합해 구성하여 병렬 작업이 서로의 결과를 덮어쓰지 않도록 해야 합니다.

문제를 조사할 때는 다음 순서로 확인합니다. 먼저 이미지 크기와 런타임이 변경되었는지 확인하고, 테스트 데이터가 안정적인지 살펴본 다음, 차이 경계 상자를 검사합니다. 차이가 화면 전체에 퍼져 있다면 대개 글꼴, 화면 모드, 색 공간이 달라진 것입니다. 차이가 특정 컨트롤 하나에만 집중되어 있을 때 비로소 레이아웃 변경일 가능성이 커집니다.

기준 이미지 업데이트도 별도의 절차로 진행해야 합니다. 후보 이미지를 생성하고, 사람이 차이를 확인한 뒤, 변경 의도가 맞는지 검증하고 나서 교체합니다. 테스트 작업이 실패했을 때 기준 이미지를 자동으로 덮어써서는 안 됩니다. 그렇게 하면 실제 회귀가 다음 실행에서 곧바로 승인될 수 있습니다.

완성된 게이트는 세 가지 특성을 갖춰야 합니다. 환경을 다시 구축할 수 있고, 임계값을 설명할 수 있으며, 실패를 재현할 수 있어야 합니다. 이 세 가지를 충족해야 스크린샷 테스트가 가끔 수동 정리가 필요한 이미지 저장소가 아니라 제대로 된 엔지니어링 검증 수단이 됩니다.

자주 묻는 질문

모든 픽셀이 완전히 같아야 테스트를 통과하도록 해야 하나요?

대부분의 화면에서는 권장하지 않습니다. 채널별 차이와 초과 픽셀 비율을 함께 제한하고, 커서나 애니메이션처럼 불가피한 동적 영역만 작은 마스크로 제외하는 편이 안정적입니다.

같은 커밋인데 시뮬레이터마다 스크린샷이 달라지는 이유는 무엇인가요?

기기 크기, 시스템 런타임, 글꼴, 언어, 시간대, 표시 배율과 상태 표시줄이 렌더링에 영향을 줍니다. 기준 이미지는 특정 기기와 런타임 조합에 연결해야 합니다.

실패 시 어떤 결과물을 보관해야 하나요?

기준 이미지, 실제 이미지, 차이 이미지, 테스트 결과 번들, 커밋 식별자, 시뮬레이터 기종과 시스템 버전을 함께 보관해야 원인을 재현할 수 있습니다.

전용 물리 Mac mini

검증된 워크플로를 365일 지속 운영되는 클라우드 Mac으로 옮기세요

용도에 맞춰 BookaMac M4 또는 BookaMac M4 Pro를 선택하고, 주문 시 지역·대여 기간·스토리지 추가 옵션을 확인하세요.

대여 요금제 선택