회사 앱을 React Native에서 Flutter로 새로 쓴 기록을 시리즈로 남긴다. 이 글은 그 시작. 왜 갈아엎었고, 무엇을 각오했는지에 대한 이야기다.

RN 시절의 우리

원래 회사 앱은 React Native로 만들어져 있었다. react-native@0.72.15, 앱 버전은 8.6.x까지 갔던 물건이다. 오래 굴러온 만큼 기능도 많았고, 그만큼 손대기 무서운 코드도 많았다.

RN 자체가 나쁘다는 얘기는 아니다. 다만 우리 상황에서 계속 끌고 가기엔 몇 가지가 계속 발목을 잡았다.

  • 네이티브 브릿지 의존. 카메라, 딥링크, 결제 붙일 때마다 네이티브 모듈 버전 지옥을 만났다. 0.72에 물린 라이브러리들이 서로 안 맞는 경우가 잦았다.
  • iOS/Android 동작 차이. “안드로이드에선 되는데 아이폰에선 안 돼요”가 너무 자주 나왔다.
  • 빌드/배포 파이프라인이 무거웠다. 릴리스 한 번에 손이 많이 갔다.

그래서 결정했다. 새로 쓴다. 언어는 Flutter로.

왜 하필 Flutter였나

정답이 하나만 있는 문제는 아니다. 다만 우리한테는 이유가 분명했다.

  1. 렌더링을 우리가 통제한다. Skia 기반이라 플랫폼 위젯을 안 거치고 그린다. iOS/Android 픽셀이 어긋나는 문제를 근본에서 줄일 수 있었다.
  2. 단일 코드베이스에서 상태관리/라우팅을 일관되게 짤 수 있다. 우리는 GetX를 골랐다.
  3. 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), 탭 폰트/사이즈 정리, mymore 페이지 이름 변경 같은 것들.

지루하지만 이 단계가 제일 중요하다. 뼈대를 잘못 세우면 나중에 전부 다시 만진다. 그래서 초기에 아래를 먼저 못박았다.

  • 상태관리/DI/라우팅: GetX (GetMaterialApp, GetPage, 바인딩)
  • 네트워크 모델: freezed 기반 응답 타입
  • 화면 이동: Navigation 유틸로 push/pop 일원화
// GetMaterialApp 한 곳에서 라우팅/트랜지션을 잡고 시작했다.
GetMaterialApp(
  defaultTransition: Transition.cupertino,
  getPages: AppPages.pages,
  initialRoute: Routes.SPLASH.name,
  // unknownRoute 는 나중에 딥링크 안정화하면서 SPLASH 로 못박았다.
);

각오한 것들

솔직히 말하면 전환은 공짜가 아니었다. 시리즈에서 하나씩 풀 거지만, 실제로 이런 것들을 다시 다 만났다.

  • iOS 생명주기 마이그레이션UIApplicationDelegateUISceneDelegate (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-app react-native@0.72.15, 앱 버전 8.6.x — 전환 전 맥락