주완 님의 블로그
android- 이미지 라이브러리 Coil 본문
이미지 라이브러리를 왜 써야할까? 라는 주제로 탐구를 시작해 보았습니다. 먼저 저희가 Image 관련 작업을 할 때 사용하는 Image , AsyncImage 컴포져블 함수에 대해서 알아봅시다.
Image
Image 컴포저블은 Jetpack Compose 의 기본요소
이미지 로드 | Jetpack Compose | Android Developers
- 보통 언제쓸까?
로컬 리소스, 비트맵 또는 이미 메모리에 로드된 ImageVector 와 같은 이미지 데이터를 표시할 때 사용합니다.
- 작동 방식은?
이미 앱의 실행 파일 내에 포함되어 있거나, 함수가 호출될 때 이미 데이터가 준비된 이미지를 즉시 화면에 그립니다. ( 추가적으로, 네트워크 통신이나 비동기적인 로딩 과정을 처리하지 않습니다)
- 비동기 지원을 하나?
앞서 말했듯이 비동기 과정을 처리하지 못하는 걸 봐선 지원하지 않겠죠?
그렇기 때문에 보통 아래와 같이 사용합니다.

Image(
painter = painterResource(R.drawable.img_login_main),
contentDescription = stringResource(R.string.ic_login_main_image),
modifier = Modifier // Image 에 대한 수정이 필요할 때 추가로 사용하겠죵
)
AsyncImage
그렇다면 이번에는 AsyncImage 에대 대해서 알아봅시다.
AsyncImage 는 Compose 공식 라이브러리가 아닌, Coil과 같은 이미지 로딩 라이브러리에서 Compose 환경을 위해 제공하는 컴포저블 함수입니다.
GitHub - coil-kt/coil: Image loading for Android and Compose Multiplatform.
Coil을 많이 써왔기 때문에 일단, Coil 기준으로 설명하고 , 두 라이브러리의 차이는 마지막에 알아보도록 하겠습니다.
- 주요 용도는?
네트워크 URL 에서 이미지를 다운로드하거나, 로컬 저장소 등 비동기적인 로딩이 필요한 위치의 이미지를 표시할 때 사용합니다.
- 작동 방식은?
- URL응 인자값으로 받습니다.
- 이미지 로딩을 위한 비동기 작업을 시작합니다.
- 로딩 중에는 플레이스 홀더를 표시할 수 있습니다.
- 로딩이 완료되면 이미지를 화면에 표시하고, 실패하면 에러 이미지를 표시합니다.
- 로드된 이미지를 캐시에 저장하여 동일한 이미지를 요청할 때 빠르게 로드합니다.
- 비동기 지원은?
당연히 됩니다. 내부적으로 코루틴을 사용하여 네트워크 작업을 처리합니다.
아래와 같이 사용할 수 가 있습니다

AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(image)
.crossfade(true)
.build(),
contentDescription = null,
modifier = modifier
.size(64.dp)
.clip(CircleShape),
contentScale = ContentScale.Crop
)
1. ImageRequest.Builder(LocalContext.current)
- ImageRequest: Coil이 이미지를 로드하기 위해 필요한 모든 정보와 설정을 담는 객체입니다. 네트워크 URL, 캐싱 정책, 변환(transformations), 크로스페이드 여부 등 모든 요청 옵션을 정의합니다.
- .Builder(): ImageRequest 객체를 생성하는 데 사용하는 빌더(Builder) 패턴의 시작입니다. 빌더를 사용하면 필요한 옵션만 체인(chain) 형태로 설정할 수 있어 가독성이 높습니다.
- LocalContext.current: Android의 Context 객체를 받습니다. Coil은 Context를 사용하여 리소스를 해석하거나, 디스크 캐시 경로를 결정하는 등 내부적인 작업을 수행합니다. Compose 환경에서는 LocalContext.current를 통해 현재 컴포저블의 컨텍스트를 안전하게 가져올 수 있습니다.
2. .data(image)
- data(image): 이 요청이 가져와야 할 이미지의 소스(Source)를 지정합니다.
- 여기서 image는 ViewModel에서 가져온 서버 이미지 URL(String)이 되겠죵
- 이 값은 URL 외에도 File, Uri, Drawable Res ID (Int) 등 Coil이 지원하는 다양한 데이터 타입이 될 수 있습니다. 이는 AsyncImage의 model 파라미터와 동일한 역할을 수행합니다.
3. .crossfade(true)
- .crossfade(true): 이미지가 성공적으로 로드되어 화면에 표시될 때 부드러운 페이드인(Fade-in) 애니메이션을 적용하도록 지시하는 옵션입니다.
4. .build()
- .build(): 빌더 패턴의 마무리입니다. 앞서 설정한 모든 옵션(Context, data, crossfade 등)을 바탕으로 최종적인 Immutable의 ImageRequest 객체를 생성하여 AsyncImage의 model 인수로 전달합니다.
동작 방식과 코드는 아래에서 좀 더 다뤄보도록 하겠습니다. 이렇게 용도의 차이를 봤을 때 가장 큰 차이는 비동기 처리 방식으로 알 수 있습니다. 해당 코드는 AsyncImage에 대한 모든 속성들을 사용한 것은 아니기 때문에 아래에서 차근차근 뜯어보도록 해요.
⇒ Image 와 AsyncImage 의 차이는 일단 비동기 처리부터 차이가 생기네요! 근데… 그럼 Image 컴포져블 함수를 사용했을 때는 서버에서 받아온 이미지를 띄울 수 없는건가요?
아쉽게도 그렇습니다...
1. painterResource 사용 시
painterResource 함수는 Int 타입의 리소스 ID (예: R.drawable.my_image)만 인수로 받도록 설계되어 있습니다.
- 이는 앱의 res/drawable 폴더 안에 컴파일 시점에 패키징되어 포함된 정적(static) 이미지 파일을 가리킵니다.
- 네트워크 URL(String)은 리소스 ID가 될 수 없으므로, 이 방식을 통해서는 서버 이미지를 로드할 수 없습니다.
2. 다른 데이터 소스 사용 시
Image 컴포저블은 Painter, ImageBitmap, 또는 ImageVector와 같이 이미 메모리에 로드되어 있거나 즉시 사용 가능한 이미지 데이터를 요구합니다.
- 서버에서 이미지를 다운로드하는 과정은 네트워크 지연 시간이 발생하는 비동기 작업입니다.
- Image 컴포저블은 이러한 비동기 작업을 처리하고, 데이터가 준비될 때까지 기다리며, 로딩 상태를 관리하는 내장 로직이 없습니다.
- 만약 서버 통신 코드를 직접 작성해서 ImageBitmap을 받아와 Image 컴포저블에 전달할 수는 있지만, 이는 이미지 라이브러리(Coil 등)가 제공하는 캐싱, 메모리 관리, 플레이스홀더 등의 이점을 모두 포기하는 것이므로 권장되지 않습니다.
이런 이유 때문에 안된다고 합니답
Compose와의 통합 및 관리
간단한 코드를 통해 Image, AsyncImage 에 대한 차이를 알아봤는데요! 그럼 이번엔 Coil 라이브러리에 대해서 좀 더 세부적으로 알아보도록 해요.
- 비동기 처리: 이미지 로딩 작업은 코루틴(Coroutines)을 사용하여 백그라운드에서 비동기적으로 수행되며, 이는 메인 스레드를 블록하지 않습니다.
- 생명주기 인식: AsyncImage는 Compose의 생명주기(Lifecycle)를 인식하여, 화면에서 컴포저블이 사라지면(Dispose) 로딩 작업을 자동으로 취소합니다. 이는 불필요한 작업과 메모리 누수를 방지해 줍니다.
- 플레이스홀더/에러: 로딩 중에는 placeholder 이미지를, 실패 시에는 error 이미지를 설정하여 사용자 경험을 매끄럽게 관리할 수 있습니다.
그렇다면 , Coil 라이브러리는 어떻게 구성 되어있으며 어떤 방식으로 동작할까요?
일단 AsyncImage를 뜯어 보자면

이런식으로 구성이 되어 있습니다. alignment 부터는 image 의 외부 속성 값을 처리하는 것이기 때문에 다른 파라미터들과 비동기 처리에 관한 속성 부터 봐보겠습니다.


맨 윗줄 부터 보자면
ImageRequest 는 Coil 라이브러리에서 이미지를 로드하기 위한 모든 설정 정보를 담고 있는 안전한(불변의) 데이터 묶음입니다.
model = ImageRequest.Builder(LocalContext.current)
.data(image)
.crossfade(true)
.build(),
앞에서 본 해당 과정이 ImageRequest를 만드는 과정입니다. 해당 과정에서 URL, 크로스페이드 여부. 캐싱 정책등 모든 옵션을 한 번에 설정하고, 객체 생성 이후에는 값을 바꿀 수 없기 때문에, 불변 값 객체라고 불리우며, 상태가 도중에 변경되는 일이 없어 코드의 안정성이 높아집니다.
위에서 ImageRequest,Build, data, crossfade,build 에 대해서 설명을 드렸었죠!
아까 말했듯이 모든 속성을 사용한 건 아니였는데요,
crossfade : crossfade를 통해 자연스러운 전환효과를 줄수 있습니다
transformations : 로드된 이미지에 적용할 Transformation 목록을 설정합니다.
precision : 이미지의 크기를 조정하는 방식에 대한 정밀도를 설정합니다.
scale : 이미지를 타겟 크기에 맞게 조정하는 방식을 설정합니다.
해당 속성들을 통해 이미지 처리 및 변환 옵션을 줄 수도 있고
캐싱 및 메모리 관리 옵션을 통해 저장방식을 제어할 수 있다고 합니다.
⇒ ImageRequest를 통해 이미지를 조절하는 거랑 컴포저블 함수내의 modifier 를 통해 조절하는 차이가 뭔가요..? 특히 scale 같은 크기 조정은 상관없지 않나요?
저만 궁금한건 진 모르겠지만 일단 낋여와 봤어요
핵심은 Coil의 이미지 처리 파이프라인 vs. Compose의 렌더링 파이프라인의 차이입니다.
1. ImageRequest 옵션의 역할 (최적화)
ImageRequest 옵션들은 이미지를 로드하고 메모리에 저장하는 과정 자체를 최적화하는 데 중점을 둡니다.
A. Transformations (변환)
• 역할: 이미지가 메모리에 Bitmap 형태로 로드된 후, CPU 스레드에서 이미지 자체를 물리적으로 변경합니다. • 예시 (.transformations(CircleCropTransformation())): 원본 비트맵의 사각형 픽셀 데이터를 실제로 분석하고, 밖의 픽셀을 잘라내어 원형의 새로운 비트맵을 만듭니다. 이 원형 비트맵이 메모리에 저장됩니다. • 차이점: Compose의 .clip(CircleShape)는 렌더링 시 그려지는 영역만 마스킹할 뿐, 메모리에는 여전히 사각형 이미지가 남아 있습니다. Transformation은 데이터 자체를 변경하여 메모리 낭비를 줄입니다.
****B. Size/Precision/Scale (크기 및 정밀도)
• 역할: 이미지를 다운로드하거나 디코딩할 때, 필요한 크기만큼만 처리하도록 지시합니다. • 예시 (.size(width, height)): 서버에서 1000x1000 픽셀의 원본 이미지를 받아야 하지만, 화면에 100x100 픽셀로 표시될 것임을 Coil에게 미리 알려줍니다. ◦ Coil은 다운로드된 1000x1000 이미지를 디코딩할 때 100x100 크기로 줄여서 디코딩합니다. ◦ 결과: 메모리에 100x100 크기의 작은 Bitmap만 로드되어 RAM 사용량을 크게 줄이고 디코딩 시간을 단축합니다.
2. Compose 속성의 역할 (시각적 배치)
Compose의 Modifier나 contentScale 파라미터는 이미 완전히 로드된 Bitmap 데이터가 주어졌을 때, 그것을 화면의 지정된 영역에 어떻게 배치하고 그릴지를 제어합니다.
A. Modifier.size()
• 역할: 컴포저블이 화면에서 차지할 최종 레이아웃 크기를 결정합니다. 이미지 데이터의 크기가 아니라, 이미지를 담을 '프레임'의 크기를 결정합니다.
B. contentScale
• 역할: 이미지의 원본 크기와 프레임 크기가 다를 때, 이미지를 프레임 안에 어떻게 맞출지 시각적 규칙을 정의합니다. • 예시 (ContentScale.Crop): 이미지를 잘라내서 프레임을 가득 채웁니다. (데이터를 변경하는 것이 아니라, 드로잉 시 어느 부분을 그릴지 결정합니다.)
3. crossfade
• crossfade는 이미지 데이터의 변환이 아닌, 렌더링 단계에서 로딩이 완료된 이미지와 placeholder 사이를 시각적으로 부드럽게 전환하는 효과를 제공합니다. 결론적으로, ImageRequest 옵션은 성능을 위해 '로드될 데이터 자체'를 건드리고, Compose 속성은 레이아웃을 위해 '데이터를 담을 프레임과 그리는 방식'을 건드린다고 이해하시면 됩니다.
요건 캐싱 및 메모리 관리 옵션인데, 나중에 공부하려고 일단 링크만..
Jetpack Compose Coil 이미지 캐싱, 잘 하고 계신가요?
Jetpack Compose Coil 캐싱, 어떻게 하고 있을까요?- 디스크 캐싱
네, 암튼 ImageRequest에 대한 내용이였구요, 마저 속성들을 공부해 봅시다.
- 필수인자
- model
타입은 Any( 대부분 String URL 을 사용합니다)
이미지의 소스를 지정하는 속성으로, 네트워크 URL, File, Uri, Drawble Res ID(Int) 등 다양한 형태의 데이터가 될 수 있습니다. Coil은 이 모델을 기반으로 이미지를 요청하며, 이 값이 변경되면 새로운 로딩 작업을 시작합니다.
2. contentDesciption
타입은 String
접근성을 위한 설명 텍스트입니다. 스크린 리더가 이 이미지를 설명할 때 사용됩니다. 이미지를 단순히 장식용(앞에서 봤 듯이 디자인을 위해 그냥 화면에 나타내는 것이라면)이거나 사용자에게 유의미한 정보를 전달하지 않을 경우 null 로 처리할 수 있습니다.
(이건 자주 사용하는 Icon 에서도 사용되는 속성으로 값을 지정하거나 null 처리하는 방식의 차이를 알고 있으면 좋겠죠?)
- 옵셔널 인자
- imageLoader
타입은 ImageLoader 라는 객체를 사용합니다
이미지 로등 작업을 실행하는 엔진으로, 기본적으로 전역 ImageLoader 인스턴스가 사용되지만, 해당 파라미터를 통해 캐싱, 이미지 디코더 등 커스텀 ImageLoader 를 주입할 수 있습니다.
⇒ 중요한 속성 같은데, 좀 더 알아보고 싶어서..


타고 들어가면 LocalImageLoader가 ImageLoaderProvidableCompositionLocal() 로 되어 있는데,
얘를 통해서 Compose 의 CompositionLocal 을 사용하여, 애플리케이션 전역 또는 가장 가까운 상위 컴포져블에서 설정된 ImageLoader 인스턴스를 가져온다고 합니다. 즉, imageLoader를 지정하지 않으면 자동으로 이 기본 전역 인스턴스를 사용하게 되므로 , 코드가 문제없이 실행 된다고 하네요.
필요할 때는, 커스텀 설정을 통해 주입도 가능하다고 하는데
- 캐싱 정책 변경: 메모리 캐시 크기를 변경하거나 특정 도메인의 이미지만 캐시하지 않도록 설정하고 싶을 때.
- 커스텀 디코더 추가: WebP나 SVG 같은 특수한 이미지 포맷을 지원하고 싶을 때.
- 헤더/네트워크 설정: 특정 API 헤더를 모든 이미지 요청에 자동으로 추가하고 싶을 때.
이렇게 사용한다고 합니다.
2. modifier
수정자 인데요, 이건 다른곳에서도 많이 볼 수 있으니 넘어가겠습니다
3.placeholder
타입은 Painter 입니다.
이미지가 로딩되는 동안 화면에 표시할 Painter( 보통, 로딩 스피너나 progressIndicator 같은 화면을 사용하겠죠?)
4.error
얘도 Painter을 사용합니다.
이미지 요청이 실패했을 때 (400 대 에러가 뜬다던지.. 네트워크가 끊긴다던지..) 표시할 Painter를 지정해 줍니다.
5. fallback
얘 또한 Painter
model 값이 null 이거나 비어있을 때 표시할 Painter를 지정합니다. error 와 다른 점은 error 가 요청 후 이미지를 실패했을 때 뜨는 화면이라면, fallback은 그 전 단계인 요청 자체가 시작되지 않았을 때의 이미지를 나타내 줍니다
6. OnLoading, OnSuccess, OnError
((State.Loading) → Unit)
각 단계 별로 이미지 로딩이 시작되었을 때, 완료되었을 때, 실패했을 때 호출되는 콜백입니다
AsyncImage 내부의 각 속성이 어떠한 역할을 하는지 알아봤으니, 이제 코드를 확인해보면서 단계별로 동작 방식을 알아보도록 합시당
- build.gadle
// build.gradle.kts (app)
dependencies {
implementation("io.coil-kt:coil-compose:2.6.0")
// 버전은 각자 맞는 버전으로..
// 버전 카탈로그 써서 해도 됩니다
}
2. AsyncImage
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(image)
.crossfade(true)
.build(),
contentDescription = null,
modifier = modifier
.size(64.dp)
.clip(CircleShape),
contentScale = ContentScale.Crop
)
⇒ AsyncImage 를 사용할 컴포넌트를 만들어줍니다.
3. 사용할 Screen
@Composable
fun MyPageRoute(
paddingValues: PaddingValues,
navigateUp: () -> Unit,
...
modifier: Modifier = Modifier,
viewModel: MyPageViewModel = hiltViewModel()
) {
val state = viewModel.state.collectAsStateWithLifecycle()
MyPageScreen(
state = state.value,
paddingValues = paddingValues,
navigateUp = navigateUp,
navigateUserProfile = navigateUserProfile,
⇒ 해당 컴포넌트를 사용할 Screen에 viewmodel을 연결해줍니다.
4. viewmodel
@HiltViewModel
class MyPageViewModel @Inject constructor(
private val petProfileRepository: PetProfileRepository,
private val userProfileRepository: UserProfileRepository,
) : ViewModel() {
private val _state = MutableStateFlow(MyPageState())
val state: StateFlow<MyPageState>
get() = _state.asStateFlow()
private val _sideEffect = MutableSharedFlow<MyPageSideEffect>()
val sideEffect: MutableSharedFlow<MyPageSideEffect> = _sideEffect
...
fun getPetProfiles(userId: Int) {
viewModelScope.launch {
petProfileRepository.getPetProfiles(userId)
.onSuccess {
_state.value = _state.value.copy(
petName = it.first().name,
petAge = it.first().age.toString(),
petGender = it.first().gender,
petImageUrl = it.first().imageUrl,
petTags = it.first().traits.map { trait -> trait.option},
walkCount = it.first().walkCount
)
}.onFailure {
_sideEffect.emit(MyPageSideEffect.ShowSnackBar("펫 프로필 불러오기 실패"))
}
...
}
데이터 요청 및 비동기 작업 시작 (ViewModel & Screen)
⇒ Screen 과 ViewModel을 통해서 비동기 작업이 시작됩니다.
특정 이벤트에 대해 이미지를 로드할 함수를 선언해 줍니다. 여기서는 getPetProfile 함수를 통해서
init 과 같이 화면에 진입했을 때 or 특정 이벤트가 동작할 때 호출하도록 설계하였습니다.
viewmodelScope.launch { … } 를 통해 백그라운드 스레드에서 비동기 작업이 시작됩니다. 해당 작업을 통해 Repository 계층으로 네트워크 요청을 위임하고
petProfileRepositroy.getPetProfile(userId) 가 실행되게 되는데, suspend 함수를 통해서 ,앞서 말한 메인 스레드를 막지 않게 처리해줍니다.
5. Repository
interface PetProfileRepository {
suspend fun getPetProfiles(userId: Int): Result<List<PetProfileEntity>>
}
⇒ viewmodel 에서 PetProfileRepository 를 파라미터로 받아와서 사용하게 되는데, 계층 분리를 위해 repository pattern 을 사용해서 추상체랑 구현체를 나눠줍니다.
[Android] 안드로이드 Repository Pattern 및 UseCase Pattern 에 대해서 간단히 알아보자
계층 분리에 대해서는 나중에 시스템 아키텍쳐를 공부하면서 차근차근 알아보면 좋을 것 같습니
6. RepositoryImpl
class PetProfileRepositoryImpl @Inject constructor(
private val dataSource: PetProfileDataSource,
) : PetProfileRepository {
override suspend fun getPetProfiles(userId: Int): Result<List<PetProfileEntity>> = runCatching {
dataSource.getPetProfiles(userId).data.map { it.toEntity() }
}
}
⇒ 오버라이드를 통해 getPetProfile 를 구현해줍니다.
7. DataSource
class PetProfileDataSource @Inject constructor(
private val petprofileservice: PetProfileService
) {
suspend fun getPetProfiles(userId: Int) = petprofileservice.getPetProfiles(userId)
}
8. Api Service
interface PetProfileService {
@GET("users/me/pets")
suspend fun getPetProfiles(
@Header("X-USER-ID") userId: Int
): BaseResponse<List<PetProfileResponseDto>>
}
⇒ 서버에서 응답이 오면, DTO(여기서는 PetProfileREsponseDto) 형태로 JSON 데이터가 수신됩니다.
9. DTO
@Serializable
data class PetProfileResponseDto(
@SerialName("petId")
val petId: Long,
@SerialName("name")
val name: String,
...
@SerialName("imageUrl")
val imageUrl: String,
// imageUrl 을 받아오는게 메인이니 여기만 봅시다
...
@SerialName("walkCount")
val walkCount: Int
) {
fun toEntity() = PetProfileEntity(
...
...
walkCount = walkCount,
traits = traits.map { it.toEntity() }
)
}
⇒ repository 계층에서 변환되는 DTO이며 , imageUrl값은 Entity에 담겨 ViewModel로 변환됩니다.
fun getPetProfiles(userId: Int) {
viewModelScope.launch {
petProfileRepository.getPetProfiles(userId)
.onSuccess {
_state.value = _state.value.copy(
petName = it.first().name,
petAge = it.first().age.toString(),
petGender = it.first().gender,
petImageUrl = it.first().imageUrl,
petTags = it.first().traits.map { trait -> trait.option},
walkCount = it.first().walkCount
)
}.onFailure {
_sideEffect.emit(MyPageSideEffect.ShowSnackBar("펫 프로필 불러오기 실패"))
}
앞서 구현한 viewmodel 의 getPetProfiles 함수 내에서 .onSuccess {…} 블록이 실행되게 되는데, 해당 코드를 통해 _state 의 값이 새로운 petImageUrl 값으로 업데이트 됩니다.
MpageRoute 에서 val state = viewmodel.state.collectAsStateWithLifeCycle() 을 통해 업데이트가 되는 상태를 구독하고 있으므로, 상태변경을 감지하여 MypageScreen이 리컴포즈 해주게됩니다.
AsyncImage 호출이 되면 리컴포지션 과정에서 AsyncIamge 컴포져블이 새로운 model 값(여기선 viewmodelscope 내부에 있는 it.first().imageUrl 을 가지고 호출이 됩니다.
그 다음 요청 빌드인 ImageRequest.Builder(LocalContext.current).data(image).build()를 통해 model 인수가 이미지 요청 객체로 변환됩니다.
추가로 . ImageLoader 를 통해 내부적으로 메모리 캐시와 디스크 캐시를 확인하게 되고
앞서 말한, AsyncImage가 로딩하는 동안 , onLoading 콜백이나 placeholder 파라미터를 설정해 주었다면 해당 UI가 화면에 표시됩니다. 현재 보여드린 코드에서는 해당 작업은 하지 않아서.. 추가로 작업이 필요합니다!
코드와 함께 보니 좀 .. 장황하게 설명이 되었는데요, 정리하자면
1단계: 서버 데이터 요청 및 상태 변경 (Compose → ViewModel)
- 요청 시작: 화면 진입 시 MyPageViewModel의 getPetProfiles 함수가 호출되며 viewModelScope.launch 코루틴이 백그라운드에서 시작됩니다.
- 데이터 수신: Repository/DataSource/Service 계층을 거쳐 서버로부터 imageUrl을 포함한 데이터(DTO)가 수신됩니다. (이 모든 과정은 suspend 함수를 통해 비동기로 이루어집니다.)
- 상태 업데이트: .onSuccess 블록에서 _state.value가 새로운 petImageUrl로 업데이트됩니다.
- 리컴포지션: collectAsStateWithLifecycle()을 통해 상태를 구독하는 MyPageRoute와 MyPageScreen이 이 상태 변경을 감지하고 리컴포즈(Recompose)를 시작합니다.
2단계: 이미지 로딩 요청 객체 생성 (Compose → Coil)
- AsyncImage 호출: 리컴포즈 과정에서 AsyncImage 컴포저블이 새로운 petImageUrl을 가지고 호출됩니다.
- 요청 빌드: ImageRequest.Builder(LocalContext.current).data(image).crossfade(true).build()를 통해 서버 URL이 포함된 ImageRequest 객체가 생성됩니다. (crossfade(true)는 이미지가 로드될 때 부드러운 페이드인 효과를 지정합니다.)
- Coil 작업 위임: ImageRequest가 AsyncImage의 model로 전달되어 Coil의 ImageLoader에게 로딩 작업이 위임됩니다.
3단계: 비동기 이미지 로딩 및 캐싱 (Coil 내부)
- 로딩 시작: AsyncImage의 onLoading 콜백이 있다면 호출되고, placeholder가 화면에 표시됩니다.
- 캐시 확인: ImageLoader는 메모리 캐시 → 디스크 캐시 순서로 이미지를 검색합니다.
- 네트워크 요청: 캐시에 없으면, Coil 내부의 코루틴이 백그라운드에서 URL로 네트워크 요청을 수행하여 이미지를 다운로드합니다.
- 디코딩 및 저장: 다운로드된 이미지는 디코딩되어 화면에 표시될 준비를 하며, 캐시에 저장됩니다.
4단계: 이미지 표시 및 완료 (Coil → Compose)
- 성공 시: 이미지 로딩이 완료되면 onSuccess 콜백이 호출됩니다.
- 화면 렌더링: 최종 이미지가 AsyncImage 영역에 그려집니다. 이때 modifier.clip(CircleShape) 및 contentScale = ContentScale.Crop에 따라 원형으로, 잘린 형태로 표시됩니다.
- 실패 시: 만약 네트워크 요청이나 디코딩에서 문제가 발생하면 onError 콜백이 호출되고, error 파라미터로 지정된 Painter가 표시됩니다.
이처럼 Coil 라이브러리는 Jetpack Compose 환경에서 이미지를 효율적으로 처리하기 위해 단순히 URL을 표시하는 것을 넘어, 복잡한 비동기 로딩 파이프라인을 Compose 시스템에 완전히 통합하여 처리하는 라이브러리 입니다. 당장 사용하도록 하세요
++ Coil 얘기만 계속해서 Glide 와의 차이점도 한 번 적어보려했는데요, 일단 메모리 사용량 차이 정도로만 알고 있어서.. 시간 될 때 찾아보면서 써보겠습니다.
'Android' 카테고리의 다른 글
| Android에서 Domain Layer와 UseCase: 언제 필요하고 언제 생략할까? (0) | 2025.12.03 |
|---|---|
| Android - jetpack navigation 사용시 중첩 네비게이션에 대하여 (0) | 2025.11.11 |
| Android - Paging 라이브러리 (0) | 2025.11.05 |
| 회원가입 플로우 아키텍처 분석: 다중 vs 단일 ViewModel , Navigation구조 (3) | 2025.08.27 |
| 구조 분해 선언과 component 함수 (kotlin in action) (1) | 2025.06.11 |