@Composable 어노테이션은 Jetpack Compose의 기본 요소로, 일반 Kotlin 함수를 선언적 사용자 인터페이스 구성 요소로 변환합니다. 이 어노테이션 없이는 현대 Android 개발에서 어떤 화면도 만들 수 없습니다. Google Android Developers, 2026에 따르면, 80% 이상의 새로운 Kotlin 프로젝트가 UI 구축에 Compose를 사용하며, @Composable은 생태계에서 가장 자주 사용되는 어노테이션입니다.
핵심 사항
@Composable은 Kotlin 언어의 어노테이션으로, 함수를 Jetpack Compose 프레임워크에서 사용자 인터페이스를 설명하기 위한 것으로 표시합니다. Kotlin 컴파일러가 이 어노테이션을 만나면 함수가 컴포지션 컨텍스트(UI 트리 관리 시스템)에서 작동할 수 있도록 추가 코드를 생성합니다.
@Composable 어노테이션은 2021년 Jetpack Compose 1.0의 첫 번째 안정 버전과 함께 Google에 의해 도입되었습니다. 등장 이전에는 Android 인터페이스 개발이 XML 마크업과 View 시스템을 통해서만 이루어졌습니다. @Composable은 접근 방식을 근본적으로 변경했습니다. 별도의 마크업 파일에 UI를 설명하는 대신, 개발자가 Kotlin으로 직접 인터페이스를 작성합니다.
@Composable과 일반 Kotlin 함수의 주요 차이점은 상태를 읽고 변경에 반응하는 능력입니다. Composable 함수가 읽는 변수의 값이 변경되면 시스템이 자동으로 해당 함수의 재시작(재구성)을 예약합니다. 이는 개발자가 findViewById 및 setText를 통한 수동 UI 업데이트에서 해방시킵니다.
@Composable의 내부 메커니즘은 슬롯 개념에 기반합니다 — 컴포지션 내 각 함수에 할당되는 특수 메모리 영역입니다. 이 슬롯은 함수에 전달된 값과 후속 호출 시 비교에 필요한 서비스 정보를 저장합니다.
Composable 함수를 선언하려면 fun 키워드 앞에 @Composable 어노테이션을 추가하기만 하면 됩니다. 함수는 androidx.compose.runtime에서 어노테이션을 가져오는 패키지에 있어야 합니다. 함수 이름은 대문자로 시작하는 것이 좋습니다 — 이는 Compose 커뮤니티에서 널리 인정된 관례로, UI 구성 요소를 일반 함수와 시각적으로 구분합니다.
import androidx.compose.runtime.Composable
@Composable
fun Greeting(name: String) {
var count by remember { mutableStateOf(0) }
Column {
Text("안녕하세요, $name!")
Button(onClick = { count++ }) {
Text("$count번 클릭됨")
}
}
}
Composable 함수의 매개변수는 무엇이든 될 수 있습니다 — 기본 타입, 문자열, 람다, 심지어 Slot API를 통해 전달되는 다른 Composable 함수도 가능합니다. 권장되는 것은 재구성 중 부작용을 피하기 위해 매개변수를 불변(val)으로 만드는 것입니다. 모든 가변 데이터는 Compose 상태 메커니즘을 통해 관리되어야 합니다.
Composable 함수는 일반 함수처럼 임의의 값을 반환할 수 없습니다 — 유일한 작업은 UI 트리의 일부를 구축하거나 업데이트하는 것입니다. 그러나 State Hoisting과 같은 특별한 패턴이 존재하며, Compose 함수가 매개변수를 통해 상태와 콜백을 받아 순수하고 재사용 가능한 상태를 유지합니다.
Compose 시스템은 Composable 함수의 모양과 동작에 대해 몇 가지 엄격한 제약을 부과합니다. 첫 번째 규칙: Composable 함수는 다른 Composable 함수나 부작용이 없는 일반 함수만 호출할 수 있습니다. 이는 컴포지션의 예측 가능성과 Compose 최적화의 올바른 작동을 보장합니다.
두 번째 규칙은 실행 순서에 관한 것입니다. Compose는 Composable 함수를 어떤 순서로든 호출할 권리가 있으므로, 이러한 함수의 본문에 있는 코드는 이웃 함수의 호출 시퀀스에 의존해서는 안 됩니다. 각 Composable 함수는 UI 트리 내 위치 수준에서 자체적으로 완결되어야 합니다.
세 번째 규칙 — Composable 함수 본문 내 부작용 금지. 데이터베이스 쓰기, 네트워크 요청 전송 또는 외부 변수 수정과 같은 작업은 LaunchedEffect, DisposableEffect 또는 SideEffect와 같은 특수 효과 내에서만 수행되어야 합니다. 이 규칙을 위반하면 재구성 중 예측할 수 없는 동작이 발생합니다.
네 번째 규칙: Composable 함수는 멱등성을 가져야 합니다. 동일한 인수로 다시 호출하면 동일한 UI가 생성되어야 합니다. 이 요구 사항은 입력 데이터가 변경되지 않은 함수의 다시 그리기를 Compose가 건너뛰는 스킵 최적화의 올바른 작동에 필요합니다.
// 올바름: 부작용이 없는 순수 Composable 함수
@Composable
fun UserCard(user: User, onClick: () -> Unit) {
Card(modifier = Modifier.clickable { onClick() }) {
Text(text = user.name)
}
}
// 틀림: 바디 내 부작용
@Composable
fun WrongCard(userId: String) {
// val result = viewModel.loadUser(userId) // 허용되지 않음
Text("로딩 중...")
}
@Composable 어노테이션을 사용하여 프로필 화면을 만드는 실제 예제를 살펴보겠습니다. 여기서는 여러 Composable 함수의 조합, 상태 및 수정자 작업을 시연합니다 — 모든 Compose 레이아웃의 핵심 요소입니다.
@Composable
fun ProfileScreen(userId: String) {
var isFollowed by remember { mutableStateOf(false) }
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
ProfileHeader(userId = userId)
Spacer(modifier = Modifier.height(16.dp))
StatsRow(posts = 42, followers = 1280)
Spacer(modifier = Modifier.height(24.dp))
FollowButton(
isFollowed = isFollowed,
onToggle = { isFollowed = !isFollowed }
)
}
}
@Composable
fun ProfileHeader(userId: String) {
Row(verticalAlignment = Alignment.CenterVertically) {
AsyncImage(model = "https://example.com/avatars/$userId",
contentDescription = "User avatar")
Spacer(modifier = Modifier.width(12.dp))
Text(text = "사용자 #$userId", style = MaterialTheme.typography.headlineMedium)
}
}
@Composable
fun StatsRow(posts: Int, followers: Int) {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
StatItem("Posts", posts)
StatItem("Followers", followers)
}
}
@Composable
fun StatItem(label: String, value: Int) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = "$value", style = MaterialTheme.typography.headlineSmall)
Text(text = label, style = MaterialTheme.typography.bodySmall)
}
}
예제에서 각 Composable 함수는 화면의 특정 부분을 담당합니다: ProfileScreen은 전체 상태와 자식 함수의 컴포지션을 관리하고, ProfileHeader는 아바타와 이름을 표시하며, StatsRow는 통계 블록을 보여줍니다. 이 접근 방식은 단일 책임 원칙을 따르며 구성 요소 재사용을 단순화합니다.
Jetpack Compose에는 Composable 함수의 세 가지 주요 유형이 있습니다. 첫 번째 유형 — 컨테이너(Row, Column, Box, LazyColumn) — 자식 요소의 레이아웃을 결정합니다. 두 번째 유형 — 표시 요소(Text, Image, Icon, Button) — 특정 UI 구성 요소를 렌더링합니다. 세 번째 유형 — 사용자 정의 Composable 함수 — 내장 구성 요소를 재사용 가능한 블록으로 결합합니다.
컨테이너는 content 람다(@Composable () -> Unit 유형의 마지막 매개변수)를 받아들인다는 점에서 일반 요소와 다릅니다. 이 메커니즘은 중첩된 UI 트리 구축을 가능하게 합니다: 각 컨테이너는 자체 컨텍스트와 메모리 영역을 가진 자식 컴포지션을 생성합니다.
사용자 정의 Composable 함수는 두 가지 하위 유형으로 나뉩니다: 스마트(smart)와 덤(dumb). 스마트 함수는 상태와 로직을 관리하며 remember, LaunchedEffect 및 기타 Compose API 호출을 포함합니다. 덤 함수는 매개변수를 통해 모든 데이터를 받아 표시만 합니다. 스마트 구성 요소와 덤 구성 요소의 분리는 테스트 용이성과 코드 재사용성을 향상시킵니다.
| 유형 | 예시 | 목적 |
|---|---|---|
| 컨테이너 | Column, Row, Box | 자식 요소의 레이아웃 관리 |
| 요소 | Text, Image, Button | 콘텐츠 표시 및 입력 처리 |
| 사용자 정의 | ProfileCard, UserList | 표준 구성 요소의 조합 |
@Composable 어노테이션의 주요 장점은 상속 및 복잡한 클래스 계층 없이 재사용 가능한 UI 구성 요소를 만들 수 있는 능력입니다. 각 사용자 정의 요소에 생성자가 있는 Java 클래스가 필요했던 View 시스템과 달리, Composable 구성 요소는 단순히 매개변수가 있는 Kotlin 함수입니다.
재사용성을 보장하기 위해 Slot API 패턴이 사용되며, Composable 함수는 레이아웃의 다른 영역에 대한 content 람다를 받아들입니다. 예를 들어, Card 구성 요소는 헤더, 본문 및 바닥글에 대해 별도의 콘텐츠를 받아들일 수 있어 애플리케이션의 모든 화면에 보편적으로 사용할 수 있습니다.
수정자(Modifier)는 재사용성에서 핵심적인 역할을 합니다: 구성 요소 자체를 변경하지 않고 패딩, 크기, 클릭 및 애니메이션을 구성할 수 있습니다. 기본값(Modifier = Modifier)으로 Composable 함수의 매개변수로 Modifier를 항상 전달하는 것이 좋습니다 — 이는 Google 공식 라이브러리에서 채택된 표준 관행입니다.
@Composable
fun SectionCard(
modifier: Modifier = Modifier,
title: String,
content: @Composable () -> Unit
) {
Card(modifier = modifier) {
Column(modifier = Modifier.padding(16.dp)) {
Text(text = title, style = MaterialTheme.typography.titleMedium)
Spacer(modifier = Modifier.height(8.dp))
content()
}
}
}
Slot API 덕분에 SectionCard 구성 요소는 양식, 목록, 텍스트 블록 등 다양한 콘텐츠로 다른 화면에서 사용할 수 있습니다. 수정자와 Slot API의 조합은 Kotlin이 제공하는 타입 안전성을 잃지 않으면서 Compose 구성 요소를 매우 유연하게 만듭니다.
자주 묻는 질문
@Composable 함수는 컴포지션 컨텍스트에서 실행되며 상태를 읽고 변경 시 자동으로 재시작됩니다. 일반 Kotlin 함수는 상태 추적 메커니즘에 접근할 수 없으며 UI 트리 구축에 참여하지 않습니다.
아니요, Composable 함수는 특수 컴포지션 컨텍스트가 필요하므로 다른 Composable 함수에서만 호출할 수 있습니다. Compose 코드를 일반 Kotlin과 통합하려면 Activity의 setContent { } 메서드 또는 View 시스템의 ComposeView를 사용합니다.
이는 Compose 커뮤니티에서 채택된 명명 규칙입니다. 대문자는 UI 구성 요소를 일반 함수와 시각적으로 구분하며, 클래스 명명 규칙을 따릅니다. 컴파일러 요구 사항이 아니라 Google 문서에서 권장하는 관행입니다.
숫자에 제한은 없습니다. 실제로 큰 화면에는 내장 구성 요소(Text, Button)와 사용자 정의 구성 요소를 포함하여 50–100개의 Composable 함수가 포함될 수 있습니다. Compose는 함수 트리를 최적화하고 입력 데이터가 변경된 함수만 실행합니다.
일반적으로 Composable 함수는 Unit을 반환합니다. 그 작업이 UI를 구축하는 것이기 때문입니다. 그러나 @Composable로 표시되고 값을 반환하는 remember 및 derivedStateOf와 같은 특수화된 함수가 있습니다. 이는 예외일 뿐 규칙이 아닙니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.