[Trinap] 이미지 모듈 도입

도윤·2022년 12월 17일
0

부스트캠프

목록 보기
1/1
post-thumbnail

다음 게시글은 네이버 부스트캠프의 최종 프로젝트인 Trinap에 기반하여 설명을 드리겠습니다

적용된 코드를 보고 싶으시다면 아래의 링크를 참고해주시면 감사하겠습니다.

https://github.com/boostcampwm-2022/iOS02-Trinap

이미지 모듈은 왜 도입했을까?

저희가 제일 크게 신경 쓴 부분은 이미지 캐시 기능과 메모리 효율성을 위한 resize입니다.

이유는 아래에서 설명해드리겠습니다.

이미지 Resize

네트워크 통신을 통해 ImageData를 가져와서 CollectionViewCell에 보여주는 과정을 거치고 있습니다.

하지만 이렇게 이미지를 원본으로 보여주게 될 경우에 기존의 이미지 파일보다 Xcode에서는 더 큰 메모리를 사용하고 있는 것을 아시나요?

WWDC Memory Deep Dive에 나와있는 내용을 기반으로 작성하겠습니다.

이미지의 용량은 파일의 크기가 아니라 이미지의 크기와 관련있습니다.

1536x2048의 해상도는 약310만의 픽셀을 가지게 됩니다.

하나의 픽셀에는 R,G,B,투명도의 정보를 담아야해서
각각 1바이트씩 총 4바이트의 크기를 갖습니다.

따라서 해당 이미지는 310만개의 픽셀을 가지고 각각의 픽셀은 4바이트로 구성이 되어있어서 약 12MB의 크기를 갖게 됩니다.

iOP의 작은 모바일 화면에서 60*60의 크기를 갖는다면 약 (1536 x 2048 - 60 x 60)의 픽셀이 낭비가 됩니다. 사실상 대부분의 픽셀은 버려지는거나 마찬가지입니다. 제한된 모바일 환경에서는 이러한 메모리 낭비는 치명적일 수 있습니다.

이미지 처리 과정

대부분 Asset으로 저장해둔 데이터가 아니라면 네트워크 통신을 이용하여 이미지 데이터를 불러올 것입니다.

네트워크를 통해 JPG파일을 Swift의 Data형태로 변환하는 과정을 로딩이라고 합니다.

자세히는 Data Buffer라고 불리는 형태가 되어 메모리에 저장됩니다. 이 크기는 파일 크기와 동일하게 590KB 사이즈가 됩니다.

이렇게 저장된 Data Buffer, 즉 Data를 Image Buffer로 변환하는 과정을 거쳐야 Image로 변환할 수 있습니다.
이러한 과정을 Decoding이라고 합니다.

이러한 과정에서 아까 말씀드린 이미지의 원본 크기가 불리면서 12MB의 크기로 변환이 됩니다.

다운 샘플링 과정은 해당 DataBuffer가 현재 1536x2048의 데이터이므로 이미지 크기를 60x60크기로 바꾸는 과정입니다.

이미지 크기 x 4byte(rgb,alpha) x 3(iphoneX기준 해상도)
아마도 크기는 60x60x4 = 대략 0.04MB로 줄어들 것으로 예상이 됩니다.

다운 샘플링을 적용한 코드는 다음과 같습니다

    public func imageWithDownsampling(to targetSize: CGSize, scale: CGFloat = 1) -> QFImage? {
        let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
        guard let imageSource = CGImageSourceCreateWithData(self as CFData, imageSourceOptions) else { return nil }
        
        let maxDimension = Swift.max(targetSize.width, targetSize.height) * scale
        let downsamplingOptions = [
            kCGImageSourceCreateThumbnailFromImageAlways: true,
            kCGImageSourceShouldCacheImmediately: true,
            kCGImageSourceCreateThumbnailWithTransform: true,
            kCGImageSourceThumbnailMaxPixelSize: maxDimension
        ] as CFDictionary
        
        guard let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsamplingOptions) else {
            return nil
        }
        
        return UIImage(cgImage: downsampledImage)
    }

동일한 뷰에 다운샘플링 전,후를 비교하게 되면 2배의 메모리 효율성을 볼 수 있게 되었습니다!

이미지 캐시

저희의 앱은 다음과 같이 메인 화면에서 세로 스크롤을 사용하고 있고, 또한 가로스크롤로 여러개의 이미지를 지원하고 있습니다.

위의 gif는 스크롤 할 때 마다 동일한 이미지에 접근하더라도 매번 네트워크를 통해 이미지를 불러오는 작업을 하는 UI가 보입니다.

이렇게 불필요한 UX를 제거하기 위해 캐시정책을 도입하여 이러한 문제점을 없애고자 했습니다.

Memory Cache

메모리 캐시를 Swift에서 구현하는 방법은 크게 두가지 방법이 있다.

  1. Dictionary를 통해 직접 캐시 메모리를 구현하는 방법
  2. NSCache를 사용하는 방법.

Dictionary를 통해 구현하게 된다면 고려해야할 점들이 많다.

  • 이미지를 삭제하는 교체 알고리즘 구현
  • Dictionary의 버퍼 크기를 고려
  • Cache에 대해 동시다발적으로 동작을 했을 때 동기화문제가 발생할 수도 있습니다.

NSCacheThread Safe하고 시스템 메모리를 너무 많이 사용하게 될 경우에 자동으로 제거를 해주게 됩니다.

따라서 NSCache를 이용하여 구현하였습니다.

 private let cache = NSCache<NSString, NSData>()

NSCache의 주요 프로퍼티 및 메소드

- var name: String // 캐시의 이름
- var countLimit: Int // 캐시가 가질 수 있는 최대한의 객체 수
- var totalCountLimit: Int // 객체를 제거하기 전 캐시가 보유할 수 있는 최대 비용
- func setObject(ObjectType, forKey: KeyType) // 데이터 삽입
- func object(forKey: KeyType) -> ObjectType? // get에 해당하는 함수
- func removeObject(forKey: KeyType) // 삭제
- func removeAllObject() // 캐시 전부 삭제

이미지 처리하기

public func fetch(at url: URL, completion: @escaping (QFData?) -> Void) {
    let key = key(for: url)

	if let data = cache.object(forKey: key) {
	    completion(data as QFData)
    } else {
    	fetchImage(at: url) { [weak self] fetchedData in
        	guard let self, let fetchedData else {
            	completion(nil)
            	return
			}
    		self.cache.setObject(fetchedData as NSData, forKey: key)
            completion(fetchedData)
         }
     }
}
func fetchImage(at url: URL, completion: @escaping (QFData?) -> Void) {
     DispatchQueue.global().async {
         let data = try? Data(contentsOf: url)
         completion(data)
     }
 }	

이미지 URL을 키값으로 설정하여 cache.object(forKey: key)로 조회하여 있다면 해당 Data를 사용하고, 없으면 해당 URL을 직접 조회하여 사용하도록 했습니다.

캐시처리를 하게되었을 때 아래처럼 중복된 이미지를 스크롤로 조회할 때 메모리 캐시에 있는 이미지를 가져오기 때문에 더 자연스러운 화면을 만들 수 있게 되었습니다.

0개의 댓글