LazyColumn 내에서 아이템 내에 이미지를 넣어줄 때, 너비(width)뿐만 아니라, 높이(height) 또한 빼먹지 않고 지정해주도록 하자.
LazyColumn 을 통해 이미지가 포함된 아이템 리스트를 구성할때, 이미 로드된 아이템을 확인하기 위해 위로 스크롤 하였을 때 이미지가 다시 로드되는 문제의 원인과 이를 해결하는 방법에 대해 작성해보도록 하겠다.
LazyColumn 을 통해 가로 길이는 화면 내 아이템의 전체 너비로 고정하고, 세로 길이는 원본 비율을 해치지 않도록 설정되도록 하여, 유동적인 높이의 이미지를 가진 작품 리스트 화면을 구성하던 중, 의도되지 않는 동작이 수행되는 것을 확인할 수 있었다.
// 아이템 내에 이미지
NetworkImage(
imageUrl = artwork.artworkImageUrl,
contentDescription = "${artwork.title} by ${artwork.artist.name}",
modifier = Modifier.fillMaxWidth(),
contentScale = ContentScale.FillWidth,
)
// NetworkImage 구현체
@Composable
fun NetworkImage(
imageUrl: String?,
contentDescription: String?,
modifier: Modifier = Modifier,
loadingImage: Painter = painterResource(id = R.drawable.placeholder),
failureImage: Painter = painterResource(id = R.drawable.placeholder),
contentScale: ContentScale = ContentScale.Crop,
) {
val context = LocalContext.current
if (LocalInspectionMode.current) {
Image(
painter = loadingImage,
contentDescription = "Image for Preview",
modifier = modifier,
)
} else {
AsyncImage(
model = ImageRequest.Builder(context)
.data(imageUrl)
.crossfade(true)
.build(),
contentDescription = contentDescription,
modifier = modifier,
error = failureImage,
contentScale = contentScale,
onError = { exception ->
Timber.e("${exception.result.throwable}")
},
)
}
}
https://youtube.com/shorts/goYc5Yl-Od4
이미 한번 로드된 이미지 및 아이템이 다시 로드되는 현상이 발생하였다.
내가 알기론, Coil 을 통해 한번 로드된 이미지는 메모리 및 디스크에 캐싱되기에, 위와 같은 현상은 발생하지 않는 것으로 알고 있다...여태 경험 해본적이 없는...
또한 좀 더 디테일하게 확인해보면, 첫 로드시 화면에 보이는 작품 아이템이 무한의 높이로 설정이 되었다가, 이미지 로드 후 그 높이가 고정됨을 확인할 수 있었다. 또한 위로 스크롤할때는 한번씩 튕기는 듯한 현상도 존재한다. 매우 치명적 +_+...
@HiltAndroidApp
class ZiineApplication : Application(), ImageLoaderFactory {
override fun newImageLoader(): ImageLoader {
return ImageLoader.Builder(this)
.diskCache {
DiskCache.Builder()
.directory(cacheDir.resolve("image_cache"))
.maxSizeBytes(10 * 1024 * 1024)
.build()
}
.logger(DebugLogger())
.respectCacheHeaders(false)
.build()
}
}
Coil Image 캐싱과 관련 코드는 아래의 글들을 참고하여 설정 해주었다.
Jetpack Compose Coil 이미지 캐싱, 잘 하고 계신가요?
Jetpack Compose Coil 캐싱, 어떻게 하고 있을까요?- 디스크 캐싱

위로 스크롤하였을 때, 메모리 캐싱된 이미지가 성공적으로 로드되었다는 로그가 찍히는 걸로 보아, 이미지가 캐싱되지 않아서 발생하는 문제는 아니라는 것을 알 수 있었다... 그럼 뭐지?
앱의 첫 출시 MVP 에서는 작품 등록시 사진의 어떠한 크롭 기능 및 편집 기능을 지원하지 않도록 결정되어, 안내를 통해 사진을 미리 정방형의 비율(1:1)로 편집 후 올려달라는 안내 메세지를 추가하였다.
따라서, 등록되는 작품내 이미지의 높이 역시 너비와 거의 같다고 가정할 수 있게되어, contentScale 옵션을 수정하고, aspectRatio Modifier 를 통해 너비와 같게 높이가 설정되도록 하는 코드를 추가 해주었다.
NetworkImage(
imageUrl = artwork.artworkImageUrl,
contentDescription = "${artwork.title} by ${artwork.artist.name}",
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f),
)
그랬더니...
https://youtube.com/shorts/aujoshQLO8o
다음과 같이 기존의 발생했던 문제가 해결되어, 이미 로드된 이미지가 다시 로드되는 듯한 현상은 발생하지 않았다!(이미지가 튕기는 듯한 현상도 사라짐!)
LazyColumn 은 RecyclerView 와 다르게 화면에 보이는 뷰만 스크롤시 화면 밖으로 사라진 뷰를 재사용하지 않고, 삭제 및 새로 생기는 뷰는 새로 생성한다.
정확히는 유사한 UI 구조를 가진 항목의 LayoutNode(UI 요소의 레이아웃 정보를 담고 있는 객체)를 재사용하여 레이아웃 계산 비용을 최소화한다. 만약 크키나 속성이 동일하다면, 재측정 과정을 생략할 수 있다.
더욱 자세한 내용은 아래의 글을 참고
LazyColumn 작동 방식 이해하기
LazyColumn 내에 아이템의 높이가 NetworkImage Composable 에 의해 고정되지(정해지지) 않았기 때문에, 위로 스크롤을 하여 이미 로드된 이미지를 가진 아이템을 삭제 후 다시 생성할때, 높이를 재측정하는 과정을 매번 거쳐야 했고, 따라서 이미지가 다시 로드되는 듯한 현상이 발생한 것이었다!
또한 LazyColumn 은 초기 측정시 무한의 높이 제약 조건을 사용하므로 문제가 되었던 부분 중, 무한의 높이로 설정된 이후 이미지 로드 완료시 높이가 고정되는 현상은 의도된 동작임을 알 수 있었다.
(placeholder 또는 초기 크기를 지정해야하는 이유)
만약 기존의 스펙처럼 작품의 원본 이미지 비율을 살리는 방향으로 구현한다면, aspectRatio Modifier 를 통해 비율을 고정하는 식의 해결방법은 적절하지 않을 것이다. 그렇다면 어떤 해결 방법을 사용 해야할까?
placeholder 를 사용하기엔, 각 아이템의 이미지의 너비 대비 높이의 비율이 매번 다르므로, 일정한 높이를 가진 placeholder 를 넣어주는 것은 적합하지 않다.
높이를 지정해주지 않은 것이 문제라면 wrapContentHeight 와 같은 Modifier 등을 통해 적용해주면 되지 않나 싶었는데, 적용 이후에도 높이 재측정으로 인한 위로 스크롤시 튕김 현상은 그대로 였다.
ContentScale.Fit은 이미지 전체가 보이도록 가장 큰 차원을 기준으로 크기를 일관되게 조정한다.
테스트 해본 결과, 이 방식이 LazyColumn 의 아이템의 높이를 modifier 를 통해 지정해주지 않아도, 아이템의 크기가 다시 측정되어 발생하는 문제들이 나타나지 않았다.
다만 높이를 지정하지 않았기에, 초기 로드시 이미지의 높이가 무한으로 설정되고 이미지 로드 완료 이후 높이가 고정된다.
또 다른 해결법을 공유해주신 junyong008님 감사합니다.
API 를 통해 이미지 url 을 받아올때, 이미지의 높이와 너비를 같이 받아올 수 있다면, modifier 를 통해 이미지의 높이, 너비 혹은, 비율 등을 지정하여 로드할 수 있기 때문에, 매번 그 크기를 재측정하는 문제는 발생하지 않을 것이다.
클라이언트에서 이미지 url 을 받아올 때, 이 이미지의 높이 및 너비를 측정하는 방식이 불가능하진 않겠지만, 오버헤드가 있을 수 있기 때문에, 서버에 이미지를 저장할때 DB 에 넣어두고 이를 바로바로 꺼내오는게 성능적으로 이점을 챙길 수 있을 듯 하다.
어떤 문제를 클라이언트단에서만 서버팀과 같이 협력한다면, 아주 간단하게 해결할 수 있다. 이 케이스도 여기에 해당하는 듯 하다.
뿐만 아니라, 서버와 협력을 통해 이미지의 평균 색상 등을 서버에서 저장하여, 이를 같이 받아온다면 스켈레톤 이미지에 이 색상을 적용할 수 도 있을 듯하다. 헤이딜러 처럼
enablePlaceholders 옵션을 true 로 설정하면, 가변적인 높이를 가진 아이템들에 대해 대응을 할 수 있다.
핀터레스트와 같이 여러개의 열을 가진 리스트의 경우, enablePlaceholders 옵션을 false 로 설정할 경우, 항목의 위치가 섞이는 등의 문제가 있는데, enablePlaceholders 를 활성화하여 해결할 수 있다.
reference)
https://medium.com/@csh153/jetpack-compose-coil-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%BA%90%EC%8B%B1-%EC%9E%98-%ED%95%98%EA%B3%A0-%EA%B3%84%EC%8B%A0%EA%B0%80%EC%9A%94-806252d9c73a
https://medium.com/@csh153/jetpack-compose-coil-%EC%BA%90%EC%8B%B1-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%95%98%EA%B3%A0-%EC%9E%88%EC%9D%84%EA%B9%8C%EC%9A%94-%EB%94%94%EC%8A%A4%ED%81%AC-%EC%BA%90%EC%8B%B1-b6595c05bd5a
https://developer.android.com/develop/ui/compose/lists?hl=ko
https://medium.com/@wisemuji/lazycolumn-%EC%9E%91%EB%8F%99-%EB%B0%A9%EC%8B%9D-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-0a5433f31306
https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:paging/paging-common/src/commonMain/kotlin/androidx/paging/PagingConfig.kt?q=enablePlaceholders&ss=androidx%2Fplatform%2Fframeworks%2Fsupport
https://velog.io/@vov3616/%EC%82%BD%EC%A7%88-paging-3-remoteMediator-%EA%B9%9C%EB%B9%A1%EC%9E%84%ED%98%84%EC%83%81-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0