본문 바로가기

카테고리 없음

[iOS] Kingfisher로 원격 저장소 이미지 캐싱하기

1. iOS UICollectionView, UITableView

iOS는 UICollectionView, UITableView를 처리할 때 한번에 데이터 수만큼 cell을 만들지 않는다.

메모리 정책에 기반하여 view에 보여지는 만큼만 cell을 생성하고 나머지는 재사용한다. 이것이 cell 재사용 정책이다.

다음과 같이 cellForItemAt() 안에서 cell을 반환할 때 dequeueReusableCell 에서 구별자로 cell을 가져오는 것을 볼 수 있다.

 

collectionView(collectionView:, cellForItemAt:) 메소드에서 cell을 반환하는 경우

그런데 이렇게 UICollectionView나 UITableView를 스크롤 하다 보면 이미지를 원격 저장소, 즉 이미지 url에서 이미지를 가져와서 cell에 보여주는 경우 이미지가 잘못 들어가거나 왔다 갔다 제대로 보이지 않는 경우가 발생할 수도 있다.

왜 이런 것일까?

 

2. 문제 상황의 원인: cell 재사용과 이미지 로딩

재사용 큐에 의해서 cell은 재사용된다. 즉 기존 cell의 정보들을 지우고 다시 새로운 정보를 넣어야 하는데 원격 저장소로부터 이미지를 로딩하는 작업은 디코딩 작업을 필요로 하기 때문에 global queue에서 작업한다.

queue에 대한 설명은 다음을 참고하면 좋다.

 

 

[iOS] GCD의 기본: DispatchQueue의 종류와 특성

지난 번에는 GCD가 무엇인지 기본을 알아보았다. [iOS] GCD의 기본: sync, async, serial, concurrent ios multi-threading ios에서 멀티 스레딩을 지원하는 방식은 Thread, OperationQueue, GCD 이다. 이 중에서 Thread는 복잡

josushell.tistory.com

 

global queue이기 때문에 concurrent고 이 말은 즉 끝나는 순서를 보장할 수 없다는 뜻이 된다. 또한 cell을 dequeue 할때 분명 queue임에도 불구하고 먼저 들어간게 먼저 재사용되지는 않는다.

따라서 기존 이미지가 로딩되었다가 새로운 이미지가 로딩되는 사이가 오~래 걸릴 수도 있고, 혹은 순서가 반대로 되서 새로운 이미지가 먼저 로딩된다면 그 위에 기존 이미지가 overwrite되어 기존 이미지가 계속 보일 수도 있다. 즉 엉망진창이 되어버린다.

 

3. 해결

일단 백에서 Async로 처리하는 것도 하나의 방법이지만 이미지 캐싱을 사용한다면 더 좋을 것 같다.

물론 swift만으로도 해결 가능하다. 비동기 처리를 위한 API가 많기 때문이다. preparingForDisplay() 라던지 prepareThumbnail() 등을 활용할 수 있을듯하다.

하지만 오늘은 Kingfisher 라이브러리를 사용하여 문제를 해결할 것이다.

 

 

4. Kingfisher

Kingfisher를 설치해준다. SPM, cocoaPod 모두 사용가능하다.

 

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. - GitHub - onevcat/Kingfisher: A lightweight, pure-Swift library for downloading and caching images from the web.

github.com

 

위의 README를 읽어보면 아주 쉽게 사용법이 나와있다.

import Kingfisher

let url = URL(string: "https://example.com/image.png")
imageView.kf.setImage(with: url)

진짜 이게 다다.

 

공식 문서에는 더 응용된 사용법도 나와있다.

1. 고해상도의 이미지를 다운로드한다.

2. imageView 사이즈에 맞게 다운 샘플링한다.

3. 코너에 둥글기 (radius)를 설정하여 둥글게 만들 수 있다.

4. 다운로드 되는 동안 placeholder 이미지나(준비 중 이미지), 로딩 인디케이터를 보여줄 수도 있다.

5. 준비가 되면 썸네일 이미지에 fade 효과를 주어 최종 이미지를 보여줄 수 있다.

6. 원래 큰 이미지는 디스크에 캐싱되어, 나중에 다시 다운로드할 필요가 없다.

7. 작업이 끝나거나, 성공, 실패시에는 콘솔에 log가 찍히게 된다.

 

각각의 step에 해당되는 코드는 다음과 같다.

// 1. 고해상도의 이미지를 다운로드한다.
let url = URL(string: "https://example.com/high_resolution_image.png")

// 2. imageView 사이즈에 맞게 다운 샘플링한다.
// 3. 코너에 둥글기 (radius)를 설정하여 둥글게 만들 수 있다.
let processor = DownsamplingImageProcessor(size: imageView.bounds.size)
             |> RoundCornerImageProcessor(cornerRadius: 20)

 

// 4. 다운로드 되는 동안 placeholder 이미지나(준비 중 이미지), 로딩 인디케이터를 보여줄 수도 있다.
imageView.kf.indicatorType = .activity

 

imageView.kf.setImage(
    with: url,
    placeholder: UIImage(named: "placeholderImage"),	// placeholder 이미지
    options: [
        .processor(processor),
        .scaleFactor(UIScreen.main.scale),
        .transition(.fade(1)),	// 5. 준비가 되면 썸네일 이미지에 fade 효과를 주어 최종 이미지를 보여줄 수 있다.
        .cacheOriginalImage	// 6. 원래 큰 이미지는 디스크에 캐싱되어, 나중에 다시 다운로드할 필요가 없다.
    ])
{
	// 7. 작업이 끝나거나, 성공, 실패시에는 콘솔에 log가 찍히게 된다.
    result in
    switch result {
    case .success(let value):
        print("Task done for: \(value.source.url?.absoluteString ?? "")")
    case .failure(let error):
        print("Job failed: \(error.localizedDescription)")
    }
}

 

전체 코드는 다음과 같다.

let url = URL(string: "https://example.com/high_resolution_image.png")
let processor = DownsamplingImageProcessor(size: imageView.bounds.size)
             |> RoundCornerImageProcessor(cornerRadius: 20)
imageView.kf.indicatorType = .activity
imageView.kf.setImage(
    with: url,
    placeholder: UIImage(named: "placeholderImage"),
    options: [
        .processor(processor),
        .scaleFactor(UIScreen.main.scale),
        .transition(.fade(1)),
        .cacheOriginalImage
    ])
{
    result in
    switch result {
    case .success(let value):
        print("Task done for: \(value.source.url?.absoluteString ?? "")")
    case .failure(let error):
        print("Job failed: \(error.localizedDescription)")
    }
}

 

 

5. 프로젝트에 적용하기

직접 사용해보자.

먼저 원래 프로젝트의 코드는 다음과 같다.

여기서 88번줄 대신 kingfisher를 이용해서 이미지를 로드해보자.

 

참고로 원래 load하는 코드는 다음과 같이 작성했었다.

 

다음처럼 설정하고 코드를 변경했다.

cell.layout_img.kf.indicatorType = .activity
cell.layout_img.kf.setImage(with: book[0], placeholder: UIImage(named: "noBookImg"), options: [.transition(.fade(1)), .cacheOriginalImage], progressBlock: nil)

 

.forceTransition은 이미지 로딩에 오랜 시간이 걸리지 않아도(캐싱해서) 강제로 fade 효과를 주는 것이다.

즉 언제나 fade 효과가 보인다.

 

결과는 다음과 같다. 원래는 이렇게 끊겨서 보였던게

 

매끄럽게 잘 보인다!

만족 그 자체

캐싱을 삭제하고 관리하는 부분은 나중에 해야겠다