프로그래밍/Android

Coil 인터셉터를 활용한 이미지 로딩 최적화 방법

Lou Park 2024. 1. 28. 12:23

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 36482.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)
    }
}


UnsplashResizingInterceptor
라는 커스텀 Interceptor를 구현했다. okhttp3 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

 

만들어진 Interceptor를 ImageLoader component에 추가하고 다시 이미지 요청을 해보면 이렇게 widthPx, heightPx가 실제 이미지뷰의 크기로 넘어옴을 알 수 있다. chain.size부터 Px단위이기는하지만 Coil 내부에서 정의한 Dimension 타입을 사용하기 때문에 pxOrElse로 확실히 Px로 풀어주는게 좋다.

 

개선된 이미지 요청 결과 확인

 

이제 다시 Network Interceptor로 이미지 요청 결과를 보면 44.02kb로 무려 98%나 용량을 절감했다. 데이터가 가벼우니 요청시간도 38ms로 대폭 줄었다. 라쿤앱을 사용하는 사용자들의 데이터 비용을 많이 아껴주었으니 보람찬 여정이었다..후

 

다만! 이건 이미지뷰 사이즈가 200dp일때의 라쿤 사진이지만, 앱에서 이미지뷰 사이즈가 계속 들쭉날쭉하다면 각각 다른 요청을 보낼 것이고, 캐시파일도 따로 생성될 것이다. 이런 경우 캐싱을 어떻게 효율적으로 할 것인가? 고민해볼 수 있을 것이다.

 

참고

Extending the Image Pipeline