iyOmSd/Title: Swift

[Swift] Image Cache처리 (NSCache, FileManager)

냄수 2020. 8. 9. 08:26
반응형

서버에서 이미지를 불러올때

이미지 URL을 받고

다시 해당 URL로 통신을 하는 식으로 이미지를 받아온다면 비효율적이겠죠??

또한 이미지크기도 크다고하면 더욱 좋지 않을거같아요..!

 

그래서!!

효율적인 캐시처리에 대해서 알아볼거에요

 

캐시

기기 안의 임시 저장소를 뜻하는거에요

임시 저장소를 통해서 이미지를 넣어두고 빠르게 보여줄 수 있고 효율적인 통신을 할 수 있어요

 

캐시에는 두 가지 종류가 있어요

 

memory cache - 기기를 끄면 사라져요

이 캐시기능을 스위프트에서 지원을 해주죠

NSCache

를 사용해서 구현 할 수 있어요

 

disk cache - 기기안에 저장 되어있고 껐다 켜도 남아 있어요 

이 부분은 경로에 따라서 앱을 삭제할때 사라지게할수도, 앱을 삭제해도 남아있게할수도 있을거같아요

UserDefault를 통해서 간단하게 저장한다면 앱을 삭제하면 같이 사라질거에요

하지만 파일경로에 이미지를 저장하도록 한다면 앱이 삭제되도 캐시가 남아있게될거에요

이건 개발자편하게 사용하면 되지만 보통 파일경로에 이미지를 저장하는것같아요

 

이제부터 구현에 필요한 개념들을 정리해볼건데요!!

 

우선 캐시처리에 대한 구현은 3번의 분기를 해야 한다고 생각해요

 

1. 이미지가 memory cache(NSCache)에 있는지 확인하고

원하는 이미지가 없다면 

2. disk cache(UserDefault 혹은 기기Directory에있는 file형태)에서 확인하고

있다면 memory cache에 추가해주고 다음에는 더 빨리 가져 올수 있도록 할 수 있어요

이마저도 없다면

3. 서버통신을 통해서 받은 URL로 이미지를 가져와야해요

이때 서버통신을 통해서 이미지를 가져왔으면 memory와 disk cache에 저장해줘야 캐시처리가 되겠죠?!

 

 

 

memory cache

NSCache를 구현하는 방법은 간단해요

private let imageCache = NSCache<NSString, UIImage>()

NSCache<KeyType, ObjectType> 형태로 이루어져있어요

키값으로 쓸타입과 캐시에 넣을 타입을 정해주면되요

저는 URL String을 키값으로 구분하고 image를 넣어줄거에요

 

 

imageCache.setObject(image, forKey: url.lastPathComponent as NSString)

 

이미지url의 lastPathComponent를 찍으면 url에서 딱 파일명만 가져올 수 있어요

www.abcd.com/image/test.jpg  라면 

test.jpg만 딱 가지고 올수 있는거에요

이 파일명으로 이미지를 저장할거에요

 

disk cache

이미지를 file로 저장할거구요! 이때필요한게

FileManager

에요!!

 

    guard let path = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first else {
                    return
            }

기본적인 여러 경로를 제공해주고 원하는 경로를 사용할 수 있는데 저는 cache폴더를 사용하려고 선택했어요

저 경로를 출력해보면

Caches폴더가 경로가 찍히네요!!

 

먼저 파일을 생성 해야하니까

var filePath = URL(fileURLWithPath: path)
filePath.appendPathComponent(url.lastPathComponent)

 

여기도 마찬가지로 이미지이름으로 이미지파일을 만들어서 저장하기위해서 경로에 추가를했구요

 

    if !fileManager.fileExists(atPath: filePath.path) {
                fileManager.createFile(atPath: filePath.path,
                contents: image.jpegData(compressionQuality: 0.4),
                attributes: nil)
            }

FilManager에서 file이 없다면 해당 경로에 파일을 만들어주는거에요

 

 

 

저장된 이미지를 불러올 때는

// Memory Cache
guard let image = imageCache.object(forKey: url.lastPathComponent as NSString) else {
return
}

// Disk Cache
guard let path = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first else {
    return
}
var filePath = URL(fileURLWithPath: path)
filePath.appendPathComponent(url.lastPathComponent)
if fileManager.fileExists(atPath: filePath.path) {
    guard let imageData = try? Data(contentsOf: filePath) else {
        completion?(.failure(NSError(domain: "disk cache image data nil",
                                     code: 0,
                                     userInfo: nil)))
        return
    }
                
    guard let image = UIImage(data: imageData) else {
        completion?(.failure(NSError(domain: "image convert error",
                                     code: 0,
                                     userInfo: nil)))
        return
    }
}

이런식으로 간단하게 가져올 수 있어요

 

 

 

 

하지만

이 방법들은 한계가 있어요

이미지 캐시를 저장해놓았는데

서버에서 이미지가 변경되었다면...

서버는 바뀐사실을 알지만

클라이언트는 바뀐사실을 모르겠죠???

 

파일명이 같으면 클라이언트는 캐시처리로 받아올테니

다른 이미지로 변경된 사실을 알 수 없어요...

계속 이전의 이미지를 가져오게되겠죠

 

 

이 부분을 해결해주는 개념이

HTTP통신의 개념중 ETag라는 개념이 있어요

developer.mozilla.org/ko/docs/Web/HTTP/Headers/ETag

리소스 식별자라고 생각하면 될것같아요

 

 

사용방법은

서버에서 구현이 되어있어야하는 전제조건이 있구요

ETag값을 UserDefault에 저장하는 방식이에요

 

    func createRequest(_ url: URL, eTag: String?) -> NetworkRequest {
        // eTag header 추가한 request생성
        var request = URLRequest(url: url)
        request.addValue(eTag ?? "", forHTTPHeaderField: "If-None-Match")
        return request
    }
    
    func handleResult(_ result: Session.GetDataResult, completion: ((DownloadResult) -> Void)?) {
        switch result {
        case .success((let data, let response)):
            // 수정 할 필요없는 이미지
            if response.statusCode == 304 {
                completion?(.notModified)
                return
            }
            
            let etag = response.allHeaderFields["Etag"] as? String
            guard let data = data,
                let image = UIImage(data: data) else {
                    completion?(.failure(NSError(domain: "image data convert error", code: 0, userInfo: nil)))
                    return
            }
            
            completion?(.success((image, etag)))
        case .failure(let error):
            completion?(.failure(error))
        }
    }

 

 

Request Header에 "If-None-Match" 라는 key값으로 저장된 ETag값을 넣어서 통신을 요청하고

만약에 리소스에 변경이없다면

응답코드 (여기서는 304)가 다르게 올거에요

이 코드를 받으면 클라이언트는 새로 이미지를 업데이트 할 필요가 없으니 캐시에서 꺼내서 사용하면 되겠죠??

 

만약 아니라면 재통신을 통해서 이미지를 가져와야할거에요

 

ETag값은 responseHeader에 "Etag"란 이름으로 받아 올 수 있어요

키값 대소문자 주의해서 받아오세요!!

 

 

위에서한 내용을 그림으로 요약해봤어요

서버에 이미지가 같다면

저장된 Etag를 담아서 보내면 응답코드로 304가 날라오겠죠

304를 받았다면 캐시를 검사해서 이미지를 받아오는 로직으로 가면되구요!!

 

만약 Etag가 없다(처음 이미지통신) 혹은 서버에서 이미지가 변경됬다면

응답코드로 200이나 뭐 400..?  다른 코드가 날라오겠죠??

그러면 이미지통신을 하고 받아온 이미지를 캐시에도 저장해주고 적용시켜주면 되요

 

반응형