Compose Multiplatform의 iOS에서 Skia 렌더링

발행: (2026년 4월 2일 PM 04:47 GMT+9)
15 분 소요
원문: Dev.to

Source: Dev.to

아래는 해당 글의 내용을 한국어로 번역한 것입니다. 원본의 마크다운 구조와 코드 블록, URL 등은 그대로 유지했습니다.


Compose Multiplatform의 iOS에서 Skia 렌더링

Compose Multiplatform은 Kotlin 기반 UI 프레임워크로, Android, Desktop, Web, 그리고 iOS 등 다양한 플랫폼에서 동일한 UI 코드를 공유할 수 있게 해줍니다.
iOS에서는 현재 Skia를 이용한 렌더링 파이프라인이 기본 제공되며, 이를 통해 GPU 가속고성능 그래픽을 활용할 수 있습니다.

왜 Skia인가?

  • 크로스 플랫폼: Skia는 Google이 개발한 2D 그래픽 엔진으로, Android, Chrome, Flutter 등 여러 프로젝트에서 사용됩니다.
  • GPU 가속: Metal(또는 OpenGL) 백엔드를 통해 하드웨어 가속을 제공하므로, 복잡한 UI와 애니메이션도 부드럽게 렌더링됩니다.
  • 일관된 픽셀: 동일한 렌더링 엔진을 사용하므로, Android와 iOS 사이에 픽셀 단위 차이가 거의 없습니다.

프로젝트 설정

1️⃣ Gradle 설정

plugins {
    kotlin("multiplatform") version "2.0.0"
    id("org.jetbrains.compose") version "1.5.0"
}

kotlin {
    iosArm64()
    iosX64()
    iosSimulatorArm64()

    sourceSets {
        val iosMain by getting {
            dependencies {
                implementation(compose.runtime)
                implementation(compose.foundation)
                implementation(compose.material3)
                // Skia 의존성 추가
                implementation(compose.ui)
            }
        }
    }
}

핵심 포인트

  • compose.ui 를 추가하면 Skia 기반 렌더링이 자동으로 활성화됩니다.
  • iosArm64, iosX64, iosSimulatorArm64 를 모두 선언해 실제 디바이스와 시뮬레이터 모두에서 빌드가 가능하도록 합니다.

2️⃣ Xcode 프로젝트 연결

  1. Gradleframework 를 빌드합니다.

    ./gradlew :shared:linkReleaseFrameworkIosArm64
  2. Xcode에서 Framework Search Pathsshared/build/bin/iosArm64/releaseFramework 를 추가합니다.

  3. AppDelegate.swift 에서 Kotlin/Native 프레임워크를 임포트하고, ComposeView 를 루트 뷰로 설정합니다.

    import UIKit
    import shared // Kotlin/Native 프레임워크
    
    @UIApplicationMain
    class AppDelegate: UIResponder, UIApplicationDelegate {
        var window: UIWindow?
    
        func application(
            _ application: UIApplication,
            didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
        ) -> Bool {
            let root = ComposeViewController()
            window = UIWindow(frame: UIScreen.main.bounds)
            window?.rootViewController = root
            window?.makeKeyAndVisible()
            return true
        }
    }

Skia 렌더링 확인하기

Compose UI 코드를 작성하면 자동으로 Skia 백엔드가 사용됩니다. 아래 예제는 Canvas 를 이용해 원을 그리는 간단한 샘플입니다.

@Composable
fun SkiaDemo() {
    Canvas(modifier = Modifier.fillMaxSize()) {
        drawCircle(
            color = Color.Red,
            radius = size.minDimension / 4,
            center = Offset(size.width / 2, size.height / 2)
        )
    }
}
  • iOS 시뮬레이터 혹은 실제 디바이스에서 실행하면, Metal을 통해 GPU 가속 원이 부드럽게 렌더링되는 것을 확인할 수 있습니다.
  • ComposeView 내부에서 Modifier.graphicsLayer 를 사용하면 그림자, 클리핑, 투명도 등 고급 효과도 동일하게 적용됩니다.

성능 팁

상황권장 설정
복잡한 애니메이션Modifier.animate* 대신 rememberInfiniteTransition 사용
텍스처 매핑ImageBitmapSkiaBitmap 으로 변환 후 drawImage 호출
메모리 관리Canvas 에서 drawRect 등 기본 도형을 사용하고, 큰 이미지 로드 시 ImageBitmapremember 로 캐시
디버깅Xcode 콘솔에 skia 로그 레벨을 DEBUG 로 설정 (-DsKiaLogLevel=DEBUG)

iOS와 Android 간 차이점

항목Android (Skia)iOS (Skia)
백엔드OpenGL / VulkanMetal
색 공간sRGBDisplay‑P3 (기본)
텍스처 포맷RGBA_8888BGRA_8888 (Metal)
폰트 렌더링HarfBuzz + FreeTypeCoreText + Skia (내부)

주의: iOS에서는 Display‑P3 색 공간을 기본으로 사용하므로, 색상이 약간 다르게 보일 수 있습니다. 필요에 따라 ColorSpace.sRGB 로 강제 변환이 가능합니다.

마무리

Compose Multiplatform은 단일 코드베이스 로 Android와 iOS 모두에서 네이티브 수준의 UI 성능을 제공하도록 설계되었습니다.
Skia 렌더링 엔진을 활용하면:

  • GPU 가속을 통한 부드러운 애니메이션
  • 플랫폼 간 일관된 픽셀 표현
  • 쉽게 확장 가능한 커스텀 그래픽

을 구현할 수 있습니다.

앞으로도 JetBrains와 Kotlin 팀이 지속적으로 Compose UISkia 를 개선해 나갈 예정이니, 최신 릴리즈 노트를 주시하면서 새로운 기능을 실험해 보세요! 🚀


위 번역은 원본 글의 구조와 기술 용어를 그대로 유지하면서 한국어로 자연스럽게 표현하도록 노력했습니다.

배울 내용

이 워크스루를 마치면 iOS에서 Compose Multiplatform의 프레임 드롭을 일으키는 가장 흔한 세 가지 원인—Metal 셰이더 컴파일 정지, 텍스처 아틀라스 스러싱, ProMotion 마감 시간 초과—을 진단하고 해결하는 방법을 알게 됩니다. 또한 언제 네이티브 UIKitView 로 전환하는 것이 올바른 아키텍처 선택인지 정확히 판단할 수 있게 됩니다.

사전 요구 사항

  • iOS를 타깃으로 하는 작동하는 Compose Multiplatform 프로젝트
  • Instruments가 설치된 Xcode
  • ProMotion이 탑재된 실제 iOS 기기 (iPhone 13 Pro 이상)
  • Kotlin 및 기본 Compose 개념에 대한 숙지

Step 1: iOS에서 Skiko가 렌더링되는 방식 이해하기

시간을 절약해줄 핵심 포인트는 다음과 같습니다: Compose Multiplatform iOS 버전은 UIKit의 레이아웃 엔진을 사용하지 않습니다. Skiko를 통해 자체 Skia 인스턴스를 번들링하여 Metal을 직접 타깃으로 합니다. 당신의 @Composable 트리는 Skia 그리기 명령을 생성하고, 이는 Metal 셰이더 프로그램으로 컴파일되어 CAMetalLayer에 렌더링됩니다. UIKit 레이아웃 패스가 없으며, Core Animation의 암시적 트랜잭션도 없습니다.

Android에서는 OS가 Skia를 내부적으로 처리하고 백그라운드 RenderThread에서 셰이더를 사전 컴파일합니다. iOS에서는 새로운 셰이더 변형마다 Metal 컴파일러가 메인 스레드에서 실행됩니다.

요소Android (Pixel 7+)iOS (iPhone 13+)
Skia 백엔드OS‑integrated SkiaBundled Skiko/Skia
셰이더 컴파일Background RenderThreadMain thread via Metal
텍스처 아틀라스 메모리Managed by OS compositorApp‑level Metal allocations
120 fps 목표8.3 ms frame budget8.3 ms frame budget (ProMotion)
GPU 프로파일링Android GPU InspectorXcode Metal System Trace

Step 2: Diagnose Shader Compilation Stalls

가장 흔한 jank(프레임 끊김)의 원인은 셰이더 컴파일입니다. Skia가 새로운 그리기 구성—예를 들어 새로운 블렌드 모드, 클립 형태, 혹은 그라디언트 타입—을 만나면 Metal은 파이프라인 상태 객체(PSO)를 컴파일해야 합니다. 이 과정은 변형당 몇 밀리초씩 차단될 수 있습니다.

Xcode Instruments를 열고 GPUMetal System Trace 템플릿을 연결합니다. 다음 항목을 확인하세요:

  1. Metal System Trace에서 2 ms를 초과하는 MTLCreatePipelineState 호출
  2. 명령 버퍼 제출이 지연되어 GPU 트랙에 나타나는 프레임 간격
  3. Skia의 GrMtlPipelineStateBuilder와 연관된 Time Profiler의 메인 스레드 정지

문서에는 언급되지 않지만, 프레임‑시간 평균값은 이러한 스파이크를 완전히 숨깁니다. 프레임당 GPU 정지 데이터를 확인해야 합니다.

3단계: 시작 시 셰이더 워밍

스플래시 화면 동안 Skia가 가장 일반적인 셰이더 변형을 컴파일하도록 강제합니다. 오프‑스크린 캔버스를 렌더링하여 일반적인 그리기 작업을 수행합니다: 둥근 사각형, 흐릿한 그림자, 그라디언트 채우기, 다양한 스타일의 텍스트. 이렇게 하면 사용자가 인터랙티브 콘텐츠를 보기 전에 PSO 컴파일이 미리 수행됩니다.

Step 4: 텍스처 아틀라스 스래싱 해결

몇 초마다 주기적인 끊김이 보이면 Instruments의 Metal Resource Allocations를 확인하세요. GPU 메모리 할당/해제가 급증한다는 것은 아틀라스가 스래싱되고 있다는 의미이며, Skia의 글리프 캐시와 작은 이미지 영역이 퇴출되고 다시 업로드되고 있다는 뜻입니다.

Fix: 핫 경로에서 서로 다른 글꼴 크기와 이미지 스케일을 줄이세요. 아틀라스 영역에 깔끔하게 매핑되는 정수 스케일 이미지를 선호하세요.

Step 5: ProMotion에서 120 fps 달성

120 fps에서는 프레임 예산이 8.3 ms이며, 이는 60 fps의 대략 절반에 해당합니다. 두 개의 프레임이 떨어지면 끊김 현상으로 보입니다. 모든 프로젝트에서 적용 가능한 패턴:

  • derivedStateOf, 안정적인 키, 그리고 @Immutable 어노테이션을 적극적으로 사용하여 재구성 범위를 최소화합니다
  • 소프트웨어 렌더링 폴백을 주시하세요 — 복잡한 경로 클리핑에 RenderEffect 블러를 결합하면 CPU 래스터화로 전환될 수 있습니다
  • 높은 프레임 레이트에서 GC 압력을 줄이기 위해 PaintPath 객체를 draw 람다 외부에서 미리 할당합니다

중요한 애니메이션 경로에 대해 GPU 측 렌더링을 확인하려면 Metal System Trace로 프로파일링하세요.

Step 6: Native UIKitView를 언제 사용해야 할지 알기

UIKitView 인터옵을 도구로 여기고, 실패로 보지 마세요. 네이티브 UIKitView를 다음 경우에 사용합니다:

  • Maps — MapKit은 Skia로는 복제할 수 없는 방식으로 GPU 최적화가 되어 있습니다
  • Video playbackAVPlayerLayer는 OS 컴포지터와 직접 작동합니다
  • Web contentWKWebView는 네이티브 임베드로 사용할 때 가장 좋습니다
  • Text input — 네이티브 UITextField는 IME 가장자리 사례의 한 범주를 피합니다

주의사항

  • 시뮬레이터로 프로파일링하지 마세요. 시뮬레이터에서의 Metal 동작은 실제 기기와 일치하지 않습니다. 항상 실제 ProMotion 기기를 사용하세요.
  • 프레임 시간 평균은 속일 수 있습니다. 부드러운 119 fps 평균이 사용자에게 확실히 느껴지는 40 ms 셰이더 컴파일 스파이크를 숨길 수 있습니다. 프레임별 트레이스를 사용하세요.
  • Atlas 스래싱이 재구성 버그처럼 보입니다. 실제 문제는 단일 미워밍 셰이더 변형이나 너무 많은 글꼴 크기임에도 팀이 재구성 경계를 재배치하는 데 며칠을 소비합니다.
  • 플랫폼‑특정 컴포넌트를 위해 렌더러와 싸우는 것은 몇 주를 낭비합니다. 결국 누군가가 원래 UIKitView 래퍼를 작성하게 되지만, 더 늦고 더 좌절된 상태가 됩니다.

마무리

Metal 셰이더를 실행 시에 워밍업하고, 프레임 카운터 대신 Metal System Trace로 프로파일링하며, UIKitView를 사용해 플랫폼에 최적화된 컴포넌트를 부담 없이 활용하세요. 크로스‑플랫폼 일관성이 측정 가능한 제품 가치를 제공할 때만 렌더러와 맞서 싸세요.

0 조회
Back to Blog

관련 글

더 보기 »