코루틴 기반 이미지 로딩 라이브러리.
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()가 필요하다.