Android

[Compose] LazyVerticalGrid 버벅거림 현상 해결해보기

하나쓰 2025. 4. 25. 18:12
728x90
반응형

 

 

테스트 기기 : 갤럭시 s8(Android 9)

 

tab layout과 compose의 LazyVerticalGrid를 사용해 ui를 만들었다.

포스팅의 이미지가 여러 개 보이는데, 문제는 다음과 같았다

 

1. 이미지 로드 속도가 느리다

2. 스크롤을 할 때 버벅거리는 현상이 발생한다

 

이미지의 경우 coil을 사용해 불러오고 있었고

스크롤을 할 때 버벅거림이 생기는 것은 불러올 포스팅의 갯수가 많아지면 점점 더 심해졌다

사용자들이 만약 이 앱을 사용한다면 버벅여서 불편함을 겪었을 것이다(나같으면 신고했음)

 

처음에는 이러한 문제들 때문에 이미지를 업로드할 때 bitmap.compress를 사용해서 이미지 파일의 크기를 70% 수준으로 압축해서 줄였으나 소용이 없었다^^...

 

메모리 덤프를 떠보고 네이티브 사이즈를 보았는데 뭔가 엄청 커보여서 이미지 파일들을 살펴보기 시작했다

파일 크기는 괜찮은 거 같아서 storage에 업로드된 이미지를 다운로드 받아보았다

근데 이상하게 이미지 사이즈가 너무 컸다

 

다른 이미지들도 살펴보니 가로 세로가 최소 3000, 4000이었다

이미지 파일의 용량과는 관계없이 이미지 크기가 다 저렇게 컸다...

 

저렇게 큰게 맞는건가 싶어서 좀 찾아보니 스포티파이에서 표로 정리해준 웹/모바일 이미지 크기 기준이 있었다

https://www.shopify.com/kr/blog/image-sizes

 

2024년 웹사이트에 가장 적합한 이미지 크기 가이드 - Shopify 대한민국

이 글을 참고하여 모바일과 데스크톱에서 웹사이트에 가장 적합한 이미지 크기를 찾아보세요.

www.shopify.com

 

내가 생각하기에도 저 사이즈는 좀 아니다

 

근데 내가 여기서 의문이 드는 것은 왜 파일 크기가 작은 이미지임에도 느리게 로드되는지였다

내가 무언갈 놓치고 있나하는 생각이 들어 검색해보던 중 아주 도움이 되는 글을 발견했다

 

https://istiakmorsalin.medium.com/practical-image-processing-on-android-a195054f6795

 

PRACTICAL IMAGE PROCESSING ON ANDROID

Hello, Today, I am going to write about image processing on Android. Obviously, This is not new to you But I am hoping the audience of my…

istiakmorsalin.medium.com

 

https://medium.com/@n20/title-how-images-are-stored-and-displayed-in-android-a-simple-easy-guide-png-url-vector-44bc172c11c8

 

Title: How Images Are Stored and Displayed in Android: A Simple, Easy Guide : PNG / URL / Vector

Ever wonder how Android turns image files like .png, .jpg, or .svg into something you see on your phone’s screen? It might seem like magic…

medium.com

 

- 로컬에 있는 이미지의 경우 

기기내에 있는 이미지를 가져온 후, 이 이미지 파일(png, jpg등)을 bitmap으로 압축해제(decode)해서 메모리에 올림 -> 메모리에 올라간 비트맵을 GPU를 사용해 화면에 보여줌

 

- url 등으로 받아오는 이미지(로컬에 있는 이미지가 아닌 경우)의 경우

백그라운드로 이미지를 다운로드 함(coil이나 glide같은 라이브러리 사용) -> 다운로드 한 이미지 캐싱 -> bitmap으로 decode하고 메모리에 올려 GPU를 사용해 화면에 보여줌

 

여기서 bitmap이 뭔지 알아야한다

bitmap은 픽셀들이 2차원 배열 형태(width, height)로 있는 것을 말하며, 픽셀 하나당 4byte를 가지고 있다(포맷마다 다를 수있음(SRGB 포맷 기준) red, green, blue, alph(투명도))

이러한 픽셀들이 모여서 이미지나 영상을 나타낼 수 있다.

그리고 비트맵은 기본적으로 압축을 하지 않기 때문에 전체 크기(가로 x 세로)에 해당하는 픽셀 정보를 다 저장해야함 -> 메모리 용량을 많이 차지한다고 한다.

 

그러니까 예를 들면 이미지의 크기가 3040 * 4080이라면 

3,040 * 4,080 = 12,403,200

12,403,200 * 4 = 49,612,800

49,612,800 Bytes

49,612,800 / 1024 = 48,450KB

48,450kb / 1024 = 47.314MB

 

그러면 대충 이미지 하나당 50MB를 메모리에서 차지하고 있다는 얘기가 된다

 

비트맵으로 decode하고 나서 메모리에 저렇게 많은 공간을 차지하고 있는줄은 몰랐다

어쩐지 맨 처음 올린 gif 파일처럼 해당 탭에만 가면 자꾸 메모리가 엄청 튀더라니만

 

그러면 decode할 때 이미지 사이즈를 줄여줘야겠다는 생각이 들었다

나는 Coil을 통해 이미지를 가져오고 있었고 원본 사이즈대로 가져오고 있었기 때문에 아래의 코드를 수정해주었다

 

// 수정 전
class CoilImgRequest {
    companion object {
        fun getImgRequest(sourceImg: String) =
            ImageRequest.Builder(ChakSaiApplication.getInstance())
                .data(if (sourceImg.isNotBlank()) sourceImg else R.drawable.err_img)
                .size(Size.ORIGINAL)
                .error(R.drawable.err_img)
                .fallback(R.drawable.err_img)
                .crossfade(true)
                .build()
 }
}


// 수정 후 
class CoilImgRequest {
    companion object {
        fun getImgRequest(sourceImg: String, size: Size = Size.ORIGINAL) =
            ImageRequest.Builder(ChakSaiApplication.getInstance())
                .data(if (sourceImg.isNotBlank()) sourceImg else R.drawable.err_img)
                .size(size)
                .error(R.drawable.err_img)
                .fallback(R.drawable.err_img)
                .crossfade(true)
                .build()
 }
}

 

그리고 이미지를 로드하는 쪽에서 width, height를 지정해주었다

 

@Composable
fun GridNoteList(
    posts: List<PostWithUser>,
    onNavigationToDetailPost: (postId: String) -> Unit,
) {
    LazyVerticalGrid(
        columns = GridCells.Fixed(2),
        contentPadding = PaddingValues(AppTheme.size.small)
    ) {
        items(
           items = posts,
            key = {post -> post.id}
        ) {post ->
            ListItem(post, onNavigationToDetailPost)
        }
    }
}

@Composable
fun ListItem(data: PostWithUser,  onNavigationToDetailPost: (postId: String) -> Unit,) {
    Card(
        modifier = Modifier
            .width(dimensionResource(R.dimen.cardMedium))
            .height(dimensionResource(R.dimen.cardMedium))
            .padding(dimensionResource(R.dimen.cardPadding)),
        shape = RoundedCornerShape(AppTheme.size.normal),
        elevation = CardDefaults.cardElevation(
            defaultElevation = AppTheme.size.xs,
            pressedElevation = AppTheme.size.small
        ),
        onClick = { onNavigationToDetailPost(data.id) }
    ) {
        AsyncImage(
            modifier = Modifier.fillMaxSize(),
            model = CoilImgRequest.getImgRequest(data.image, size = Size(160, 160)),
            contentScale = ContentScale.Crop,
            contentDescription = stringResource(R.string.post_img_desc),
        )
    }
}

 

 

 

 

네이티브쪽 메모리 차지도 많이 줄었고 버벅거림도 사라졌다!

그리고 메모리 덤프도 보니까 훨씬 안정적으로 바뀌었다

(이전에는 800MB까지 메모리가 튀기도 했음;;)

 

 

해결방법은 너무나도 간단했지만 어디서 메모리가 많이 나가고 그 원인을 찾아가는 것이 재미있었다

그리고 단순히 이미지를 사용하는 것 뿐만 아니라 이미지가 어떻게 로드되는지 프로세스를 알고 있어야 이런 문제도 해결할 수 있겠다는 생각이 들었다

기본을 더욱더 갈고 닦아야겠다ㅎ

반응형