이미지를 로드하는 방법에는 매우 여러 가지가 있다.
Asset 카탈로그에 추가해서 불러오는 경우도 있겠지만, 대게는 원격 저장소에 저장되어 있는 이미지에 대해 url을 사용하여 이미지를 로드시켜야 할 것이다.
아직 그 많은 방법들을 다 경험해보지는 못했지만, 이번 글에서는 크게 세 가지를 방법을 소개하고자한다!
1) 라이브러리를 사용하는 방법(Kingfisher)
2) Data(contentsOf:)를 사용하는 방법
3) URLSession, Alamofire을 사용하는 방법
왜 하필 이 세 가지 방법을 소개하는가? 하면..
이미지가 대량으로 뜨는 작은 프로젝트를 만들었는데 이미지를 로드하는 코드에 대해 위의 세 가지 방법으로 점진적으로 리팩토링을 하며 느낀 점이 많기 때문 ..!!! 나의 블로그 글은 지극히 개인적인 동기와 흥미를 기반으로 작성하게 된다 ㅎㅎ
그리고 위에서 말한 세 가지 방법들로 구현하는 과정에서 마주친 스레드 우선 순위 역전(priority inversion) 이슈와 이미 로드된 이력이 있는 이미지에 대해 캐싱을 해보며 NSCache에 대해 정리하겠다.
# 📌 Kingfisher로 이미지 로드하기
- Kingfisher란, 고유의 원격 저장소 주소를 가지고 있는 이미지에 대해 네트워킹을 통해 앱 내에서 보여지게 해주는 라이브러리이고
- 웹에서 이미지를 비동기로 다운로드하고 이미지를 캐싱하는 기능까지 내장되어 있기 때문에 쉽게 사용할 수 있다는 장점이 있다.
아직 Kingfisher를 세세하게 만져본 건 아니지만 캐시동작도 상세하게 제어가 가능하고 이것저것 제공해 주는 기능이 많다고 한다!
https://github.com/onevcat/Kingfisher?tab=readme-ov-file#features
GitHub - onevcat/Kingfisher: A lightweight, pure-Swift library for downloading and caching images from the web.
A lightweight, pure-Swift library for downloading and caching images from the web. - onevcat/Kingfisher
github.com
이렇게 아주 간단하게도 사용이 가능하고
let url = URL(string: "https://example.com/image.png")
imageView.kf.setImage(with: url)
여러 가지를 커스텀하게 설정해 줄 수 있다.
let url = URL(string: "https://example.com/image.png")
productImageView.kf.indicatorType = .activity
productImageView.kf.setImage(
with: url,
placeholder: UIImage(systemName: "star"),
options: [
.transition(.fade(2)),
.cacheOriginalImage
],
progressBlock: nil
)
# 📌 Data(contentsOf:)로 이미지 로드하기
이미지 url을 사용하여 이미지를 Data 객체화하여 이미지를 로드할 수 있는데, 이 때 사용할 수 있는게 Data(contentsOf : )이다.
extension UIImageView {
func setImageDataFromUrlString(url:String) {
guard let url = URL(string: url) else {return }
DispatchQueue.global().async {
do{
let data = try Data(contentsOf: url)
DispatchQueue.main.async {
self.image = UIImage(data: data)
}
} catch {
print(error)
}
}
}
}
이렇게 이미지를 세팅하는 함수를 만들어주었다고 했을 때, 실제로 UIImageView에 이미지를 넣기 위해 이렇게 메서드를 실행해 줄 수 있다.
imageView.setImageDataFromUrlString(url: "https://example.com/image.png")
이렇게 해서 이미지 잘 뜨네~ 했는데 Xode터미널에 낯선 경고문이 떴다.
🚨 Thread Performance Checker: Thread running at User-initiated quality-of-service class waiting on a lower QoS thread running at Default quality-of-service class. Investigate ways to avoid priority inversions
이런 경고는 초면이어서 찾아보니 🚨 스레드 우선순위 역전(priority inversion)🚨 에 대한 경고라고 한다!
원인
근본적인 원인은 Data(contentsOf:)가 동기적으로 데이터를 로드하기 때문!!
잉? 이미지를 로드하는데 동기적으로 처리한다구? 문제가 될 수 있겠군..ㅇㅇ라는 생각이 들었고 , 좀 더 자세히 보자면
- Data(contentsOf:)는 동기적으로 데이터를 로드하므로, 이 작업이 비록 DispatchQueue.global().async 내에서 수행되고 있어도 여전히 해당 작업이 완료될 때까지 대기하게 된다고 한다!
- 이미지 로딩 작업이 동기적으로 처리되면, 이 작업이 완료될 때까지 스레드를 차단하기 때문에 높은 우선순위의 메인 스레드가 낮은 우선 순위의 이미지 로딩 작업을 기다리게 되어 우선 순위 역전(priority inversion) 문제가 발생할 수 있다는 것이다.
- UserInitiated와 같이 qos가 높은 작업이 있다고 했을 때에도 네트워크 응답을 기다리는 작업이 동기적으로 실행된다면 이 작업에 의해 qos가 높은 작업이 방해받을 수도 있다.
Data(contentsOf : ) 공식문서
Data(contentsOf:) 에 대해 더 궁금해져서 공식문서를 보니 이미 안내가 되어 있었다..!
https://developer.apple.com/documentation/foundation/nsdata/1413892-init
init(contentsOf:) | Apple Developer Documentation
Creates a data object from the data at the specified file URL.
developer.apple.com
이 메서드는 동기식으로 실행되고 완료될 때까지 호출 스레드를 차단하므로 main 스레드에서 호출하지 마세요.
대신 file coordination이나 nonblocking file-related APIs 중 하나를 사용하세요.
라고 중요 표시까지 해서 안내가 되어 있다!
(다른 블로그 글에서 이전 공식문서 캡처한걸 보니 이전에는 좀 더 명시적으로 네트워크 기반 url은 요청하지 말라고 쓰여있던데 지금은 돌려서 안내하고 있는 듯하다?!)
네트워킹 기반으로 하는 url은 사용하지 말고 파일 기반 url은 사용해도 되는 것 같다!
그 이유는 네트워킹 처리를 하려면 아무래도 시간이 걸리고 그동안 스레드가 block되어 있으면 중요한 작업도 기다리는 상태가 되니까..겠죠??
해결하려면?
그래서 해결 방법을 찾아보니 크게 아래 두 가지 방법으로 처리해 볼 수 있을 것 같았다.
- URLSession을 사용하여 비동기적으로 데이터를 로드. URLSession을 사용하면 네트워크 요청이 비동기적으로 처리되므로, 메인 스레드나 다른 높은 QoS 스레드가 블로킹되지 않음.
- QoS 조정: 적절한 QoS 에서 작업을 수행하여 우선순위 역전을 방지.
일단 2번 방법대로 해결을 해보려고 했는데,
큐의 우선순위를 설정해 줄 수 있는 qos를 수정해서 DispatchQueue.global(qos:.background).async 이렇게 코드 수정해 줬더니 경고 메시지는 없어졌다
=> DispatchQueue.global(qos:.background).async 내에서 실행되고 있으므로 이미지 데이터 동기적으로 불러와도 qos가 낮으니 Data(contentsOf:)로 데이터 불러오는 게 좀 느려도 괜찮겠군 ㅇㅇ하는 것! 이기 때문이라고 사료된다.
하지만... 2번 방법으로 해줄 경우 이미지가 눈에 띄게 느리게 로딩되는 것을 볼 수 있다..
21세기에 살고 있는 앱 유저들은 참지 못해..!
그래서 1번 방법으로 코드를 개선해 보았다.
# 📌 URLSession, Alamofire으로 이미지 로드하기
URLSession을 사용하여 이미지를 로드하는 게 가장 정석인 방법이 아닐까 싶다!
물론 kingfisher에서는 쉽게 해줄 수 있는 비동기, 캐싱 처리를 하려면 따로 작업을 해주어야 하지만..🤯
extension UIImageView {
func setImageDataFromUrlString(urlString: String) {
ImageLoadManager.shared.downloadImage(urlString: urlString) { data in
DispatchQueue.main.async() { [weak self] in
guard let self else {return}
self.image = UIImage(data: data)
}
}
}
}
class ImageLoadManager {
static let shared = ImageLoadManager()
private init() {}
private func getData(from url: URL, completion: @escaping (Data?, URLResponse?, Error?) -> ()) {
URLSession.shared.dataTask(with: url, completionHandler: completion).resume()
}
func downloadImage(urlString: String, completion: @escaping (_ data : Data) -> Void ) {
guard let url = URL(string: urlString) else { return }
getData(from: url) { data, response, error in
guard let data = data, error == nil else { return }
completion(data)
}
}
}
URLSession 사용하여 이미지를 불러왔다면 URlSession에 기반하여 만들어진 라이브러리인 alamofire을 사용하는 것도 가능하다!
import Alamofire
extension UIImageView {
func setImageDataFromUrlString(urlString: String) {
guard let url = URL(string: urlString) else {return}
let request = AF.request(url, method: .get)
request.responseData{ [weak self] response in
guard let self else {return }
switch response.result {
case .success(let imageData):
DispatchQueue.main.async {
guard let image = UIImage(data: imageData) else {return}
self.image = image
}
case .failure(let error):
print(error)
}
}
}
}
이번 글에서는 이미지를 로드할 수 있는 여러 가지 방법 & 이미지 로드하기 위해 Data(contentsOf:)를 사용했을 때 발생할 수 있는 문제점에 대해 알아보았다.
이 글에서 이미지 캐싱하는 방법 중 메모리 캐시 방법까지 이어서 쓰려고 했는데 그럼 글이 너무 길어질 것 같아서 다음 글에 이어서 메모리 캐시와 디스크 캐시의 내용을 작성해 보겠다!
'Swift' 카테고리의 다른 글
[Swift] Property Wrapper를 사용해서 UserDefaults 반복 코드 줄여보기 (0) | 2024.07.26 |
---|---|
[Swift] iOS의 메모리 캐시 NSCache | NSMutableDictionary객체와의 차이점, NSCopying, 깊은복사/얕은복사 (1) | 2024.07.04 |
네임스페이스를 관리해보자 | enum, struct로 namespace 관리하기 (0) | 2024.06.21 |
[Swift] 날짜를 포맷팅하는 또다른 방법 .formatted() | DateFormatter말고 formatted 사용해보자 ( + FormatStyle) (2) | 2024.05.29 |
[Swift] 프로토콜 뽀개볼까 (5) | Identifiable (0) | 2024.05.10 |