담당 범위
- 아키텍처 설계부터 전체 구현까지 1인 수행
- Flavor 시스템·JS Bridge·원격 버전 게이팅 설계
- iOS·Android 테스트 배포 파이프라인 구성
문제와 해결
이 저장소에서 실제로 부딪힌 문제를 케이스로 정리했습니다. 각 케이스는 어떤 상황이었고 무엇이 제약이었는지에서 시작해, 그래서 어떻게 판단했고 무엇이 남았는지까지 적었습니다.
sealed class Flavor { ... }Flutter Shell + WebView
Flutter를 얇은 네이티브 래퍼로, 기존 웹 자산을 WebView로 렌더링합니다. React, Vue, Nuxt 등 모든 웹 프레임워크와 호환되어 중복 개발 없이 iOS · Android 네이티브 기능을 활용할 수 있습니다.
- 모든 웹 프레임워크 호환
- Firebase Remote Config 연동
- 쿠키 기반 앱 환경 전달
6개 빌드 타깃, 단일 코드베이스
Dart 3.0 Sealed Class로 Flavor를 타입 안전하게 표현합니다. exhaustive switch가 누락된 환경 분기를 컴파일 타임에 잡아냅니다.
- LOCAL / TEST / PROD × 2 서비스
- Flavor별 독립 Firebase 프로젝트
- 버전 접미사(-alpha) 자동 관리
window.nativeBridge — 네이티브 기능 노출
WebView 로드 완료 시 window.nativeBridge 객체를 주입합니다. 웹이 앱 버전·카메라·갤러리 저장 등을 직접 호출할 수 있습니다.
- 앱 버전 / 빌드 번호 조회
- 카메라 · 갤러리 접근
- 딥링크 · 카카오톡 실행
참고: 이 프로젝트는 개발 단계에서 사업 우선순위의 변경으로 배포 전 중단되었습니다. 이하 내용은 구현된 설계와 아키텍처의 기록이며, 운영 데이터로 검증된 성과는 없습니다.
고지: 보안상의 이유 및 기업 기밀 보호를 위해, App ID · 쿠키명 · 파일 경로 등 일부 항목은 실제 프로젝트와 다르게 표기되어 있습니다.
배경
마켓보로에는 마켓봄 프로(현 마켓봄), 식봄, 마켓봄 등 여러 개의 네이티브 앱이 이미 존재했습니다. 이 앱들은 서비스 도메인은 달랐지만, 모두 웹뷰(WebView) 기반이라는 공통 구조를 갖고 있었습니다. 즉, 각 앱은 내부적으로 웹 앱을 띄우는 얇은 네이티브 Shell에 불과했고, 실제 비즈니스 로직은 웹 레이어에 있었습니다.
앞선 두 하이브리드 앱(→ 마켓봄)에서 WebView 환경의 문제를 반복해서 다루면서, 같은 대응을 앱마다 따로 하고 있다는 것이 분명해진 것이 이 프로젝트의 출발점입니다.
같은 Shell 코드가 앱 수만큼 복제돼 있었다
상황
Shell들이 앱마다 별도의 코드베이스로 관리되고 있었습니다. 기능적으로 거의 동일한 코드가 서비스 수만큼 복제되어 있었고, 하나의 공통 기능을 수정할 때마다 모든 앱에 동일한 변경을 반복해야 했습니다.
Shell이 하는 일은 대체로 같습니다. 웹을 띄우고, 네이티브 기능을 브리지로 노출하고, 딥링크를 처리하고, 버전을 확인합니다. 서비스마다 다른 것은 어느 URL을 띄우는지 정도입니다. 그런데 그 공통 부분이 물리적으로 복제되어 있으니, 수정 비용이 서비스 수에 비례했습니다.
제약
기존 웹 자산은 그대로 살려야 했습니다. 각 서비스의 웹 레이어는 React·Vue·Nuxt로 제각각이고, 실제 비즈니스 로직이 전부 거기 있습니다. 웹을 다시 만드는 선택지는 없었습니다.
접근과 결정
모든 앱을 단일 Flutter 프로젝트로 통합하고, 빌드 타깃(Flavor)만 변경해 각 서비스 앱을 생성하는 구조를 목표로 삼았습니다. 먼저 유통사 앱과 식봄 앱 두 개를 통합하는 것을 첫 번째 마일스톤으로 설정했습니다.
Flutter를 얇은 네이티브 Shell로 두고, 기존 웹 앱을 flutter_inappwebview로 렌더링하는 하이브리드 아키텍처를 채택했습니다. 핵심은 웹 프레임워크에 무관하다는 점입니다. React든 Vue든 Nuxt든 Shell 입장에서는 URL 하나이므로, 웹 자산을 손대지 않고 그대로 얹을 수 있습니다.
Flutter를 고른 이유는 이 조건에서 iOS·Android Shell을 한 벌만 쓰면 되기 때문입니다. Shell이 하는 일이 얇을수록 크로스플랫폼의 약점(네이티브 세부 제어)은 덜 드러나고, 장점(코드 한 벌)은 그대로 남습니다.
┌──────────────────────────────────────┐
│ Flutter Native Shell │ ← Dart / Flavor 시스템
│ ┌─────────────────────────────────┐ │
│ │ flutter_inappwebview (WebView) │ │ ← JS Bridge
│ │ ┌───────────────────────────┐ │ │
│ │ │ Web Application │ │ │ ← 모든 웹 프레임워크 호환
│ │ └───────────────────────────┘ │ │
│ └─────────────────────────────────┘ │
│ Firebase Remote Config / Analytics │
└──────────────────────────────────────┘단일 Flutter 프로젝트 안에서 네 개의 핵심 관심사를 분리했습니다.
| 모듈 | 역할 |
|---|---|
| Flavor 시스템 | 서비스·환경별 빌드 타깃을 타입 안전하게 분기 |
| JavaScript Bridge | 네이티브 기능을 웹에 window.nativeBridge API로 노출 |
| 버전 게이팅 | Firebase Remote Config로 강제·권장 업데이트를 원격 제어 |
| URL 라우팅 | 모든 URL 스킴을 scheme-first 분기로 처리 |
결과
식봄·마켓봄(구 마켓봄 프로) 두 서비스를 단일 Flutter 프로젝트로 통합하는 아키텍처를 설계·구현했습니다.
다만 배포 전 중단되었으므로, 유지보수 비용이 실제로 줄었는지는 확인되지 않았습니다.
한계
중단 시점까지 검증된 것은 “두 서비스가 하나의 코드베이스에서 빌드된다”까지입니다. 실사용자 환경에서의 안정성, 기존 네이티브 Shell 대비 성능, 통합 이후의 실제 수정 비용은 전부 미검증입니다.
환경별 설정 실수는 빌드해 봐야 안다
상황
서비스와 환경의 조합만큼 빌드 타깃이 생깁니다. 첫 마일스톤 기준으로는 이렇습니다.
2개 서비스 × 3개 환경(LOCAL / TEST / PROD) = 6개 빌드 타깃
각 타깃은 서로 다른 Firebase 프로젝트, 앱 ID, 버전 접미사를 갖습니다. 이런 설정은 문자열 상수와 조건 분기로 관리되기 쉽고, 그러면 분기를 하나 빠뜨려도 컴파일은 통과합니다. 잘못된 Firebase 프로젝트로 붙은 앱은 빌드해서 실행해 봐야 알 수 있고, 최악의 경우 운영 빌드가 테스트 설정으로 나갑니다.
접근과 결정
Dart 3.0의 Sealed Class로 Flavor를 타입으로 정의했습니다. Sealed Class는 하위 타입이 닫혀 있어, switch에서 모든 경우를 다루지 않으면 컴파일이 실패합니다.
즉 “분기를 빠뜨리지 말자”는 규칙 대신, 빠뜨리면 빌드가 안 되는 구조로 바꾼 것입니다. 새 서비스를 추가하면 Flavor를 다루는 모든 지점이 컴파일 오류로 드러나므로, 어디를 고쳐야 하는지 컴파일러가 알려줍니다.
flutter_flavor가 빌드 타임 값 주입을, Sealed Class가 런타임 타입 안전 분기를 담당하는 이중 구조입니다. 서비스별 빌드 스위칭 CLI도 함께 만들어, 타깃 전환을 명령 한 줄로 처리하도록 했습니다.
결과
Flavor 관련 분기 누락이 컴파일 타임에 검출되는 구조를 확보했습니다.
앱은 고치려면 심사를 기다려야 한다
상황
웹과 달리 앱은 배포에 스토어 심사가 끼어듭니다. 긴급하게 막아야 할 상황(치명적 버그가 있는 버전 차단, 서버 점검 중 진입 차단)에서 심사를 기다리는 것은 대응이 되지 않습니다.
접근과 결정
Firebase Remote Config로 버전 게이팅을 원격 제어하도록 설계했습니다. 앱이 재개될 때마다 원격 버전 정보를 가져와, pub_semver로 현재 버전과 비교합니다.
분기는 네 가지입니다 — 강제 업데이트 / 점검 중 / 권장 업데이트 / 정상 진입. Dart 3.0 패턴 매칭으로 각 분기를 타입 안전하게 처리했습니다.
핵심은 판단 기준을 앱 바깥에 둔 것입니다. “어떤 버전을 막을지”가 코드에 있으면 배포가 필요하지만, 원격 설정에 있으면 즉시 바꿀 수 있습니다.
결과
앱 배포 없이 강제 업데이트·점검 모드를 원격으로 전환할 수 있는 구조를 완성했습니다.
그 밖의 담당 기여
JavaScript Bridge
WebView 로드 완료 시점에 Flutter가 JS 객체를 주입합니다. 웹 앱은 이 브릿지를 통해 앱 버전·빌드 번호·실행 환경 등 네이티브 정보를 조회할 수 있습니다.
앱 실행 환경 식별을 위한 쿠키(실행 모드·기기 플랫폼·앱 버전·빌드 번호)도 플랫폼에 맞게 자동 세팅하여, 웹 앱이 WebView 환경임을 인지할 수 있게 했습니다.
URL 라우팅 & 딥링크
shouldOverrideUrlLoading에서 URL 스킴을 우선 분류하고 처리합니다.
| 스킴 | 처리 방식 |
|---|---|
kakaolink: / intent:// | 카카오톡 앱 실행 (미설치 시 스토어 폴백) |
tel: / sms: / mailto: | 시스템 앱 위임 |
data:image/*;base64,... | base64 디코딩 → 갤러리 저장 |
| 일반 HTTP(S) | 기본 WebView 탐색 허용 |
배포 파이프라인
Firebase Multi-Project 구성(Flavor별 독립 google-services.json)과, Bitbucket Pipeline + Firebase App Distribution 기반 iOS·Android 테스트 배포 자동화를 구성했습니다.
이 프로젝트를 위해 Flutter와 Dart를 신규 학습했습니다.
참고 — 기술 선택과 기술 스택
기술 선택 이유
- Flutter + WebView 하이브리드 — 기존 웹 자산 재사용으로 중복 개발 최소화, 동시에 iOS/Android 네이티브 기능 활용
- Sealed Class Flavor —
switch분기 누락을 컴파일러가 감지. 환경별 설정 실수를 원천 차단 - Firebase Remote Config — 핫픽스 없이 앱 동작을 원격 제어. 스토어 심사를 기다리지 않고 대응
기술 스택
퇴사 시점 기준입니다.
| 기술 | 역할 |
|---|---|
| Flutter 3.x | 크로스플랫폼 네이티브 Shell |
| Dart 3.0 | Sealed Class, Pattern Matching |
| flutter_inappwebview | 웹 자산 렌더링 (모든 웹 프레임워크 호환) |
| flutter_flavor | 멀티 플레이버 환경 분리 |
| Firebase Remote Config | 원격 기능 플래그 · 버전 게이팅 |
| firebase_analytics | 앱 이벤트 트래킹 |
| pub_semver | Semantic Versioning 비교 |
| shared_preferences | 로컬 데이터 영속성 |
| permission_handler | 네이티브 권한 요청 |
| image_gallery_saver | base64 이미지 갤러리 저장 |
| url_launcher | 딥링크 · 외부 앱 실행 |
관련 링크
- 내부 프레임워크로 별도 공개 링크 없음