Coil의 이미지 파이프라인은 아래 5가지의 메인 파트로 이루어져 있는데, Interceptor는 그 중 첫번째로 실행되는 녀석이다.
Interceptor -> Mapper > Keyer -> Fetcher -> Decoder
커스텀 Interceptor를 이용하면 일종의 캐시 레이어(Cache Layer)를 만들 수 있다. 요청을 가로채서 요청 파라미터를 수정하거나...HTTP Request를 했지만 휴대폰 내에 파일이 있다면 File로 돌려버리거나 말이다. 또, 앱에서 정의한 커스텀 스키마로 이미지를 불러오는 것도 가능해진다.
어찌되었건 지금 간단히 예시로 볼 것은 Unsplash 이미지를 불러올때 이미지 사이즈를 최적화 시켜주는 Interceptor다. (Github에 많이 떠돌아다니는 코드다 ㅋㅋ)
라쿤사진을 보여주는 앱의 문제점
여기, 아주 귀여운 라쿤사진을 보여주는 앱이 있다.
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
AsyncImage(
modifier = Modifier.size(200.dp),
model = "https://images.unsplash.com/photo-1497752531616-c3afd9760a11",
contentScale = ContentScale.Crop,
contentDescription = null,
)
}
}
200dp 사이즈의 이미지 뷰에다가 Unsplash URL을 통해 이미지를 띄워준다. 별 문제 없어보이지만, Network Inspector로 오간 요청을 보게되면 뭔가 잘못되었음을 깨닫게된다.
저 작은 이미지뷰에 이미지를 그리기 위해 원본사이즈가 5472 x 3648인 2.99MB의 이미지를 다운받았고, 이는 290ms가 소요되는 작업이었다. 테스트로 하나 넣어보고, 코일의 캐시기능을 믿고 이대로 계속 개발했다간 큰코다칠 수 있다.
Unsplash는 URL Query Parameter를 이용한 이미지 리사이징을 지원한다. 예로, 다음과 같이 넘겨주면 이미지 가로(Width)길이가 최대 380px이 되게끔 이미지를 리사이즈하여 반환해준다. 이를 이용해 적절한 사이즈의 이미지만을 요청하도록 해보겠다.
https://images.unsplash.com/photo-1497752531616-c3afd9760a11?w=380
Unsplash 이미지를 리사이징하는 Intercetor 구현
import coil.intercept.Interceptor
import coil.request.ImageResult
import coil.size.pxOrElse
import okhttp3.HttpUrl.Companion.toHttpUrl
object UnsplashResizingInterceptor : Interceptor {
private const val UNSPLASH_URL_PREFIX = "https://images.unsplash.com/photo-"
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
val data = chain.request.data
val widthPx = chain.size.width.pxOrElse { -1 }
val heightPx = chain.size.height.pxOrElse { -1 }
if (
widthPx > 0 &&
heightPx > 0 &&
data is String &&
data.startsWith(UNSPLASH_URL_PREFIX)
) {
val url =
data
.toHttpUrl()
.newBuilder()
.addQueryParameter("w", widthPx.toString())
.addQueryParameter("h", heightPx.toString())
.build()
val request = chain.request.newBuilder().data(url).build()
return chain.proceed(request)
}
return chain.proceed(chain.request)
}
}
라는 커스텀 Interceptor를 구현했다.
UnsplashResizingInterceptorokhttp3
Interceptor와 인터페이스는 동일하지만 coil.intercept.Interceptor
임에 유의하자! Coil은 File, Drawable, Bitmap, URL등 다양한 타입을 지원하기 때문에 Any로 데이터를 받고 있는데, data
가 String일 경우 URL이므로 이때 요청이 Unsplash URL이라면 가로채는 코드다.
커스텀 Interceptor ImageLoader에 붙이기
class MyApplication : Application(), ImageLoaderFactory {
override fun newImageLoader(): ImageLoader {
return ImageLoader.Builder(this)
.components {
add(UnsplashResizingInterceptor)
}
.build()
}
}
만들어진 Interceptor를 ImageLoader component에 추가하고 다시 이미지 요청을 해보면 이렇게 widthPx
, heightPx
가 실제 이미지뷰의 크기로 넘어옴을 알 수 있다. chain.size
부터 Px단위이기는하지만 Coil 내부에서 정의한 Dimension 타입을 사용하기 때문에 pxOrElse
로 확실히 Px로 풀어주는게 좋다.
개선된 이미지 요청 결과 확인
이제 다시 Network Interceptor로 이미지 요청 결과를 보면 44.02kb로 무려 98%나 용량을 절감했다. 데이터가 가벼우니 요청시간도 38ms로 대폭 줄었다. 라쿤앱을 사용하는 사용자들의 데이터 비용을 많이 아껴주었으니 보람찬 여정이었다..후
다만! 이건 이미지뷰 사이즈가 200dp일때의 라쿤 사진이지만, 앱에서 이미지뷰 사이즈가 계속 들쭉날쭉하다면 각각 다른 요청을 보낼 것이고, 캐시파일도 따로 생성될 것이다. 이런 경우 캐싱을 어떻게 효율적으로 할 것인가? 고민해볼 수 있을 것이다.
참고
'프로그래밍 > Android' 카테고리의 다른 글
Mastring Android ViewModels: 필요하다면, 생성자에 의존성을 Lazy하게 주입하라 (0) | 2024.06.23 |
---|---|
[안드로이드] MyMavenRepo로 Private Repository 라이브러리 무료 배포하기 (0) | 2024.04.24 |
[Hilt] Custom Component의 활용 - 지역별 DB 생성하기 (2) | 2024.01.01 |
[Jetpack Compose] @Immutable과 @Stable이란 (1) | 2023.12.25 |
Mainframer - 리모트 서버에서 빌드를 돌려보자 (0) | 2023.10.21 |