상품 목록에 그라데이션 레이어, 이미지 디코딩 또는 복잡한 그림자를 추가한 뒤에도 기능 테스트는 모두 통과할 수 있지만, 스크롤에서는 프레임 저하가 발생하기 시작할 수 있습니다. 사람이 몇 차례 직접 스크롤하는 것만으로는 일시적인 끊김인지 코드 회귀인지 판단하기 어렵습니다. 더 안정적인 방법은 클라우드 Mac에서 시뮬레이터, 데이터 세트, 제스처를 고정하고 XCTMetric으로 실행 시간, CPU, 메모리를 연속 측정한 다음, 지속적 통합에서 기준선을 초과한 변화만 차단하는 것입니다.
결과에 영향을 주는 변수부터 고정하기
성능 게이트를 구축할 때 가장 먼저 할 일은 단언문을 작성하는 것이 아니라 환경 노이즈를 줄이는 것입니다. Xcode와 macOS 버전, 시뮬레이터 모델, 시스템 언어, 화면 방향, 테스트 데이터를 고정합니다. 테스트 계정은 대상 화면으로 바로 진입하도록 구성해 로그인 요청, 이미지 다운로드, 서버 응답 시간이 스크롤 지표에 섞이지 않게 해야 합니다.
앱에 UI 테스트에서만 활성화되는 시작 인수(예: -UITestSeed fixed)를 추가합니다. 이 인수는 로컬 픽스처를 불러오고, 개수와 순서가 고정된 목록 항목을 생성하며, 무작위 애니메이션을 비활성화해야 합니다. 목록에는 feed.list, feed.reset처럼 안정적인 손쉬운 사용 식별자도 지정해야 하며, 테스트 코드에서 현지화된 텍스트를 기준으로 요소를 찾으면 안 됩니다.
첫 실행에는 일반적으로 앱 설치, 동적 라이브러리 로드, 글꼴 초기화, 캐시 워밍업이 포함됩니다. 정식 측정에 앞서 점수에 반영하지 않는 테스트를 한 번 실행하되, 캐시를 삭제해 일상적인 사용 방식과 다른 인위적인 “완전 콜드 스타트” 환경을 만들지는 마십시오.
성능 기준선은 전체 환경을 설명하는 값이지, 모든 Mac과 모든 시뮬레이터에 그대로 적용할 수 있는 보편적인 밀리초 값이 아닙니다.
반복 가능한 스크롤 경로 작성하기
swipeUp()을 한 번만 실행하면 시작 지점과 목록 길이의 영향을 지나치게 많이 받습니다. 더 신뢰할 수 있는 테스트를 만들려면 먼저 목록 맨 위로 돌아가 첫 번째 앵커가 보이는지 확인한 다음, 정해진 횟수만큼 제스처를 실행해야 합니다. 각 측정이 끝날 때마다 상태를 다시 초기화해 다음 측정이 목록 하단에서 시작되지 않도록 합니다.
import XCTest
final class FeedScrollPerformanceTests: XCTestCase {
func testFeedScrollPerformance() {
let app = XCUIApplication()
app.launchArguments += ["-UITestSeed", "fixed"]
app.launch()
let list = app.collectionViews["feed.list"]
XCTAssertTrue(list.waitForExistence(timeout: 10))
app.buttons["feed.reset"].tap()
XCTAssertTrue(app.cells["feed.item.0"].waitForExistence(timeout: 5))
let options = XCTMeasureOptions()
options.iterationCount = 5
measure(
metrics: [XCTClockMetric(), XCTCPUMetric(), XCTMemoryMetric()],
options: options
) {
app.buttons["feed.reset"].tap()
for _ in 0..<6 {
list.swipeUp(velocity: .fast)
}
}
}
}
초기화 버튼은 테스트 모드에서만 표시해도 되지만, 데이터를 다시 요청하는 대신 항상 동일하게 동작하는 스크롤을 수행해야 합니다. 화면에서 페이지 단위 로딩을 사용한다면 네트워크 계층을 로컬 응답으로 대체하거나, 페이지 로딩 시간을 별도 테스트 항목으로 명확히 분리해야 합니다.
고정된 대상에서 실행하고 결과 보관하기
먼저 사용 가능한 시뮬레이터 목록을 확인해 지속적 통합 환경에서 기기가 몰래 변경되지 않았는지 점검합니다. 그런 다음 테스트에 고유한 결과 번들 경로를 지정합니다. 기존 결과 번들은 미리 삭제해야 하며, 그렇지 않으면 xcodebuild가 덮어쓰기를 거부합니다.
set -euo pipefail
RESULT="$PWD/artifacts/FeedScroll.xcresult"
rm -rf "$RESULT"
mkdir -p "$PWD/artifacts"
xcodebuild test \
-workspace App.xcworkspace \
-scheme AppUITests \
-destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' \
-only-testing:AppUITests/FeedScrollPerformanceTests \
-resultBundlePath "$RESULT"
OS=latest는 현재 툴체인을 항상 따라가는 파이프라인에 적합합니다. 장기간 비교해야 한다면 특정 런타임을 고정하고, Xcode나 시스템을 업그레이드한 뒤 기준선을 다시 구축해야 합니다. OVPS 노드에서 실행하는 작업도 호스트 구성, 커밋 번호, Xcode 버전, 시뮬레이터 런타임을 기록해 환경 업그레이드를 코드 성능 저하로 잘못 판단하지 않도록 해야 합니다.
현재 Xcode에 포함된 xcresulttool을 사용하기 전에는 도움말을 먼저 확인하십시오. 툴체인에 따라 하위 명령이 달라질 수 있기 때문입니다. 스크립트와 테스트 보고서 파서 중 어느 방식을 사용하더라도 원본 xcresult는 실패 작업의 첨부 파일로 보관해야 합니다.
중앙값과 상대 임계값으로 판정하기
한 번의 느린 샘플 때문에 병합을 차단해서는 안 됩니다. 각 커밋을 다섯 번에서 일곱 번 실행하고, 워밍업 실행을 제외한 뒤 중앙값을 계산합니다. 기준선은 첫 측정값을 영구적으로 보존하는 대신, 기본 브랜치에서 최근 여러 차례 안정적으로 실행된 결과의 중앙값을 사용할 수 있습니다.
세 가지 변화를 각각 관찰하는 것이 좋습니다. 총 실행 시간은 사용자가 기다리는 시간을 반영하고, CPU 지표는 반복 레이아웃이나 과도한 그리기를 드러내기 쉬우며, 메모리 지표는 이미지 캐시와 뷰 재사용 문제를 찾는 데 도움이 됩니다. 특정 커밋에서 실행 시간만 증가하고 CPU와 메모리는 안정적이라면 먼저 시뮬레이터 부하를 확인해야 합니다. 세 지표가 동시에 악화될 때 앱 코드 회귀일 가능성이 더 큽니다.
임계값은 비율로 설정하되 최소 절대 차이도 함께 지정해야 합니다. 예를 들어 중앙 실행 시간이 기준선보다 12% 넘게 증가하고, 동시에 절대 증가량이 팀에서 정의한 노이즈 하한을 초과할 때만 실패로 판정할 수 있습니다. 임계값은 동일한 환경에서 수집한 과거 변동을 바탕으로 정해야 하며, 다른 프로젝트의 값을 그대로 복사해서는 안 됩니다.
실패 증거를 보존하고 원인 범위 좁히기
게이트가 실패하더라도 기준선을 즉시 덮어쓰지 마십시오. 먼저 동일한 커밋을 한 번 더 실행합니다. 그래도 실패한다면 xcresult, 전체 로그, 테스트 데이터 버전, 시스템 및 툴체인 버전과 함께 CPU, 메모리, 실행 시간 요약을 저장합니다. 그런 다음 커밋 범위를 기준으로 변경 사항을 좁히면서 이미지 디코딩, 그림자와 블러, 자동 레이아웃 제약 조건, 메인 스레드의 동기식 I/O, 목록 재사용, 로그 출력을 집중적으로 점검합니다.
“지표 저하”와 “테스트 경로 변경”도 구분해야 합니다. 손쉬운 사용 식별자가 더 이상 유효하지 않거나 초기화 동작이 목록 맨 위로 돌아가지 않는다면 테스트 자체는 계속 실행되더라도 다른 화면 영역을 측정할 수 있습니다. 단순히 재시도 횟수를 늘리는 것보다 핵심 시작 지점과 종료 지점에 가시성 단언을 추가하는 편이 더 효과적입니다.
기준선 업데이트는 환경 업그레이드나 허용 가능한 인터랙션 변경 사유를 설명하고, 업데이트 전후의 샘플을 첨부한 별도 변경 사항으로 커밋해야 합니다. 이렇게 하면 일시적인 노이즈 때문에 성능 게이트가 지나치게 자주 작동하는 것을 막으면서도, 기준선을 임의로 갱신해 게이트가 제 역할을 잃는 상황을 방지할 수 있습니다.
자주 묻는 질문
고정된 밀리초 값을 성능 임계값으로 사용해야 하나요?
권장하지 않습니다. 동일한 Mac, 운영체제, 시뮬레이터와 테스트 데이터에서 기준선을 만들고 최근 안정 실행의 중앙값에 상대 임계값을 적용해야 합니다.
첫 번째 실행을 기준선에서 제외하는 이유는 무엇인가요?
앱 설치, 동적 라이브러리 로딩, 글꼴 초기화와 캐시 준비 비용이 포함될 수 있기 때문입니다. 측정 전에 점수에 넣지 않는 예열 실행을 한 번 수행합니다.
성능 테스트 실패 후 무엇을 보관해야 하나요?
xcresult, 테스트 로그, 커밋 ID, Xcode와 macOS 버전, 시뮬레이터 모델, 테스트 데이터 버전, CPU 및 메모리 측정값을 보관해야 합니다.
다음 빌드는 독점 물리 노드에서 실행하세요
세 가지 Apple Silicon 구성과 현재 판매 중인 여섯 개 노드 중에서 선택할 수 있으며, 실제 이용 가능 여부는 콘솔에 실시간으로 표시되는 상태를 기준으로 합니다.