어플리케이션, 앱 (Application)/안드로이드 (Android)

안드로이드 jetpack compose 공부 정리 5강 (Jetpack Compose 기본 UI )

sobal 2025. 6. 27. 23:21

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("확인")
                }
            }
        }
    }
}

 

핵심 개념

  1. 선언적 UI 개발: XML 대신 Kotlin 함수로 UI를 구성하는 새로운 접근 방식
  2. Composable 함수: 재사용 가능하고 조합 가능한 UI 컴포넌트 작성법
  3. 레이아웃 컨테이너: Column, Row, Box를 활용한 다양한 배치 방법
  4. 상태 관리: remember와 mutableStateOf를 통한 UI 상태 관리
  5. 사용자 상호작용: 버튼 클릭, 텍스트 입력 등의 이벤트 처리
  6. 공간 관리: Padding과 Spacer를 활용한 효과적인 레이아웃 설계

실무 적용 팁

  • 컴포넌트 분리: 복잡한 UI는 작은 Composable들로 나누어 관리한다.
  • Preview 활용: 개발 중 @Preview를 적극 활용하여 빠른 피드백을 받는다.
  • 상태 끌어올리기: 상태를 적절한 레벨에서 관리하여 재사용성을 높인다.
  • 접근성 고려: contentDescription을 제공하여 모든 사용자가 앱을 사용할 수 있도록 한다.