1. XML vs. Jetpack Compose
안드로이드 UI 개발에는 두 가지 주요 접근 방식이 있다. 대략 3년 전까지 주로 쓰였던 XML 방식과 구글이 새로 채택한 Jetpack Compose 방식이다. XML이라고 무조건 안 쓰는 게 좋은 건 아니라서 각각의 특징과 장단점을 이해하는 것이 중요하다.
1.1. Jetpack Compose
Jetpack Compose는 Android 앱 개발을 위한 최신 선언적 UI 툴킷이다. 기존의 명령형 UI 개발 방식에서 벗어나 더 직관적이고 효율적인 개발을 가능하게 한다.
주요 특징:
- 선언적 UI: "어떻게 보여야 하는지"를 선언하면 프레임워크가 렌더링과 UI 변경 관리를 자동으로 처리
- Kotlin 기반: Kotlin의 강력한 기능을 활용하여 간결하고 표현력 있는 코드 작성 가능
- 실시간 미리보기: 코드 변경 사항을 즉시 확인할 수 있는 Preview 기능
- 상태 관리 간소화: 상태 변경에 따른 UI 업데이트가 자동으로 처리됨
@Composable
fun WelcomeScreen() {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Jetpack Compose!",
style = MaterialTheme.typography.headlineMedium
)
Button(onClick = { /* 버튼 클릭 처리 */ }) {
Text("Get Started")
}
}
}
1.2. XML (eXtensible Markup Language)
XML은 전통적으로 Android UI 레이아웃을 디자인하는 데 사용되어온 방식이다. 많은 기존 프로젝트에서 여전히 사용되고 있다.
주요 특징:
- 관심사 분리: UI 디자인과 앱 로직이 명확하게 분리됨
- 정적 레이아웃: XML 레이아웃은 기본적으로 정적이며, 동적 UI 변경을 위해서는 추가적인 Java/Kotlin 코드가 필요
- 복잡한 상태 관리: UI 상태 변경과 사용자 상호작용 관리에 많은 보일러플레이트 코드가 필요
- 성능 오버헤드: 레이아웃 중첩이 깊어질수록 성능에 영향을 미칠 수 있음
1.3. Android 개발에서 UI의 중요성
UI(User Interface)는 사용자가 앱과 상호작용하는 모든 접점을 의미한다. 좋은 UI 설계는 앱의 성공에 직접적인 영향을 미친다.
UI의 구성 요소:
- 시각적 구성 요소: 버튼, 텍스트 필드, 이미지, 슬라이더 등 사용자가 보고 상호작용하는 모든 요소
- 레이아웃 및 구조: 시각적 요소들을 사용자 친화적이고 직관적인 방식으로 배치하는 것
- 상호작용: 사용자 입력에 대한 반응, 화면 전환, 피드백 제공 등 앱의 반응성을 결정하는 요소
2. Jetpack Compose의 Composable 이해 및 생성
Composable은 Jetpack Compose의 핵심 개념으로, UI를 구성하는 기본 단위이다. 함수형 프로그래밍의 개념을 UI 개발에 적용한 혁신적인 접근 방식이라고 할 수 있다.
2.1. Composable이란?
Composable은 @Composable 어노테이션이 붙은 Kotlin 함수로, UI의 일부를 정의한다. 작은 UI 요소부터 전체 화면까지 다양한 범위의 UI를 표현할 수 있다.
특징:
- 함수형 접근: UI를 함수로 정의하여 재사용성과 테스트 용이성 확보
- 조합 가능: 작은 Composable들을 조합하여 복잡한 UI 구성
- 상태 기반: 상태가 변경되면 자동으로 UI가 재구성됨
2.2. Composable 생성 가이드
효과적인 Composable을 만들기 위한 단계별 접근법이다.
1단계: 함수 정의
@Composable
fun MyCustomComponent() {
// UI 로직 작성
}
2단계: 매개변수 활용
@Composable
fun CustomText(
text: String,
fontSize: TextUnit = 18.sp,
color: Color = Color.Black
) {
Text(
text = text,
fontSize = fontSize,
color = color
)
}
3단계: 조합하여 사용
@Composable
fun ProfileCard(name: String, email: String) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp)
) {
Column(
modifier = Modifier.padding(18.dp)
) {
CustomText(
text = name,
fontSize = 21.sp,
color = Color.Blue
)
Spacer(modifier = Modifier.height(10.dp))
CustomText(
text = email,
fontSize = 16.sp,
color = Color.Gray
)
}
}
}
2.3. 실전 예제
@Composable
fun WelcomeMessage(
userName: String,
onGetStartedClick: () -> Unit
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "환영합니다, $userName!",
style = MaterialTheme.typography.headlineLarge,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Jetpack Compose로 앱을 만들어보자",
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center,
color = Color.Gray
)
Spacer(modifier = Modifier.height(32.dp))
Button(
onClick = onGetStartedClick,
modifier = Modifier.fillMaxWidth()
) {
Text("시작하기")
}
}
}
3. Jetpack Compose: Column
Column은 자식 요소들을 세로 방향으로 배열하는 레이아웃 컴포저블이다. 웹 개발의 Flexbox와 유사한 개념으로 이해할 수 있다.
3.1. 기본 사용법
@Composable
fun BasicColumn() {
Column {
Text("첫 번째 아이템")
Text("두 번째 아이템")
Text("세 번째 아이템")
}
}
3.2. Modifier 속성 활용
Modifier는 Composable의 외관과 동작을 커스터마이징하는 강력한 도구이다.
@Composable
fun StyledColumn() {
Column(
modifier = Modifier
.fillMaxSize() // 전체 화면 크기로 확장
.padding(16.dp) // 외부 여백 추가
.background(Color.LightGray) // 배경색 설정
) {
Text("스타일이 적용된 컬럼")
}
}
3.3. 세로 배치 옵션 (Vertical Arrangement)
@Composable
fun ArrangementExamples() {
// 중앙 정렬
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center
) {
Text("중앙 정렬")
}
// 균등 분배
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.SpaceEvenly
) {
Text("첫 번째")
Text("두 번째")
Text("세 번째")
}
// 사이 공간 균등 분배
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.SpaceBetween
) {
Text("맨 위")
Text("중간")
Text("맨 아래")
}
}
3.4. 가로 정렬 옵션 (Horizontal Alignment)
@Composable
fun AlignmentExamples() {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("중앙 정렬")
Button(onClick = { }) { Text("버튼") }
Icon(Icons.Default.Star, contentDescription = null)
}
}
3.5. 실용적인 Column 예제
@Composable
fun UserProfileForm() {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// 프로필 이미지 영역
Box(
modifier = Modifier
.size(120.dp)
.background(Color.Gray, CircleShape),
contentAlignment = Alignment.Center
) {
Icon(
Icons.Default.Person,
contentDescription = "프로필 이미지",
modifier = Modifier.size(60.dp),
tint = Color.White
)
}
// 사용자 정보 입력 필드들
OutlinedTextField(
value = "",
onValueChange = { },
label = { Text("이름") },
modifier = Modifier.fillMaxWidth()
)
OutlinedTextField(
value = "",
onValueChange = { },
label = { Text("이메일") },
modifier = Modifier.fillMaxWidth()
)
OutlinedTextField(
value = "",
onValueChange = { },
label = { Text("전화번호") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(24.dp))
Button(
onClick = { },
modifier = Modifier.fillMaxWidth()
) {
Text("저장")
}
}
}
4. Jetpack Compose: Row
Row는 자식 요소들을 가로 방향으로 배열하는 레이아웃 컴포저블이다. Column과 유사하지만 방향이 다르다.
4.1. 기본 사용법과 응용
@Composable
fun BasicRow() {
Row {
Text("첫 번째")
Text("두 번째")
Text("세 번째")
}
}
@Composable
fun StyledRow() {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Icon(Icons.Default.Star, contentDescription = null)
Text("중요한 알림")
Switch(checked = true, onCheckedChange = { })
}
}
4.2. 가로 배치와 세로 정렬 조합
@Composable
fun ActionBar() {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
// 왼쪽: 뒤로가기 버튼
IconButton(onClick = { }) {
Icon(Icons.Default.ArrowBack, contentDescription = "뒤로가기")
}
// 중앙: 제목
Text(
text = "설정",
style = MaterialTheme.typography.titleLarge
)
// 오른쪽: 메뉴 버튼
IconButton(onClick = { }) {
Icon(Icons.Default.MoreVert, contentDescription = "메뉴")
}
}
}
5. Jetpack Compose: Text Composable
텍스트는 모든 앱에서 가장 기본적이면서 중요한 UI 요소이다. Jetpack Compose는 다양한 텍스트 입력 컴포저블을 제공한다.
5.1. TextField
기본적인 텍스트 입력 필드로, Material Design 스타일을 따른다.
@Composable
fun BasicTextFieldExample() {
var name by remember { mutableStateOf("") }
TextField(
value = name,
onValueChange = { name = it },
label = { Text("이름을 입력하세요") },
placeholder = { Text("홍길동") },
leadingIcon = {
Icon(Icons.Default.Person, contentDescription = null)
}
)
}
5.2. OutlinedTextField
외곽선이 있는 텍스트 필드로, 더 명확한 경계를 제공한다.
@Composable
fun OutlinedTextFieldExample() {
var email by remember { mutableStateOf("") }
var isError by remember { mutableStateOf(false) }
OutlinedTextField(
value = email,
onValueChange = {
email = it
isError = !android.util.Patterns.EMAIL_ADDRESS.matcher(it).matches()
},
label = { Text("이메일") },
supportingText = {
if (isError) {
Text(
text = "유효한 이메일 주소를 입력하세요",
color = MaterialTheme.colorScheme.error
)
}
},
isError = isError,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Email
)
)
}
5.3. BasicTextField
최소한의 스타일링만 적용된 기본 텍스트 필드이다.
@Composable
fun BasicTextFieldExample() {
var text by remember { mutableStateOf("") }
BasicTextField(
value = text,
onValueChange = { text = it },
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.background(
Color.LightGray,
RoundedCornerShape(8.dp)
)
.padding(12.dp)
)
}
5.4. 상태 관리와 onValueChange
텍스트 필드의 핵심은 상태 관리다. remember와 mutableStateOf를 사용하여 상태를 관리한다.
@Composable
fun LoginForm() {
var username by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var passwordVisible by remember { mutableStateOf(false) }
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
OutlinedTextField(
value = username,
onValueChange = { username = it },
label = { Text("사용자명") },
modifier = Modifier.fillMaxWidth()
)
OutlinedTextField(
value = password,
onValueChange = { password = it },
label = { Text("비밀번호") },
visualTransformation = if (passwordVisible) {
VisualTransformation.None
} else {
PasswordVisualTransformation()
},
trailingIcon = {
IconButton(onClick = { passwordVisible = !passwordVisible }) {
Icon(
if (passwordVisible) Icons.Default.VisibilityOff else Icons.Default.Visibility,
contentDescription = if (passwordVisible) "비밀번호 숨기기" else "비밀번호 보이기"
)
}
},
modifier = Modifier.fillMaxWidth()
)
Button(
onClick = { /* 로그인 처리 */ },
modifier = Modifier.fillMaxWidth(),
enabled = username.isNotBlank() && password.isNotBlank()
) {
Text("로그인")
}
}
}
6. Jetpack Compose: Preview Composable
Preview는 개발 중에 UI를 빠르게 확인할 수 있는 강력한 도구이다. 앱을 실행하지 않고도 Composable의 모습을 미리 볼 수 있다.
6.1. 기본 Preview 사용법
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
MyTheme {
WelcomeMessage(
userName = "김개발자",
onGetStartedClick = { }
)
}
}
6.2. 다양한 Preview 옵션
@Preview(
name = "Light Mode",
showBackground = true,
backgroundColor = 0xFFFFFFFF
)
@Preview(
name = "Dark Mode",
showBackground = true,
backgroundColor = 0xFF000000,
uiMode = Configuration.UI_MODE_NIGHT_YES
)
@Preview(
name = "Tablet",
showBackground = true,
device = Devices.TABLET
)
@Composable
fun MultiPreview() {
MyTheme {
UserProfileForm()
}
}
6.3. 매개변수가 있는 Preview
@Preview(showBackground = true)
@Composable
fun PreviewWithParameters() {
MyTheme {
ProfileCard(
name = "홍길동",
email = "hong@example.com"
)
}
}
7. Jetpack Compose: Button Composable
Button은 사용자 상호작용의 핵심 요소로, 다양한 스타일과 기능을 제공한다.
7.1. 기본 Button 사용법
@Composable
fun BasicButtons() {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
// 기본 버튼
Button(onClick = { }) {
Text("기본 버튼")
}
// 외곽선 버튼
OutlinedButton(onClick = { }) {
Text("외곽선 버튼")
}
// 텍스트 버튼
TextButton(onClick = { }) {
Text("텍스트 버튼")
}
// 아이콘 버튼
IconButton(onClick = { }) {
Icon(Icons.Default.Favorite, contentDescription = "좋아요")
}
}
}
7.2. 버튼 커스터마이징
@Composable
fun CustomButton() {
Button(
onClick = { },
modifier = Modifier
.fillMaxWidth()
.height(48.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.Blue,
contentColor = Color.White
),
shape = RoundedCornerShape(12.dp),
elevation = ButtonDefaults.buttonElevation(
defaultElevation = 8.dp,
pressedElevation = 12.dp
)
) {
Icon(
Icons.Default.Download,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text("다운로드")
}
}
8. onClick과 상호작용 처리
onClick은 사용자의 클릭 이벤트를 처리하는 핵심 메커니즘이다.
8.1. 기본 onClick 처리
@Composable
fun InteractiveExample() {
var clickCount by remember { mutableStateOf(0) }
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("버튼이 $clickCount 번 클릭되었습니다")
Button(onClick = {
clickCount++
}) {
Text("클릭하세요")
}
if (clickCount > 0) {
Button(onClick = {
clickCount = 0
}) {
Text("리셋")
}
}
}
}
9. Context 활용
Context는 안드로이드 시스템 서비스와 리소스에 접근하기 위한 인터페이스이다.
9.1. LocalContext 사용법
@Composable
fun ContextExample() {
val context = LocalContext.current
Button(onClick = {
Toast.makeText(context, "안녕하세요!", Toast.LENGTH_SHORT).show()
}) {
Text("토스트 보이기")
}
}
9.2. 실용적인 Context 활용
@Composable
fun ShareButton(content: String) {
val context = LocalContext.current
Button(onClick = {
val shareIntent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, content)
type = "text/plain"
}
context.startActivity(Intent.createChooser(shareIntent, "공유하기"))
}) {
Icon(Icons.Default.Share, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
Text("공유")
}
}
10. Toast 메시지
Toast는 사용자에게 간단한 피드백을 제공하는 효과적인 방법이다.
10.1. Toast 기본 사용법
@Composable
fun ToastExamples() {
val context = LocalContext.current
Column(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Button(onClick = {
Toast.makeText(context, "짧은 토스트", Toast.LENGTH_SHORT).show()
}) {
Text("짧은 토스트")
}
Button(onClick = {
Toast.makeText(context, "긴 토스트 메시지입니다", Toast.LENGTH_LONG).show()
}) {
Text("긴 토스트")
}
}
}
10.2. 커스텀 토스트 대안
@Composable
fun SnackbarExample() {
val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
Box(modifier = Modifier.fillMaxSize()) {
Button(
onClick = {
scope.launch {
snackbarHostState.showSnackbar(
message = "작업이 완료되었습니다",
actionLabel = "실행 취소"
)
}
}
) {
Text("스낵바 보이기")
}
SnackbarHost(
hostState = snackbarHostState,
modifier = Modifier.align(Alignment.BottomCenter)
)
}
}
11. Jetpack Compose: Box Composable
Box는 요소들을 겹쳐서 배치할 수 있는 컨테이너로, CSS의 position: relative와 유사한 개념이다.
11.1. 기본 Box 사용법
@Composable
fun BasicBox() {
Box(
modifier = Modifier.size(200.dp),
contentAlignment = Alignment.Center
) {
Text(
"배경",
modifier = Modifier
.fillMaxSize()
.background(Color.LightGray)
.wrapContentSize(Alignment.Center)
)
Text(
"전경",
color = Color.Blue,
fontSize = 20.sp
)
}
}
11.2. 복잡한 Box 레이아웃
@Composable
fun OverlayCard() {
Box(
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
) {
// 배경 이미지
Image(
painter = painterResource(id = R.drawable.background),
contentDescription = null,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
// 반투명 오버레이
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.4f))
)
// 텍스트 오버레이
Column(
modifier = Modifier
.align(Alignment.BottomStart)
.padding(16.dp)
) {
Text(
text = "카드 제목",
color = Color.White,
style = MaterialTheme.typography.headlineSmall
)
Text(
text = "카드 설명",
color = Color.White.copy(alpha = 0.8f),
style = MaterialTheme.typography.bodyMedium
)
}
// 플로팅 액션 버튼
FloatingActionButton(
onClick = { },
modifier = Modifier.align(Alignment.BottomEnd)
) {
Icon(Icons.Default.Add, contentDescription = "추가")
}
}
}
12. Jetpack Compose: Icon Composable
Icon은 시각적 의사소통을 위한 중요한 UI 요소이다.
12.1. 기본 Icon 사용법
@Composable
fun BasicIcons() {
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Icon(
Icons.Default.Star,
contentDescription = "별표",
tint = Color.Yellow
)
Icon(
Icons.Default.Favorite,
contentDescription = "좋아요",
tint = Color.Red,
modifier = Modifier.size(32.dp)
)
Icon(
Icons.Outlined.Info,
contentDescription = "정보",
tint = MaterialTheme.colorScheme.primary
)
}
}
12.2. 커스텀 아이콘과 상태 관리
@Composable
fun InteractiveIcon() {
var isFavorite by remember { mutableStateOf(false) }
IconButton(onClick = { isFavorite = !isFavorite }) {
Icon(
imageVector = if (isFavorite) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder,
contentDescription = if (isFavorite) "좋아요 취소" else "좋아요",
tint = if (isFavorite) Color.Red else Color.Gray
)
}
}
13. Dropdown Menu
Dropdown Menu는 공간을 절약하면서 여러 옵션을 제공하는 효율적인 UI 패턴이다.
13.1. 기본 Dropdown Menu
@Composable
fun BasicDropdownMenu() {
var expanded by remember { mutableStateOf(false) }
var selectedOption by remember { mutableStateOf("옵션 선택") }
val options = listOf("옵션 1", "옵션 2", "옵션 3", "옵션 4")
Box {
OutlinedButton(
onClick = { expanded = true },
modifier = Modifier.fillMaxWidth()
) {
Text(selectedOption)
Spacer(modifier = Modifier.width(8.dp))
Icon(
Icons.Default.ArrowDropDown,
contentDescription = null,
modifier = Modifier.rotate(if (expanded) 180f else 0f)
)
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
options.forEach { option ->
DropdownMenuItem(
text = { Text(option) },
onClick = {
selectedOption = option
expanded = false
}
)
}
}
}
}
13.2. Dropdown Menu 예제
@Composable
fun LanguageSelector() {
var expanded by remember { mutableStateOf(false) }
var selectedLanguage by remember { mutableStateOf("한국어") }
val languages = mapOf(
"한국어" to "🇰🇷",
"English" to "🇺🇸",
"日本語" to "🇯🇵",
"中文" to "🇨🇳"
)
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Box(
modifier = Modifier.padding(16.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { expanded = true },
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = languages[selectedLanguage] ?: "",
fontSize = 20.sp
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = selectedLanguage,
style = MaterialTheme.typography.bodyLarge
)
}
Icon(
Icons.Default.ArrowDropDown,
contentDescription = "언어 선택",
modifier = Modifier.rotate(if (expanded) 180f else 0f)
)
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
languages.forEach { (language, flag) ->
DropdownMenuItem(
text = {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(text = flag, fontSize = 18.sp)
Spacer(modifier = Modifier.width(8.dp))
Text(text = language)
}
},
onClick = {
selectedLanguage = language
expanded = false
}
)
}
}
}
}
}
14. Parent Containers (레이아웃 컨테이너)
Parent Container는 다른 UI 요소들을 포함하고 배치를 결정하는 컨테이너들이다. 효과적인 레이아웃 설계를 위해서는 각 컨테이너의 특성을 이해하는 것이 중요하다.
14.1. 주요 컨테이너 비교
@Composable
fun ContainerComparison() {
Column(
modifier = Modifier.fillMaxSize()
) {
// Column: 세로 배치
Text("Column (세로 배치)", style = MaterialTheme.typography.headlineSmall)
Column(
modifier = Modifier
.fillMaxWidth()
.background(Color.LightBlue)
.padding(8.dp)
) {
Text("첫 번째")
Text("두 번째")
Text("세 번째")
}
Spacer(modifier = Modifier.height(16.dp))
// Row: 가로 배치
Text("Row (가로 배치)", style = MaterialTheme.typography.headlineSmall)
Row(
modifier = Modifier
.fillMaxWidth()
.background(Color.LightGreen)
.padding(8.dp)
) {
Text("첫 번째")
Spacer(modifier = Modifier.width(8.dp))
Text("두 번째")
Spacer(modifier = Modifier.width(8.dp))
Text("세 번째")
}
Spacer(modifier = Modifier.height(16.dp))
// Box: 겹친 배치
Text("Box (겹친 배치)", style = MaterialTheme.typography.headlineSmall)
Box(
modifier = Modifier
.fillMaxWidth()
.height(100.dp)
.background(Color.LightCyan)
) {
Text("배경", modifier = Modifier.align(Alignment.Center))
Text("왼쪽 위", modifier = Modifier.align(Alignment.TopStart))
Text("오른쪽 아래", modifier = Modifier.align(Alignment.BottomEnd))
}
}
}
14.2. 중첩된 레이아웃 예제
@Composable
fun NestedLayoutExample() {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Column(
modifier = Modifier.padding(16.dp)
) {
// 헤더 영역 (Row)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "알림 카드",
style = MaterialTheme.typography.headlineSmall
)
IconButton(onClick = { }) {
Icon(Icons.Default.Close, contentDescription = "닫기")
}
}
Spacer(modifier = Modifier.height(8.dp))
// 컨텐츠 영역 (Box)
Box(
modifier = Modifier
.fillMaxWidth()
.height(120.dp)
.background(
Color.Blue.copy(alpha = 0.1f),
RoundedCornerShape(8.dp)
)
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.Center
) {
Text(
text = "중요한 알림",
style = MaterialTheme.typography.titleMedium,
color = Color.Blue
)
Text(
text = "새로운 업데이트가 있습니다.",
style = MaterialTheme.typography.bodyMedium
)
}
// 배지
Box(
modifier = Modifier
.align(Alignment.TopEnd)
.size(24.dp)
.background(Color.Red, CircleShape),
contentAlignment = Alignment.Center
) {
Text(
text = "!",
color = Color.White,
fontWeight = FontWeight.Bold
)
}
}
Spacer(modifier = Modifier.height(16.dp))
// 액션 영역 (Row)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
TextButton(onClick = { }) {
Text("나중에")
}
Spacer(modifier = Modifier.width(8.dp))
Button(onClick = { }) {
Text("확인")
}
}
}
}
}
15. Space vs. Padding (공간 관리)
UI에서 적절한 공간 관리는 사용성과 미적 감각에 큰 영향을 미친다. Padding과 Spacer의 차이점과 활용법을 이해해보자.
15.1. Padding과 Spacer의 차이점
Padding: 요소 내부의 여백으로, 요소의 크기에 포함된다. Spacer: 요소들 사이의 독립적인 공간으로, 별도의 컴포저블이다.
@Composable
fun SpaceVsPaddingDemo() {
Column(
modifier = Modifier.fillMaxSize().padding(16.dp)
) {
Text("Padding 예제", style = MaterialTheme.typography.headlineSmall)
// Padding 사용
Text(
text = "이 텍스트는 패딩이 적용되었습니다",
modifier = Modifier
.background(Color.LightBlue)
.padding(16.dp) // 내부 여백
.fillMaxWidth()
)
Spacer(modifier = Modifier.height(24.dp))
Text("Spacer 예제", style = MaterialTheme.typography.headlineSmall)
// Spacer 사용
Text(
text = "첫 번째 텍스트",
modifier = Modifier.background(Color.LightGreen)
)
Spacer(modifier = Modifier.height(16.dp)) // 요소 사이 공간
Text(
text = "두 번째 텍스트",
modifier = Modifier.background(Color.LightCyan)
)
}
}
15.2. 다양한 Padding 적용법
@Composable
fun PaddingExamples() {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// 전체 패딩
Text(
text = "전체 패딩 16dp",
modifier = Modifier
.background(Color.Red.copy(alpha = 0.2f))
.padding(16.dp)
)
// 수평/수직 패딩
Text(
text = "수평 24dp, 수직 8dp",
modifier = Modifier
.background(Color.Green.copy(alpha = 0.2f))
.padding(horizontal = 24.dp, vertical = 8.dp)
)
// 개별 방향 패딩
Text(
text = "개별 방향 패딩",
modifier = Modifier
.background(Color.Blue.copy(alpha = 0.2f))
.padding(
start = 32.dp,
top = 8.dp,
end = 16.dp,
bottom = 24.dp
)
)
}
}
15.3. Spacer의 다양한 활용
@Composable
fun SpacerExamples() {
Column(
modifier = Modifier.fillMaxSize().padding(16.dp)
) {
Text("고정 크기 Spacer")
Text("첫 번째 줄")
Spacer(modifier = Modifier.height(20.dp))
Text("두 번째 줄")
Spacer(modifier = Modifier.height(32.dp))
Text("동적 크기 Spacer")
Row(
modifier = Modifier.fillMaxWidth()
) {
Text("왼쪽")
Spacer(modifier = Modifier.weight(1f)) // 남은 공간 모두 차지
Text("오른쪽")
}
Spacer(modifier = Modifier.height(32.dp))
Text("비율로 공간 나누기")
Row(
modifier = Modifier.fillMaxWidth()
) {
Text("1/4", modifier = Modifier.weight(1f).background(Color.Red.copy(alpha = 0.3f)))
Spacer(modifier = Modifier.width(8.dp))
Text("3/4", modifier = Modifier.weight(3f).background(Color.Blue.copy(alpha = 0.3f)))
}
}
}
15.4. 실용적인 공간 관리 예제
@Composable
fun ResponsiveCard() {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp), // 카드 외부 여백
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
) {
Column(
modifier = Modifier.padding(20.dp) // 카드 내부 여백
) {
// 제목과 부제목
Text(
text = "카드 제목",
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(4.dp)) // 제목과 부제목 사이 작은 여백
Text(
text = "카드 부제목",
style = MaterialTheme.typography.bodyMedium,
color = Color.Gray
)
Spacer(modifier = Modifier.height(16.dp)) // 제목 영역과 내용 사이 여백
// 내용
Text(
text = "이것은 카드의 주요 내용입니다. 적절한 여백과 간격으로 가독성을 높였습니다.",
style = MaterialTheme.typography.bodyLarge,
lineHeight = 24.sp
)
Spacer(modifier = Modifier.height(24.dp)) // 내용과 버튼 사이 큰 여백
// 액션 버튼들
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp) // 버튼들 사이 일정한 간격
) {
OutlinedButton(
onClick = { },
modifier = Modifier.weight(1f)
) {
Text("취소")
}
Button(
onClick = { },
modifier = Modifier.weight(1f)
) {
Text("확인")
}
}
}
}
}
핵심 개념
- 선언적 UI 개발: XML 대신 Kotlin 함수로 UI를 구성하는 새로운 접근 방식
- Composable 함수: 재사용 가능하고 조합 가능한 UI 컴포넌트 작성법
- 레이아웃 컨테이너: Column, Row, Box를 활용한 다양한 배치 방법
- 상태 관리: remember와 mutableStateOf를 통한 UI 상태 관리
- 사용자 상호작용: 버튼 클릭, 텍스트 입력 등의 이벤트 처리
- 공간 관리: Padding과 Spacer를 활용한 효과적인 레이아웃 설계
실무 적용 팁
- 컴포넌트 분리: 복잡한 UI는 작은 Composable들로 나누어 관리한다.
- Preview 활용: 개발 중 @Preview를 적극 활용하여 빠른 피드백을 받는다.
- 상태 끌어올리기: 상태를 적절한 레벨에서 관리하여 재사용성을 높인다.
- 접근성 고려: contentDescription을 제공하여 모든 사용자가 앱을 사용할 수 있도록 한다.
'어플리케이션, 앱 (Application) > 안드로이드 (Android)' 카테고리의 다른 글
안드로이드 jetpack compose 공부 정리 7강 (UI 기능 정리) (0) | 2025.06.28 |
---|---|
안드로이드 jetpack compose 공부 정리 6강 (상태(State) 이해) (0) | 2025.06.28 |
안드로이드 jetpack compose 공부 정리 4강 (리스트(List)와 객체(Object)) (0) | 2025.06.27 |
안드로이드 jetpack compose 공부 정리 3강 (코틀린, 함수와 클래스) (0) | 2025.05.29 |
애드몹 GDPR 메시지 적용 준비 (0) | 2025.03.27 |