Flutter 전환기: 왜 RN에서 Flutter로 갔나
회사 앱을 React Native에서 Flutter로 새로 쓴 기록을 시리즈로 남긴다. 이 글은 그 시작. 왜 갈아엎었고, 무엇을 각오했는지에 대한 이야기다.
RN 시절의 우리
원래 회사 앱은 React Native로 만들어져 있었다. react-native@0.72.15, 앱 버전은 8.6.x까지 갔던 물건이다. 오래 굴러온 만큼 기능도 많았고, 그만큼 손대기 무서운 코드도 많았다.
RN 자체가 나쁘다는 얘기는 아니다. 다만 우리 상황에서 계속 끌고 가기엔 몇 가지가 계속 발목을 잡았다.
- 네이티브 브릿지 의존. 카메라, 딥링크, 결제 붙일 때마다 네이티브 모듈 버전 지옥을 만났다.
0.72에 물린 라이브러리들이 서로 안 맞는 경우가 잦았다. - iOS/Android 동작 차이. “안드로이드에선 되는데 아이폰에선 안 돼요”가 너무 자주 나왔다.
- 빌드/배포 파이프라인이 무거웠다. 릴리스 한 번에 손이 많이 갔다.
그래서 결정했다. 새로 쓴다. 언어는 Flutter로.
왜 하필 Flutter였나
정답이 하나만 있는 문제는 아니다. 다만 우리한테는 이유가 분명했다.
- 렌더링을 우리가 통제한다. Skia 기반이라 플랫폼 위젯을 안 거치고 그린다. iOS/Android 픽셀이 어긋나는 문제를 근본에서 줄일 수 있었다.
- 단일 코드베이스에서 상태관리/라우팅을 일관되게 짤 수 있다. 우리는 GetX를 골랐다.
- Shorebird라는 OTA 코드푸시 선택지가 있었다. RN의 CodePush 감성을 Flutter에서도 이어갈 수 있다는 게 컸다. (이건 시리즈 뒷 글에서 따로 다룬다.)
한 줄로 요약하면, “플랫폼 차이에 쓰는 시간을 제품 만드는 시간으로 돌리고 싶었다.”
새 앱은 9.0.0부터
재밌는 결정이 하나 있었다. 새 Flutter 앱의 버전을 1.0.0이 아니라 **9.0.0**부터 시작했다.
이유는 단순하다. RN 앱이 이미 8.x까지 와 있었으니까. 스토어에서 사용자가 보는 건 결국 하나의 “회사 앱”이다. 새 앱이 갑자기 1.0.0으로 내려가면 사용자도 헷갈리고 스토어 버전 정렬도 꼬인다. 그래서 RN 앱의 버전 위에서 이어받는 쪽을 택했다.
# pubspec.yaml (Flutter)
name: myapp_app
version: 9.0.5 # RN 8.x 를 이어받아 9.0.0 부터 시작
environment:
sdk: ^3.8.0
이런 사소해 보이는 결정들이 실제로는 배포/버전관리 정책 전체에 영향을 줬다. (versionCode 계산, Shorebird release-version 매칭 같은 것들. 역시 뒷 글에서.)
프로젝트 구조부터 다시
빈 프로젝트에서 시작했기 때문에 초반 커밋들은 죄다 “설정”이었다. iOS 디버그 모드 기기 연결 세팅, build 산출물 트래킹 제거, 폰트 교체(Spoqa Han Sans → Pretendard), 탭 폰트/사이즈 정리, my → more 페이지 이름 변경 같은 것들.
지루하지만 이 단계가 제일 중요하다. 뼈대를 잘못 세우면 나중에 전부 다시 만진다. 그래서 초기에 아래를 먼저 못박았다.
- 상태관리/DI/라우팅: GetX (
GetMaterialApp,GetPage, 바인딩) - 네트워크 모델: freezed 기반 응답 타입
- 화면 이동:
Navigation유틸로 push/pop 일원화
// GetMaterialApp 한 곳에서 라우팅/트랜지션을 잡고 시작했다.
GetMaterialApp(
defaultTransition: Transition.cupertino,
getPages: AppPages.pages,
initialRoute: Routes.SPLASH.name,
// unknownRoute 는 나중에 딥링크 안정화하면서 SPLASH 로 못박았다.
);
각오한 것들
솔직히 말하면 전환은 공짜가 아니었다. 시리즈에서 하나씩 풀 거지만, 실제로 이런 것들을 다시 다 만났다.
- iOS 생명주기 마이그레이션 —
UIApplicationDelegate→UISceneDelegate(iOS 13+). Flutter가 구버전 방식을 쓰고 있어서 손봐야 했다. - iOS 최소 버전 상향 — 13 → 15. iOS 15는 2021년 9월 릴리스라, 그 이전 기기(대략 iPhone 6s/SE 1세대 이전)는 컷했다.
- 라우팅 재설계 — RN의 네비게이션 감성을 GetX Navigator로 옮기면서 바텀시트 컨트롤러 같은 커스텀 구조를 걷어냈다.
- 크래시 방어 — WebView dispose, scheme 누락 링크 등 네이티브 경계에서 터지는 것들.
- 딥링크 cold start — 앱이 꺼진 상태에서 딥링크로 진입할 때의 검정화면/assertion.
하나하나가 글 한 편씩 나올 분량이라, 시리즈로 쪼갰다.
결론
RN이 틀렸다기보다, 우리 제품이 필요로 하는 통제권과 배포 속도를 Flutter가 더 잘 줬다. 새 앱을 9.0.0으로 이어받은 것부터가 “갈아엎지만 사용자 입장에선 같은 앱”이라는 원칙의 표현이었다.
다음 글부터는 실제로 부딪힌 문제들을 코드와 함께 하나씩 푼다. 라우팅부터.
관련 작업
- (RN 앱)
myapp-appreact-native@0.72.15, 앱 버전8.6.x— 전환 전 맥락