Ver2.0 안드로이드 Jetpack Compose로 모던한 UI 개발하기: 초보자도 쉽게 따라할 수 있는 완벽 가이드 🚀

안드로이드 Jetpack Compose로 모던한 UI 개발하기: 초보자도 쉽게 따라할 수 있는 완벽 가이드 🚀
안녕? 오늘은 안드로이드 앱 개발의 판도를 완전히 바꿔놓은 Jetpack Compose에 대해 함께 알아볼 거야! 🎮 기존의 XML 레이아웃을 사용한 UI 개발에 지쳤다면, 이제 Kotlin으로 선언적 UI를 구현하는 새로운 세계로 함께 떠나보자고! 👨💻
이 글에서는 Compose의 기본 개념부터 실제 앱에 적용하는 방법까지 초보자도 이해하기 쉽게 설명할 거야. 특히 요즘 재능넷 같은 플랫폼에서도 Jetpack Compose 개발 능력을 가진 개발자들의 수요가 늘고 있어서, 이 기술을 익히면 너의 개발 커리어에도 큰 도움이 될 거야! 🌟
1. Jetpack Compose란 무엇인가? 🤔
Jetpack Compose는 구글이 안드로이드 앱 개발자들을 위해 만든 현대적인 UI 툴킷이야. 기존의 View 시스템을 대체하기 위해 만들어졌고, 코틀린 프로그래밍 언어를 기반으로 하고 있지. 2021년에 정식 버전이 출시된 이후로 많은 개발자들이 이 기술을 채택하고 있어!
1.1 Compose의 핵심 특징 ✨
- 선언적 UI: XML 대신 코틀린 코드로 UI를 직접 선언해. 이게 무슨 말이냐면, "이런 버튼을 만들고, 여기에 이런 텍스트를 넣고..."처럼 UI가 어떻게 보여야 하는지를 직접 코드로 표현한다는 거야.
- 반응형 프로그래밍: 데이터가 변경되면 UI가 자동으로 업데이트돼. 더 이상 findViewById()나 데이터 바인딩을 사용할 필요가 없어!
- 컴포저블 함수: UI 요소들을 재사용 가능한 함수로 만들 수 있어. 이렇게 하면 코드 중복을 줄이고 유지보수가 쉬워지지.
- 미리보기 기능: 앱을 실행하지 않고도 UI가 어떻게 보일지 Android Studio에서 바로 확인할 수 있어.
- 애니메이션 API: 복잡한 애니메이션도 간단한 코드로 구현할 수 있어.
1.2 왜 Jetpack Compose를 배워야 할까? 🧐
XML 레이아웃으로 UI를 만드는 것은 꽤 번거로운 작업이었어. 특히 복잡한 화면을 구현할 때는 여러 XML 파일을 관리해야 했고, 동적인 UI 변경도 쉽지 않았지. Compose는 이런 문제들을 해결하고, 더 직관적이고 효율적인 UI 개발 경험을 제공해.
또한, 재능넷 같은 플랫폼에서 모바일 앱 개발 프로젝트를 수주할 때 Jetpack Compose 경험이 있으면 큰 경쟁력이 될 수 있어. 최신 기술을 습득하는 것은 항상 좋은 투자니까! 💼
2. Jetpack Compose 시작하기 🚀
2.1 개발 환경 설정하기 🛠️
Compose를 사용하기 위해서는 다음과 같은 환경이 필요해:
- Android Studio Arctic Fox(2020.3.1) 이상
- Kotlin 1.5.0 이상
- Java 11 이상
- 최소 API 레벨 21 (Android 5.0) 이상의 타겟 디바이스
프로젝트의 build.gradle 파일에 다음과 같은 의존성을 추가해야 해:
dependencies {
implementation 'androidx.compose.ui:ui:1.5.0'
implementation 'androidx.compose.material:material:1.5.0'
implementation 'androidx.compose.ui:ui-tooling-preview:1.5.0'
debugImplementation 'androidx.compose.ui:ui-tooling:1.5.0'
}
그리고 Compose를 활성화하기 위해 app 수준의 build.gradle 파일에 다음 설정을 추가해:
android {
...
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion '1.5.0'
}
...
}
2.2 첫 번째 Composable 함수 만들기 🎨
Compose에서는 UI 요소를 Composable 함수로 만들어. 이 함수들은 @Composable 어노테이션을 사용해 정의하고, UI 계층 구조를 구성하는 데 사용돼. 간단한 텍스트를 표시하는 함수를 만들어 볼게:
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
@Composable
fun Greeting(name: String) {
Text(text = "안녕, $name!")
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
Greeting("Jetpack Compose")
}
이 코드에서 @Composable 어노테이션은 이 함수가 UI를 생성하는 함수임을 컴파일러에게 알려주고, @Preview 어노테이션은 Android Studio에서 이 UI를 미리 볼 수 있게 해줘. 정말 간단하지? 😊
3. Compose의 기본 구성 요소 📦
3.1 기본 UI 요소들 🧩
Jetpack Compose는 다양한 기본 UI 컴포넌트를 제공해. 가장 자주 사용하는 것들을 살펴볼게:
- Text: 텍스트를 표시하는 컴포넌트
- Button: 클릭 가능한 버튼 컴포넌트
- Image: 이미지를 표시하는 컴포넌트
- TextField: 사용자 입력을 받는 텍스트 필드
- Box: 자식 요소들을 겹쳐서 배치할 수 있는 레이아웃
- Row: 자식 요소들을 가로로 배치하는 레이아웃
- Column: 자식 요소들을 세로로 배치하는 레이아웃
간단한 예제로 이 컴포넌트들을 사용해볼게:
@Composable
fun SimpleUI() {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
) {
Text(
text = "Jetpack Compose 예제",
fontSize = 24.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(16.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
Image(
painter = painterResource(id = R.drawable.compose_logo),
contentDescription = "Compose 로고",
modifier = Modifier.size(40.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(text = "Compose로 UI 만들기", fontSize = 18.sp)
}
Spacer(modifier = Modifier.height(16.dp))
var text by remember { mutableStateOf("") }
TextField(
value = text,
onValueChange = { text = it },
label = { Text("여기에 입력하세요") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = { /* 버튼 클릭 시 동작 */ },
modifier = Modifier.align(Alignment.End)
) {
Text("확인")
}
}
}
3.2 상태 관리하기 (State) 🔄
Compose에서 상태(State)는 UI의 현재 모습을 결정하는 중요한 개념이야. 상태가 변경되면 Compose는 자동으로 UI를 다시 그려주지. 상태를 관리하는 방법은 다음과 같아:
@Composable
fun CounterExample() {
// remember를 사용해 상태 변수 생성
var count by remember { mutableStateOf(0) }
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "현재 카운트: $count",
fontSize = 20.sp
)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { count++ }) {
Text("증가")
}
}
}
위 예제에서 mutableStateOf()는 변경 가능한 상태를 생성하고, remember는 컴포지션이 재구성될 때 상태를 기억하도록 해. 그리고 by 키워드는 Kotlin의 속성 위임을 사용해서 코드를 더 간결하게 만들어주지. 😎
3.3 Modifier로 UI 커스터마이징하기 🎭
Compose에서 Modifier는 UI 요소의 크기, 패딩, 마진, 배경색 등을 설정하는 데 사용돼. 여러 Modifier를 체인 형태로 연결해서 사용할 수 있어:
@Composable
fun ModifierExample() {
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Blue)
.padding(16.dp)
.clip(RoundedCornerShape(8.dp))
.background(Color.Red)
.clickable { /* 클릭 시 동작 */ }
) {
Text(
text = "Hello",
color = Color.White,
modifier = Modifier.align(Alignment.Center)
)
}
}
이 예제에서는 크기가 100dp인 파란색 배경의 Box를 만들고, 16dp의 패딩을 주고, 모서리를 둥글게 만들고, 내부 배경을 빨간색으로 설정하고, 클릭 가능하게 만들었어. 그리고 그 안에 "Hello"라는 텍스트를 중앙에 배치했지. Modifier를 사용하면 이렇게 복잡한 스타일링도 간단하게 할 수 있어! 👍
4. Compose로 레이아웃 구성하기 📐
4.1 기본 레이아웃 컴포넌트 📏
Compose에서는 세 가지 기본 레이아웃 컴포넌트를 제공해:
- Box: 자식 요소들을 겹쳐서 배치 (FrameLayout과 유사)
- Row: 자식 요소들을 가로로 배치 (LinearLayout horizontal과 유사)
- Column: 자식 요소들을 세로로 배치 (LinearLayout vertical과 유사)
이 세 가지 레이아웃을 조합하면 거의 모든 UI 레이아웃을 만들 수 있어. 예를 들어, 간단한 프로필 카드를 만들어볼게:
@Composable
fun ProfileCard() {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
elevation = 4.dp
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
// 프로필 이미지
Box(
modifier = Modifier
.size(60.dp)
.clip(CircleShape)
.background(Color.Gray)
) {
// 실제로는 여기에 Image 컴포넌트가 들어갈 수 있어
Text(
text = "JP",
color = Color.White,
modifier = Modifier.align(Alignment.Center)
)
}
// 프로필 정보
Column(
modifier = Modifier
.padding(start = 16.dp)
.weight(1f)
) {
Text(
text = "Jetpack Compose",
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
Text(
text = "안드로이드 개발자",
fontSize = 14.sp,
color = Color.Gray
)
}
// 팔로우 버튼
Button(
onClick = { /* 버튼 클릭 시 동작 */ },
modifier = Modifier.wrapContentSize()
) {
Text("팔로우")
}
}
}
}
4.2 LazyColumn과 LazyRow로 리스트 만들기 📜
기존 안드로이드의 RecyclerView와 같은 역할을 하는 LazyColumn과 LazyRow를 사용하면 효율적인 리스트를 쉽게 만들 수 있어:
@Composable
fun SimpleList() {
val items = List(100) { "Item #$it" }
LazyColumn {
items(items) { item ->
Text(
text = item,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
)
Divider()
}
}
}
LazyColumn은 화면에 보이는 항목만 렌더링하기 때문에 메모리 효율이 좋아. 또한, 리스트 항목의 추가, 제거, 업데이트도 간단하게 처리할 수 있지. 😌
4.3 ConstraintLayout 사용하기 🧩
더 복잡한 레이아웃을 위해 Compose에서도 ConstraintLayout을 사용할 수 있어. 먼저 의존성을 추가해야 해:
implementation "androidx.constraintlayout:constraintlayout-compose:1.0.1"
그리고 다음과 같이 사용할 수 있어:
@Composable
fun ConstraintLayoutExample() {
ConstraintLayout(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
// 참조 생성
val (title, image, description, button) = createRefs()
Text(
text = "제목",
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.constrainAs(title) {
top.linkTo(parent.top)
start.linkTo(parent.start)
}
)
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Gray)
.constrainAs(image) {
top.linkTo(title.bottom, margin = 16.dp)
start.linkTo(parent.start)
}
)
Text(
text = "이것은 설명 텍스트입니다. 여기에 자세한 내용이 들어갑니다.",
modifier = Modifier
.constrainAs(description) {
top.linkTo(image.top)
start.linkTo(image.end, margin = 16.dp)
end.linkTo(parent.end)
width = Dimension.fillToConstraints
}
)
Button(
onClick = { /* 버튼 클릭 시 동작 */ },
modifier = Modifier.constrainAs(button) {
top.linkTo(image.bottom, margin = 16.dp)
end.linkTo(parent.end)
}
) {
Text("확인")
}
}
}
ConstraintLayout을 사용하면 UI 요소들 간의 관계를 정의해서 복잡한 레이아웃도 쉽게 만들 수 있어. 특히 반응형 UI를 구현할 때 유용하지! 🧠
5. Compose에서 테마 적용하기 🎨
5.1 Material Design 테마 🎭
Compose는 Material Design을 기본적으로 지원해. MaterialTheme 컴포저블을 사용해서 앱 전체의 색상, 타이포그래피, 모양 등을 일관되게 적용할 수 있어:
@Composable
fun MyApp() {
MaterialTheme(
colors = lightColors(
primary = Purple500,
primaryVariant = Purple700,
secondary = Teal200
),
typography = Typography(
body1 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp
)
// 다른 텍스트 스타일도 여기서 정의할 수 있어
),
shapes = Shapes(
small = RoundedCornerShape(4.dp),
medium = RoundedCornerShape(8.dp),
large = RoundedCornerShape(12.dp)
)
) {
// 앱 콘텐츠
Surface(color = MaterialTheme.colors.background) {
Greeting("Android")
}
}
}
5.2 다크 테마 지원하기 🌙
Compose에서는 다크 테마를 쉽게 지원할 수 있어. isSystemInDarkTheme() 함수를 사용해서 시스템의 다크 모드 설정을 확인하고, 그에 맞는 테마를 적용할 수 있지:
@Composable
fun MyApp() {
val isDarkTheme = isSystemInDarkTheme()
MaterialTheme(
colors = if (isDarkTheme) {
darkColors(
primary = Purple200,
primaryVariant = Purple700,
secondary = Teal200
)
} else {
lightColors(
primary = Purple500,
primaryVariant = Purple700,
secondary = Teal200
)
}
) {
// 앱 콘텐츠
Surface(color = MaterialTheme.colors.background) {
Greeting("Android")
}
}
}
5.3 커스텀 테마 색상 사용하기 🌈
MaterialTheme에서 정의한 색상을 앱 전체에서 일관되게 사용하면 디자인의 일관성을 유지할 수 있어:
@Composable
fun ThemedButton() {
Button(
onClick = { /* 버튼 클릭 시 동작 */ },
colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.primary,
contentColor = MaterialTheme.colors.onPrimary
)
) {
Text("테마 색상을 사용한 버튼")
}
}
이렇게 하면 다크 테마로 전환해도 자동으로 적절한 색상이 적용돼. 또한, MaterialTheme.colors, MaterialTheme.typography, MaterialTheme.shapes 등을 통해 테마에서 정의한 스타일을 쉽게 가져와 사용할 수 있어! 🎯
6. Compose에서 애니메이션 구현하기 ✨
6.1 기본 애니메이션 🎬
Compose에서는 animateXxxAsState 함수들을 사용해 간단한 애니메이션을 쉽게 구현할 수 있어:
@Composable
fun AnimatedButton() {
var expanded by remember { mutableStateOf(false) }
// 크기 애니메이션
val size by animateDpAsState(
targetValue = if (expanded) 120.dp else 60.dp,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
)
// 색상 애니메이션
val color by animateColorAsState(
targetValue = if (expanded) Color.Red else Color.Blue,
animationSpec = tween(durationMillis = 500)
)
Box(
modifier = Modifier
.size(size)
.background(color, shape = CircleShape)
.clickable { expanded = !expanded },
contentAlignment = Alignment.Center
) {
Text(
text = if (expanded) "축소" else "확장",
color = Color.White
)
}
}
이 예제에서는 버튼을 클릭할 때마다 크기와 색상이 애니메이션되는 효과를 구현했어. animateDpAsState와 animateColorAsState를 사용해서 값의 변화를 부드럽게 애니메이션할 수 있지! 🎭
6.2 전환 애니메이션 (Transition) 🔄
여러 속성을 동시에 애니메이션하려면 Transition을 사용할 수 있어:
@Composable
fun TransitionExample() {
var state by remember { mutableStateOf(false) }
val transition = updateTransition(targetState = state, label = "transition")
val size by transition.animateDp(label = "size") { targetState ->
if (targetState) 100.dp else 50.dp
}
val color by transition.animateColor(label = "color") { targetState ->
if (targetState) Color.Red else Color.Blue
}
val cornerRadius by transition.animateDp(label = "corner") { targetState ->
if (targetState) 50.dp else 0.dp
}
Box(
modifier = Modifier
.size(size)
.clip(RoundedCornerShape(cornerRadius))
.background(color)
.clickable { state = !state },
contentAlignment = Alignment.Center
) {
Text(
text = if (state) "활성" else "비활성",
color = Color.White
)
}
}
6.3 애니메이션 가시성 (AnimatedVisibility) 👁️
AnimatedVisibility를 사용하면 컴포넌트의 표시/숨김 상태를 애니메이션할 수 있어:
@Composable
fun AnimatedVisibilityExample() {
var visible by remember { mutableStateOf(true) }
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(onClick = { visible = !visible }) {
Text(if (visible) "숨기기" else "보이기")
}
Spacer(modifier = Modifier.height(16.dp))
AnimatedVisibility(
visible = visible,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
Card(
modifier = Modifier
.fillMaxWidth()
.height(200.dp),
elevation = 4.dp
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
Text("애니메이션 카드 콘텐츠")
}
}
}
}
}
이 예제에서는 버튼을 클릭할 때마다 카드가 페이드 인/아웃되면서 수직으로 확장/축소되는 애니메이션을 보여줘. enter와 exit 파라미터를 사용해 진입/퇴장 애니메이션을 지정할 수 있어. 정말 멋지지? 🌟
7. Compose에서 상태 관리 심화 🧠
7.1 ViewModel과 함께 사용하기 📊
실제 앱에서는 ViewModel을 사용해 UI 상태를 관리하는 것이 좋아. Compose와 ViewModel을 함께 사용하는 방법을 알아볼게:
// ViewModel 클래스
class CounterViewModel : ViewModel() {
// UI 상태를 나타내는 StateFlow
private val _count = MutableStateFlow(0)
val count: StateFlow<Int> = _count
// 상태를 변경하는 함수
fun increment() {
_count.value++
}
fun decrement() {
if (_count.value > 0) {
_count.value--
}
}
}
// Compose UI
@Composable
fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
// StateFlow를 State로 변환
val count by viewModel.count.collectAsState()
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "카운트: $count",
fontSize = 24.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(16.dp))
Row {
Button(
onClick = { viewModel.decrement() },
modifier = Modifier.padding(end = 8.dp)
) {
Text("-")
}
Button(onClick = { viewModel.increment() }) {
Text("+")
}
}
}
}
이 예제에서는 StateFlow를 사용해 ViewModel에서 상태를 관리하고, collectAsState() 함수를 사용해 Compose에서 이 상태를 관찰해. 이렇게 하면 UI 로직과 비즈니스 로직을 분리할 수 있어서 코드 유지보수가 쉬워지지! 🧩
7.2 상태 호이스팅 (State Hoisting) 🏗️
상태 호이스팅은 상태를 컴포저블 함수의 매개변수로 전달하고 상태 변경은 콜백으로 처리하는 패턴이야. 이렇게 하면 컴포넌트를 재사용하기 쉬워지고, 테스트하기도 쉬워져:
// 상태를 매개변수로 받는 재사용 가능한 컴포넌트
@Composable
fun Counter(
count: Int,
onIncrement: () -> Unit,
onDecrement: () -> Unit
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "카운트: $count",
fontSize = 24.sp
)
Spacer(modifier = Modifier.height(16.dp))
Row {
Button(
onClick = onDecrement,
modifier = Modifier.padding(end = 8.dp)
) {
Text("-")
}
Button(onClick = onIncrement) {
Text("+")
}
}
}
}
// 상태를 관리하는 상위 컴포넌트
@Composable
fun CounterScreen() {
var count by remember { mutableStateOf(0) }
Counter(
count = count,
onIncrement = { count++ },
onDecrement = { if (count > 0) count-- }
)
}
이런 방식으로 Counter 컴포넌트는 자체적으로 상태를 관리하지 않고, 상위 컴포넌트에서 전달받은 상태와 콜백만 사용해. 이렇게 하면 같은 Counter 컴포넌트를 여러 곳에서 다양한 방식으로 재사용할 수 있어! 💡
7.3 remember와 rememberSaveable 🔄
remember는 컴포지션이 재구성될 때 상태를 유지하지만, 화면 회전 같은 구성 변경이나 프로세스 재생성 시에는 상태가 손실돼. 이런 경우에는 rememberSaveable을 사용하면 좋아:
@Composable
fun SaveableCounter() {
// 화면 회전 등의 구성 변경에도 상태가 유지됨
var count by rememberSaveable { mutableStateOf(0) }
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "카운트: $count",
fontSize = 24.sp
)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { count++ }) {
Text("증가")
}
}
}
rememberSaveable은 내부적으로 Bundle을 사용해 상태를 저장하기 때문에, 기본 타입(Int, String 등)이나 Parcelable 객체만 저장할 수 있어. 복잡한 객체를 저장하려면 커스텀 Saver를 구현해야 할 수도 있지! 🔍
8. 실제 앱 개발에 Compose 적용하기 🚀
8.1 기존 앱에 Compose 통합하기 🔄
기존의 View 기반 앱에 Compose를 점진적으로 도입할 수 있어. ComposeView를 사용하면 XML 레이아웃 내에서 Compose UI를 사용할 수 있지:
// XML 레이아웃에서 ComposeView 사용
<androidx.compose.ui.platform.ComposeView
android:id="@+id/compose_view"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
// Activity나 Fragment에서 ComposeView 설정
findViewById<ComposeView>(R.id.compose_view).setContent {
MaterialTheme {
Surface {
Greeting("Compose in XML")
}
}
}
반대로, Compose UI 내에서 기존 Android View를 사용하려면 AndroidView 컴포저블을 사용할 수 있어:
@Composable
fun WebViewExample() {
AndroidView(
factory = { context ->
WebView(context).apply {
loadUrl("https://www.jaenung.net")
}
},
update = { webView ->
// 필요한 경우 WebView 업데이트
},
modifier = Modifier.fillMaxWidth()
)
}
8.2 Navigation Compose로 화면 전환 구현하기 🧭
Compose에서는 Navigation Compose 라이브러리를 사용해 화면 전환을 구현할 수 있어. 먼저 의존성을 추가해야 해:
implementation "androidx.navigation:navigation-compose:2.7.0"
그리고 다음과 같이 네비게이션을 구현할 수 있어:
@Composable
fun AppNavigation() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "home"
) {
composable("home") {
HomeScreen(
onNavigateToDetail = { itemId ->
navController.navigate("detail/$itemId")
}
)
}
composable(
route = "detail/{itemId}",
arguments = listOf(navArgument("itemId") { type = NavType.IntType })
) { backStackEntry ->
val itemId = backStackEntry.arguments?.getInt("itemId") ?: 0
DetailScreen(
itemId = itemId,
onNavigateBack = {
navController.popBackStack()
}
)
}
}
}
이 예제에서는 NavHost를 사용해 네비게이션 그래프를 정의하고, composable 함수를 사용해 각 화면을 등록했어. 화면 간 이동은 navController.navigate()와 navController.popBackStack() 함수를 사용해 처리할 수 있지! 🧩
8.3 Compose에서 권한 처리하기 🔒
Compose에서도 권한 요청을 처리할 수 있어. rememberLauncherForActivityResult를 사용해 권한 요청 런처를 만들 수 있지:
@Composable
fun PermissionExample() {
val context = LocalContext.current
var hasPermission by remember {
mutableStateOf(
ContextCompat.checkSelfPermission(
context,
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED
)
}
val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission()
) { isGranted ->
hasPermission = isGranted
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = if (hasPermission) "카메라 권한이 있습니다" else "카메라 권한이 필요합니다",
fontSize = 18.sp
)
Spacer(modifier = Modifier.height(16.dp))
if (!hasPermission) {
Button(
onClick = {
launcher.launch(Manifest.permission.CAMERA)
}
) {
Text("권한 요청")
}
} else {
Button(
onClick = {
// 카메라 기능 사용
}
) {
Text("카메라 사용")
}
}
}
}
이 예제에서는 ActivityResultContracts.RequestPermission()를 사용해 카메라 권한을 요청하고, 권한 상태에 따라 다른 UI를 보여주고 있어. Compose에서도 기존 안드로이드의 권한 처리 방식과 유사하게 구현할 수 있지! 🔐
9. Compose 성능 최적화 ⚡
9.1 불필요한 재구성 방지하기 🛑
Compose에서는 상태가 변경될 때마다 UI가 재구성돼. 하지만 모든 컴포넌트가 재구성될 필요는 없지. remember를 사용해 계산 비용이 큰 작업의 결과를 캐싱하고, 상태를 적절히 분리해서 불필요한 재구성을 방지할 수 있어:
@Composable
fun OptimizedList(items: List<Item>) {
LazyColumn {
items(
items = items,
// key를 사용해 아이템 식별
key = { item -> item.id }
) { item ->
// 각 아이템을 별도의 컴포저블로 분리
ItemRow(item)
}
}
}
@Composable
fun ItemRow(item: Item) {
// 이 컴포넌트는 item이 변경될 때만 재구성됨
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(text = item.title)
}
}
9.2 derivedStateOf 사용하기 🔄
다른 상태에 의존하는 계산된 상태가 필요할 때는 derivedStateOf를 사용하면 좋아. 이렇게 하면 원본 상태가 변경될 때만 계산이 다시 이루어져:
@Composable
fun FilteredList(items: List<Item>) {
var searchQuery by remember { mutableStateOf("") }
// 검색어에 따라 필터링된 아이템 목록
val filteredItems by remember(searchQuery, items) {
derivedStateOf {
items.filter { it.title.contains(searchQuery, ignoreCase = true) }
}
}
Column {
TextField(
value = searchQuery,
onValueChange = { searchQuery = it },
label = { Text("검색") },
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
)
LazyColumn {
items(filteredItems) { item ->
Text(
text = item.title,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
)
}
}
}
}
9.3 LaunchedEffect와 DisposableEffect 사용하기 🚀
부수 효과(side effect)를 처리할 때는 LaunchedEffect와 DisposableEffect를 사용하면 좋아:
@Composable
fun TimerExample() {
var seconds by remember { mutableStateOf(0) }
// 컴포지션이 시작될 때 타이머 시작
LaunchedEffect(key1 = Unit) {
while (true) {
delay(1000)
seconds++
}
}
Text(
text = "타이머: $seconds초",
fontSize = 24.sp,
modifier = Modifier.padding(16.dp)
)
}
@Composable
fun NetworkListenerExample() {
val context = LocalContext.current
var isNetworkAvailable by remember { mutableStateOf(false) }
// 네트워크 상태 리스너 등록 및 해제
DisposableEffect(key1 = Unit) {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
isNetworkAvailable = true
}
override fun onLost(network: Network) {
isNetworkAvailable = false
}
}
connectivityManager.registerDefaultNetworkCallback(networkCallback)
// 컴포지션이 종료될 때 리스너 해제
onDispose {
connectivityManager.unregisterNetworkCallback(networkCallback)
}
}
Text(
text = if (isNetworkAvailable) "네트워크 연결됨" else "네트워크 연결 안됨",
color = if (isNetworkAvailable) Color.Green else Color.Red,
fontSize = 18.sp,
modifier = Modifier.padding(16.dp)
)
}
LaunchedEffect는 컴포지션이 시작될 때 코루틴을 실행하고, DisposableEffect는 컴포지션이 종료될 때 정리 작업을 수행해. 이렇게 하면 메모리 누수를 방지하고 리소스를 효율적으로 관리할 수 있어! 🧹
10. Compose UI 테스트하기 🧪
10.1 테스트 의존성 추가하기 📚
Compose UI를 테스트하려면 다음 의존성을 추가해야 해:
// build.gradle
androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.5.0"
debugImplementation "androidx.compose.ui:ui-test-manifest:1.5.0"
10.2 간단한 UI 테스트 작성하기 ✅
Compose UI 테스트는 ComposeTestRule을 사용해 작성할 수 있어:
@RunWith(AndroidJUnit4::class)
class GreetingTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun greetingTest() {
// UI 설정
composeTestRule.setContent {
MaterialTheme {
Greeting("Compose")
}
}
// 텍스트가 화면에 표시되는지 확인
composeTestRule.onNodeWithText("안녕, Compose!").assertIsDisplayed()
}
}
10.3 상호작용 테스트하기 👆
버튼 클릭 같은 사용자 상호작용도 테스트할 수 있어:
@Test
fun counterTest() {
// UI 설정
composeTestRule.setContent {
MaterialTheme {
CounterScreen()
}
}
// 초기 상태 확인
composeTestRule.onNodeWithText("카운트: 0").assertIsDisplayed()
// 버튼 클릭
composeTestRule.onNodeWithText("+").performClick()
// 상태 변경 확인
composeTestRule.onNodeWithText("카운트: 1").assertIsDisplayed()
}
이런 식으로 Compose UI의 동작을 자동화된 테스트로 검증할 수 있어. 테스트를 작성하면 앱의 품질을 높이고 버그를 미리 발견할 수 있지! 🐛
11. 마무리: Jetpack Compose의 미래 🔮
지금까지 Jetpack Compose의 기본부터 실제 앱 개발에 적용하는 방법까지 알아봤어. Compose는 안드로이드 UI 개발의 미래라고 할 수 있어. 구글은 계속해서 Compose에 투자하고 있고, 점점 더 많은 기업들이 Compose를 도입하고 있지.
Compose를 배우면 다음과 같은 이점이 있어:
- 생산성 향상: 더 적은 코드로 더 많은 기능을 구현할 수 있어.
- 유지보수 용이성: 선언적 UI는 이해하기 쉽고 디버깅하기 쉬워.
- 최신 기술 스택: 코틀린 코루틴, Flow 등 최신 기술과 잘 어울려.
- 취업 경쟁력: Compose 경험은 이제 많은 안드로이드 개발자 채용 공고에서 우대사항이 되고 있어.
재능넷 같은 플랫폼에서도 Jetpack Compose를 활용한 모바일 앱 개발 프로젝트가 늘어나고 있어. 이 기술을 익혀두면 더 다양한 프로젝트에 참여할 수 있는 기회가 생길 거야! 🌟
Compose는 계속 발전하고 있어서, 공식 문서와 커뮤니티를 통해 최신 정보를 얻는 것이 중요해. 구글의 공식 Compose 문서와 샘플 프로젝트를 참고하면 더 많은 정보를 얻을 수 있을 거야.
이제 Jetpack Compose로 멋진 안드로이드 앱을 만들어볼 시간이야! 화이팅! 💪
댓글 작성
이 글에 대한 여러분의 생각을 들려주세요
로그인이 필요합니다
댓글을 작성하려면 먼저 로그인해주세요.
댓글 0
지식인의 숲 - 지적 재산권 보호 고지
지적 재산권 보호 고지
- 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
- AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
- 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
- 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
- AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.
아직 댓글이 없습니다.