현대 모바일 앱 개발 환경에서 '단일 코드베이스(One Codebase)'에 대한 요구는 비용 효율성과 유지보수 측면에서 끊임없이 제기되어 왔습니다. 네이티브(iOS, Android) 개발은 최상의 사용자 경험(UX)과 성능을 보장하지만, 비즈니스 로직의 중복 구현, 파편화된 기능, 그리고 두 배의 인적 자원 투입이라는 명확한 병목(Bottleneck)을 동반합니다. 이러한 배경에서 등장한 크로스플랫폼 솔루션들은 각기 다른 접근 방식으로 이 문제를 해결하려 시도합니다.
Google의 Flutter와 JetBrains의 KMM(Kotlin Multiplatform Mobile)은 현재 가장 주목받는 기술이지만, 두 기술의 지향점은 근본적으로 다릅니다. Flutter가 렌더링 엔진 자체를 통합하여 UI의 일관성을 강제한다면, KMM은 비즈니스 로직의 공유에 집중하고 UI는 각 플랫폼의 네이티브 툴킷에 위임하는 실용주의적 노선을 택합니다. 본 글에서는 단순한 기능 비교를 넘어, 아키텍처 관점에서 두 기술의 트레이드오프를 분석하고, 실제 프로덕션 환경 도입 시 고려해야 할 전략적 판단 기준을 제시합니다.
1. Flutter: 자체 렌더링 엔진을 통한 UI 추상화
Flutter의 아키텍처는 기존 크로스플랫폼 프레임워크(React Native 등)가 사용하던 OEM 위젯 브릿지(Bridge) 방식과 궤를 달리합니다. Flutter는 OS가 제공하는 UI 컴포넌트를 사용하지 않고, C++로 작성된 Skia(또는 최신 Impeller) 그래픽 엔진을 통해 화면의 모든 픽셀을 직접 그립니다(Paint). 이는 마치 게임 엔진(Unity, Unreal)이 동작하는 방식과 유사합니다.
1.1 아키텍처 개요 및 렌더링 파이프라인
Flutter 앱은 크게 세 가지 계층으로 구성됩니다. 개발자가 주로 다루는 Framework(Dart), 고성능 그래픽 처리를 담당하는 Engine(C++), 그리고 각 플랫폼별 진입점을 관리하는 Embedder입니다.
// Flutter: 선언형 UI 및 상태 관리 예시
// UI가 상태(State)의 함수임을 명시적으로 보여줌: UI = f(State)
class CounterScreen extends StatefulWidget {
@override
_CounterScreenState createState() => _CounterScreenState();
}
class _CounterScreenState extends State<CounterScreen> {
int _counter = 0;
void _increment() {
// setState 호출 시 dirty 상태로 마킹되고,
// 다음 프레임에서 build()가 재호출되어 UI가 갱신됨
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Flutter Architecture")),
body: Center(
child: Text('Count: $_counter'),
),
floatingActionButton: FloatingActionButton(
onPressed: _increment,
child: Icon(Icons.add),
),
);
}
}
1.2 기술적 트레이드오프 분석
Flutter의 'Everything is a Widget' 철학은 강력한 일관성을 제공하지만, 다음과 같은 기술적 부채를 고려해야 합니다.
- Platform Channel Overhead: 블루투스, 센서 등 하드웨어 접근이 필요한 경우
MethodChannel을 통해 네이티브 코드와 비동기 통신을 해야 합니다. 데이터 직렬화/역직렬화 비용이 발생하며, 네이티브 API 변경 시 플러그인 의존성 문제가 발생할 수 있습니다. - Bundle Size: 앱 패키지에 엔진과 프레임워크가 포함되므로, 단순한 "Hello World" 앱이라도 네이티브 대비 초기 용량이 큽니다(약 4MB+).
- Non-Native UX: 스크롤 물리 엔진이나 텍스트 선택 동작 등을 Flutter가 모방(Mimic)하여 구현했기 때문에, OS 업데이트에 따라 미묘한 이질감(Uncanny Valley)이 발생할 수 있습니다.
2. KMM: 비즈니스 로직 공유와 네이티브 UI의 결합
KMM(Kotlin Multiplatform Mobile)은 '공유할 수 있는 것만 공유한다'는 철학을 따릅니다. UI는 각 플랫폼의 최신 기술(Jetpack Compose, SwiftUI)을 그대로 사용하고, 데이터 레이어, 도메인 로직, 네트워크 통신 등 플랫폼에 구애받지 않는 순수 로직만을 Kotlin으로 작성하여 공유합니다.
2.1 멀티플랫폼 컴파일 메커니즘
KMM은 JVM(Android)과 LLVM(iOS - Native Binary) 컴파일러를 활용합니다. Android에서는 일반적인 라이브러리처럼 동작하지만, iOS에서는 Objective-C 프레임워크로 컴파일되어 Swift 코드에서 호출 가능한 형태가 됩니다. 이를 가능하게 하는 핵심 키워드는 expect와 actual입니다.
// [Common] shared/src/commonMain/kotlin/Platform.kt
// 인터페이스 정의와 유사하게 기대(expect)되는 선언
expect class Platform() {
val platform: String
}
// [Android] shared/src/androidMain/kotlin/Platform.kt
// 안드로이드 플랫폼에 특화된 실제(actual) 구현
actual class Platform actual constructor() {
actual val platform: String = "Android ${android.os.Build.VERSION.SDK_INT}"
}
// [iOS] shared/src/iosMain/kotlin/Platform.kt
// iOS 플랫폼에 특화된 실제(actual) 구현 (UIKit 등 접근 가능)
import platform.UIKit.UIDevice
actual class Platform actual constructor() {
actual val platform: String =
UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
}
2.2 아키텍처 및 통합 전략
KMM 프로젝트는 일반적으로 클린 아키텍처(Clean Architecture)와 잘 어울립니다. Data Layer와 Domain Layer는 shared 모듈에 두고, Presentation Layer(ViewModel 포함 여부는 선택)와 UI는 각 플랫폼 앱(App Module)에 위치시킵니다.
- 100% Native UI: SwiftUI와 Jetpack Compose의 최신 기능을 즉시 활용할 수 있습니다. OS가 업데이트되어 새로운 UI 컴포넌트가 나와도 래퍼(Wrapper) 없이 바로 적용 가능합니다.
- 점진적 도입(Gradual Adoption): 기존에 운영 중인 거대한 네이티브 앱이 있을 때, 전체를 재작성할 필요 없이 특정 비즈니스 로직(예: 새로운 인증 모듈)부터 KMM으로 대체해 나갈 수 있습니다. 이는 리스크 관리에 탁월합니다.
3. 비교 분석 및 의사결정 프레임워크
기술 선택은 "어떤 것이 더 좋은가?"가 아니라 "우리 조직의 제약사항과 목표에 무엇이 더 적합한가?"로 귀결되어야 합니다. 다음은 주요 지표별 비교 분석입니다.
| 비교 항목 | Flutter | KMM (Kotlin Multiplatform) |
|---|---|---|
| UI Consistency | Pixel-perfect (동일함 보장) | Platform-specific (네이티브 룩앤필) |
| Code Sharing | High (UI + Logic, >90%) | Medium (Logic only, ~50-70%) |
| Performance | Near Native (Skia rendering) | Native (Logic compiled to binary) |
| Team Structure | Dart 개발자 중심 단일 팀 | Android/iOS 전문성 + Kotlin 역량 필요 |
| Use Case | MVP, 신규 서비스, UI 커스텀이 많은 앱 | 기존 네이티브 앱 고도화, 로직 복잡도가 높은 앱 |
3.1 시나리오별 추천 전략
Greenfield Project (신규 구축)
스타트업이나 신규 프로젝트로, 빠른 시장 진입(Time-to-market)이 최우선이라면 Flutter가 강력한 선택지입니다. 단일 코드베이스로 두 플랫폼을 동시에 릴리즈할 수 있으며, 핫 리로드(Hot Reload)를 통한 개발 생산성은 압도적입니다. 특히 디자이너가 요구하는 독창적인 UI를 플랫폼 제약 없이 구현해야 할 때 유리합니다.
Brownfield Project (기존 앱 마이그레이션)
이미 수십만 이상의 사용자를 보유한 네이티브 앱이 있고, 이를 유지보수하면서 효율화를 꾀해야 한다면 KMM이 정답에 가깝습니다. UI를 갈아엎는 리스크 없이, 내부적으로 중복된 데이터 모델과 네트워크 로직을 shared 모듈로 통합하여 기술 부채를 줄일 수 있습니다. iOS 개발자들은 기존 Swift UI 코드를 유지하면서 로직만 Kotlin 프레임워크에서 가져다 쓰면 되므로 저항감이 덜합니다.
Hardware Heavy App
IoT 제어, 카메라 필터링, AR 등 하드웨어 의존도가 극도로 높은 앱의 경우, Flutter의 채널 통신 오버헤드나 플러그인 관리 비용이 증가할 수 있습니다. 이 경우 KMM을 통해 로직만 공유하고, 하드웨어 제어는 각 플랫폼의 네이티브 API를 직접 호출하는 방식이 더 안정적일 수 있습니다.
결론: 은탄환은 없다
Flutter와 KMM은 서로 다른 문제를 해결하기 위해 설계된 도구입니다. Flutter는 '개발 속도와 UI 통일성'을 위해 네이티브와의 결합을 느슨하게 만들었고, KMM은 '네이티브 품질과 로직 재사용'을 위해 플랫폼 종속성을 인정하고 들어갑니다. 엔지니어링 리드로서 중요한 것은 트렌드를 쫓는 것이 아니라, 현재 팀의 인적 구성(Dart vs Kotlin/Swift)과 프로젝트의 성격(UI 중심 vs 로직 중심)을 냉철하게 분석하여, 향후 3년 이상의 유지보수 비용을 최소화할 수 있는 아키텍처를 선택하는 것입니다.
Post a Comment