본문으로 건너뛰기

두 노선에서 한 노선으로: TAP! 앱, Flutter로 환승하다

모빌리티FlutterMobileTAP!
TAP!민경운2026년 8월 7일

안드로이드와 iOS로 각각 만들어지던 TAP! 사용자 앱을, 왜 그리고 어떻게 Flutter 하나로 합쳤는지 이야기하고자 합니다.

1. TAP!, 그리고 저희 팀이 만드는 서비스

안녕하세요, UMOSONE TAP! 개발팀의 민경운입니다.

TAP!은 승객 이동, 기업 카셰어링, 사내 이동 솔루션까지 아우르는 B2B 모빌리티 서비스입니다. 사용자를 실제로 만나는 접점은 이용자·운전자·운영자가 각자 쓰는 몇 개의 서비스로 나뉩니다.

TAP!의 서비스 라인업

TAP! 서비스 라인업 — 사용자 앱, 드라이버 앱, 어드민, 카쉐어링

  • 사용자 앱: 호출부터 승하차까지, 이동의 전 과정을 지원
  • 드라이버 앱(콘솔): 배차부터 운행 완료까지, 드라이버의 하루를 지원
  • 어드민(운영): 전체 운행 현황을 실시간으로 지휘하는 관제 백오피스
  • 카쉐어링: 기업을 위한 차량 공유 솔루션

이 글은 이 중 사용자 앱의 Native → Flutter 전환 과정을 다룹니다.

2. Native에서 굳이 Flutter로 전환한 계기

사용자 앱은 원래 안드로이드와 iOS가 완전히 분리된 두 개의 네이티브 프로젝트였습니다. 하지만 한정된 인원으로 안드로이드와 iOS를 따로따로 대응하는 것이 리소스 관점에서 문제가 있었고, 자연스럽게 "하나의 플랫폼에서 두 OS를 효율적으로 커버할 방법이 없을까"를 고민하게 됐습니다.

다만 크로스플랫폼으로 가기로 방향을 잡았다고 해서 곧바로 프레임워크를 정한 것은 아니었습니다. 가장 유력한 두 후보는 React Native와 Flutter였고, 최종 선택에 앞서 두 프레임워크를 여러 관점에서 비교하는 사전 조사부터 진행했습니다.

React Native vs Flutter

간단히 소개하면, React Native는 Meta(구 Facebook)가, Flutter는 Google이 만든 크로스플랫폼 프레임워크입니다. 화면을 그리는 방식부터 다릅니다.

React Native vs Flutter 렌더링 아키텍처 비교

팀 내부에서 정리했던 비교 자료를 간략히 요약하면 다음과 같습니다.

관점React NativeFlutter
렌더링 / 성능Bridge를 거치는 구조라 복잡한 UI에서 프레임 저하 가능자체 렌더링 엔진(Skia 기반)으로 애니메이션·고난도 UI에 강함
UI 일관성플랫폼 네이티브 컴포넌트에 의존, OS별로 미묘하게 다름위젯을 직접 그려 플랫폼 간 동일한 UI 보장
생태계 / DXJS/TS 자산 활용 가능하지만 라이브러리 품질 편차가 큼Google 공식 지원, Hot Reload·DevTools 완성도가 높음
플랫폼 커버리지Android/iOS 중심, Web/Desktop은 정식 지원 아님Web/Desktop/임베디드까지 지원 범위 확장 중
학습 난이도JS 경험자가 많다면 진입장벽이 낮음Dart, Widget Tree를 새로 학습해야 함
  • React Native가 유리한 경우: 이미 JS/TS 기반 조직, 빠른 MVP가 필요할 때
  • Flutter가 유리한 경우: 고품질 UI·애니메이션·성능이 중요할 때, 장기 유지보수가 필요할 때, 여러 플랫폼을 함께 가져가야 할 때

표로 정리된 일반적인 비교 외에, 실제 판단에는 몇 가지 현실적인 이유가 더 크게 작용했습니다. React Native는 기능 하나를 구현할 때도 서드파티 라이브러리에 의존해야 하는 경우가 많았고, 그 품질과 유지보수 여부가 라이브러리마다 제각각이라 장기적으로 믿고 갈 수 있는 선택인지 확신이 서지 않았습니다. 반면 Flutter는 팀 내에 이미 사용해본 경험이 있는 팀원이 있어 도입 초기 리스크를 줄일 수 있었고, 자체 렌더링 엔진(Skia 기반)을 쓰는 구조 덕분에 여러 성능 비교 지표에서도 꾸준히 우수한 결과를 보여줬습니다.

최종 결론: Flutter를 선택하다

Flutter는 성능·UI 일관성·멀티플랫폼 확장성에서, React Native는 진입장벽과 기존 JS 자산 활용에서 우세하다는 결론이었습니다. 사용자 앱은 지도·실시간 위치 표현처럼 복잡한 UI와 애니메이션 비중이 컸기 때문에, 최종적으로 Flutter를 선택했습니다. 안드로이드·iOS를 넘어 다른 플랫폼까지 확장할 수 있다는 점은 결정적인 이유는 아니었지만, 선택의 폭을 넓혀준다는 점에서는 나쁘지 않은 옵션이었습니다.

안드로이드 개발자에게 낯설지 않았던 이유

막상 선택은 했지만, 안드로이드 네이티브 개발자였던 팀원들에게 Flutter가 완전히 낯선 개념은 아니었습니다. Flutter는 위젯(Widget) 단위로 선언하고 상태가 바뀐 부분만 build()로 다시 그리는데, 이는 이미 써오던 Jetpack Compose의 선언형 UI(Declarative UI) 패러다임과 사실상 같은 사고방식입니다. 명령형으로 뷰를 직접 조작하던 XML 기반 Android View 시스템보다 오히려 Compose 쪽에 가까웠던 덕분에, 팀원들은 Flutter의 위젯 트리와 State 개념에 생각보다 빠르게 적응했습니다.

3. 기존 네이티브 아키텍처를 기준으로 세운 전환 전략

기존 안드로이드 네이티브 아키텍처 구조도

전환 전략을 세울 때 가장 먼저 잡은 원칙은 "아키텍처를 새로 설계하지 않는다"였습니다. 기존 안드로이드 네이티브 프로젝트는 위 그림과 같은 레이어링으로 구성되어 있었습니다.

레이어링은 그대로, 자리만 옮기다

이미 오래 검증된 레이어링이었기 때문에, 새 구조를 고민하기보다는 이 구조를 그대로 Flutter 프로젝트에 옮기는 쪽을 택했습니다.

lib/
├── core/ # 기존 Core:XX 자리 — network, storage, design_system 등 N개 모듈
└── feature/ # 기존 Feature 자리 — Feature A, Feature B, Feature C
└── home/
├── data/ # 기존 Repository 패턴
├── domain/ # 기존 UseCase 개념
└── presentation/ # 화면 + ViewModel

각 기능 모듈 내부는 데이터·도메인·화면(프레젠테이션) 세 계층으로 나누어, 기존의 UseCase 개념과 Repository 패턴을 그대로 유지했습니다.

흥미롭게도 이 구조는 저희만의 선택이 아니라, Flutter 공식 아키텍처 가이드가 권장하는 형태이기도 합니다. 공식 가이드도 UI 레이어와 Data 레이어를 나누고, 필요하면 그 사이에 UseCase로 구성된 Domain 레이어를 두라고 안내하는데, Jetpack 권장 아키텍처를 써본 안드로이드 개발자라면 익숙한 그림입니다.

CI/CD도 그대로 옮기다

CI/CD도 새로 설계하기보다는 기존 네이티브 파이프라인의 뼈대를 그대로 옮겼습니다.

Android, iOS 배포 파이프라인

기존에는 안드로이드·iOS 빌드 환경이 플랫폼별로 따로 돌아갔는데, 이제는 하나의 파이프라인 안에서 Android 빌드가 끝난 뒤 iOS 빌드가 순차적으로 이어지는 구조로 합쳤습니다. 두 빌드가 동시에 돌면서 배포 버전이 꼬이는 걸 막기 위한 순서이고, 운영 브랜치에 병합되면 자동으로 릴리즈가 생성되는 것도 이전 네이티브 파이프라인과 동일한 방식입니다.

상태관리: 왜 Riverpod인가

레이어링과 별개로, Flutter의 상태관리는 어떤 프레임워크로 가져갈지 따로 조사가 필요했습니다. 후보는 Provider, Bloc, GetX, Riverpod이었습니다.

  • Provider: 선언은 간단하지만 컴파일 타임에 의존성 오류를 잡아주지 못하고, BuildContext에 의존하는 구조라 테스트하기 까다로웠습니다.
  • Bloc: Event/State 모델이 명확한 대신, 화면 하나에도 Event·State·Bloc 클래스를 각각 만들어야 해서 보일러플레이트가 많다는 인상이었습니다.
  • GetX: 러닝커브는 낮지만 전역 상태와 의존성 주입이 암묵적으로 얽혀 있어, 규모가 커질수록 추적이 어려울 것이라는 우려가 있었습니다.
  • Riverpod: BuildContext 없이도 상태에 접근할 수 있고, 컴파일 타임에 의존성을 검증할 수 있다는 점이 눈에 띄었습니다.

무엇보다 Riverpod의 Notifier가 안드로이드에서 써오던 ViewModel과 거의 같은 형태(상태를 들고 있고 UI가 그 상태를 구독하는 구조)라는 점이 크게 작용했습니다. 핵심만 단순화한 예시 코드로 보면 다음과 같은 형태입니다.


class RideStatusViewModel extends _$RideStatusViewModel {

FutureOr<RideStatus> build() => repository.fetchStatus();
}

final status = ref.watch(rideStatusViewModelProvider);
return status.when(
data: (value) => StatusView(value),
loading: () => const LoadingView(),
error: (e, _) => ErrorView(e),
);

Notifier가 ViewModel 역할을, AsyncValue가 로딩·에러·성공 상태를 표현합니다. 안드로이드에서 ViewModel과 StateFlow를 구독하던 흐름과 사실상 같아서, 상태관리 자체를 새로 배우는 데 드는 적응 비용은 크지 않았습니다.

Flutter 공식 아키텍처 가이드도 UI 레이어의 패턴으로 MVVM(Model-View-ViewModel)을 명시하고 있어서, Riverpod의 Notifier를 ViewModel처럼 쓰기로 한 결정은 이 가이드와 같은 방향이기도 했습니다.

4. 트러블슈팅 ① Pigeon 네이티브 브릿지가 백그라운드에서 실패하다

전환한 사용자 앱에는 Flutter와 네이티브 코드 사이를 타입 안전하게 이어주는 Pigeon으로 만든 브릿지가 있습니다. 일부 푸시 알림(iOS Live Activity, Android Live Updates처럼 실시간으로 갱신되는 알림)은 이 브릿지를 통해 네이티브 쪽에서 직접 UI를 그리도록 설계했습니다.

증상: 포그라운드는 멀쩡한데 백그라운드에서만 안 된다

앱이 백그라운드/종료 상태일 때 푸시가 표시되지 않는 문제가 있었습니다. 포그라운드에서는 멀쩡히 잘 동작했기 때문에, 처음엔 원인을 좁히기가 까다로웠습니다.

원인: 백그라운드 Isolate는 다른 세계였다

원인은 FCM(Firebase Cloud Messaging)의 백그라운드 메시지 처리 방식에 있었습니다. 앱이 백그라운드/종료 상태에서 푸시를 받으면, Dart 백그라운드 핸들러는 메인 앱과는 별도의 Isolate, 별도의 Flutter 엔진 인스턴스에서 실행됩니다. Pigeon이 만든 플랫폼 채널은 특정 엔진의 BinaryMessenger에 등록되는 구조라, 메인 엔진에서만 등록해둔 Pigeon 핸들러는 이 백그라운드 엔진에서는 호출되지 않았습니다.

문제는 이 실패가 조용히 일어난다는 점이었습니다. 에러조차 명확히 나지 않고 무시되는 경우가 있어 재현과 원인 파악에 시간이 오래 걸렸습니다. FlutterFire 공식 문서의 백그라운드 메시지 처리 안내에도 이 핸들러가 별도 컨텍스트에서 실행된다는 점이 명시되어 있습니다.

해결: 타이밍이 중요한 알림은 네이티브가 직접

네이티브 플러그인을 모든 Flutter 엔진(메인 + 백그라운드)에 등록해 Pigeon 채널을 백그라운드에서도 열어봤지만, urgent 채널처럼 즉시성이 중요한 알림은 여전히 타이밍이 불안정했습니다. 결국 이 알림 타입만큼은 Dart 백그라운드 핸들러를 거치지 않고, 안드로이드의 FCM 서비스 콜백(Dart Isolate가 아니라 시스템 프로세스에서 바로 실행되는 콜백)에서 네이티브가 직접 그리도록 했습니다.

// Android, 실제 코드를 단순화한 예시
override fun onMessageReceived(message: RemoteMessage) {
// Pigeon 브릿지는 백그라운드 isolate의 BinaryMessenger가 달라 호출이 보장되지 않는다.
if (message.data["channel"] == "urgent") {
NativeNotification.show(message.data)
return
}
super.onMessageReceived(message) // 나머지는 Dart 백그라운드 핸들러로 위임
}
// Flutter, 실제 코드를 단순화한 예시
('vm:entry-point')
Future<void> fcmBackgroundMessageHandler(RemoteMessage message) async {
WidgetsFlutterBinding.ensureInitialized();

if (message.data["channel"] == "urgent") {
return; // 네이티브 쪽에서 이미 처리했으므로 여기서는 스킵
}

try {
await showLocalNotification(message);
} on PlatformException {}
}

즉시성이 중요하지 않은 나머지 알림은 그대로 Dart 백그라운드 핸들러에서 flutter_local_notifications로 처리하고, 타이밍이 중요한 알림만 네이티브가 직접 책임지는 쪽으로 역할을 나눈 것입니다. Pigeon 자체의 결함이라기보다는, Isolate 경계를 넘나드는 플랫폼 채널의 한계를 인정하고 우회한 사례에 가깝습니다.

5. 트러블슈팅 ② iOS WebView에서만 키보드가 뜨면 화면이 밀려 올라가다

사용자 앱 안에는 일부 화면을 완전한 Flutter 네이티브 UI 대신 WebView로 띄우는 페이지가 있습니다.

증상: iOS에서만 화면이 밀린다

텍스트 입력창을 눌러 키보드가 올라올 때, iOS에서만 페이지 레이아웃이 위로 밀리거나 하단 영역이 키보드에 가려지는 현상이 있었습니다. 같은 페이지를 Android WebView에서 열면 문제가 없었기 때문에, 처음엔 앱의 WebView 위젯 설정 문제로 의심했습니다.

flutter/flutter 저장소에도 비슷한 이슈가 등록되어 있었습니다. Safari나 순수 WKWebView에서는 재현되지 않고 Flutter WebView에서만 나타난다는 점까지, 저희가 겪은 상황과 같았습니다.

1차 확인: Flutter Scaffold는 문제가 없었다

가장 먼저 확인한 건 Flutter ScaffoldresizeToAvoidBottomInset 옵션이었습니다. 키보드가 올라올 때 화면(body)을 키보드 높이만큼 자동으로 줄여주는 기능으로 기본값이 true인데, 해당 화면도 이미 true로 켜져 있어 Flutter 쪽은 정상 동작 중이었습니다. Flutter/네이티브 레이어의 문제는 아니라는 뜻이었고, 그래서 원인을 WebView 안쪽, 즉 페이지 자체로 좁힐 수 있었습니다.

진짜 원인: 스크롤을 따라가지 못하는 #root

원인을 웹팀과 함께 뜯어보니, 핵심은 iOS Safari의 스크롤 동작과 페이지 구조가 맞물린 문제였습니다.

  • 텍스트 인풋에 포커스가 잡히면, iOS Safari는 그 인풋이 화면에 보이도록 document 자체를 scrollY만큼 스크롤시킵니다.
  • 문제는 앱의 루트 엘리먼트(#root)가 document 맨 위에 고정된 채였다는 점입니다. document는 스크롤되는데 #root는 그 스크롤을 따라가지 않으니, 실제로는 화면이 위로 밀린 게 아니라 #root는 원래 자리에 그대로 있고 그 위로 빈 document 배경이 드러나는 모습이었습니다. 저희가 영상에서 본, 화면 위쪽에 정체불명의 빈 공간이 잠깐 나타나는 현상이 정확히 이것이었습니다.
  • 여기에 iOS는 키보드를 위한 여유 공간을 만들려고 스크롤 가능한 범위 자체를 실제 콘텐츠보다 늘리는 **가상 영역(virtual space)**까지 만드는데, 이 여유 공간 때문에 스크롤이 필요 이상으로 멀리까지 가능해지는 문제도 겹쳤습니다.

비슷한 증상은 Flutter 이슈 트래커에도 보고되어 있습니다. iOS 17.2에서 WebView 안 입력 포커스를 옮길 때 화면 상단에 정체불명의 빈 영역이 생기는 사례webview_flutter뿐 아니라 flutter_inappwebview에서도 똑같이 보고돼 있어서, 결이 비슷한 문제로 보입니다. 다만 저희 경우는 웹 페이지 자체의 스크롤·포지셔닝 구조에서 원인을 구체적으로 특정할 수 있었습니다.

iOS WebView에서 키보드가 올라올 때 화면이 밀리는 현상

해결: visualViewport로 #root를 다시 붙잡다

웹팀이 실제로 적용한 대응은 #root 높이 조정, 스크롤 위치 보정, 가상 영역 clamp를 하나의 훅으로 묶은 형태였습니다.

export const useKeyboardViewport = () => {
useEffect(() => {
const viewport = window.visualViewport;
const root = document.getElementById('root');

if (!viewport || !root) return;

const sync = () => {
const isKeyboardOpen = window.innerHeight > viewport.height;

if (isKeyboardOpen) {
root.style.height = `${viewport.height}px`;

// 가상 영역으로의 스크롤 방지
const maxScrollY = document.body.offsetHeight - viewport.height - 1;
if (window.scrollY > maxScrollY) {
window.scrollTo(0, maxScrollY);
}
} else {
root.style.height = '';
}

// 브라우저 scrollY 보정: root가 스크롤을 따라가도록 이동
const viewportTopGap = viewport.pageTop - viewport.offsetTop;
const translateY = window.scrollY - viewportTopGap;
root.style.transform = translateY !== 0 ? `translateY(${translateY}px)` : '';
};

viewport.addEventListener('resize', sync);
viewport.addEventListener('scroll', sync);
window.addEventListener('scroll', sync);

return () => {
viewport.removeEventListener('resize', sync);
viewport.removeEventListener('scroll', sync);
window.removeEventListener('scroll', sync);
root.style.height = '';
root.style.transform = '';
};
}, []);
};

키보드가 열려 있는 동안은 #root 높이를 줄이고 스크롤 위치를 clamp하고, 닫히면 원래 상태로 되돌립니다. visualViewportresize/scrollwindowscroll을 모두 구독해서, 키보드가 뜨고 닫히는 순간뿐 아니라 그 사이에 일어나는 스크롤에도 계속 위치를 맞춰줍니다.

앱을 감싸는 WebView 위에서 벌어지는 플랫폼 차이가 항상 Flutter나 네이티브 레이어의 문제인 것은 아니었습니다. 이번 경우는 문제가 실제로 발생하는 지점(WebView 컨테이너인지, 그 안의 웹 페이지인지)을 먼저 구분한 덕분에, 앱 쪽을 붙잡고 있는 대신 웹팀과 빠르게 원인을 공유하고 해결할 수 있었습니다.

6. 트러블슈팅 ③ Flutter SDK가 없는 지도를 네이티브로 직접 연동하다

전환 전 사용자 앱은 지도 SDK로 Mapbox를 쓰고 있었는데, Mapbox는 pub.dev에 공식 Flutter 패키지를 제공하고 있어서 다른 화면들처럼 Dart 코드만으로 지도를 붙일 수 있었습니다. 그런데 내부 사정으로 지도 SDK를 네이버 지도로 교체해야 하는 상황이 됐고, 여기서 문제가 생겼습니다. 네이버 지도는 공식 Flutter 패키지가 없고, 안드로이드·iOS 네이티브 SDK만 제공합니다.

아키텍처: Pigeon과 PlatformView로 브릿지를 직접 만들다

기존 Feature 코드가 지도 SDK 구현체를 직접 알지 못하도록, MapController라는 SDK 비종속 인터페이스를 두고 Naver 구현체가 이를 만족하도록 설계했습니다. 지도 SDK를 한 번 더 교체하거나, 서비스에 따라 다른 지도 SDK를 함께 써야 하는 상황이 오더라도 이 인터페이스 뒤에 구현체만 추가하면 되도록 확장 지점을 열어둔 셈입니다. 카메라 이동, 마커/오버레이 관리 같은 명령은 Pigeon으로 정의해 Dart → 네이티브로 호출하고, 지도 준비 완료·카메라 정지·마커 탭 같은 이벤트는 반대로 네이티브 → Dart로 콜백을 보냅니다.

지도 SDK 브릿지 구조 — Feature 코드, MapController, Pigeon Bridge, 네이티브 뷰

지도 화면 자체는 Android는 AndroidView, iOS는 UiKitView로 네이버 지도의 네이티브 뷰를 그대로 심고, Pigeon 호출은 뷰 ID를 기준으로 해당 네이티브 뷰 인스턴스에 라우팅하는 방식입니다. 제스처 처리, 오버레이 렌더링, 카메라 이벤트 전달까지 하나하나 안드로이드(Kotlin)와 iOS(Swift) 양쪽에 직접 구현해야 했다는 점에서, 이 구조를 만드는 과정 자체가 곧 네이티브 개발이었습니다.

이 밖에도 마이그레이션 과정에서 자잘한 문제를 여럿 마주쳤습니다.

  • 마커 아이콘 줌 스케일링: Mapbox는 줌 값에 따라 아이콘 크기를 자동으로 보간해주는 기능이 있었지만, 네이버 지도에는 이런 기능이 없어서 카메라 줌이 바뀔 때마다 마커 아이콘 크기를 직접 재계산해 다시 그려야 했습니다.
  • 비동기 초기화 레이스: 네이티브 지도 SDK 초기화가 비동기라, 지도가 준비되기 전에 Dart 쪽에서 카메라 이동 같은 명령이 먼저 들어오는 경우가 있었습니다. 초기화 전에 들어온 명령은 큐에 쌓아뒀다가, 지도가 준비되면 순서대로 흘려보내도록 했습니다.

공식 Flutter SDK가 없는 지도 서비스를 붙이는 작업은 결국, 네이티브 SDK를 얼마나 자연스러운 Flutter 인터페이스로 감싸 노출하느냐의 문제였습니다.

7. 완성된 Flutter 프로젝트의 구조와 빌드 시스템

세 가지 트러블슈팅을 거쳐 자리 잡은 프로젝트는 큰 틀에서 다음과 같은 모습입니다.

완성된 Flutter 프로젝트 구조와 환경별 빌드

lib 아래는 3장에서 다룬 레이어링(공통 모듈 / 화면 단위 기능)을 그대로 따르고, android·ios 폴더에는 Pigeon 브릿지나 FCM 서비스처럼 네이티브 코드가 필요한 부분만 최소한으로 남아 있습니다. 빌드 설정도 네이티브 시절 안드로이드·iOS 각각 관리하던 것을, 이제는 하나의 파이프라인에서 환경 값만 갈아 끼우는 방식으로 관리합니다.

8. 전환 이후 얻은 것들

전환의 가장 큰 변화는 기술 스택보다 협업 방식에서 나타났습니다.

전환 전후 협업 방식 변화 — Native(두 코드베이스, 두 언어) vs Flutter(한 사람, 한 코드베이스)

기존에는 안드로이드와 iOS 개발자가 같은 화면을 두고도 서로 다른 언어(Kotlin/Swift)와 코드베이스로 이야기해야 했고, 리뷰도 두 코드베이스에서 따로 이뤄졌습니다. 지금은 한 사람이 하나의 Dart 코드베이스로 화면 전체를 오너십을 가지고 개발합니다. 트러블슈팅 ①처럼 네이티브 코드까지 들여다봐야 하는 예외도 있지만, 대부분은 같은 코드를 보면서 논의합니다.

9. 앞으로의 계획: Mobile 너머로

사용자 앱을 시작으로 전환했지만, TAP!이 만드는 모바일 앱은 사용자 앱 하나가 아닙니다. 드라이버 앱(콘솔)도 같은 방식으로 Flutter 전환을 마쳤습니다.

더 나아가, Flutter가 Mobile뿐 아니라 Web·Desktop까지 하나의 코드베이스로 다룰 수 있다는 점(2장에서 살펴본 강점이기도 합니다)을 살려서, Web 화면과 Desktop·차량 인포테인먼트 같은 임베디드 환경까지 아우르는 아키텍처로 확장하는 것도 준비하고 있습니다.

Flutter 확장 범위 — Mobile(전환 완료), Web(전환 예정), Desktop·Embedded(Optional)

이런 방향에 확신을 더해주는 사례도 있습니다. Flutter 공식 쇼케이스에는 이미 LG Electronics, BMW, eBay, Google Pay처럼 굵직한 기업들의 실사용 사례가 올라와 있는데, 최근에는 2026년형 Toyota RAV4의 차량 인포테인먼트 시스템도 Flutter로 개발됐다는 사례가 소개됐습니다. 모바일 앱을 넘어 차량 임베디드 환경까지 하나의 코드베이스로 대응할 수 있다는 건, 저희가 지금 준비 중인 Mobile 너머로의 확장 방향에도 좋은 참고가 됩니다.

10. 마치며

네이티브 두 벌을 유지하던 사용자 앱을, 리소스 문제에서 출발해 Flutter 하나로 합치기까지의 과정을 정리했습니다. 기술 선택의 배경, 기존 아키텍처를 최대한 살린 전환 전략, 그리고 실제로 겪은 세 가지 트러블슈팅까지 다뤘습니다. 결국 이 전환의 핵심은 프레임워크 자체보다, 안드로이드와 iOS로 나뉘어 있던 팀이 하나의 코드베이스와 하나의 언어로 같은 화면을 이야기할 수 있게 됐다는 점이었습니다.

환승은 끝났지만, 다음 정거장까지는 아직 갈 길이 남아 있습니다. Mobile 너머로 확장하는 그 여정도 차근차근 기록해 나가겠습니다.