iOS 진행률 검증: 단계별 가중치, 취소와 완료 상태
파일을 일괄 처리하다 보면 진행률이 먼저 100%에 도달한 뒤 실패 메시지가 뜨거나, 사용자가 취소를 눌렀는데도 백그라운드에서 결과가 반영되는 경우가 있습니다. 대개 화면 표시의 문제가 아니라 작업량, 취소 신호, 비즈니스 로직의 최종 상태를 한데 섞어 다룬 데서 생기는 문제입니다. GNUMac 클라우드 Mac에서는 네트워크나 시뮬레이터에 의존하지 않는 단위 테스트로 이 계약을 먼저 확정한 다음 iOS 화면을 검증할 수 있습니다.
먼저 진행률의 의미를 정의하기
이 글에서는 ‘입력 네 개를 변환한 뒤 출력 두 개를 검증하는 작업’을 테스트 픽스처로 사용합니다. 변환은 전체 작업량의 60%, 검증은 40%를 차지합니다. 이는 테스트에서 정한 가중치이며, 성능 측정 결과도 아니고 남은 시간을 나타내는 값도 아닙니다.
부모 진행률의 총 작업량은 100으로 설정하고, 자식 진행률은 각각 파일 수를 단위로 사용합니다. 변환할 파일 수와 업로드할 바이트 수를 그대로 더해서는 안 됩니다. 단위가 다르면 먼저 부모 진행률의 가중치로 환산해야 합니다.
비즈니스 상태는 대기, 실행 중, 취소 중, 취소됨, 실패, 성공으로 별도 정의합니다. 진행률 객체는 수량만 나타내며 오류를 분류하는 역할은 맡지 않습니다.
백분율은 ‘정해진 작업량을 얼마나 완료했는가’에 답하고, 비즈니스 상태는 ‘결과가 확실하게 전달되었는가’에 답합니다. 둘은 서로를 대신할 수 없습니다.
구현에 앞서 세 가지 제약을 명시합니다. 각 자식 진행률은 한 번만 연결하고, 완료량은 총량을 넘지 않으며, 결과를 최종 반영하기 전에 반드시 취소 여부를 확인해야 합니다. 부모와 자식 객체는 개별 작업이 소유하도록 하고, 모든 작업이 공유하는 전역 변수에 두지 않습니다.
결정적 테스트로 가중치 검증하기
기존 프로젝트의 단위 테스트 타깃에 새 테스트 파일을 만들고 다음 코드를 추가합니다. 이 테스트는 Foundation과 XCTest만 사용합니다. 실제 파일 변환이 끝날 때까지 기다릴 필요도 없고, 실행 순서를 짐작해 대기 시간을 넣지도 않습니다.
import Foundation
import XCTest
final class WeightedProgressTests: XCTestCase {
func testWeightedStages() {
let root = Progress(totalUnitCount: 100)
let convert = Progress(totalUnitCount: 4)
let verify = Progress(totalUnitCount: 2)
root.addChild(convert, withPendingUnitCount: 60)
root.addChild(verify, withPendingUnitCount: 40)
XCTAssertEqual(root.fractionCompleted, 0, accuracy: 0.000001)
convert.completedUnitCount = 2
XCTAssertEqual(root.fractionCompleted, 0.3, accuracy: 0.000001)
verify.completedUnitCount = 1
XCTAssertEqual(root.fractionCompleted, 0.5, accuracy: 0.000001)
convert.completedUnitCount = 4
verify.completedUnitCount = 2
XCTAssertEqual(root.fractionCompleted, 1, accuracy: 0.000001)
}
}
여기서는 두 단계를 의도적으로 번갈아 진행시켜 부모 진행률이 현재 단계만 반영하는 것은 아닌지 확인합니다. 부동소수점 결과는 허용 오차를 두고 비교하며, 문자열로 서식화한 백분율을 검증 값으로 사용하지 않습니다.
클라우드 Mac의 테스트 내비게이터에서 이 클래스를 실행하고 커밋 해시, 테스트 결과, 사용한 툴체인 정보를 함께 기록합니다. 이미 명령줄 테스트 실행 경로가 있다면 이 테스트 타깃만 추가합니다. 진행률 테스트만을 위해 별도의 시뮬레이터 매트릭스를 만들지는 않습니다.
취소는 실행 로직의 협력이 필요하다
Progress.cancel()은 신호일 뿐, 실행 중인 루프를 강제로 종료하지는 않습니다. 각 작업 단위를 처리하기 전에 상태를 확인하고, 실제로 완료한 뒤에만 완료량을 늘려야 합니다. 최종 출력 파일을 교체하기 전에도 다시 확인해야 합니다.
다음 테스트는 ‘취소 후에는 다음 작업 단위를 시작하지 않는다’는 조건을 검증합니다. 동시 실행에서 발생하는 경쟁 상태까지 검증하는 테스트는 아닙니다. 앞서 만든 테스트 클래스에 그대로 추가할 수 있습니다.
func testCancellationStopsNextUnit() {
let progress = Progress(totalUnitCount: 4)
progress.isCancellable = true
var processed = 0
func runNextUnit() {
guard !progress.isCancelled else { return }
processed += 1
progress.completedUnitCount += 1
}
runNextUnit()
progress.cancel()
runNextUnit()
XCTAssertTrue(progress.isCancelled)
XCTAssertEqual(processed, 1)
XCTAssertEqual(progress.completedUnitCount, 1)
}
비동기 실행 로직에는 테스트에서 제어할 수 있는 일시 정지 지점이 두 곳 더 필요합니다. 시간이 오래 걸리는 연산을 시작하기 직전과 결과를 최종 반영하기 직전입니다. 테스트에서 작업을 멈춘 다음 취소 신호를 보내고 다시 진행시켜, 새 작업이 시작되지 않고 최종 결과도 공개되지 않는지 검증합니다. 고정된 시간만큼 기다리는 대신 테스트용 게이트로 실행을 조율합니다.
하위 연산이 취소 인터페이스를 제공한다면 취소 신호도 함께 전달해야 합니다. 중단할 수 없는 연산이라면 반환될 때까지 기다린 뒤 결과를 버리고 임시 파일을 정리합니다. 그동안 화면은 ‘취소 중’을 유지해야 하며, 이미 중단된 것처럼 표시해서는 안 됩니다.
알 수 없는 총량과 재시도를 진행률로 포장하지 않기
전체 파일 수를 아직 스캔 중이라면 우선 ‘집계 중’을 표시합니다. 총량을 0으로 놓고 억지로 백분율을 계산하지 않습니다. 스캔이 끝나면 작업 목록을 확정하고, 부모와 자식 관계를 모두 구성한 다음 처리를 시작합니다.
실행 중에 분모를 계속 늘리면 진행률이 뒤로 움직입니다. 작업을 동적으로 찾아야 하는 경우에는 단계 상태나 처리한 수량을 표시하고, 현재 백분율이 확정된 단계에만 해당한다는 점을 명확히 해야 합니다.
재시도할 때도 같은 자식 진행률을 반복해서 0으로 초기화하지 않습니다. 비즈니스 계층에서는 ‘몇 번째 시도인지’를 관리할 수 있지만, 진행률 계층에서는 성공적으로 완료한 논리적 작업 단위를 누적합니다. 한 번의 시도가 실패했다고 해서 파일 하나를 더 완료한 것은 아닙니다.
입력이 없는 경우에도 별도의 규칙이 필요합니다. 빈 입력이 유효하다면 ‘처리할 항목 없음’이라는 성공 결과를 즉시 반환합니다. 호출자가 반드시 입력을 제공해야 한다면 시작 전에 오류를 반환하고, 시작점에서 영원히 멈춰 있는 작업을 만들지 않습니다.
화면 연동과 지속적인 검증으로 마무리하기
진행률 변경을 관찰한 뒤 화면 상태를 갱신할 때는 메인 액터로 전환합니다. 실행할 때마다 고유한 작업 식별자를 부여하고, 상태를 반영할 때 식별자를 대조해 이전 작업에서 늦게 도착한 알림이 새 진행률을 덮어쓰지 못하게 합니다.
화면에 표시하는 백분율은 정수로 반올림해도 되지만, 반올림한 값으로 성공 여부를 판단해서는 안 됩니다. 마지막 파일 처리가 끝난 뒤에도 검증과 원자적 커밋이 필요하다면 해당 단계의 작업량을 미리 배정해야 합니다. 실패했을 때는 완료량을 억지로 채우지 말고 오류 상태를 유지합니다.
병합 전에 다음 항목을 하나씩 확인합니다.
- 중간 지점의 가중치와 단계 간 교차 진행에 대한 검증이 통과하며, 결과가 실행 속도에 의존하지 않는다.
- 시작 전 취소, 처리 중 취소, 결과 반영 직전 취소에 각각 독립적인 테스트 케이스가 있다.
- 오류 경로가 성공으로 처리되지 않으며, 임시 출력물을 정리할 수 있다.
- 새 작업이 시작된 뒤에는 이전 작업의 알림이 현재 화면을 변경할 수 없다.
- 빈 입력, 알 수 없는 총량, 재시도에 대한 표시 규칙이 명확하다.
최종적으로 제공해야 할 것은 부드럽게 움직이는 것처럼 보이는 진행률 표시줄이 아니라 검증 가능한 상태 계약입니다. 먼저 집계 수치를 신뢰할 수 있게 만들고, 그다음 취소를 제어 가능하게 하며, 마지막으로 결과를 실제로 사용할 수 있을 때만 성공으로 처리해야 합니다.
자주 묻는 질문
진행률이 100퍼센트면 성공으로 표시해도 되나요?
아니요. 진행률은 처리한 작업량만 나타냅니다. 최종 검증과 결과 전달까지 성공했는지 별도의 작업 상태로 확인해야 합니다.
cancel()을 호출하면 실행 중인 작업이 자동으로 멈추나요?
아니요. 실행 코드가 취소 상태를 확인하고 실제 작업에 취소를 전달해야 합니다. 최종 결과를 반영하기 전에도 다시 확인해야 합니다.
워크플로에 맞는 전용 Mac mini를 구성하세요
프로젝트에 맞춰 모델, 노드, 대여 기간을 선택하세요. 주문 전에 메모리, 저장 공간, 연결 요구 사항을 확인하세요. 실제 이용 가능 여부는 콘솔에 실시간으로 표시됩니다.