고해상도 사진도 iOS 목록에서는 작은 셀 하나만 차지하지만, 빠르게 스크롤하는 동안 반복해서 디코딩될 수 있다. 원본 이미지를 먼저 불러온 뒤 UIImage를 표시 크기로 줄이면 화면에는 썸네일이 보이더라도 디코딩 과정에서는 이미 전체 이미지를 처리했을 수 있다. 클라우드 Mac에서 회귀 테스트를 실행할 때는 목록이 부드럽게 스크롤되는지만 살피기보다, 「썸네일의 최대 픽셀 변 길이는 정해진 상한을 넘지 않는다」는 조건을 검증 가능한 계약으로 만드는 편이 더 확실하다.
파일 크기를 추측하지 말고 출력 크기부터 정하기
이 글에서는 최대 변 길이 320픽셀을 테스트 조건으로 사용한다. 모든 화면에 적용할 공통값이라는 뜻은 아니다. 실제 프로젝트에서는 셀의 포인트 크기, 대상 화면의 배율, 이미지 자르기 방식을 바탕으로 요청 크기를 산정해야 한다. 예를 들어 고배율 화면에 표시할 이미지라면 셀의 포인트 크기만으로 출력 픽셀 크기를 계산해서는 안 된다. 상한을 정했다면 이미지 로딩 진입점이 그 값을 받도록 하고, 목록 컨트롤러마다 상수를 따로 두지 않는다.
테스트용 JPEG를 테스트 타깃의 리소스에 landscape.jpg라는 이름으로 추가한다. 샘플의 원본 가로·세로 크기는 모두 테스트 상한보다 커야 한다. 그렇지 않으면 구현이 원본 이미지를 그대로 반환해도 테스트가 통과할 수 있다. 샘플 파일은 저장소에 함께 커밋하고, 테스트 중 네트워크에서 임시로 다운로드하지 않는다. 한 가지 레이아웃만 검사하지 않도록 가로세로 비율이 다른 샘플도 준비한다. 테스트 타깃의 리소스 설정을 확인해 런타임에 테스트 bundle에서 파일을 실제로 찾을 수 있는지도 점검한다.
출력 크기 검증은 「생성된 이미지가 얼마나 큰가」에 답하지만, 「최대 메모리 사용량이 얼마나 높은가」에는 답하지 않는다. 두 항목은 별도로 검증해야 한다.
디코딩 진입점에서 썸네일 만들기
ImageIO로 파일 URL에서 이미지 소스를 만들고, 썸네일 생성 옵션에 최대 픽셀 변 길이를 지정한다. 예제 함수는 테스트에서 픽셀 너비와 높이를 바로 읽을 수 있도록 CGImage를 반환한다. UI 계층에서 UIImage가 필요하다면 호출하는 곳에서 변환한다. 이미지 소스를 만들 때 즉시 캐싱을 꺼 원본 이미지가 먼저 디코딩되지 않도록 하고, 썸네일을 만들 때는 즉시 디코딩하도록 설정한다.
import Foundation
import ImageIO
func makeThumbnail(at url: URL, maxPixelSize: Int) -> CGImage? {
guard maxPixelSize > 0 else { return nil }
let sourceOptions: [CFString: Any] = [
kCGImageSourceShouldCache: false
]
guard let source = CGImageSourceCreateWithURL(
url as CFURL, sourceOptions as CFDictionary
) else {
return nil
}
let thumbnailOptions: [CFString: Any] = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceThumbnailMaxPixelSize: maxPixelSize,
kCGImageSourceShouldCacheImmediately: true
]
return CGImageSourceCreateThumbnailAtIndex(
source, 0, thumbnailOptions as CFDictionary
)
}
여기서는 CreateThumbnailFromImageAlways를 사용해 테스트가 썸네일 생성 경로를 명확히 검증하도록 한다. CreateThumbnailWithTransform은 출력 이미지에 방향 정보를 반영한다. 방향이 다른 샘플은 가로와 세로 값이 바뀔 수 있으므로, 모든 이미지에 「가로가 세로보다 커야 한다」는 규칙을 적용하지 말고 두 변 중 큰 값을 검사해야 한다. 입력이 유효하지 않거나 디코딩할 수 없으면 nil을 반환한다. UI 계층에도 사용자에게 보이는 실패 상태를 마련해야 하며, 모든 파일이 유효하다고 가정해서는 안 된다.
고정 샘플로 출력 크기 회귀 테스트 작성하기
프로덕션 함수를 앱 타깃에 두고 테스트 타깃에서 접근할 수 있게 한다. 다음 테스트는 테스트 리소스가 bundle에 추가되어 있다고 가정한다. 리소스 파일이 하위 디렉터리에 있다면 프로젝트의 실제 리소스 경로에 맞춰 검색 방식을 조정한다. XCTUnwrap을 쓰면 샘플이 없거나 디코딩에 실패했을 때 옵셔널 값이 무시되는 대신 실패 지점을 바로 확인할 수 있다.
import XCTest
import ImageIO
@testable import Sample
final class ThumbnailTests: XCTestCase {
func testThumbnailRespectsPixelLimit() throws {
let url = try XCTUnwrap(
Bundle(for: Self.self).url(
forResource: "landscape",
withExtension: "jpg"
)
)
let image = try XCTUnwrap(
makeThumbnail(at: url, maxPixelSize: 320)
)
XCTAssertGreaterThan(image.width, 0)
XCTAssertGreaterThan(image.height, 0)
XCTAssertLessThanOrEqual(
max(image.width, image.height), 320
)
}
func testRejectsInvalidLimit() throws {
let url = try XCTUnwrap(
Bundle(for: Self.self).url(
forResource: "landscape",
withExtension: "jpg"
)
)
XCTAssertNil(makeThumbnail(at: url, maxPixelSize: 0))
}
}
Sample은 실제 모듈 이름으로 바꾼다. 크기 테스트 외에도 손상된 파일 샘플을 추가해 호출 측에서 nil을 처리할 수 있는지 검증하고, 방향 정보가 포함된 샘플로 표시 방향이 예상과 일치하는지 확인한다. 테스트 이미지의 출처와 사용 권한을 명확히 하고, 개인 사진 보관함의 파일을 저장소에 커밋하지 않도록 주의한다.
클라우드 Mac에서 실행하고 잘못된 실패 원인 찾기
먼저 xcrun simctl list devices available로 현재 설치된 시뮬레이터를 확인한 뒤, 실제로 존재하는 기기를 xcodebuild의 destination으로 선택한다. 프로젝트 이름, scheme, 테스트 타깃도 저장소에서 사용하는 이름으로 바꿔야 한다. 예제 인수를 그대로 복사하고 해당 기기를 사용할 수 있다고 가정하지 않는다.
xcrun simctl list devices available
xcodebuild \
-project Sample.xcodeproj \
-scheme Sample \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-only-testing:SampleTests/ThumbnailTests \
test
「이미지를 찾을 수 없다」는 오류가 나면 크기 상한을 높이기 전에 테스트 타깃의 리소스 포함 여부부터 확인한다. 출력 크기가 상한을 넘는다면 목록에서 실제로 makeThumbnail을 호출하는지, 원본 UIImage를 먼저 생성하는 기존 경로로 돌아가지는 않았는지 점검한다. CI에서는 실패한 테스트의 명령 출력도 보관하고 같은 샘플 파일을 계속 사용해야 한다. 이미지를 바꾸면서 픽셀 크기와 방향을 확인하지 않으면 변경 전후 결과를 비교하기 어려워진다.
크기 검증과 메모리 관찰 분리하기
크기 테스트는 커밋할 때마다 실행하기 좋지만, 실제 목록에서 여러 이미지를 동시에 보유하고 이미지를 캐싱하거나 빠르게 스크롤하는 동작까지 재현하지는 못한다. 병합하기 전에는 대상 기기에서 대표적인 이미지 모음을 사용해 실제 목록 경로를 반복해서 스크롤하며 최대 메모리 사용량과 이미지 해제 상태를 관찰한다. 기기, 시스템 버전, 이미지 수, 작업 순서를 기록한 뒤 변경 전후를 비교한다. 시뮬레이터에서 한 번 측정한 값을 모든 기기에 적용되는 결론으로 삼지 않는다.
마지막으로 세 가지를 확인한다. 썸네일 요청 크기가 표시 요구사항에서 도출되는지, 모든 이미지 로딩 경로가 동일한 다운샘플링 함수를 거치는지, 유효하지 않은 이미지에 대해 사용자에게 보이는 처리 경로가 있는지다. 이렇게 하면 클라우드 Mac의 자동화 테스트는 픽셀 크기 계약을 지키고, 기기에서의 측정은 실제 메모리 문제에 답하게 된다. 두 근거가 담당하는 범위는 서로 다르다.
자주 묻는 질문
UIImage를 불러온 뒤 줄여도 다운샘플링과 같나요?
아닙니다. 원본이 이미 전체 크기로 디코딩되었을 수 있습니다. ImageIO에서 썸네일을 생성할 때 픽셀 상한을 지정해야 합니다.
크기 테스트를 통과하면 메모리 사용량도 안전한가요?
아닙니다. 테스트는 출력 픽셀만 확인합니다. 메모리 최고치는 대표 이미지와 실제 스크롤 동작을 사용해 대상 기기에서 별도로 측정해야 합니다.
다음 작업에 맞춰 클라우드 Mac 구성하기
모델의 메모리 용량이나 빌드 작업량에 맞춰 Mac mini를 선택하세요. 세 가지 모델을 일·주·월·분기 단위로 대여할 수 있으며, 노드의 실시간 이용 가능 여부는 콘솔에서 확인할 수 있습니다.