티스토리 뷰
안녕하세요,
앨범 앱을 만들고 있는 Diana 입니다.
저번 게시물에서는 앨범 앱을 만들기 위해 접근권한을 설정하고 이미지 메타데이터를 사용하여 PHCachImageManager를 통해 이미지를 가져와 CollectionView에 뿌려주었습니다.
하지만 여기서 몇 가지 문제가 발생했는데요.
오늘은 문제를 해결하기 위해 PHCachImageManager의 requestImage()에 대해 알아보려고 합니다.
좀 두서가 없긴 하지만 미래의 저를 위해 쓰는 글이기 때문에 괜찮을..지도?
시작하겠습니다.
이전 포스팅까지 구현한 코드를 실행해보면 발생하는 문제는 크게 세 가지가 있습니다.
첫 번째, 앨범에 사진이 별로 없는 테스트 폰에서는 상관 없지만 수 만개의 이미지를 가진 제 핸드폰에서 코드를 실행하면 이미지 로딩까지 약 40초의 시간이 소요됩니다. 40초동안 사용자는 사진이 보일때까지 기다리고 있는 셈이죠.
두 번째, 아래 창을 보면 40초동안 앱이 실행될 때 메모리와 에너지 사용량이 폭발합니다. 그리고 이미지가 뜬 이후에도 메모리 사용량은 계속 증가합니다. 이는 PHAsset과 PHCacheImageManager의 작동 방식을 좀더 알아볼 필요가 있을 것 같습니다.


세 번째, 그렇게 40초동안 열심히 해서 사진을 가져와놨건만 앱을 켜놓고 가만히 기다리면 사진의 정렬이 자꾸 달라집니다. 이제 슬슬 제가 뭘 만든건지 불안해지기 시작했습니다.
어쨋든 이번 포스팅에서는 이 세 가지 문제점의 원인을 찾고 해결하는데 중점을 두도록 하겠습니다.
✅ 원인파악
문제가 되는 부분을 찾아내기 위해서는 우리는 앨범에서 이미지를 받아오는 과정을 다시 상기시켜볼 필요가 있습니다.
func fetchAlbumContents(mediaType: PHAssetMediaType, targetSize: CGSize) -> Observable<[AlbumInfo]> {
return Observable.create { observer in
// 1. FetchAssets의 옵션 설정
let options = PHFetchOptions()
options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
// 2. 이미지의 메타데이터를 가지고 오는 부분
let fetchResult = PHAsset.fetchAssets(with: mediaType, options: options)
var albumInfos: [AlbumInfo] = []
// 3. 이미지 요청 옵션 설정
let imageOptions = PHImageRequestOptions()
imageOptions.isSynchronous = false
imageOptions.deliveryMode = .highQualityFormat
let group = DispatchGroup()
fetchResult.enumerateObjects { asset, _, _ in
group.enter()
// 4. 메타데이터로 이미지 요청
self.imageManager.requestImage(for: asset,
targetSize: targetSize,
contentMode: .aspectFill,
options: imageOptions) { image, _ in
let info = AlbumInfo(asset: asset,
thumbnail: image,
creationDate: asset.creationDate,
mediaType: asset.mediaType)
albumInfos.append(info)
group.leave()
}
}
group.notify(queue: .main) {
observer.onNext(albumInfos)
observer.onCompleted()
}
return Disposables.create()
}
}
제 코드에서 이미지를 가져오는 부분은 이렇습니다.
첫 번째, 어떤 앨범 데이터를 가지고 올 것인지 PHFetchOptions를 통해 옵션을 설정합니다.
PHFetchOptions에는 아래와 같은 설정들이 있습니다.

이 중 저는 sortDescriptors를 사용하여 최신 이미지 데이터부터 내림차순으로 가져왔습니다.
하지만 자세히보니 fetchLimit이란 속성도 보입니다. 초기 단계에 몇개의 데이터를 가져올지 설정할 수 있는 속성이라고 하네요.
해당 속성을 50으로 설정한 뒤 앱을 실행시켜보겠습니다.

앱이 실행됨과 거의 동시에 이미지가 표시됩니다.
디버깅 세션을 확인해본 결과 메모리와 CPU 사용량도 더이상 늘지 않는 것을 확인할 수 있습니다.
하지만 이건 일시적인 해결책일 뿐 우리는 앨범의 모든 이미지를 가져와야하기 때문에 그닥 맘에드는 해결책은 아닙니다.
제대로된 해결책을 찾기 위해 requestImage()에 대해 자세히 알아봅시다.
공식문서에 따르면 requestImage 는 Photos 내부에서 이미지를 디코딩하고 리사이즈를 한 뒤 반환해준다고 합니다. 이때 이미 캐시에 좀더 큰 썸네일이 있는 경우 바로 반환해주므로 성능을 위해서 이미지는 우리가 설정한 targetSize보다 살짝 차이가 있을 수 있습니다.
또한 isNetworkAccessAllowed가 true로 설정되어 있는 경우 iCloud에서 이미지를 받아오기도 하는 등 생각보다 시간이 걸리는 작업을 처리합니다.
또한 해당 함수는 기본적으로 비동기적으로 작동하나 백그라운드 스레드에서 호출되는 경우 isSynchronous를 true로 설정해줘야 문제가 발생하는 것을 방지할 수 있습니다.
그리고 asynchronous하게 request됨에 따라 Photos는 request handler 블록을 한번 이상 호출할 수 있습니다.
첫 번째 호출된 경우 일시적으로 낮은 퀄리티의 이미지가 제공되며 그 동안 높은 퀄리티의 이미지가 준비된 뒤 준비가 완료되는 경우 request handler는 다시 호출될 수 있습니다.
자세한 내용을 알게되었으니 이제 문제를 해결해봅시다.
우선 로직을 처리하고 있는 fetchAlbumContents()가 어느 스레드에서 실행되고 있는지 확인해봅시다.
Thread.isMainTread로 확인한 결과 해당 로직은 메인스레드에서 작동되고 있는것을 알 수 있습니다.
하지만 제가 AlbumFetcher로 굳이 굳이 나눈 이유가 로직을 별도로 분리하려고 한 것인데 UI 처리만 담당해야하는 메인스레드가 이 로직마저 담당하고 있다니..
메인스레드에서 돌아가고 있는 상황인데 PHFetchOptions에 isSynchronous를 true로 해버리면 UI를 표시해야하는 메인스레드 입장에서 블로킹과 과도한 리소스 사용이 발생해 앱이 강제 종료되는 문제가 발생할 것 입니다.
따라서 이 부분을 백그라운드 스레드로 변경해줍니다.
백그라운드 스레드로 변경해주기 위해 GCD의 DispatchQueue.global()을 사용해주었습니다.
isSynchronous를 false로 설정하는 경우 requestImage의 request Image Handler는 비동기로 실행되게 됩니다. 하지만 위에서 알아봤듯이 해당 함수는 이미지를 가져오는 것 뿐만이 아닌 디코딩하고 리사이즈 하는 등의 무거운 작업을 진행합니다.
비동기로 작동함에 따라 해당 블록은 한꺼번에 호출되고 실행은 이후 모든 작업이 끝난 뒤 동시다발적으로 진행되게 되는데요.
이 경우 Thread들에 해당 작업들이 동시에 몰림으로 인해 엄청난 과부하가 발생하게 되고 그 결과 CPU와 메모리의 사용량은 폭발하게 됩니다.
이때 isSynchronous를 true로 변경하는 경우 해당 클로저 블록은 동기로 실행되게 되며 하나의 작업이 호출 되면 이미지를 가져와 디코딩하고 리사이즈까지 마친 뒤 Thread는 작업에서 release되고 그다음 작업을 맞이하게 됩니다.
그 결과 디버깅 세션을 확인해보면 CPU와 에너지 사용량은 금새 정상 범주로 돌아오게 됩니다.


이렇게 정리하려고 하는데 의문이 생길 수 있습니다.
이미지를 두번 호출한다며!!
requestImage() 문서를 보면 처음엔 Cach에 있는 저화질의 썸네일을 가져온 뒤 이후 고화질의 이미지를 다시 받아오는 작업이 발생할 수 있다고 나와있습니다.
하지만 실은 이 문제는 PHFetchOptions의 isSynchronous를 true로 변경하면서 자동으로 해결이 완료되었습니다.
무슨말이냐?
말 그대로 저화질의 썸네일을 먼저 가져오는 경우 해당 문제가 발생할 수 있는데 이는 Option의 deliveryMode를 .highQualityFormat으로 변경하면 해결되는 문제였습니다.
하지만 deliveryMode는 isSynchronous가 true인 경우 디폴트로 highQualityFormat으로 설정되며 우리가 다른 설정으로 변경되더라도 무시하고 highQualityFormat으로 작동하게 됩니다.
이렇게 하여 결과물은 아래와 같이 됩니다.
import Photos
import RxSwift
import UIKit
final class AlbumFetcher {
private let imageManager = PHCachingImageManager()
func fetchAlbumContents(mediaType: PHAssetMediaType, targetSize: CGSize) -> Observable<[AlbumInfo]> {
return Observable.create { observer in
let options = PHFetchOptions()
options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
let fetchResult = PHAsset.fetchAssets(with: mediaType, options: options)
var albumInfos: [AlbumInfo] = []
let imageOptions = PHImageRequestOptions()
imageOptions.isSynchronous = true
imageOptions.deliveryMode = .highQualityFormat // 기본 값
fetchResult.enumerateObjects { asset, _, _ in
self.imageManager.requestImage(for: asset,
targetSize: targetSize,
contentMode: .aspectFill,
options: imageOptions) { image, _ in
let info = AlbumInfo(asset: asset,
thumbnail: image,
creationDate: asset.creationDate,
mediaType: asset.mediaType)
albumInfos.append(info)
}
}
observer.onNext(albumInfos)
observer.onCompleted()
return Disposables.create()
}
}
}
이렇게 해서 앱을 실행시켜봅시다.(아직 안끝났어?)
앱을 실행시켜본 결과 CPU와 에너지 사용량도 정리가 되었고 멋대로 이미지가 재 정렬되던 문제도 해결이 되었습니다.
하지만 아직도 앱은 20초정도의 로딩시간을 필요로 합니다.
처음보다는 확연히 짧아진 시간이지만 유저 입장에서는 아직도 답답한 속도일 것 입니다.
글이 너무 길어지는 관계로 이 부분은 다음 포스팅에서 이어서 하도록 하겠습니다.
감사합니다!
'Swift' 카테고리의 다른 글
| 앨범 만들기 4편 - Transitioning Delegate 를 통한 화면전환 애니메이션 개념편 (1) | 2025.11.07 |
|---|---|
| 앨범 만들기 3편 - 무한 스크롤 구현하기 (0) | 2025.11.06 |
| 앨범 만들기 1편 - 앨범에 접근하여 사진 가져오기 (0) | 2025.11.03 |
| Swift - Opaque Type 이란? (1) | 2024.10.17 |
| Swift - private(set) 이란? (0) | 2024.09.19 |
- Total
- Today
- Yesterday
- iphone
- 스위프트
- 코어데이터
- capsulation
- 팁킷
- tipview
- kakaomapssdk
- SwiftUI
- ViewBuilder
- 알고리즘
- boxedprotocoltype
- asymptoticnotation
- swift
- private(set)
- opaquetype
- 개발자코테
- tipkit
- Xcode
- PHCachImageManager
- coredata
- tuist v4
- 빅세타표기법
- Tuist
- Concurrency
- Algorithm
- ios
- BoxedType
- boxedprotocol
- opaque
- reusablelist
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | ||||||
| 2 | 3 | 4 | 5 | 6 | 7 | 8 |
| 9 | 10 | 11 | 12 | 13 | 14 | 15 |
| 16 | 17 | 18 | 19 | 20 | 21 | 22 |
| 23 | 24 | 25 | 26 | 27 | 28 | 29 |
| 30 |
