0Pricing
Android Academy · 강의

캐싱과 성능

메모리와 디스크 캐싱의 기초를 익힙니다.

캐싱과 성능은(는) CoddyKit의 무료 Android Academy 강의입니다. 이것은 4개 중 3번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Android Academy 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Android Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

캐싱이 중요한 이유

이미지를 다운로드하고 디코딩하는 작업에는 많은 비용이 듭니다. 대역폭과 배터리, CPU를 사용하기 때문입니다. 목록을 스크롤할 때마다 같은 이미지를 다시 가져오면 앱이 느려지고 데이터도 많이 소모됩니다.

캐싱은 디코딩된 이미지와 다운로드한 바이트를 저장하여 다음 요청을 거의 즉시 처리할 수 있게 합니다. Coil에는 기본적으로 두 가지 캐시가 제공됩니다. 하나는 메모리 캐시(빠르고 디코딩된 비트맵 저장)이고, 다른 하나는 디스크 캐시(지속적이며 원본 바이트 저장)입니다.

이 단원에서는 두 캐시의 작동 방식과 조정 방법을 설명합니다.

두 계층: 메모리와 디스크

Coil은 다음 순서로 캐시를 확인합니다.

  • 메모리 캐시: 최근 사용한 디코딩된 비트맵을 RAM에 보관합니다. 여기서 찾으면 디코딩할 필요가 없어 즉시 표시됩니다.
  • 디스크 캐시: 기기에 다운로드한 원본 바이트를 저장합니다. 여기서 찾으면 네트워크 요청은 생략되지만 여전히 디코딩해야 합니다.
  • 네트워크: 두 캐시에서 모두 찾지 못했을 때만 사용하는 느린 경로입니다.

메모리는 빠르지만 용량이 작고 앱이 종료되면 삭제됩니다. 디스크는 용량이 더 크고 앱을 다시 시작해도 유지됩니다.

ImageLoader 구성하기

캐시는 모든 요청을 처리하는 엔진인 ImageLoader에 있습니다. 일반적으로 사용자 지정 로더를 하나 만든 다음 재사용합니다. 여기서는 메모리 캐시를 사용 가능한 RAM의 일정 비율로 설정하고, 디스크 캐시는 고정된 바이트 용량으로 설정합니다.

싱글턴 로더를 사용하면 중복 캐시와 불필요한 메모리 사용을 피할 수 있습니다.

import android.content.Context
import coil3.ImageLoader
import coil3.disk.DiskCache
import coil3.memory.MemoryCache
import okio.Path.Companion.toOkioPath

fun newImageLoader(context: Context): ImageLoader =
    ImageLoader.Builder(context)
        .memoryCache {
            MemoryCache.Builder()
                .maxSizePercent(context, 0.25) // 25% of app RAM
                .build()
        }
        .diskCache {
            DiskCache.Builder()
                .directory(context.cacheDir.resolve("image_cache").toOkioPath())
                .maxSizeBytes(50L * 1024 * 1024) // 50 MB
                .build()
        }
        .build()

싱글턴 로더 제공하기

Coil 3에서는 Application 클래스에 SingletonImageLoader.Factory를 구현하여 애플리케이션 전체에서 사용할 단일 로더를 설정할 수 있습니다. 그러면 모든 AsyncImage이 자동으로 이 로더를 사용하므로 매번 로더를 전달할 필요가 없습니다.

import android.app.Application
import coil3.ImageLoader
import coil3.PlatformContext
import coil3.SingletonImageLoader

class MyApp : Application(), SingletonImageLoader.Factory {
    override fun newImageLoader(context: PlatformContext): ImageLoader =
        ImageLoader.Builder(context)
            .crossfade(true)
            .build()
}

캐시 정책 제어하기

각 요청에서 memoryCachePolicy와 diskCachePolicy를 사용하여 캐싱을 활성화하거나 비활성화할 수 있습니다. 정책에는 ENABLED, READ_ONLY, WRITE_ONLY, DISABLED가 있습니다.

예를 들어 RAM에 보관할 필요가 없는 아주 큰 일회성 이미지에는 메모리 캐시를 비활성화하고, 민감한 콘텐츠에는 디스크 캐시를 비활성화할 수 있습니다.

import android.content.Context
import coil3.request.CachePolicy
import coil3.request.ImageRequest

fun noMemoryCacheRequest(context: Context, url: String) =
    ImageRequest.Builder(context)
        .data(url)
        .memoryCachePolicy(CachePolicy.DISABLED)
        .diskCachePolicy(CachePolicy.ENABLED)
        .build()

크기 조정: 거대한 비트맵을 디코딩하지 않기

가장 큰 성능 문제는 4000x3000 크기의 사진을 100dp 썸네일로 디코딩하는 것입니다. 메모리와 시간이 낭비됩니다. 기본적으로 Coil은 비트맵 크기를 컴포저블의 측정된 영역에 맞추므로 작은 AsyncImage은 작은 비트맵으로 디코딩됩니다.

크기를 알고 있다면 요청에서 size()를 사용하여 명시적인 대상 크기를 지정할 수 있습니다.

import android.content.Context
import coil3.request.ImageRequest
import coil3.size.Size

fun thumbnailRequest(context: Context, url: String) =
    ImageRequest.Builder(context)
        .data(url)
        .size(Size(200, 200)) // decode at most 200x200
        .build()

안정적인 캐시 키

Coil은 요청 데이터에서 캐시 키를 만듭니다. URL에 변경되는 쿼리 매개변수(예: 서명된 토큰)가 포함되어 있으면 같은 이미지라도 서로 다른 키가 생성되어 캐시를 절대 적중하지 못합니다.

이미지 ID처럼 변하지 않는 부분을 기준으로 명시적인 memoryCacheKey와 diskCacheKey를 설정하면 캐싱이 정상적으로 작동합니다.

import android.content.Context
import coil3.request.ImageRequest

fun stableKeyRequest(context: Context, id: String, signedUrl: String) =
    ImageRequest.Builder(context)
        .data(signedUrl)
        .memoryCacheKey("image_$id")
        .diskCacheKey("image_$id")
        .build()

이미지 미리 로드하기

더 매끄러운 사용자 경험을 위해 이미지를 표시하기 전에 캐시를 미리 준비할 수 있습니다. 예를 들어 피드의 다음 페이지를 미리 가져올 수 있습니다. 대상이 없는 요청과 함께 로더의 enqueue를 사용하세요.

사용자가 이미지에 도달하면 이미 캐시되어 있으므로 즉시 표시됩니다.

import android.content.Context
import coil3.ImageLoader
import coil3.request.ImageRequest

fun preload(context: Context, loader: ImageLoader, urls: List<String>) {
    urls.forEach { url ->
        val request = ImageRequest.Builder(context)
            .data(url)
            .build()
        loader.enqueue(request)
    }
}

캐시 삭제하기

사용 가능한 공간을 확보하거나 강제로 새로 고쳐야 할 때가 있습니다. 예를 들어 사용자가 아바타를 업데이트한 경우입니다. 메모리 캐시나 디스크 캐시 전체를 삭제하거나 특정 키 하나를 제거할 수 있습니다.

모든 항목을 삭제하는 작업은 부담이 크므로 가능하면 특정 키만 제거하세요.

import coil3.ImageLoader
import coil3.memory.MemoryCache

fun refreshAvatar(loader: ImageLoader, key: String) {
    // Remove one entry so the next load refetches
    loader.memoryCache?.remove(MemoryCache.Key(key))
    loader.diskCache?.remove(key)
}

fun clearAll(loader: ImageLoader) {
    loader.memoryCache?.clear()
    // Disk clearing is heavier; do it sparingly
}

크로스페이드와 캐시 적중

크로스페이드는 네트워크로 로드할 때 보기 좋지만, 메모리 캐시에서 바로 가져와 즉시 표시할 수 있는 이미지에서는 느리게 느껴질 수 있습니다. Coil은 이 점을 고려하여 기본적으로 메모리 캐시에서 가져온 이미지에는 크로스페이드 애니메이션을 생략하고, 실제로 로드된 이미지에만 애니메이션을 적용합니다.

따라서 처음 로드할 때는 부드럽게 페이드되고, 캐시된 이미지를 스크롤할 때는 즉시 표시됩니다. 별도의 작업도 필요하지 않습니다.

import android.content.Context
import coil3.request.ImageRequest
import coil3.request.crossfade

fun feedRequest(context: Context, url: String) =
    ImageRequest.Builder(context)
        .data(url)
        // Crossfade plays on real loads; memory-cache hits show instantly
        .crossfade(true)
        .build()

목록 성능 점검 목록

LazyColumn이나 그리드에 많은 이미지를 표시할 때는 부드러운 스크롤을 위해 다음 규칙을 따르세요.

  • 각 이미지에 고정된 크기를 지정하여 Coil이 작은 비트맵을 디코딩하도록 하세요.
  • 싱글턴 ImageLoader 하나를 재사용하여 캐시를 공유하세요.
  • 레이아웃이 갑자기 바뀌는 현상을 피하려면 ContentScale.Crop을 사용하세요.
  • 새로고침할 때도 캐시 키가 변하지 않도록 유지하세요.
  • 화면 밖 요청은 Coil이 자동으로 취소하도록 두고, 이를 방해하지 마세요.
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import coil3.compose.AsyncImage

@Composable
fun PhotoGrid(urls: List<String>) {
    LazyVerticalGrid(columns = GridCells.Fixed(3)) {
        items(urls) { url ->
            AsyncImage(
                model = url,
                contentDescription = null,
                contentScale = ContentScale.Crop,
                modifier = Modifier.aspectRatio(1f)
            )
        }
    }
}

빠른 확인

Coil은 네트워크에 접근하기 전에 캐시를 확인합니다. 적중했을 때 디코딩을 완전히 생략하므로 가장 빠른 캐시는 무엇인가요?

복습

Coil이 이미지를 빠르게 유지하는 방법을 배웠습니다.

  • 두 가지 캐시를 사용합니다. 메모리 캐시는 디코딩되어 빠르지만 작고, 디스크 캐시는 원본 바이트를 저장하며 더 크고 지속적입니다.
  • 사용자 지정 ImageLoader에서 크기를 구성하고, SingletonImageLoader.Factory를 통해 하나의 로더를 제공하세요.
  • memoryCachePolicy와 diskCachePolicy로 요청별 동작을 조정하세요.
  • 항상 표시 크기에 맞춰 디코딩하고, 필요하면 명시적인 size()를 설정하세요.
  • 안정적인 캐시 키를 사용하고, 곧 표시할 이미지를 미리 로드하며, 새로 고칠 때는 특정 키를 삭제하세요.

다음에는 이미지에서 더 나아가 오디오와 비디오를 재생하는 방법을 살펴봅니다.

자주 묻는 질문

“캐싱과 성능” 강의는 무료인가요?

네 — “캐싱과 성능” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Android Academy 강의 전체를 잠금 해제할 수 있습니다. Android Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

“캐싱과 성능”에서 뭘 배우나요?

메모리와 디스크 캐싱의 기초를 익힙니다. 브라우저에서 직접 실행하는 실습 코드로 Android Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

Android Academy을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 Android Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 3번째 강의입니다.

“캐싱과 성능” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 Android Academy 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 Android Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. Coil로 이미지 로드
  2. 자리표시자와 오류 상태
  3. 캐싱과 성능
  4. 오디오와 동영상 재생
← Android Academy(으)로 돌아가기