9148c358d0
Topic_Agent/Topic_Blog/Topics/Topics_Biz/Topics_Meeting/Topics_Rag의 마크다운 지식 문서를 Topic_General/Topic_Programming/Topic_Graphic/Topic_Business 4개 카테고리로 재분류. - 중복 제거: frontmatter의 status:duplicate/merged + duplicate_of/redirect_to 필드로 자기 자신을 중복으로 선언한 리다이렉트 stub 1032개 제거, 완전 동일 내용 파일 472개 제거, 동일 파일명·다른 내용 충돌 시 더 큰(완전한) 버전만 유지(162개 제거) — 총 1639개 중복 제거. - 분류: 폴더 단위로 명확한 항목(AI_and_ML/Coding/Architecture 등 → Programming, Comfyui/Visual_Effects → Graphic, Topics_Biz/Topics_Meeting/사업 등 → Business, Poetic_Blog_Writing/창의성/Game_Design 등 → General)은 폴더 우선순위로, 나머지 혼재 폴더(Topic_Agent/Topic_Blog/Topics 루트/Thinking & Reasoning/Other/UI_UX_Assets)는 title/tags 키워드 스코어링으로 파일 단위 분류(불명확한 경우 General로 폴백). 원본 폴더명은 "From_*" 서브폴더로 보존해 추적 가능성 유지. - 최종 배치: Programming 2784 / General 1608 / Graphic 285 / Business 249 = 4926개 문서. - 에이전트 운영 상태(.astra/.agent/.obsidian/sessions/memory/_company/docs/lessons/_shared/src)는 지식 콘텐츠가 아니므로 재분류 대상에서 제외하고 원위치 유지. - Topics/Topic_email(상위 보호 폴더 Topic_email과 파일명 100% 중복) 삭제 — 보호 폴더 자체는 미변경. - 완전히 비게 된 Topic_Agent/Topic_Blog/Topics_Biz/Topics_Rag 폴더 제거.
7.1 KiB
7.1 KiB
id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
| id | title | category | status | source_trust_level | verification_status | created_at | updated_at | tags | tech_stack | applied_in | aliases | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| mobile-kmp-compose | Kotlin Multiplatform / Compose Multiplatform | Coding | draft | B | conceptual | 2026-05-09 | 2026-05-09 |
|
|
|
Kotlin Multiplatform (KMP) + Compose Multiplatform
Android / iOS / Desktop / Web 같은 비즈니스 로직 (KMP). UI 도 공유 가능 (Compose Multiplatform). Native UI 가 strict 면 KMP 만, fast share 면 둘 다.
📖 핵심 개념
- KMP: 비즈니스 로직 (data, network, repository) 공유.
- Compose Multiplatform: UI 도 같은 코드.
- expect/actual: platform-specific.
- iOS = framework / Cocoapods 으로 import.
💻 코드 패턴
폴더 (gradle)
shared/
src/
commonMain/ # 모든 platform
androidMain/ # Android only
iosMain/ # iOS only
desktopMain/ # JVM desktop
jsMain/ # Web
androidApp/
iosApp/
desktopApp/
shared module (build.gradle.kts)
plugins {
kotlin("multiplatform")
id("com.android.library")
}
kotlin {
androidTarget()
iosX64()
iosArm64()
iosSimulatorArm64()
jvm("desktop")
cocoapods {
version = "1.0"
summary = "Shared module"
homepage = "..."
ios.deploymentTarget = "17.0"
framework {
baseName = "Shared"
isStatic = true
}
}
sourceSets {
commonMain.dependencies {
implementation("io.ktor:ktor-client-core:2.3.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.0")
}
androidMain.dependencies {
implementation("io.ktor:ktor-client-okhttp:2.3.0")
}
iosMain.dependencies {
implementation("io.ktor:ktor-client-darwin:2.3.0")
}
}
}
Common code
// shared/commonMain/Repository.kt
class UserRepository(private val client: HttpClient) {
suspend fun fetchUser(id: String): User {
return client.get("$BASE/users/$id").body()
}
}
@Serializable
data class User(val id: String, val email: String, val name: String)
expect/actual (platform 별 구현)
// commonMain
expect class Platform() {
val name: String
}
// androidMain
actual class Platform actual constructor() {
actual val name: String = "Android ${android.os.Build.VERSION.SDK_INT}"
}
// iosMain
import platform.UIKit.UIDevice
actual class Platform actual constructor() {
actual val name: String = "iOS ${UIDevice.currentDevice.systemVersion}"
}
Android 사용
// androidApp
val repo = UserRepository(httpClient)
val user = lifecycleScope.launch { repo.fetchUser("u1") }
iOS 사용 — Swift
import Shared
let repo = UserRepository(client: ...)
Task {
let user = try await repo.fetchUser(id: "u1")
}
// Kotlin suspend → Swift async/await 자동 (KMP 1.9+)
Compose Multiplatform (UI 공유)
// shared/commonMain/App.kt
import androidx.compose.runtime.*
import androidx.compose.material3.*
@Composable
fun App(repo: UserRepository) {
var user by remember { mutableStateOf<User?>(null) }
LaunchedEffect(Unit) { user = repo.fetchUser("u1") }
Scaffold { padding ->
Text(user?.name ?? "Loading", modifier = Modifier.padding(padding))
}
}
// iosApp/iOSApp.swift
import SwiftUI
import Shared
struct ContentView: View {
var body: some View {
ComposeView()
.ignoresSafeArea(.all, edges: .bottom)
}
}
struct ComposeView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
return Main_iosKt.MainViewController()
}
func updateUIViewController(_ vc: UIViewController, context: Context) {}
}
Network — Ktor Client
val client = HttpClient {
install(ContentNegotiation) {
json(Json { ignoreUnknownKeys = true })
}
defaultRequest {
url(BASE_URL)
header("Authorization", "Bearer $token")
}
}
Database — SQLDelight
// shared/commonMain/sqldelight/com/acme/AppDatabase.sq
CREATE TABLE User (
id TEXT PRIMARY KEY NOT NULL,
email TEXT NOT NULL
);
selectAll:
SELECT * FROM User;
insert:
INSERT OR REPLACE INTO User VALUES (?, ?);
val db = AppDatabase(driver)
db.userQueries.insert("u1", "a@b.com")
val users = db.userQueries.selectAll().executeAsList()
State management
class UserViewModel(private val repo: UserRepository) {
private val _state = MutableStateFlow<UiState>(UiState.Loading)
val state: StateFlow<UiState> = _state
suspend fun load(id: String) {
_state.value = UiState.Loading
try {
_state.value = UiState.Success(repo.fetchUser(id))
} catch (e: Exception) {
_state.value = UiState.Error(e.message ?: "")
}
}
}
sealed class UiState {
object Loading : UiState()
data class Success(val user: User) : UiState()
data class Error(val msg: String) : UiState()
}
iOS 호출 — coroutines 변환
// KMP 1.9+ 자동 async/await.
// 또는 NativeCoroutines 라이브러리 — Combine / async stream.
Test (commonTest)
class UserRepositoryTest {
@Test
fun fetchUser() = runTest {
val mock = MockEngine { ... }
val repo = UserRepository(HttpClient(mock))
val user = repo.fetchUser("u1")
assertEquals("u1", user.id)
}
}
→ JVM 에서 실행 (commonTest), iOS / Android target 에 자동 적용.
Trade-offs
KMP 비즈니스 로직만:
+ Native UI 강력 / 잘 쓰면 빠름
+ iOS / Android 가 Swift / Kotlin 그대로
- UI 코드 두 번
Compose Multiplatform:
+ UI 도 한 코드
- iOS UI 가 Material — Apple Human Interface 와 차이
- 일부 native 기능 어려움 (camera, notification)
🤔 의사결정 기준
| 상황 | 추천 |
|---|---|
| Logic 만 공유 | KMP only |
| Logic + UI 같이 | Compose Multiplatform |
| Native UX 강 critical | KMP only |
| Internal tool / B2B | Compose MP |
| Consumer app | KMP + native UI |
| Cross-platform alternative | React Native / Flutter |
❌ 안티패턴
- Compose iOS = Material design 그대로: HIG 위반. 별 styling.
- Common code 안 platform 의존: 빌드 깨짐. expect/actual.
- iOS 의 Combine / SwiftUI 통합 무시: NativeCoroutines.
- Cocoapods + SPM 혼합: 한 가지 선택.
- 빌드 시간 무시: KMP 가 처음 빌드 길음.
- Native API + KMP 의존: 양쪽 의존. Either / Or.
- Web target 가정 prod ready: Compose Web 은 아직 alpha.
🤖 LLM 활용 힌트
- 비즈니스 로직 = KMP, UI = native (대부분 case).
- Ktor + SQLDelight + kotlinx-serialization 3종 표준.
- expect/actual = platform-specific.