앱을 실행하지 않고도 작성 중인 UI를 Android Studio 내에서 미리 볼 수 있게 해주는 기능
Jetpack Compose는 UDF(단방향 데이터 흐름) 을 따른다.
→ 데이터는 위에서 아래로 흐르고, 이벤트는 아래에서 위로 전달되어야 한다.
Preview는 ViewModel이나 실제 앱의 상태를 연결하지 않는다.
→ 실사용 상태와 분리되어 작동하는 정적인 환경이다.
그렇기 때문에, Composable이 자체적으로 상태를 가지면 Preview 작성이 어렵다.
Compose에서 각 컴포넌트가 자신의 상태를 갖는 것은 Anti-Pattern 이다.
→ 상태를 갖는 Composable은 미리보기, 테스트, 재사용이 어려워진다.
따라서, 가능한 최상단 Composable에서 상태를 관리하고
하위 자식 Composable들은 Stateless (상태 없는) 구조로 설계해야 한다.
이처럼 상태를 외부에서 주입받고, 변경은 콜백으로 위에 전달하는 구조를
👉 상태 호이스팅(State Hoisting) 이라고 한다.
@Composable
fun TextView() {
var text by remember { mutableStateOf("Hello") }
TextField(
value = text,
onValueChange = { text = it },
)
}
@Composable
fun TextView(
text: String,
onValueChange: (String) -> Unit,
) {
TextField(
value = text,
onValueChange = onValueChange,
)
}
TextView는 상태를 외부로부터 주입받아 사용하며,👉 즉,
데이터는 위에서 아래로 흐르고,
이벤트는 아래에서 위로 전달되는
Jetpack Compose의 단방향 데이터 흐름(UDF)을 잘 따르고 있는 구조이다.
@Preview
@Composable
private fun TextViewPreview() {
TextField(
value = "Hello, World!",
onValueChange = {},
)
}
Preview 함수는 보통 private 로 선언
Preview 네이밍 규칙 정리
컴포넌트 단위의 Preview는 해당 함수명 뒤에 Preview를 붙인다.
▪ 예: LoginButtonPreview, UserCardPreview
화면 상태(UIState)를 표현하는 경우는 Success, Error, Loading 등 상태명을 명시하여 구분한다.
▪ 예: LoginScreenSuccessPreview, LoginScreenErrorPreview, LoginScreenLoadingPreview
Jetpack Compose에서 미리보기를 하려면 @Preview 어노테이션이 필요하다.
@Preview에는 다양한 속성을 설정할 수 있는데,
이걸 잘 활용하면 다양한 상황에서의 UI 모습을 테스트하거나,
여러 조건에 맞는 디자인 검토가 훨씬 쉽다.
@Preview가 지원하는 파라미터 종류
파라미터 이름 타입 기본값 설명 nameString""미리보기 이름. 여러 Preview 구분 시 유용함 groupString""Preview를 논리적으로 묶는 그룹명 apiLevelInt (from 1)-1Android API 버전 지정. 렌더링 환경 시뮬레이션 widthDpInt-1미리보기 화면의 너비(dp 단위) heightDpInt-1미리보기 화면의 높이(dp 단위) localeString""로케일 설정. 예: "en","ko","ja"fontScaleFloat (from 0.01)1f글자 크기 배율. 접근성 테스트에 유용 showSystemUiBooleanfalse상태바/내비게이션바 포함 여부 showBackgroundBooleanfalse흰 배경 표시 여부 backgroundColorLong (ARGB)0배경색 지정. 예: 0xFFEFEFEFuiModeInt (@UiMode)0UI 모드 설정. 다크모드 등 deviceString (@Device)Devices.DEFAULT미리보기에 사용할 디바이스 종류 wallpaperInt (@Wallpaper)Wallpapers.NONEAndroid 12 이상: 배경화면 스타일 설정
Compose에서 동일한 조건의 @Preview를 반복해서 작성해야 할 경우,
매번 같은 속성을 붙여 쓰다 보면 코드가 중복되고 지저분해질 수 있습니다.
예를 들어:
이럴 때는 중복되는 @Preview 설정을 하나의 어노테이션으로 묶어
필요할 때마다 간결하게 재사용할 수 있습니다.
위에서 설명한 Custom Preview 어노테이션은 실제 프로젝트의 Compose Preview에서 다음과 같이 적용되었습니다.
@Preview(
uiMode = Configuration.UI_MODE_NIGHT_NO,
name = "Cstd Light theme",
widthDp = 720,
heightDp = 540,
showBackground = true,
)
@Preview(
uiMode = Configuration.UI_MODE_NIGHT_YES,
name = "Cstd Dark theme",
widthDp = 720,
heightDp = 540,
showBackground = true,
)
annotation class FreeThemePreviews
@Preview(
uiMode = Configuration.UI_MODE_NIGHT_NO,
name = "Clite Light theme",
widthDp = 540,
heightDp = 405,
showBackground = true,
)
@Preview(
uiMode = Configuration.UI_MODE_NIGHT_YES,
name = "Clite Dark theme",
widthDp = 540,
heightDp = 405,
showBackground = true,
)
annotation class PaidThemePreviews
위처럼 커스텀 어노테이션을 정의해두면,
@CstdThemePreviews,@CliteThemePreviews등으로 반복된 Preview 설정을 간편하게 재사용할 수 있습니다.
@FreeThemePreviews
@Composable
fun ContentPreview() {
Content(
composeView = ComposeView(LocalContext.current),
windowManager = LocalContext.current.getSystemService(WindowManager::class.java),
layoutParams = LayoutParams(),
UiState = UiState.LEFT,
ViewOption = ViewOption.LHD,
onShowWindow = {},
onShowDragPreviewsWithClosestIndex = {},
onShowTapPreviews = {},
removeDragPreviews = {},
onSaveClosestIndex = {},
onHighlightPreview = {},
onClickedClosed = {},
)
}
@FreeThemePreviews어노테이션을 통해 Light/Dark 테마 조건별 Preview를 간편하게 적용하고 있음
PreviewParameterProvider : Preview에서 더미 데이터를 쉽게 넣는 방법PreviewParameterProvider는 Preview에서 여러 개의 더미 데이터를 반복적으로 주입할 수 있게 해주는 도구임입니다.
Compose Preview에서는 ViewModel 없이 더미 데이터를 전달해야 하는 경우가 많습니다.
이럴 때 PreviewParameterProvider를 사용하면, Preview 함수에 필요한 더미 데이터를 깔끔하게 주입할 수 있습니다.
아래는 유저 이름 리스트를 보여주는 UsersList 컴포저블과
그에 맞는 Preview를 구성한 예시입니다:
아래는 유저 이름 리스트를 보여주는 UsersList 컴포저블과
그에 맞는 Preview를 구성한 예시입니다:
@Composable
fun UsersList(users: List<String>) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.background(Color.White),
) {
items(users) { user ->
Text(text = user)
}
}
}
더미 데이터를 공급하는 Provider 클래스는 다음과 같이 작성합니다:
class UsersPreviewParameterProvider : PreviewParameterProvider<List<String>> {
override val values: Sequence<List<String>> = sequenceOf(
listOf(
"User1", "User2", "User3", "User4", "User5",
"User6", "User7", "User8", "User9", "User10"
)
)
}
그리고 Preview에서는 다음과 같이 사용합니다:
@CstdThemePreviews
@Composable
fun UsersListPreview(
@PreviewParameter(UsersPreviewParameterProvider::class) users: List<String>
) {
UsersList(users = users)
}
위와 같은 방식으로 Preview에 더미 데이터를 반복 주입하며, 다양한 상황의 UI를 손쉽게 테스트할 수 있습니다.
💡 요약
PreviewParameterProvider는 Preview에서 다양한 더미 데이터를 손쉽게 주입할 수 있도록 도와주는 기능입니다.ViewModel 없이 다양한 상태나 리스트 데이터를 구성해 UI 검토와 테스트에 매우 유용합니다.PreviewParameterProvider는 어떤 Preview 함수에서든 재사용 가능하며,Jetpack Compose의 @Preview는 정적이고 제한된 환경에서 UI를 렌더링하기 때문에,
실제 앱에서 동작하는 로직과 완전히 동일하게 동작하지는 않습니다.
아래 항목들을 사전에 인지하고 Preview를 설계하는 것이 중요합니다.
Preview는 실행 환경이 아니기 때문에 ViewModel을 생성하거나 주입할 수 없습니다.
예를 들어 아래 코드처럼 ViewModel에서 값을 collect 하여 상위 Composable (SampleScreen)에서 처리하고
하위 Composable (SampleContent)로 필요한 UI 상태만 넘기는 방식이 올바른 패턴입니다:
이런 방식으로 구성하면:
Compose Preview는 정적인 UI만 렌더링할 수 있는 환경입니다.
따라서 AndroidView를 통해 카메라 화면, SurfaceView, 외부 스트리밍 뷰 등을 가져오는 경우,
Preview에서는 정상적으로 작동하지 않으며 렌더링 오류가 발생할 수 있습니다.
아래는 실제 코드에서 사용하는 예시입니다:
Box(
modifier = modifier,
) {
if (LocalInspectionMode.current) {
val dummyImage = if (UiState == UiState.LEFT) {
R.drawable.img_dummy_left
} else {
R.drawable.img_dummy_right
}
CoilImage(
modifier = Modifier
.fillMaxSize(),
painterResource = dummyImage,
contentDescription = "Mask Image",
)
} else {
AndroidView(
modifier = Modifier
.fillMaxSize(),
factory = {
CustomGLSurfaceView.create(
context.applicationContext,
arrayListOf(bufferCallback),
)
},
)
}
}
✅ 해결 방법: LocalInspectionMode.current 조건 분기
Preview 환경 여부는 LocalInspectionMode.current를 통해 감지할 수 있습니다.
위 코드에서는 Preview 시에는 실제 카메라 뷰 대신
상태에 따라 좌/우 더미 이미지를 보여주도록 분기 처리하였습니다.
이런 구조를 사용하면:
AndroidView 또는 실시간 이미지 처리를 사용하는 Composable은
반드시 LocalInspectionMode로 분기 처리하여
Preview 환경에서도 렌더링 오류 없이 UI를 확인할 수 있도록 해야 합니다.
CompositionLocal 사용 시 주의사항Jetpack Compose의 CompositionLocal은 상위에서 제공된 값을
하위 컴포저블이 context처럼 받아서 사용하는 방식입니다.
하지만 Preview 환경에서는 상위 Composition이 존재하지 않기 때문에,
커스텀 CompositionLocal을 사용할 경우 null이거나 기본값(default)으로 대체될 수 있습니다.
보통 Theme에서
ColorScheme나Typography등을CompositionLocal로 주입하는데,
Preview에서는 Theme를 감싸지 않으면 색상이나 폰트 정보가 적용되지 않거나,
예외가 발생할 수 있습니다.
❗ 문제 예시
@Composable
fun ColoredBox() {
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.primary)
)
}
@CstdThemePreviews
@Composable
fun ColoredBoxPreview() {
ColoredBox() // Theme 없이 호출 → colorScheme 값이 없어 오류 발생 가능
}
✅ 해결 방법 1: CompositionLocalProvider로 직접 주입
Preview 전용으로 ColorScheme을 직접 구성하여 LocalColorScheme에 주입합니다:
@CstdThemePreviews
@Composable
fun ColoredBoxPreviewWithManualColors() {
val dummyColors = lightColorScheme(
primary = Color.Red,
secondary = Color.Blue,
background = Color.White,
surface = Color.LightGray,
)
CompositionLocalProvider(
LocalColorScheme provides dummyColors
) {
ColoredBox()
}
}
✅ Preview에서도
MaterialTheme.colorScheme.primary등 컬러 정보 오류 없이 사용 가능
✅ 해결 방법 2: Theme로 감싸기 (권장 방식)
앱에서 사용하는 공통 Theme 컴포저블로 Preview를 감싸면
ColorScheme, Typography, Shapes 등 필요한 CompositionLocal 값이 자동으로 주입됩니다.
@CstdThemePreviews
@Composable
fun ColoredBoxPreviewWithTheme() {
MyAppTheme {
ColoredBox()
}
}
✅ 이 방식은 실제 앱 환경과 동일한 스타일을 유지할 수 있어
Preview 테스트와 디자인 검토 모두에 유리합니다.
Compose Preview는 UI 개발 과정에서 반복 작업을 줄이고,
구조화된 코드 작성과 시각적 검토를 쉽게 만들어줍니다.
프로젝트 유지보수와 협업 효율을 높이기 위해 적극적으로 활용하는 것을 권장합니다.
Preview를 잘 활용하면
더 빠르게, 더 명확하게, 더 안정적으로 UI를 개발할 수 있습니다.