Communitygithub.com

compose-multiplatform-patterns

Compose Multiplatform 및 Jetpack Compose 패턴 — 상태 관리, 네비게이션, 테마, 성능 및 플랫폼별 UI. KMP 프로젝트에서 Compose 또는 Jetpack Compose UI, 상태, 네비게이션, 테마를 구축할 때 사용합니다.

compose-multiplatform-patterns란 무엇인가요?

이 스킬은 Compose Multiplatform과 Jetpack Compose를 사용하여 Android, iOS, Desktop, Web에서 공유 UI를 구축하기 위한 모범 사례와 패턴을 제공합니다. ViewModel과 단일 상태 객체를 활용한 상태 관리, Material 3 동적 테마, 타입 안전 네비게이션(Compose Navigation 2.8+), 슬롯 기반 API 설계, Modifier 순서, 안정적인 타입을 통한 재구성 최적화 등 Compose 기반 KMP 프로젝트에서 필요한 핵심 패턴을 다룹니다. 또한 `expect/actual` 패턴을 사용한 플랫폼별 UI 구현, 이벤트 싱크 패턴, Lazy List 성능 최적화, 피해야 할 안티 패턴을 포함합니다. 이 스킬은 Claude Code, Cursor, Codex와 같은 AI 에이전트와 함께 사용할 때 유용하며, 생산성을 높이고 일반적인 실수를 방지합니다.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/everything-claude-code/tree/main/skills/compose-multiplatform-patterns

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

Compose Multiplatform 패턴

Compose Multiplatform과 Jetpack Compose를 사용하여 Android, iOS, Desktop, Web에서 공유 UI를 구축하기 위한 패턴입니다. 상태 관리, 네비게이션, 테마, 성능을 다룹니다.

활성화 시점

  • Compose UI (Jetpack Compose 또는 Compose Multiplatform) 구축 시
  • ViewModel 및 Compose 상태로 UI 상태 관리 시
  • KMP 또는 Android 프로젝트에서 네비게이션 구현 시
  • 재사용 가능한 컴포저블 및 디자인 시스템 설계 시
  • 재구성 및 렌더링 성능 최적화 시

상태 관리

ViewModel + 단일 상태 객체

화면 상태에 단일 데이터 클래스를 사용합니다. StateFlow로 노출하고 Compose에서 수집합니다:

data class ItemListState(
    val items: List<Item> = emptyList(),
    val isLoading: Boolean = false,
    val error: String? = null,
    val searchQuery: String = ""
)

class ItemListViewModel(
    private val getItems: GetItemsUseCase
) : ViewModel() {
    private val _state = MutableStateFlow(ItemListState())
    val state: StateFlow<ItemListState> = _state.asStateFlow()

    fun onSearch(query: String) {
        _state.update { it.copy(searchQuery = query) }
        loadItems(query)
    }

    private fun loadItems(query: String) {
        viewModelScope.launch {
            _state.update { it.copy(isLoading = true) }
            getItems(query).fold(
                onSuccess = { items -> _state.update { it.copy(items = items, isLoading = false) } },
                onFailure = { e -> _state.update { it.copy(error = e.message, isLoading = false) } }
            )
        }
    }
}

Compose에서 상태 수집

@Composable
fun ItemListScreen(viewModel: ItemListViewModel = koinViewModel()) {
    val state by viewModel.state.collectAsStateWithLifecycle()

    ItemListContent(
        state = state,
        onSearch = viewModel::onSearch
    )
}

@Composable
private fun ItemListContent(
    state: ItemListState,
    onSearch: (String) -> Unit
) {
    // 상태 없는 컴포저블 — 미리보기 및 테스트 용이
}

이벤트 싱크 패턴

복잡한 화면에서는 여러 콜백 람다 대신 이벤트를 위한 sealed 인터페이스를 사용합니다:

sealed interface ItemListEvent {
    data class Search(val query: String) : ItemListEvent
    data class Delete(val itemId: String) : ItemListEvent
    data object Refresh : ItemListEvent
}

// ViewModel 내
fun onEvent(event: ItemListEvent) {
    when (event) {
        is ItemListEvent.Search -> onSearch(event.query)
        is ItemListEvent.Delete -> deleteItem(event.itemId)
        is ItemListEvent.Refresh -> loadItems(_state.value.searchQuery)
    }
}

// Composable 내 — 여러 람다 대신 단일 람다
ItemListContent(
    state = state,
    onEvent = viewModel::onEvent
)

네비게이션

타입 안전 네비게이션 (Compose Navigation 2.8+)

라우트를 @Serializable 객체로 정의합니다:

@Serializable data object HomeRoute
@Serializable data class DetailRoute(val id: String)
@Serializable data object SettingsRoute

@Composable
fun AppNavHost(navController: NavHostController = rememberNavController()) {
    NavHost(navController, startDestination = HomeRoute) {
        composable<HomeRoute> {
            HomeScreen(onNavigateToDetail = { id -> navController.navigate(DetailRoute(id)) })
        }
        composable<DetailRoute> { backStackEntry ->
            val route = backStackEntry.toRoute<DetailRoute>()
            DetailScreen(id = route.id)
        }
        composable<SettingsRoute> { SettingsScreen() }
    }
}

다이얼로그 및 바텀 시트 네비게이션

명령형 show/hide 대신 dialog() 및 오버레이 패턴을 사용합니다:

NavHost(navController, startDestination = HomeRoute) {
    composable<HomeRoute> { /* ... */ }
    dialog<ConfirmDeleteRoute> { backStackEntry ->
        val route = backStackEntry.toRoute<ConfirmDeleteRoute>()
        ConfirmDeleteDialog(
            itemId = route.itemId,
            onConfirm = { navController.popBackStack() },
            onDismiss = { navController.popBackStack() }
        )
    }
}

컴포저블 설계

슬롯 기반 API

유연성을 위해 슬롯 매개변수로 컴포저블을 설계합니다:

@Composable
fun AppCard(
    modifier: Modifier = Modifier,
    header: @Composable () -> Unit = {},
    content: @Composable ColumnScope.() -> Unit,
    actions: @Composable RowScope.() -> Unit = {}
) {
    Card(modifier = modifier) {
        Column {
            header()
            Column(content = content)
            Row(horizontalArrangement = Arrangement.End, content = actions)
        }
    }
}

Modifier 순서

Modifier 순서가 중요합니다 — 다음 순서로 적용합니다:

Text(
    text = "Hello",
    modifier = Modifier
        .padding(16.dp)          // 1. 레이아웃 (padding, size)
        .clip(RoundedCornerShape(8.dp))  // 2. 형태
        .background(Color.White) // 3. 그리기 (background, border)
        .clickable { }           // 4. 상호 작용
)

KMP 플랫폼별 UI

플랫폼 컴포저블을 위한 expect/actual

// commonMain
@Composable
expect fun PlatformStatusBar(darkIcons: Boolean)

// androidMain
@Composable
actual fun PlatformStatusBar(darkIcons: Boolean) {
    val systemUiController = rememberSystemUiController()
    SideEffect { systemUiController.setStatusBarColor(Color.Transparent, darkIcons) }
}

// iosMain
@Composable
actual fun PlatformStatusBar(darkIcons: Boolean) {
    // iOS는 UIKit interop 또는 Info.plist를 통해 처리
}

성능

건너뛸 수 있는 재구성을 위한 안정적인 타입

모든 속성이 안정적인 경우 클래스를 @Stable 또는 @Immutable로 표시합니다:

@Immutable
data class ItemUiModel(
    val id: String,
    val title: String,
    val description: String,
    val progress: Float
)

key() 및 Lazy 목록 올바르게 사용하기

LazyColumn {
    items(
        items = items,
        key = { it.id }  // 안정적인 키는 항목 재사용 및 애니메이션 활성화
    ) { item ->
        ItemRow(item = item)
    }
}

derivedStateOf로 읽기 지연

val listState = rememberLazyListState()
val showScrollToTop by remember {
    derivedStateOf { listState.firstVisibleItemIndex > 5 }
}

재구성 시 할당 피하기

// 나쁨 — 매 재구성마다 새 람다와 목록 생성
items.filter { it.isActive }.forEach { ActiveItem(it, onClick = { handle(it) }) }

// 좋음 — 각 항목에 키를 부여하여 콜백이 올바른 행에 유지됨
val activeItems = remember(items) { items.filter { it.isActive } }
activeItems.forEach { item ->
    key(item.id) {
        ActiveItem(item, onClick = { handle(item) })
    }
}

테마

Material 3 동적 테마

@Composable
fun AppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    dynamicColor: Boolean = true,
    content: @Composable () -> Unit
) {
    val colorScheme = when {
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            if (darkTheme) dynamicDarkColorScheme(LocalContext.current)
            else dynamicLightColorScheme(LocalContext.current)
        }
        darkTheme -> darkColorScheme()
        else -> lightColorScheme()
    }

    MaterialTheme(colorScheme = colorScheme, content = content)
}

피해야 할 안티 패턴

  • ViewModel에서 MutableStateFlow와 collectAsStateWithLifecycle이 수명 주기에 더 안전한데 mutableStateOf 사용
  • NavController를 컴포저블 깊숙이 전달 — 대신 람다 콜백 전달
  • @Composable 함수 내에서 무거운 계산 — ViewModel 또는 remember {}로 이동
  • ViewModel 초기화 대신 LaunchedEffect(Unit) 사용 — 일부 설정에서 구성 변경 시 재실행됨
  • 컴포저블 매개변수에서 새 객체 인스턴스 생성 — 불필요한 재구성 유발

참고 자료

스킬 참조: android-clean-architecture — 모듈 구조 및 계층화 스킬 참조: kotlin-coroutines-flows — 코루틴 및 Flow 패턴

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/claude-api

Anthropic Claude API patterns for Python and TypeScript. Covers Messages API, streaming, tool use, vision, extended thinking, batches, prompt caching, and Claude Agent SDK. Use when building applications with the Claude API or Anthropic SDKs.

affaan-m/everything-claude-code

End-to-end marketing campaign planning and execution. Covers audience research, positioning, campaign angle definition, landing page copy, email sequences, social posts, ad copy, short-form video scripts, and content calendars. Use as the orchestration layer for multi-channel product launches. Use when planning or executing a multi-channel product launch, or producing landing page, email, social, or ad copy.

affaan-m/everything-claude-code

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/everything-claude-code-conventions

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/frontend-design

Create distinctive, production-grade frontend interfaces with high design quality. Use when the user asks to build web components, pages, or applications and the visual direction matters as much as the code quality.

affaan-m/gget

gget CLI and Python workflow for quick genomic database queries, sequence lookup, BLAST-style searches, enrichment checks, and reproducible bioinformatics evidence logs.

affaan-m/literature-review

Systematic literature-review workflow for academic, biomedical, technical, and scientific topics, including search planning, source screening, synthesis, citation checks, and evidence logging.

affaan-m/motion-ui

Production-ready UI motion system for React/Next.js. Use when implementing animations, transitions, or motion patterns.

affaan-m/project-guidelines-example

Example project-specific skill template based on a real production application.

affaan-m/pubmed-database

Direct PubMed and NCBI E-utilities search workflows for biomedical literature, MeSH queries, PMID lookup, citation retrieval, and API-backed literature monitoring.

affaan-m/scholar-evaluation

Structured scholarly-work evaluation for papers, proposals, literature reviews, methods sections, evidence quality, citation support, and research-writing feedback.

affaan-m/uspto-database

USPTO patent and trademark data workflow for official record lookup, PatentSearch queries, TSDR checks, assignment data, and reproducible IP research logs.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

관련 스킬