Kotlin Compose 기반 Circuit 프레임워크 이해하기
Circuit란 무엇인가
Circuit는 Kotlin으로 앱을 개발할 때 사용하는 경량 프레임워크입니다. Slack에서 실제 서비스에 적용 중이며, 빠르게 변화하는 API와 오픈 개발 환경이 특징입니다. 핵심 목적은 확장성과 단순함에 있으며, Compose 환경에서 동작하도록 설계되었습니다.
Compose Runtime과 Compose UI의 차이
Compose는 크게 두 부분으로 나뉩니다: 컴파일러(및 런타임)와 UI 라이브러리입니다. 일반적으로 UI에 초점이 맞춰진 것처럼 보이지만, 컴파일러와 런타임은 UI에 제한되지 않으며 상태 관리 등 다양한 기능을 제공합니다.
Circuit의 핵심 구조: Presenter와 Ui
Circuit의 중심은 Presenter와 Ui 인터페이스입니다. 둘 사이에는 직접적인 연결이 없고, 오직 상태(State)와 이벤트(Event)를 통해 소통합니다. 이 방식이 앱 내 데이터 흐름을 명확하고 안전하게 만듭니다.
Presenter와 Ui 사용 방식
Presenter와 Ui 모두 컴포저블 함수(Composable Function)로 정의됩니다. Presenter는 Compose 런타임을 활용해 상태를 생성 및 관리하고, Ui는 그 상태를 받아 화면을 구성합니다. 대부분의 경우, Circuit가 자동으로 이 둘을 연결해줍니다.
Screen의 개념
앱은 여러 개의 'Screen'으로 구성됩니다. 하나의 Screen은 Presenter와 Ui의 한 쌍이며, 화면 단위의 동작과 데이터를 담당합니다. 예를 들어, 카운터 기능이 있는 화면은 CounterScreen이 됩니다. Screen이 중첩되거나 합쳐지는 경우도 있으며, 각각 별도로 관리됩니다.
Circuit의 실시간 오픈 개발
Circuit의 GitHub 저장소는 외부 개발자와 자유롭게 협업할 수 있도록 공개되어 있습니다. 공개 샘플앱을 통해 다양한 활용 패턴을 익힐 수 있습니다.
카운터 화면 구현 예시
아래 예시는 Circuit로 만든 간단한 카운터 화면입니다. 버튼 클릭으로 숫자를 올리고 내릴 수 있고, Compose 스타일을 따릅니다.
@Parcelize
data object CounterScreen : Screen {
data class CounterState(
val count: Int,
val eventSink: (CounterEvent) -> Unit,
) : CircuitUiState
sealed interface CounterEvent : CircuitUiEvent {
data object Increment : CounterEvent
data object Decrement : CounterEvent
}
}
@CircuitInject(CounterScreen::class, AppScope::class)
@Composable
fun CounterPresenter(): CounterState {
var count by rememberSaveable { mutableStateOf(0) }
return CounterState(count) { event ->
when (event) {
CounterEvent.Increment -> count++
CounterEvent.Decrement -> count--
}
}
}
@CircuitInject(CounterScreen::class, AppScope::class)
@Composable
fun Counter(state: CounterState) {
Box(Modifier.fillMaxSize()) {
Column(Modifier.align(Alignment.Center)) {
Text(
modifier = Modifier.align(CenterHorizontally),
text = "Count: ${state.count}",
style = MaterialTheme.typography.displayLarge
)
Spacer(modifier = Modifier.height(16.dp))
Button(
modifier = Modifier.align(CenterHorizontally),
onClick = { state.eventSink(CounterEvent.Increment) }
) {
Icon(rememberVectorPainter(Icons.Filled.Add), "Increment")
}
Button(
modifier = Modifier.align(CenterHorizontally),
onClick = { state.eventSink(CounterEvent.Decrement) }
) {
Icon(rememberVectorPainter(Icons.Filled.Remove), "Decrement")
}
}
}
}
라이선스와 사용 시 주의사항
Circuit는 Apache 2.0 라이선스로 배포됩니다. 사용, 변경, 재배포가 자유롭지만, 라이선스 규약을 반드시 확인해야 하며, 보장이나 책임은 제공되지 않습니다.
출처 및 참고 : Circuit