Android
네트워크 기반 Compose 컴포넌트 Preview가 안보인다면?
Ted 서재원 / Android Team여기어때
2025년 5월 27일
원문에서 보기 ↗네트워크 기반 Compose Preview가 안보인다면?
안녕하세요, 여기어때컴퍼니(이하 여기어때) Android개발팀 테드입니다.
YDS 컴포넌트 중 YDSImage의 Compose Preview가 렌더링되지 않는 문제를 발견했고, 이를 어떻게 개선했는지 공유드리고자 합니다.
본격적인 내용에 들어가기 전에, YDS와 YDSImage가 무엇인지 간단히 짚고 넘어가겠습니다.
※ YDS가 낯선 분들은 아래 설명을 읽어보시면 전체 내용을 이해하는데 도움이 됩니다.

YDS(Yeogi Design System)?
여기어때는 UI 일관성을 매우 중요하게 생각합니다. 그래서 사내 디자인 시스템인 YDS(Yeogi Design System)를 활용하여 텍스트, 색상, 여백, 컴포넌트 규격은 물론, 상태 처리 방식까지 체계적으로 정의하고 있습니다. 이를 바탕으로 Android개발팀과 iOS개발팀이 모두 동일한 기준으로 화면을 구성하고 있습니다. 제가 속한 Android개발팀 역시 YDS의 가이드라인을 기반으로 컴포넌트를 작성하고 있고, 실제 앱 화면에서도 이를 적극 활용하고 있습니다. 덕분에 화면 간 일관성을 자연스럽게 유지할 수 있고, 코드 재사용성이 높아 유지보수와 기능 확장이 쉬운 구조를 갖추고 있습니다.
저는 아직 인턴으로 합류한 지 3개월밖에 되지 않았지만, YDS의 장점을 크게 체감하고 있습니다. 디자이너 분들께서 Figma 내에서 YDS에 정의된 규격을 기반으로 화면을 일관성 있게 설계해 주신 덕분에, 제가 디자인 의도를 파악하는 데 혼란이 없었습니다. 코드 역시 컴포넌트 단위로 잘 추상화되어 있어, UI 구현 과정이 매우 수월했습니다.
그 결과 UI 구현 과정에서 드는 시간과 시행착오가 눈에 띄게 줄었고, 동일한 피쳐를 개발하는 iOS개발팀과의 싱크를 맞추기 위한 별도의 시간이나 비용이 거의 들지 않아서 개발 효율 면에서도 크게 만족하고 있습니다 :)
YDSImage 컴포넌트?
YDSImage는 YDS의 여러 컴포넌트에서 공통적으로 사용하는 네트워크 기반 이미지 렌더링 컴포넌트입니다.
@Composable
fun YDSImage(
modifier: Modifier = Modifier,
url: String?,
customDefaultImage: @Composable (() -> Unit)? = null,
...
) {
var imageLoadState: State by remember { mutableStateOf(State.Empty) }
val imageRequest = remember(url) {
defaultImageRequest(context = LocalContext.current, data = url)
}
Box(modifier = modifier) {
when (imageLoadState) {
is State.Loading -> Skeleton()
is State.Error -> customDefaultImage?.invoke() ?: DefaultImage()
else -> Unit
}
AsyncImage(
model = imageRequest,
contentDescription = null,
onState = { imageLoadState = it },
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize()
)
}
}
위는 YDSImage의 구조를 간단히 표현한 예시입니다. 구현 단계에서는 이보다 더 다양한 상황을 고려하여, URL 유효성 검사, GIF 처리, 로딩/기본/에러 이미지 분기, 사용자 정의 이미지 처리 등을 일관성 있게 대응할 수 있도록 설계되어 있습니다.
모바일 앱의 특성상 대부분의 이미지는 정적 리소스가 아니라 네트워크를 통해 로딩됩니다. 여기어때 앱 역시 마찬가지로 사용자 프로필, 썸네일, 배너처럼 시각적으로 중심이 되는 컴포넌트이미지를 표시해야 하는 경우가 많습니다. 이러한 이유로, 전체 150개 YDS 컴포넌트 파일 중 40개 이상 이 YDSImage를 직접 활용하고 있습니다.
즉, YDSImage는 다양한 컴포넌트에서 공통으로 사용되는 만큼, 화면 구성에서 중심적인 역할을 합니다.
문제 발견: Preview가 보이지 않는다?
Jetpack Compose는 Android Studio에서 컴포저블 UI를 실시간으로 확인할 수 있는 Preview 기능을 제공합니다. Preview를 활용하면 코드만으로도 화면 UI를 빠르게 구성하고 확인할 수 있습니다. 이를 통해 디자인을 조정하거나 상호작용을 시뮬레이션하고, 나아가 에뮬레이터나 실제 기기에서 바로 테스트해 볼 수도 있습니다.
공식 문서에서도 Preview는 빠른 UI 검토, 레이아웃 확인, 동료와의 협업을 위한 중요한 도구로 소개되고 있습니다.
최근 YDSImage를 사용하는 컴포넌트들의 Preview가 보이지 않는 것 을 확인하였습니다. 해당 컴포넌트들은 이미지 URL이 없거나 잘못된 경우에 기본 이미지를 보여주도록 설계되어 있었지만, Preview 창에서는 어떤 UI도 나타나지 않았습니다. YDSImage 단독 Preview뿐만 아니라, 이를 포함하고 있는 YDSAvatar 등의 컴포넌트에서도 이미지 영역이 통째로 비어 있는 현상이 발생했습니다.


YDSImage 프리뷰(좌) / YDSImage를 사용하는 YDSAvatar 프리뷰(우)
결과적으로 이 컴포넌트들이 포함된 화면 전체를 @Preview로 띄울 경우,
이미지가 들어갈 자리가 비어 보여 전체 레이아웃이나 구조를 확인하는 데 아쉬움이 생길 수 있습니다.
사실 이 이슈는 사용자나 성능에 직접적인 영향을 주는 문제는 아니지만,
Preview의 유무는 Compose UI 개발의 효율성과 직결됩니다. 작은 이슈라도 팀에 도움이 되고자 하는 마음으로, 저는 직접 원인을 분석하고 해결책을 찾기 시작하였습니다.
원인 분석: 왜 안 보이는 걸까?
YDSImage의 내부 로직은 다음과 같이 구성되어 있습니다.
var imageLoadState: State by remember { mutableStateOf(State.Empty) }
Box {
when (imageLoadState) {
is State.Empty -> Unit
is State.Loading -> Skeleton()
is State.Error -> DefaultImage()
is State.Success -> Unit
}
AsyncImage(
model = imageRequest,
onState = { imageLoadState = it },
...
)
}
핵심은 AsyncImage가 네트워크를 통해 이미지를 비동기적으로 로딩한다는 점입니다. 이 컴포넌트는 Coil 라이브러리를 사용해 이미지를 가져오며, 이미지 로드 상태에 따라 UI를 분기해 보여줍니다. 그러나 Preview 환경은 네트워크 요청을 허용하지 않기 때문에 AsyncImage는 실행되더라도 이미지를 불러올 수 없고, imageLoadState는 State.Empty 상태에서 벗어나지 않습니다. 이 상태에서는 Box 내부에 아무것도 그려지지 않아, Preview 화면에는 결과적으로 아무것도 보이지 않게 됩니다.
해결 방법: LocalInspectionMode를 통한 분기 처리

구글 공식 문서에 따르면, Preview 환경에서는 네트워크 접근이 불가능 하다고 명시되어 있습니다. 따라서 AsyncImage와 같이 네트워크를 통해 이미지를 로드하는 컴포넌트는 Preview에서 정상적으로 동작하지 않는 것이 자연스러운 현상입니다.

이를 보완하기 위해 구글은 Preview 환경에서만 동작하는 로직을 구현할 때 LocalInspectionMode를 사용하는 것을 권장 하고 있습니다.
LocalInspectionMode는 현재 컴포지션이 IDE의 Preview 환경에서 발생하고 있는지를 알려주는 CompositionLocal로, LocalInspectionMode.current가 true일 경우 해당 컴포저블이 Preview 모드에서 실행 중임을 의미합니다.
이를 활용하면 Preview 환경에서만 동작하는 로직을 조건부로 구성하는 것이 가능합니다. 저는 이 방법을 활용하여 Preview 모드를 감지하면 기본 이미지를 렌더링하는 방식으로 문제를 해결하였습니다.
개선된 YDSImage 코드
@Composable
fun YDSImage(
modifier: Modifier = Modifier,
url: String?,
customDefaultImage: @Composable (() -> Unit)? = null,
...
) {
if (LocalInspectionMode.current) {
Box(modifier = modifier) {
customDefaultImage?.invoke() ?: DefaultImage()
}
return
}
var imageLoadState: State by remember { mutableStateOf(State.Empty) }
Box {
when (imageLoadState) {
is State.Empty -> Unit
is State.Loading -> Skeleton()
is State.Error -> DefaultImage()
is State.Success -> Unit
}
AsyncImage(
model = imageRequest,
onState = { imageLoadState = it },
...
)
}
}
위처럼 LocalInspectionMode.current 값을 활용해 분기하면, Preview 환경에서는 네트워크 요청 및 이미지 로딩 과정을 건너뛰기 때문에 이 경우에도 기본 이미지가 정상적으로 렌더링됩니다. 즉, Preview 모드에서만 동작하는 안전한 디폴트 UI를 가질 수 있게 되는 것이죠.
적용 결과


LocalInspectionMode 적용 전 후 모습 (DefaultImage 적용)


LocalInspectionMode 적용 전 후 모습 (customDefaultImage 적용)
적용 이후, YDSImage를 사용하는 컴포넌트들의 Preview 품질이 크게 개선 되었습니다. 특히, 위 사진의 YDSAvatar를 포함한 여러 컴포넌트들이 비어있는 채로 노출하지 않고, 디자인 가이드에 부합하는 기본 이미지로 채웠습니다.
저는 이 개선 내용을 팀원들에게 문서로 정리해 공유하고, 간단한 발표를 통해 설명드렸습니다. 감사하게도 인턴의 제안을 팀원분들께서 적극적으로 검토해 주셨고, 긍정적인 피드백과 함께 상용 프로젝트 코드에도 반영할 수 있었습니다.
마무리하며: 작은 개선과 “Pursue excellence”
여기어때에는 팀원들의 일하는 방식을 이끄는 리더십 원칙 이 존재합니다.
그중에서도 제가 인턴 생활을 하며 가장 인상 깊게 느꼈던 원칙은 바로
“항상 최고 수준을 지향합니다(Pursue excellence)”입니다.

여기어때 리더쉽 원칙
이번 Preview 개선도 그런 맥락에서 출발한 작은 시도였습니다. 크고 눈에 띄는 변화는 아니지만, 작은 불편을 그냥 넘기지 않고 개선해 보려는 시도 하나하나가 ‘최고 수준’을 만들어가는 과정이라고 생각합니다.
여러분의 프로젝트에서도 지금 눈앞에 있는 작은 불편 하나를 개선하는 것부터 시작해보는 건 어떨까요?
참고자료.
https://developer.android.com/develop/ui/compose/tooling/previews?hl=ko#preview-definitions
https://developer.android.com/develop/ui/compose/tooling/previews?hl=ko#localinspectionmode
https://developer.android.com/develop/ui/compose/tooling/previews?hl=ko#preview-limitations
작성자가 궁금하다면 : https://www.linkedin.com/in/librarywon