안드로이드 시스템·Compose 용어 사전
미디어AsyncImage · 이미지 로딩

Coil

코루틴 기반 이미지 로딩 라이브러리. 다운샘플링과 생명주기 취소를 대신 해 준다.

코루틴 기반 이미지 로딩 라이브러리.

AsyncImage(
    model = ImageRequest.Builder(LocalContext.current)
        .data(user.avatarUrl)
        .crossfade(true)
        .build(),
    contentDescription = "프로필 사진",
    placeholder = painterResource(R.drawable.ph),
    error = painterResource(R.drawable.err),
    contentScale = ContentScale.Crop,
    modifier = Modifier.size(48.dp).clip(CircleShape)
)

직접 하면 무엇을 다 해야 하나

  • 네트워크 다운로드 + 취소

  • 화면 크기에 맞춘 다운샘플링

  • 메모리 캐시 (LruCache) + 디스크 캐시

  • 목록 재활용 시 잘못된 이미지가 뜨는 문제 (뷰 재사용 경합)

  • 생명주기에 맞춘 취소

  • 비트맵 재사용

  • 라이브러리가 전부 대신한다

왜 이미지가 메모리의 지뢰밭인가

4000 × 3000 사진을 ARGB_8888 로 디코딩하면 4000 × 3000 × 4바이트 = 48MB

앱 힙 한도가 기기에 따라 128~512MB 다

  • 목록에서 몇 장만 로드해도 OutOfMemoryError

Coil 은 표시 크기(예: 48dp = 144px) 에 맞춰 축소해 디코딩한다 144 × 144 × 4 = 약 83KB — 600배 차이

크기를 반드시 제한한다

// ❌ 크기가 없으면 원본 크기로 디코딩될 수 있다
AsyncImage(model = url, contentDescription = null)

// ✅ Modifier 로 크기를 주면 Coil 이 그에 맞춰 샘플링한다
AsyncImage(model = url, contentDescription = null, modifier = Modifier.size(48.dp))

contentDescription은 접근성이다

contentDescription = "프로필 사진"   // TalkBack 이 읽는다
contentDescription = null           // 장식용 이미지 — 의도적으로 건너뛴다

null 을 '귀찮아서' 넣는 것과 '장식이라서' 넣는 것은 다르다 의미 있는 이미지에 null 을 주면 시각장애 사용자에게 그 정보가 사라진다

Media3 · CameraX도 같은 원리

// ExoPlayer — 생명주기에 묶어 반드시 해제한다
DisposableEffect(Unit) {
    val player = ExoPlayer.Builder(ctx).build()
    onDispose { player.release() }        // 안 하면 오디오 포커스와 코덱이 남는다
}

// CameraX — LifecycleOwner 를 넘기면 알아서 붙었다 떨어진다
cameraProvider.bindToLifecycle(lifecycleOwner, selector, preview, imageCapture)

무거운 자원의 공통 규칙

  • ① 필요한 만큼만 크게 만든다 (다운샘플링 · 해상도 선택)
  • ② 생명주기에 묶어 제때 놓는다 (release · unbind)

면접 함정

  • "Glide/Coil을 쓰면 OOM이 안 난다" → 크기를 안 주면 여전히 크게 디코딩된다.
  • "ExoPlayer는 GC가 정리한다" → 네이티브 코덱과 오디오 포커스는 release()가 필요하다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 이미지·미디어 — Coil·Media3·CameraX