Firebase Storage 이미지를 coil로 로딩할 때 캐싱이 되지 않는 이유
DoDoBest
·2024. 12. 11. 01:13
원인 분석
Coil 라이브러리를 이용해 Firebase Storage 이미지를 다시 로딩하면 매번 네트워크에서 새롭게 요청합니다.
위 로그는 Coil ImageLoader 클래스에 로거를 추가하면 확인할 수 있습니다.
Application 클래스에서 ImageLoaderFactory 인터페이스의 newImageLoader 함수를 구현합니다. 그러면 coil이 이미지를 로딩할 때 모두 적용됩니다.
class App : Application(), ImageLoaderFactory {
....
override fun newImageLoader(): ImageLoader {
return ImageLoader(this).newBuilder()
.memoryCachePolicy(CachePolicy.ENABLED)
.memoryCache {
MemoryCache.Builder(this)
.maxSizePercent(0.1)
.strongReferencesEnabled(true)
.build()
}
.diskCachePolicy(CachePolicy.ENABLED)
.diskCache {
DiskCache.Builder()
.maxSizePercent(0.03)
.directory(cacheDir)
.build()
}
.logger(DebugLogger()) // coil이 이미지를 로딩할 때 로그가 찍히도록 설정
.build()
}
}
Coil에서 캐싱을 하지 않는 것은 아닙니다. 비행기 모드를 킨 후, 이전에 이미지를 다시 요청하면 캐싱된 이미지를 로딩하는 것을 확인할 수 있습니다.
원인은 Firebase Storage Header에 있는 캐싱 정책 때문입니다. cmd 창에서 아래와 같은 명령어를 입력해서 Header 값을 확인할 수 있습니다. 뒤에 있는 firebasestorage url은 사용하시는 파이어베이스 저장소에 있는 이미지 URL로 변경해주세요.(대문자 i 입니다)
curl -I https://firebasestorage.googleapis.com/v0/b/{name}.appspot.com/o/{fileName}?alt=media&token={tokenValue}
HTTP/1.1 200 OK
X-GUploader-UploadID: XXXXX
Expires: Tue, 10 Dec 2024 15:07:31 GMT
Date: Tue, 10 Dec 2024 15:07:31 GMT
Cache-Control: private, max-age=0
Last-Modified: Mon, 25 Nov 2024 17:30:21 GMT
ETag: "XXXX"
x-goog-generation: XXXX
x-goog-metageneration: 1
x-goog-stored-content-encoding: identity
x-goog-stored-content-length: 40251
x-goog-meta-firebaseStorageDownloadTokens: XXXXX
Content-Type: image/png
Content-Disposition: inline; filename*=utf-8''XXXX
x-goog-hash: XXXXX
x-goog-hash: XXXXX
x-goog-storage-class: STANDARD
Accept-Ranges: bytes
Content-Length: 40251
Server: UploadServer
Cache-Control에 max-age=0 라는 값을 볼 수 있는데, 이 값이 Coil의 캐싱 정책을 무시하고 매번 새롭게 서버에서 이미지를 불러오도록 만듭니다.
해결 방법 1. respectCacheHeaders
가장 간단한 해결 방법은 네트워크 요청 시 CacheHeader를 무시하는 것입니다. ImageLoader 빌더에서 respectCacheHeaders 함수에 false를 설정하면 됩니다.
ImageLoader(this).newBuilder()
.respectCacheHeaders(false) // cache header 정책 무시
...
하지만 이렇게 할 경우, 캐시된 데이터가 삭제되지 않는 한 서버에 있는 이미지가 변경 돼도 반영되지 않는 문제가 있습니다.
해결 방법 2. OkHttp Interceptor
Okhttp의 interceptor를 이용해 Cache-Control을 변경하도록 구현해봅니다. max-age가 0인 경우, 24시간으로 변경하여 최소 하루 동안 캐시된 데이터를 사용할 수 있도록 설정합니다. max-age는 초 단위이기 때문에 86400을 입력해줍니다.
request가 아닌 response의 header를 변경해야 합니다.
아래와 같이 request의 header를 변경해서 요청하더라도 firebase 서버 단의 정책은 max-age=0이기 때문에 무시됩니다.
import okhttp3.Interceptor
import okhttp3.Response
class CacheControlInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val modifiedCacheControl = changeCacheControl(originalRequest.header("Cache-Control"))
val modifiedRequest = originalRequest.newBuilder()
.header("Cache-Control", modifiedCacheControl)
.build()
return chain.proceed(modifiedRequest)
}
private fun changeCacheControl(cacheControl: String?): String {
return cacheControl?.split(",")?.joinToString(",") { value ->
if (value.contains("max-age")) {
"max-age=86400"
} else {
value
}
} ?: "max-age=86400"
}
}
response의 header를 변경하는 코드는 다음과 같습니다.
import okhttp3.Interceptor
import okhttp3.Response
class CacheControlInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val response = chain.proceed(originalRequest)
val modifiedCacheControl = changeCacheControl(response.header("Cache-Control"))
return response.newBuilder()
.header("Cache-Control", modifiedCacheControl)
.build()
}
private fun changeCacheControl(cacheControl: String?): String {
return cacheControl?.split(",")?.joinToString(",") { value ->
if (value.contains("max-age")) {
"max-age=86400"
} else {
value
}
} ?: "max-age=86400"
}
}
만든 Interceptor를 ImageLoader에 추가해줍니다.
ImageLoader(this).newBuilder()
.okHttpClient {
OkHttpClient.Builder()
.addInterceptor(CacheControlInterceptor())
.build()
}
...
Coil Interceptor
Coil Interceptor에서도 캐싱을 설정할 수 있지만, 제대로 적용되지 않는 것 같습니다.
Firebase Storage에 있는 이미지 URL을 받아올 때 Token 값을 임시로 발급받아 사용하도록 설정했는데, memory, disk cache의 key 값에 token 값을 제거해서 token에 영향을 받지 않도록 설정해도 캐시된 데이터를 사용하지 않았습니다.
Coil보다 OkHttp 설정이 우선시 되서 그런 것으로 추측되는데, 정확한 원인을 아시는 분이 있다면 댓글로 공유해주세요😊
import coil.intercept.Interceptor
import coil.request.ErrorResult
import coil.request.ImageResult
import coil.request.SuccessResult
class CacheControlInterceptor : Interceptor {
private val tokenRegex = Regex("&token=[^&]+")
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
val request = chain.request
val modifiedUrl = extractToken(request.data.toString())
val modifiedRequest = request.newBuilder()
.data(modifiedUrl)
.build()
return when (val result = chain.proceed(modifiedRequest)) {
is ErrorResult -> result
is SuccessResult -> {
val modifiedCacheControl =
changeCacheControl(result.request.headers["Cache-Control"])
val modifiedResultRequest = result.request.newBuilder()
.setHeader("Cache-Control", modifiedCacheControl)
.build()
result.copy(
request = modifiedResultRequest,
)
}
}
}
private fun changeCacheControl(cacheControl: String?): String {
return cacheControl?.split(",")?.joinToString(",") { value ->
if (value.contains("max-age")) {
"max-age=86400"
} else {
value
}
} ?: "max-age=86400"
}
private fun extractToken(url: String): String {
return url.replace(tokenRegex, "")
}
}
Coil Interceptor는 ImageLoader의 components block을 통해 추가할 수 있습니다.
ImageLoader(this).newBuilder()
.components {
add(CacheControlInterceptor())
}
...
'학습' 카테고리의 다른 글
deprecated된 firebase dynamic link 없이 Firebase email link authentication 구현하기 - 2 (0) | 2024.09.16 |
---|---|
deprecated된 firebase dynamic link 없이 Firebase email link authentication 구현하기 - 1 (2) | 2024.09.11 |
Safe Navigation Action (0) | 2024.08.22 |
Flow, Channel, ChannelFlow, CallbackFlow (0) | 2024.08.15 |
RecyclerView Itemdecoration 올바르게 사용하기 (0) | 2024.07.24 |