아티클 목록으로 가기

Compose Remote란 무엇이며, 서버 주도 UI를 구축하는 데 어떻게 활용할 수 있을까

skydovesJaewoong Eum (skydoves)||23분 소요

Compose Remote란 무엇이며, 서버 주도 UI를 구축하는 데 어떻게 활용할 수 있을까

동적인 사용자 인터페이스를 구성하는 일은 안드로이드 개발에서 오랫동안 근본적인 과제로 남아 있었습니다. 기존의 전통적인 접근 방식에서는 UI를 변경할 때마다 애플리케이션 전체를 재컴파일하고 다시 배포해야 하며, 이 과정이 A/B 테스트, 피처 플래그(feature flag), 실시간 콘텐츠 업데이트에 큰 걸림돌이 됩니다. 가령, 마케팅 팀이 결제 버튼 디자인을 새롭게 테스트하고 싶다고 가정해 봅시다. 전통적인 모델에서는 이 간단한 변경 하나에도 개발자의 작업 시간, 코드 리뷰, QA 테스트, 앱 스토어 제출 과정이 필요하고, 사용자가 실제로 업데이트하기까지 몇 주나 기다려야 합니다. Compose Remote는 바로 이런 문제를 해결하기 위해 등장한 강력한 솔루션으로, 개발자가 Jetpack Compose UI 레이아웃을 재컴파일 없이 런타임에서 생성, 전송, 렌더링할 수 있게 해 줍니다.

이 글에서는 Compose Remote가 무엇인지 살펴보고, 핵심 아키텍처를 이해한 뒤, Jetpack Compose를 활용한 동적 화면 설계에 어떤 이점을 가져다주는지 알아보겠습니다. 이 글은 라이브러리 사용 튜토리얼이 아니라, Compose Remote가 안드로이드 UI 개발에 가져올 패러다임 전환을 깊이 탐구하는 글입니다. 안드로이드 개발을 하시면서 서버 주도 UI에 관심이 있으셨던 분이라면, 특히 유용한 내용이 될 것입니다.

핵심 추상화 이해하기: Compose Remote가 특별한 이유

Compose Remote는 본질적으로 Compose UI 컴포넌트를 원격으로 렌더링할 수 있는 프레임워크입니다. 기존 UI 접근 방식과 구별되는 핵심은 **선언적 문서 직렬화(declarative document serialization)**와 **플랫폼 독립 렌더링(platform-independent rendering)**이라는 두 가지 근본 원칙을 따른다는 점에 있습니다.

선언적 문서 직렬화

선언적 문서 직렬화란 Jetpack Compose 레이아웃을 압축된 직렬화 형식으로 캡처할 수 있다는 뜻입니다. UI의 "스크린샷"을 찍는 것과 비슷하지만, 픽셀 대신 실제 드로잉 명령어를 캡처한다는 점이 다릅니다. 캡처된 문서에는 UI를 재현하는 데 필요한 모든 정보, 즉 도형, 색상, 텍스트, 이미지, 애니메이션, 심지어 인터랙티브 터치 영역까지 포함됩니다.

// 서버 또는 생성 측
val document = captureRemoteDocument(
    context = context,
    creationDisplayInfo = displayInfo,
    profile = profile
) {
    // 일반 Compose 코드와 완전히 동일한 표준 Compose UI
    Column(modifier = RemoteModifier.fillMaxSize()) {
        Text("Dynamic Content")
        Button(onClick = { /* action */ }) {
            Text("Click Me")
        }
    }
}
// 결과: 네트워크를 통해 전송할 수 있는 ByteArray

이 접근 방식의 핵심은 생성 측에서 표준 Compose 코드를 그대로 작성한다는 것입니다. 새로운 DSL을 배울 필요도, JSON 스키마를 유지 관리할 필요도, 템플릿 언어를 익힐 필요도 없습니다. Compose로 작성할 수 있는 것이라면 Compose Remote로 캡처할 수 있습니다.

플랫폼 독립 렌더링

플랫폼 독립 렌더링이란 캡처된 문서를 네트워크를 통해 전송하고, 원본 Compose 코드 없이도 모든 안드로이드 디바이스에서 렌더링할 수 있다는 의미입니다. 클라이언트 디바이스는 컴포저블 함수, ViewModel, 비즈니스 로직이 전혀 필요 없으며, 문서 바이트와 플레이어만 있으면 충분합니다.

// 클라이언트 또는 플레이어 측
RemoteDocumentPlayer(
    document = remoteDocument.document,
    documentWidth = windowInfo.containerSize.width,
    documentHeight = windowInfo.containerSize.height,
    onAction = { actionId, value ->
        // 사용자 인터랙션 처리
    }
)

이러한 특성은 단순한 편의 기능이 아니라, UI 정의와 배포를 완전히 분리(decoupling)할 수 있게 해 주는 아키텍처적 제약 조건입니다. 문서 형식은 정적 레이아웃뿐만 아니라 상태, 애니메이션, 인터랙션까지 캡처하므로, UI 경험 전체를 담아낼 수 있습니다.

접근 방식 비교: JSON이나 WebView를 사용하지 않는 이유

본격적으로 들어가기 전에, Compose Remote가 왜 다른 대안 대신 이러한 접근 방식을 택했는지 이해할 필요가 있습니다.

JSON 기반 서버 주도 UI(Airbnb의 Epoxy나 Shopify의 접근 방식 등)는 네이티브 컴포넌트에 매핑되는 스키마를 정의해야 합니다. 구조화된 콘텐츠에는 잘 작동하지만, 다음과 같은 영역에서는 한계가 있습니다.

  • 복잡한 애니메이션과 전환 효과
  • 커스텀 드로잉과 그래픽
  • 인라인 스타일링이 적용된 리치 텍스트
  • 그라데이션, 그림자, 시각적 효과

WebView는 완전한 유연성을 제공하지만, 다음과 같은 문제가 발생합니다.

  • 성능 오버헤드(별도의 렌더링 프로세스)
  • 일관성 없는 룩앤필(웹 스타일링 vs 네이티브)
  • 메모리 부담(각 WebView의 비용이 높음)
  • 터치 처리의 복잡성(제스처 충돌)

Compose Remote는 세 번째 길을 택합니다. Compose가 실행할 실제 드로잉 연산(drawing operation)을 캡처하는 방식입니다. 이를 통해 커스텀 Canvas 드로잉, 복잡한 애니메이션, Material Design 컴포넌트를 포함하여 Compose로 만들 수 있는 모든 UI를 네이티브 성능 그대로 원격에서 캡처하고 재생할 수 있습니다.

문서 기반 아키텍처: 생성과 재생

Compose Remote의 아키텍처는 **문서 생성(document creation)**과 **문서 재생(document playback)**이라는 두 단계 사이의 명확한 분리를 기반으로 합니다. 이 분리를 이해하는 것이 프레임워크의 핵심 역량을 파악하는 열쇠입니다.

문서 생성: UI를 데이터로 캡처하기

생성 단계에서는 Compose UI 코드를 직렬화된 문서로 변환합니다. 이는 안드로이드 렌더링 파이프라인의 가장 낮은 레벨인 Canvas 수준에서 드로잉 연산을 가로채는 정교한 캡처 메커니즘을 통해 이루어집니다.

@Composable Content
        ↓
RemoteComposeCreationState (상태와 Modifier 추적)
        ↓
CaptureComposeView (가상 디스플레이 - 실제 화면 불필요)
        ↓
RecordingCanvas (모든 draw 호출 가로채기)
        ↓
Operations (모든 드로잉 프리미티브를 포괄하는 93개 이상의 연산 타입)
        ↓
RemoteComposeBuffer (효율적인 바이너리 직렬화)
        ↓
ByteArray (네트워크 전송 준비 완료, 복잡한 UI 기준 보통 10~100KB)

생성 측은 완전한 Compose 통합 레이어를 제공합니다. 표준 @Composable 함수를 작성하면, 프레임워크가 레이아웃 계층 구조, Modifier, 텍스트 스타일, 이미지, 애니메이션, 심지어 터치 핸들러까지 모든 것을 캡처합니다.

특별한 점은 캡처된 문서가 **자기 완결적(self-contained)**이라는 것입니다. UI를 재현하는 데 필요한 모든 정보가 포함되어 있습니다.

  • 시각적 요소: 도형, 색상, 그라데이션, 그림자
  • 텍스트: 문자열, 폰트, 크기, 스타일링
  • 이미지: 내장 비트맵 또는 지연 로딩을 위한 URL
  • 레이아웃: 크기, 위치, 패딩, 정렬
  • 인터랙션: 터치 영역, 클릭 핸들러, 명명된 액션
  • 상태: 런타임에 업데이트할 수 있는 변수
  • 애니메이션: 모션을 위한 시간 기반 표현식

수신 측은 코드베이스에 접근할 필요 없이 문서 바이트만 있으면 됩니다. 이는 클라이언트가 스키마를 이해하거나 미리 빌드된 컴포넌트를 보유해야 하는 다른 서버 주도 UI 접근 방식과 근본적으로 다릅니다.

문서 재생: 컴파일 없는 렌더링

재생 단계에서는 직렬화된 문서를 화면에 렌더링합니다. 플레이어는 연산(operation)을 순회하며 각각을 Canvas에 대해 실행합니다. 비디오 플레이어가 프레임을 디코딩하는 방식과 개념적으로 유사하지만, 픽셀 대신 드로잉 명령어를 디코딩한다는 차이가 있습니다.

Compose Remote는 아키텍처 요구에 맞춰 두 가지 렌더링 백엔드를 제공합니다.

Compose 기반 플레이어 (최신 앱에 권장)

@Composable
fun DynamicScreen(document: CoreDocument) {
    RemoteDocumentPlayer(
        document = document,
        documentWidth = screenWidth,
        documentHeight = screenHeight,
        modifier = Modifier.fillMaxSize(),
        onNamedAction = { name, value, stateUpdater ->
            // 문서에서 전달된 명명된 액션 처리
            when (name) {
                "addToCart" -> cartManager.addItem(value)
                "navigate" -> navController.navigate(value)
                "trackEvent" -> analytics.logEvent(value)
            }
        },
        bitmapLoader = rememberBitmapLoader()  // 지연 이미지 로딩용
    )
}

Compose 기반 플레이어는 기존 Compose UI와 자연스럽게 통합됩니다. 컴포지션 계층 구조 어디에나 배치할 수 있는 컴포저블 함수로, 다른 컴포저블과 마찬가지로 Modifier를 적용하거나 애니메이션을 줄 수 있습니다.

View 기반 플레이어 (기존 View 계층 구조와의 호환용)

class LegacyActivity : AppCompatActivity() {
    private lateinit var player: RemoteComposePlayer

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        player = RemoteComposePlayer(this)
        setContentView(player)

        // 네트워크에서 문서 로드
        lifecycleScope.launch {
            val bytes = api.fetchDocument("home-screen")
            player.setDocument(bytes)
        }

        player.onNamedAction { name, value, stateUpdater ->
            // 액션 처리
        }
    }
}

두 플레이어 모두 동일한 렌더링 품질을 제공하며, 앱 아키텍처에 따라 선택하면 됩니다. 완전한 Compose 앱이라면 컴포저블 플레이어를, View에서 마이그레이션 중이거나 View 계층 구조에 임베딩해야 한다면 View 기반 플레이어를 사용하시면 됩니다.

연산 모델: 포괄적인 드로잉 어휘

Compose Remote의 강력함은 포괄적인 연산 모델에 있습니다. 프레임워크는 UI 렌더링의 모든 측면을 아우르는 93개 이상의 고유한 연산을 정의합니다. 이 숫자는 임의로 정해진 것이 아니라, 모든 Canvas 드로잉 연산을 표현하는 데 필요한 완전한 어휘입니다.

연산이 중요한 이유

전통적인 서버 주도 UI는 높은 수준의 컴포넌트 설명을 전송합니다. 가령, "텍스트가 'Submit'인 버튼을 렌더링하라"라는 식입니다. 그러면 클라이언트가 이를 해석하여 네이티브 컴포넌트에 매핑해야 합니다. 이 방식은 서버와 클라이언트 사이에 강한 결합(tight coupling)을 만들어, 양쪽 모두 "버튼"이 무엇이고 어떻게 동작하는지 합의해야 합니다.

Compose Remote는 더 낮은 수준에서 동작합니다. "버튼을 렌더링하라" 대신 실제 드로잉 명령어를 전송합니다. 즉, "이 좌표에 이 색상으로 둥근 사각형을 그리고, 이 위치에 이 폰트로 'Submit' 텍스트를 그려라"라는 식입니다. 클라이언트는 "버튼"이 무엇인지 알 필요 없이 드로잉 연산만 실행하면 됩니다.

이러한 저수준 접근 방식은 다음과 같이 중요한 의미를 가집니다.

  • 스키마 동기화 불필요: 서버와 클라이언트가 컴포넌트 정의에 대해 합의할 필요가 없습니다
  • 완전한 시각적 충실도: Compose에서 구현 가능한 모든 시각적 효과를 캡처할 수 있습니다
  • 포워드 호환성(forward compatibility): 새로운 시각적 디자인이 기존 클라이언트에서도 동작합니다(단순히 다른 드로잉 연산일 뿐이므로)
  • 커스텀 컴포넌트 자동 지원: 커스텀 컴포저블도 별도 등록 없이 자동으로 동작합니다

드로잉 연산

드로잉 연산은 2D 그래픽의 근본적인 프리미티브인 Canvas draw 호출을 캡처합니다.

DRAW_RECT          - 사각형 (버튼, 카드, 배경)
DRAW_ROUND_RECT    - 둥근 사각형 (Material 서피스)
DRAW_CIRCLE        - 원 (아바타, 인디케이터)
DRAW_OVAL          - 타원과 타원형
DRAW_LINE          - 선과 구분선
DRAW_PATH          - 임의의 도형 (아이콘, 커스텀 그래픽)
DRAW_ARC           - 호 (프로그레스 인디케이터, 파이 차트)
DRAW_SECTOR        - 파이 섹터
DRAW_TEXT          - 전체 스타일링이 적용된 텍스트 렌더링
DRAW_TEXT_ON_PATH  - 곡선을 따르는 텍스트
DRAW_BITMAP        - 이미지
DRAW_TWEEN_PATH    - 애니메이션 패스 모핑

각 연산은 실행에 필요한 모든 정보, 즉 좌표, 색상, 페인트 스타일, 그리고 문서 내 다른 곳에 저장된 데이터(텍스트 문자열이나 비트맵 등)에 대한 참조를 담고 있습니다.

레이아웃 연산

레이아웃 연산은 컴포넌트 계층 구조와 공간적 관계를 정의합니다.

Component          - 레이아웃 컴포넌트 선언
Container          - 컨테이너 시작 (Column이나 Row와 유사)
ContainerEnd       - 컨테이너 종료
LoopOperation      - 리스트를 위한 콘텐츠 반복
Modifiers:
BackgroundModifier - 배경 색상/드로어블
BorderModifier     - 테두리 스타일링
PaddingModifier    - 내부 간격
ClickModifier      - 터치 처리

컨테이너 모델은 push/pop 방식으로 동작합니다. 플레이어가 Container 연산을 만나면 새로운 레이아웃 컨텍스트를 생성하고, 이후 모든 연산은 ContainerEnd가 팝(pop)할 때까지 해당 컨텍스트 내에서 적용됩니다. 이는 Compose의 레이아웃 시스템이 동작하는 방식과 동일합니다.

상태 및 표현식 연산

상태 연산은 런타임에 변경할 수 있는 동적 값을 지원합니다.

NamedVariable      - 명명된 상태 변수 선언
ColorAttribute     - 테마 적용이 가능한 색상
TimeAttribute      - 애니메이션 시간 참조
FloatExpression    - 매 프레임 평가되는 수학적 표현식
IntegerExpression  - 정수 표현식
ConditionalOp      - 상태 기반 조건부 렌더링

이 아티클은 구독자 전용입니다

Dove Letter를 구독하시면 안드로이드, 코틀린 개발 관련 독점 아티클의 전체 내용을 볼 수 있습니다.

구독하기
아티클 목록으로 가기