Flutter·React Native·KMP·네이티브 선택 기준
이 장을 읽기 전에: 프론트엔드 개발의 기본 개념(3장)을 파악하고 있으면 이해하기 쉽다. 다만, 모바일 개발 경험이 없어도 읽을 수 있는 구성으로 되어 있다.
모바일 개발에서 처음으로 결정해야 할 것은 언어나 프레임워크가 아니다. 먼저 결정해야 할 것은 어떤 배포 경로를 사용하는가, 어디까지 단말 기능을 사용하는가, 기존 팀의 강점을 어디에 두는가 의 3가지다.
웹 서비스의 연장으로 시작한다면 PWA로 충분한 경우도 있다. 한편 알림, 인앱 결제, 카메라, 백그라운드 처리, 스토어 배포, 단말 고유의 조작감까지 요구한다면 네이티브 앱이나 크로스플랫폼 앱을 전제로 생각하는 편이 좋다.
� 한국 시장 특이점: 한국은 Android 점유율이 약 70%(삼성·LG 중심), iOS가 약 30%다. 카카오·네이버·라인 등 국내 주요 기업은 Android와 iOS를 모두 운영하며, 배달의민족·당근마켓·토스 등은 네이티브 또는 크로스플랫폼(Flutter/React Native)을 주력으로 사용한다.
이 섹션이 답하는 질문: iOS와 Android를 별도로 만들어야 하는가, 하나의 접근 방식으로 양쪽을 다뤄야 하는가?
모바일 개발은 기술 선정을 잘못하면 나중에 수정하기 어렵다. 특히 다음 3가지는 초기 판단의 영향이 크다.
배포 경로 (스토어 vs PWA)
하드웨어 기능에 대한 의존도 (카메라, NFC, 블루투스, 생체인증 등)
팀의 기존 스킬 (웹 개발자 vs 모바일 전담)
앱 스토어 배포가 불필요하고 알림이나 오프라인도 한정적이라면 PWA로 충분한 경우가 있다. 반대로, 스토어 인앱 결제, 무거운 애니메이션, AR, 블루투스, 백그라운드 실행 등이 중요하다면 처음부터 네이티브 또는 네이티브 방향의 구성을 선택하는 편이 재작업이 적다.
⚠️ 소규모 팀은 먼저 배포 경로와 기존 스킬로 좁히면 판단이 빠르다. 그 위에서 단말 고유 요건이 강하다면 네이티브로 기울인다는 순서로 하면 실패하기 어렵다.
정답은 하나가 아니다. 단 다음 정리는 꽤 안정적이다.
PWA: 배포 속도와 웹 재이용을 최우선할 때
크로스플랫폼: 소인원으로 iOS / Android를 동시에 육성하고 싶을 때
네이티브: UX와 단말 통합이 경쟁력 그 자체가 될 때
이 섹션이 답하는 질문: Expo / React Native, Flutter, Kotlin Multiplatform은 어떻게 다른가?
� 국내 채용 및 사용 현황:
- Flutter: 카카오, 배달의민족(일부), 국내 스타트업에서 활발히 채택. 당근마켓이 Flutter 전환 사례를 공개 블로그에서 소개
- React Native: 국내 중소 IT 기업, 에이전시 위주. 네이버 일부 서비스에서 사용
- KMP: 카카오·라인 등 Kotlin 강팀 위주로 도입 검토 증가
Expo는 "네이티브 기능을 사용할 수 없는 간이판"이라는 이해로는 이제 불충분하다. 현재는 prebuild, config plugin, Expo Modules에 의해 많은 네이티브 연계를 단계적으로 다룰 수 있다.
따라서 React / TypeScript 팀이 모바일에 참입한다면, 먼저 Expo에서 시작하는 판단은 상당히 합리적이다.
단, 다음 요건이 강해질수록 빠르게 네이티브 경계를 명시하는 편이 좋다.
독자 네이티브 모듈
저수준의 블루투스 / NFC / 센서 처리
스토어 배포 전제에서의 세밀한 빌드 차이 관리
React Native 라이브러리 호환성 검증
⚠️ Expo에서 시작해 필요해지면 네이티브 측으로 내려가는 단계적 진행은 타당하다. 단 "언제든지 무통으로 내려갈 수 있다"고 생각하는 것은 위험하며, 이른 단계에서 의존 라이브러리의 대응 상황을 확인할 필요가 있다.
// Expo Router v3 — App Router 방식 파일 기반 라우팅
// app/(tabs)/index.tsx — 탭 홈 화면
import { StyleSheet, Text, View } from 'react-native';
export default function HomeScreen() {
return (
<View style={styles.container}>
<Text style={styles.title}>홈</Text>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, alignItems: 'center', justifyContent: 'center' },
title: { fontSize: 24, fontWeight: 'bold' },
});
Flutter는 UI를 강하게 제어하고 싶은 안건과 궁합이 좋다. 독자 렌더링(Impeller 엔진)에 의해 OS 차이를 흡수하면서 일관된 외관을 만들기 쉽다.
적합한 장면은 다음과 같다.
디자인 재현성을 중시한다
Android와 iOS에서 외관을 맞추고 싶다
애니메이션이나 커스텀 UI가 많다
웹 팀 자산보다 모바일 체험을 우선한다
// Flutter 3.x — Riverpod + Freezed 패턴 (국내 스타트업 표준)
@freezed
class ProductState with _$ProductState {
const factory ProductState.initial() = _Initial;
const factory ProductState.loading() = _Loading;
const factory ProductState.loaded(List<Product> products) = _Loaded;
const factory ProductState.error(String message) = _Error;
}
@riverpod
class ProductNotifier extends _$ProductNotifier {
@override
ProductState build() => const ProductState.initial();
Future<void> fetchProducts() async {
state = const ProductState.loading();
try {
final products = await ref.read(productRepositoryProvider).getAll();
state = ProductState.loaded(products);
} catch (e) {
state = ProductState.error('상품을 불러오는 데 실패했습니다: $e');
}
}
}
Kotlin Multiplatform은 UI를 완전히 공유하는 기술이라기보다, 비즈니스 로직 공유의 기술로 이해하는 편이 실무에서는 안전하다.
특히 다음 공유에 적합하다.
API 클라이언트
인증이나 권한 규칙
유효성 검사
캐시 전략
도메인 로직
ViewModel 상당의 상태 관리
그 위에서 UI는 SwiftUI와 Jetpack Compose로 각각 만든다. 이 형태가 가장 안정적이다.
Expo / React Native: 웹 팀이 최단으로 참입하고 싶다
Flutter: UI 제어를 강하게 갖고 싶다
KMP: 네이티브 UI를 유지하면서 로직을 공유하고 싶다
이 섹션이 답하는 질문: 네이티브 개발을 선택한다면 무엇을 사용하는가?
iOS
SwiftUI는 중심적인 UI 프레임워크가 되고 있지만, 기존 자산이나 일부 고급 부품에서는 UIKit의 이해가 아직 필요하다. 또한 최신 Swift 계열에서는 동시성 체크가 강화되어 있어 오래된 코드를 그대로 이식하면 경고나 수정이 늘어나는 경우가 있다.
// SwiftUI + Swift Concurrency + Observation 패턴 (iOS 17+)
import SwiftUI
import Observation
@Observable
class ProductViewModel {
var products: [Product] = []
var isLoading = false
var errorMessage: String?
func loadProducts() async {
isLoading = true
defer { isLoading = false }
do {
products = try await ProductService.shared.fetchAll()
} catch {
errorMessage = "상품 목록을 불러오지 못했습니다."
}
}
}
struct ProductListView: View {
@State private var viewModel = ProductViewModel()
var body: some View {
NavigationStack {
Group {
if viewModel.isLoading {
ProgressView("불러오는 중...")
} else {
List(viewModel.products) { product in
지금 바로 작가의 멤버십 구독자가 되어
멤버십 특별 연재 콘텐츠를 모두 만나 보세요.
오직 멤버십 구독자만 볼 수 있는,
이 작가의 특별 연재 콘텐츠