Skip to content

이미지 압축 기능 구현

summercat edited this page Oct 18, 2023 · 3 revisions

🚨 문제 상황

  • 사용자가 선택한 사진을 API에 업로드하고, 분석 결과를 받는 시간이 오래 걸려 사용자 경험 저하
  • 사용자의 사진/서버에서 받은 사진의 크기가 커 캐시 용량이 빠르게 소진되는 현상 발생

🔑 해결

  • 사용자의 사진을 사진 분석 API에 업로드하기 전에 이미지 압축 후 업로드함으로써 데이터 업로드, 사진 분석 시간을 단축시켰습니다.
  • API로부터 다운받은 이미지 데이터를 사용/캐싱하기 전 압축함으로써 캐시 공간 사용 효율성을 높였습니다.

압축 로직

용량이 큰 이미지일 수록 압축 비율을 높게 해서 설정한 maxByte 크기 미만이 될 때까지 이미지를 반복해서 압축하는 방식입니다. 압축 과정은 아래의 과정을 순서대로 반복합니다.

  1. 압축할 비율(줄일 비율. 예: 0.1만큼 줄인다)을 구한다.
  2. 해당 비율에 맞추어 canvasSize를 생성한다.
  3. 해당 canvasSize에 맞추어 이미지를 리사이징 한다.
  4. 리사이징한 이미지의 크기를 구하고, 위의 과정을 반복한다.
// ImageCompressor.swift
struct ImageCompressor {
    
    static func compress(
        imageData: Data,
        maxByte: Int = 1_024_000,
        completion: @escaping (Data?) -> Void)
    {
        DispatchQueue.global().async {
            guard let image = UIImage(data: imageData),
                  let currentImageSize = image.jpegData(compressionQuality: 1.0)?.count else {
                return completion(nil)
            }
            
            var iterationImage: UIImage? = image
            var iterationImageSize = currentImageSize
            var iterationCompression: CGFloat = 1.0
            
            while iterationImageSize > maxByte,
                  iterationCompression > 0.01 {
                let percentageDecrease = getPercentageToDecreaseTo(forDataCount: iterationImageSize)
                let canvasSize = CGSize(
                    width: image.size.width * iterationCompression,
                    height: image.size.height * iterationCompression)
                
                iterationImage = image.resize(width: canvasSize.width, height: canvasSize.height)
                
                guard let newImageSize = iterationImage?.jpegData(compressionQuality: 1.0)?.count else {
                    return completion(nil)
                }
                
                iterationImageSize = newImageSize
                iterationCompression -= percentageDecrease
            }
            
            completion(iterationImage?.jpegData(compressionQuality: 1.0))
        }
    }
    
    private static func getPercentageToDecreaseTo(forDataCount dataCount: Int) -> CGFloat {
        switch dataCount {
        case 0..<5_000_000:
            return 0.03
        case 5_000_000..<10_000_000:
            return 0.1
        default:
            return 0.2
        }
    }
}
// UIImage+resize
extension UIImage {
    func resize(width: CGFloat, height: CGFloat) -> UIImage {
        return UIGraphicsImageRenderer(size: CGSize(width: width, height: height)).image { _ in
                self.draw(in: CGRect(x: 0, y: 0, width: Int(width), height: Int(height)))
        }
    }
}

이미지 압축 로직 참고 링크와 프로젝트 내부 구현 간의 차이점

  • 참고링크
  • 이미지 데이터 매개변수/반환 타입을 UIImage에서 Data로 변경
    • 뷰모델과 같이 로직을 처리하는 객체에서 호출할 것이라고 생각했기 때문에, 해당 객체에서 UI에 의존하지 않을 수 있도록(import UIKit하지 않아도 되도록) 이미지 데이터와 관련한 매개변수/반환 타입을 Data 타입으로 변경하였습니다.
  • maxByte에 1MB를 기본값으로 설정
    • 1MB로 설정한 이유는 카카오톡에서 사진을 'Standard Quality'로 압축해서 보낼 때 파일 크기가 100Kb ~ 700Kb 사이였기 때문에 1MB 정도면 충분하다고 생각했습니다.
  • UIGraphicsBeginImageContext를 사용하지 않도록 변경
    • 공식문서를 확인해 보니 iOS17.0 이후 deprecated 될 예정이어서, UIGraphicsImageRenderer를 사용하는 메서드를 이용하도록 내부 구현을 변경해 주었습니다 (UIGraphicsImageRenderer는 iOS10.0 이상에서 사용 가능)