동적인 사용자 인터페이스를 구축하는 일은 안드로이드 개발에서 오랫동안 중요한 과제로 여겨져 왔습니다.
전통적인 방식에서는 UI를 변경할 때마다 전체 어플리케이션을 다시 컴파일하고 배포해야 하는데,
이는 A/B 테스트, 기능 플래그, 실시간 콘텐츠 업데이트에 큰 비효율을 초래합니다.
예를 들어 마케팅 팀이 새로운 결제 버튼 디자인을 테스트하고 싶다고 가정해봅시다.
기존 방식에서는 이렇게 단순한 변경조차도 개발자 작업, 코드 리뷰, QA 테스트, 앱 스토어 제출, 그리고 사용자가 업데이트를 받아들이기까지 몇 주가 걸립니다.
RemoteCompose는 이러한 문제를 해결하기 위한 강력한 솔루션으로 등작했습니다.
개발자들이 Compose UI 레이아웃을 재 컴파일 없이 런타임에 생성하고, 전송하며, 렌더링 할 수 있게 해줍니다.
이 글에서는 RemoteCompose가 무엇인지 살펴보고,
핵심 아키텍처를 이해하며
Compose 로 동적인 화면을 설계할때 RemoteCompose가 가져오는 이점들을 알아 볼 것입니다.
이 글은 라이브러리 사용 방법을 설명하는 튜토리얼이 아니라
Remote Config가 안드로이드 UI 개발에 가져오는 패러다임 변화를 탐구하는 내용입니다.
Integration and Dependencies
개념을 살펴보기 전에, 프로젝트에 RemoteCompose를 추가하는 방법을 먼저 설명합니다.
Android 의존성이 없는 JVM 기반 서버나 백엔드에서는 다음과 같이 사용할 수 있습니다.
아직 RemoteCompose는 Androidx 팀에 의해 개발중이며 공식적으로 배포된 상태가 아닙니다.
따라서 AndroidX Snapshot Maven 저장소를 통해서만 사용할 수 있습니다.
// settings.gradle
repositories {
maven {
url = uri("https://androidx.dev/snapshots/builds/14511716/artifacts/repository")
}
}
// JVM server - no Android dependencies
dependencies {
implementation("androidx.compose.remote:remote-core:1.0.0-SNAPSHOT")
implementation("androidx.compose.remote:remote-creation-compose:1.0.0-SNAPSHOT")
}
// Compose-based app
dependencies {
implementation("androidx.compose.remote:remote-player-compose:1.0.0-SNAPSHOT")
implementation("androidx.compose.remote:remote-tooling-preview:1.0.0-SNAPSHOT")
}
// View-based app
dependencies {
implementation("androidx.compose.remote:remote-player-view:1.0.0-SNAPSHOT")
}
Understanding the core abstraction
RemoteCompose의 핵심은 Compose UI 컴포넌트를 원격에서 렌더링 할 수 있도록 해주는 프레임워크라는 점입니다.
전통적인 UI방식과 구별되는 점은 두 가지 근본적인 원칙을 따른 다는 것입니다.
1. 선언적 문서 직렬화
2. 플랫폼에 의존하지 않는 렌더링
이것들은 단순한 기술적 기능이 아니라,
UI 배포 방식을 근본적으로 재정의하는 아키텍쳐적 결정입니다.
Declarative Document Serialization
선언적 문서 직렬화란
Compose 레이아웃을 작고 효율적인 직렬화 포맷으로 캡쳐할 수 있다는 의미입니다.
마치 UI의 스크린샷을 찍는것과 비슷하지만,
픽셀을 저장하는 것이 아니라
실제로 UI를 그리는 명령어를 담아내는 방식입니다.
이렇게 캡쳐된 문서에는 UI를 복원하는데 필요한 모든 정보가 포함됩니다.
도형, 색상, 텍스트, 이미지, 애니메이션, 상호작용 터치 영역
즉 이 문서 하나만으로 UI전체를 클라이언트에서 재 구성 할 수 있습니다.
val document = captureRemoteDocument(
context = context,
creationDisplayInfo = displayInfo,
profile = profile
) {
RemoteColumn(modifier = RemoteModifier.fillMaxSize()) {
RemoteText("Dynamic Content")
RemoteButton(onClick = { /* action */ }) {
RemoteText("Click Me")
}
}
}
view raw
결과는 무엇일까요?
네트워크를 통해 전송할 수 있는 ByteArray입니다.
이 접근 방식의 핵심은 UI를 만드는쪽(서버, 백엔드 포함) 이 기존 Jetpack compose 코드를 그대로 작성한다는 점에 있습니다.
새로운 DSL을 배울 필요도 없고,
유지 해야 할 JSON 스키마도 없으며
별도의 템플릿 언어를 익힐 필요도 없습니다.
Compose로 작성할 수 있다면, RemoteCompose로 캡처할 수 있습니다.
일반적인 Compose UI도 캡처 할 수 있는데
이 경우에는 그리기(draw) 호출을 캡처하게 됩니다.
하지만 이러한 draw 호출은 매우 정적이기 때문에 실용성이 떨어집니다.
그래서 일반적으로는 직렬화와 원격 재생을 위해 설계된
Compose와 1:1로 대응되는 Remote 전용 API(RemoteColumn, RemoteButton, RemoteText) 등을 사용하는것이 더 적합합니다.
Platform-Independent Rendering
플랫폼에 독립적인 렌더링이란,
이렇게 캡쳐된 문서를 네트워크를 통해 전송하고,
원래의 Compose 코드가 없이도 어떤 안드로이드 기기에서도 렌더링 할 수 있다는 의미입니다.
클라이언트 기기는 당신의 Composable 함수, ViewModel, 비즈니스 로직을 전혀 알 필요가 없습니다.
단지 문서 (bytearray)와 그것을 재생 할 플레이어만 있으면 됩니다.
// On the client or player side
RemoteDocumentPlayer(
document = remoteDocument.document,
documentWidth = windowInfo.containerSize.width,
documentHeight = windowInfo.containerSize.height,
onAction = { actionId, value ->
// Handle user interactions
}
)
이러한 특성들은 단순히 편의성을 높이기 위한것이아닙니다.
UI 정의를 앱 배포 과정과 완전히 분리하기 위한 아키텍쳐적 제약 입니다.
문서 포맷은 단순한 정적 레이아웃만 담는 것이 아니라,
상태, 애니메이션, 상호작용 까지 포함하여 UI 경험 전체를 완전하게 표현할 수 있습니다.
Comparing Approaches: Why Not JSON or WebView?
더 깊이 들어가기전에
RemoteCompose가 왜 이런 방식을 선택했는지 다른 접근방식과 비교해볼 가치가 있습니다.
JSON 기반의 서버 주도 UI(예 airbnb epoxy, shopify의 방식) 은 JSON 스키마를 정의하고
이를 네이티브 UI 컴포넌트에 매핑해야 합니다.
이 방식은 구조화된 콘텐츠를 다룰 때는 잘 작동하지만, 다음과 같은 것들에는 약합니다.
- 복잡한 애니메이션과 화면 전환
- 커스텀 드로잉과 그래픽
- 인라인 스타일링이 포함된 리치 텍스트
- 그라데이션, 그림자 등 시각 효과
웹뷰는 매우 높은 유연성을 제공하지만 다음과 같은 문제가 발생합니다.
- 별도 렌더링 프로세스 때문에 성능 오버헤드발생
- 웹 스타일과 네이티브 디자인 간 일관되지 않은 룩앤필
- WebView 하나가 매우 무겁기 때문에 메모리 압박
- 제스쳐 충돌 등 터치 처리 복잡성 증가
ReomteCompse는 이 두 방식 과 다른 세번째 길을 택합니다.
즉 Compose가 실제로 수행하는 그리기 동작 자체를 캡처하는 방식입니다.
이 말은 곧 Compose에서 만들 수 있는 어떤 UI든
커스텀 드로잉, 복잡한 애니메이션, Material Design 컴포넌트 까지 모두
네이티브 성능에서 원격으로 캡쳐하고 재생 가능하다는 뜻입니다.
The Document-Based Architecture: Creation and Playback
ReomteCompose의 아키텍쳐는 문서 생성 , 문서 재생 이라는 두 단계가 명확히 분리되어있습니다.
이 분리를 이해하는 것이 RemoteCompose의 강력함을 이해하는 핵심입니다.
Document Creation: Capturing UI as Data
이문서 생성 단계에서는 Compose UI 코드를 직렬화된 문서로 변환합니다.
이 작업은 안드로이드 렌더링 파이프라잉네 가장 낮은 레벨인 Canvas 레벨에서 그리기 연산을 가로채 수행됩니다.
@Composable Content
↓
RemoteComposeCreationState (Tracks state and modifiers)
↓
CaptureComposeView (Virtual Display - no actual screen needed)
↓
RecordingCanvas (Intercepts every draw call)
↓
Operations (93+ operation types covering all drawing primitives)
↓
RemoteComposeBuffer (Efficient binary serialization)
↓
ByteArray (Network-ready, typically 10-100KB for complex UIs)
문서를 생성하는 쪽에서는 완전한 Compose 통합 계층을 제공합니다.
개발자는 평소처럼 일반적인 @Compose 함수를 작성하면 되고,
프레임워크는 다음과 같은 모든 요소를 캡처합니다.
- 레이아웃 계층 구조,
- Modifier 정보
- 텍스트 스타일
- 이미지
- 애니메이션
- 터치 핸들러(상호작용)
RemoteCompose가 특별한 이유는 캡쳐된 문서가 완전히 자급자족형이기 때문입니다.
이 문서안에는 다음과 같은 모든 UI요소가 포함됩니다.
- 도형, 색상, 그라데이션, 그림자 같은 시각 요소
- 문자열, 폰트, 크기, 스타일링이 포함된 텍스트
- 이미지는 비트맵으로 직접 포함하거나 지연 로딩을 위한 URL로도 포함 가능
- 레이아웃 정보에는 크기, 위치, 패딩, 정렬 등이 포함됨
- 인터렉션에서는 터치 영역, 클릭 핸들러, 명명된 액션이 정의됨
- 상태 변수는 런타임에 업데이트 가능
- 애니메이션은 시간 기반 표현 으로 동작을 정의함
즉 이 문서 하나만으로 UI전체 경험을 재현할 수 있습니다.
수신자클라이언트는 당신의 코드 베이스에 접근할 필요가 없습니다.
문서만 있으면됩니다.
이 점은 다른 Server-driven UI방식과 근본적으로 다릅니다.
기존 방식에서는 클라이언트가 스키마를 해석하거나,
미리 만들어둔 UI 컴포넌트를 가지고 있어야 UI를 렌더링 할 수 있기 때문입니다.
Document Playback: Rendering Without Compilation
플레이백 단계에서는 직렬화된 문서를 받아 화면에 렌더링합니다.
플레이어는 문서 안에 포함된 연산들을 순서대로 처리하면서
각 연산을 Canvas에 실행합니다.
이 개념은 마치 비디어 플레이어가 프레임을 디코딩하는 것과 유사하지만
픽셀을 디코딩하는 것이 아니라 그리기 명령어를 디코딩 한다는 점이 다릅니다.
RemoteCompose는 다양한 아키텍처 요구에 대응하기 위해 두 가지 렌더링 백엔드를 제공합니다.
그리고 최신 앱에서는 Compose 기반 플레이어를 사용할 것을 권장합니다.
@Composable
fun DynamicScreen(document: CoreDocument) {
RemoteDocumentPlayer(
document = document,
documentWidth = screenWidth,
documentHeight = screenHeight,
modifier = Modifier.fillMaxSize(),
onNamedAction = { name, value, stateUpdater ->
// Handle named actions from the document
when (name) {
"addToCart" -> cartManager.addItem(value)
"navigate" -> navController.navigate(value)
"trackEvent" -> analytics.logEvent(value)
}
},
bitmapLoader = rememberBitmapLoader() // For lazy image loading
)
}
컴포즈 기반 플레이어는 기존 Compose UI와 자연스럽게 통합됩니다.
이는 하나의 Composable 함수로 제공되며,
기존 Compose 트리 어디에든 배치할 수 있고,
Modifier를 적용하거나 다른 Composable처럼 애니메이션을 줄 수도 있습니다.
기존 View 기반 UI 계층과의 호환성을 위해 View 기반 플레이어도 함께 제공됩니다.
class LegacyActivity : AppCompatActivity() {
private lateinit var player: RemoteComposePlayer
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
player = RemoteComposePlayer(this)
setContentView(player)
// Load document from network
lifecycleScope.launch {
val bytes = api.fetchDocument("home-screen")
player.setDocument(bytes)
}
player.onNamedAction { name, value, stateUpdater ->
// Handle actions
}
}
두 플레이어는 동일한 렌더링 품질을 제공합니다.
선택은 단지 앱의 아키텍처에 따라 달라질 뿐입니다
- 앱이 완전히 Compose 기반이라면 -> Composable Player 사용
- View 기반에서 Compose로 마이그레이션 중이거나, View 계층안에 RemoteCompose를 삽입해야 한다면 -> View 기반 PLayer 사용
The Operation Model: A Comprehensive Drawing Vocabulary
RemoteCompose의 강점은 포괄적인 Operation Model에 있습니다.
프레임워크는 UI 렌더링의 모든 측면을 표현하기 위해 93개 이상의 개별 그리기 명령을 정의합니다.
이 숫자는 임의로 정해진 것이 아니라
Canvas 기반 렌더링에서 필요한 모든 그리기 동작을 표현하기 위한 완전한 어휘를 의미합니다.
Why Operations Matter
전통적인 Server driven UI 방식에서는 서버가 텍스트가 Submit인 버튼을 랜더링해라 같은 고수준 컴포넌트 설명을 전송합니다.
그러면 클라이언트는 이 설명을 해석하고, 이를 네이티브 UI 컴포넌트로 매핑해야합니다.
이 방식은 서버와 클라이언트가 모두 버튼이 무엇인지, 어떻게 동작해야 하는지
정확하게 합의하고 있어야 하므로 강한 결합이 발생합니다.
RemoteCompose는 훨씬 더 낮은 레벨에서 동작합니다.
버튼을 렌더링하라 가 아니라
이 좌표에 이 색상에 둥근 사각형을 그려라, 그리고 그 위치에 이 폰트로 submit이라는 텍스트를 그려라. 와 같은 실제 그리기 명령을 전송합니다.
클라이언트는 버튼이라는 개념을 알 필요가 없고, 그저 명령어들을 순서대로 Canvas에 실행하면됩니다.
이러한 저수준 접근 방식은 큰 의미를 갖습니다.
- 스키마 동기화 필요 없음
- 서버와 클라이언트가 button, card와 같은 컴포넌트 정의에 합의 할 필요없음
- 완벽한 시각 충실도
- Compose로 가능한 모든 시각 효과는 그대로 캡처 가능
- 앞으로의 디자인 변화에도 호환
- 새 디자인이 나와도 클라이언트는 그저 새로운 drawing operation을 실행하면됨
- 커스텀 컴포넌트도 자동 지원
- 별도 등록 없이도 Canvas 기반으로 그려지는 UI라면 모두 동작함
'영어 데일리' 카테고리의 다른 글
| 보일러플레이트 코드를 그만작성하세요: 매일 사용하는 컴포즈 헬퍼 유틸리티 (1) | 2026.01.05 |
|---|---|
| 왜 hilt때문에 너의 앱이 느려지는가? (어떻게 수정할지) (1) | 2026.01.02 |
| Bosch 안드로이드 개발자 인터뷰 경험 (0) | 2025.12.01 |
| 불필요한 Recompositions 줄이기: Compose를 위한 실용적인 최적화 기법 3가지 (0) | 2025.11.25 |
| withContext(Dispatchers.IO)와 launch(Dispatchers.IO)의 실제 차이 (0) | 2025.11.24 |