안녕하세요. ABC Studio에서 Demaecan(出前館, 이하 데마에칸) 앱을 개발하고 있는 김종식입니다. 데마에칸은 2000년부터 서비스를 시작한 일본 최대 규모의 음식 배달 서비스로 ABC Studio는 2021년 봄부터 프로덕트 개선에 참여하고 있습니다.
Flutter 전환의 마침표 - 일본 1위 배달 앱, 세 번째 Recode 글을 통해 데마에칸의 소비자용 앱(이하 ConsumerApp) 서비스를 Flutter 기술로 전환한 작업을 소개 드린 적이 있습니다. Flutter는 단일 코드 베이스를 활용해 다양한 플랫폼의 애플리케이션을 개발할 수 있는 강력한 크로스 플랫폼 프레임워크입니다. 처음 등장한 후 모바일 앱뿐만 아니라 PC나 웹 환경에서도 활용되면서 점점 개선되고 있습니다.
사장님용 앱인 MerchantApp과 ManagerApp은 이미 제품 개선 단계에서 Flutter를 이용해 웹 버전을 활용하고 있었습니다. 특히 ManagerApp은 처음부터 테스트용으로 웹 버전을 내부에 함께 배포했는데요. 기획에서부터 디자인, QA에 이르기까지 제품 개발 과정 전반에 큰 도움이 됐다는 의견을 받았습니다. 또한 가맹점용 앱인 RetailerApp 개발 과정에서는 PR(pull request)이 생성되면 웹 빌드 및 배포를 통해 작업 완료 여부를 판단하는 용도로 실험적으로 활용하기도 했습니다.
반면 ConsumerApp에서는 개발 과정에서 웹 버전을 활용하고 있지 않았습니다. 웹 버전을 활용하지 않는 상태에서 지난 3월 UI/UX 개선 과제를 완료했는데요. ConsumerApp은 이해관계자가 많은 서비스인데다가 실제 작동 화면을 꼼꼼히 확인하며 진행해야 하는 과제였던 만큼 진행 과정에서 불편함을 느꼈고, 더 빠르고 효율적으로 서비스를 개선할 수 있는 개발 환경이 필요하다고 판단했습니다. 이번 글에서는 그와 같은 환경을 마련하기 위한 시도 중 하나로 ConsumerApp이 웹에서 작동할 수 있도록 개발 환경을 개선한 사례를 소개합니다.
웹 환경에서 ConsumerApp 지원 필요성 확인
저희는 아래 그림과 같이 6개의 개발 환경에서 ConsumerApp 제품 개선 활동을 진행하고 있습니다. 각 환경에서는 병렬로 진행되는 과제나 정기 배포 진행 여부를 판단하기 위한 검증을 진행하며, 이를 통해 출시 일정을 유연하게 조정합니다.
현재 내부 테스트나 공유를 위해서 Android는 DeployGate, iOS는 TestFlight 서비스를 이용해 실제 기기에서 작업을 확인하고 있는데요. 많은 인원이 함께 만들고 있기 때문에 실제 기기에서 작업을 확인하려면 개인 및 팀 단위로 계정과 기기를 준비해야 하는 등 불필요하게 예산이 낭비될 수 있습니다. 또한 보안을 유지하기 위해 사내 VPN 연결을 해야만 앱 서비스를 사용할 수 있는데 이를 준비하는 과정도 상당히 번거롭습니다. 이와 같은 이유로 프로덕트 메이커 분들은 프로젝트 진행 중 실제 앱 작동을 확인하면서 커뮤니케이 션하기가 쉽지 않습니다.
Flutter 기술은 출시 후 꾸준히 멀티 플랫폼 환경에서 개선돼 왔습니다. 데마에칸에서 운영 중인 서비스 앱들은 모두 Flutter로 개발하고 있으며, 덕분에 웹 환경에서도 구동이 가능해졌습니다. 이에 앞서 말씀드린 여러 가지 불편함을 개선하기 위해 ConsumerApp도 웹에서 작동을 확인할 수 있는 개발 환경이 구성되면 좋겠다는 제안이 있었고, 향후 앱 제품 개선 과정에 도움이 될 것이라고 기대하며 개발 팀 주도로 ConsumerApp 웹 작동 환경 구성을 진행했습니다.
Flutter Web을 이용해 ConsumerApp 웹 개발 환경을 구축한 과정
Flutter Web을 이용해 ConsumerApp 웹 작동 환경을 구성하는 과정은 다음과 같은 순서로 진행했습니다.
- PoC 진행하며 가능성 확인
- 작업 목록 도출 및 과제 방향 설정
- 빌드 오류가 발생하는 참조 패키지를 웹 지원 버전으로 업데이트
- 웹 미지원 기능 및 패키지 사용 코드 리팩토링
- 로컬 개발 환경에서 CORS 이슈 대응
- 웹에서 인앱 웹뷰 표시하기
- 웹에서 지도 뷰 표시하기
하나씩 자세히 살펴보겠습니다.
1. PoC 진행하며 가능성 확인
먼저 아래와 같이 로컬 개발 환경에서 로그인 화면 진입까지 실행되도록 작업하는 PoC(Proof of Concept)를 진행했습니다. ConsumerApp은 많은 기능을 제공하고 있으며 다양한 패키지를 참조해 구현돼 있기 때문에 초기 구동 환경만 준비돼도 절반의 준비는 성공한 셈입니다(참고로 ConsumerApp은 Flutter 3.19.5를 사용하고 있습니다).
2. 작업 목록 도출 및 과제 방향 설정
PoC 단계에서 작업한 내용을 바탕으로 아래 그림과 같이 웹 실행이 가능하게 만들기 위해 코드 수정 및 리팩토링이 필요한 부분을 작업 단위 목록으로 정리했습니다. 이와 같이 작업을 도출한 덕분에 이후 사이드 이펙트를 최소화하면서 코드 리뷰에 부담되지 않도록 개발을 나눠 진행할 수 있었습니다.
PoC 진행 결과를 토대로 설정한 ConsumerApp Flutter Web 버전의 방향은 아래와 같습니다.
- ConsumerApp Flutter Web 버전을 최종 사용자에게 제공하는 것은 목표가 아닙니다. 데마에칸은 이미 웹에서도 서비스를 제공하고 있으며, Flutter Web은 실제 서비스로 활용하기에는 초기 구동 속도가 느리고, 웹 환경에서 상용 서비스 수준으로 활용 가능한 도구가 부족한 점 등의 아쉬운 부분이 있기 때문에 준비 단계에서부터 이미 최종 사용자용으로는 고려하지 않았습니다(참고: Flutter Web or React Native Web: Who Will Win the Battle?).
- 실제 기기에서 작동하는 모든 기능이 모바일과 웹에서 완벽히 동일하게 작동하도록 만드는 것은 목표가 아닙니다. 기존에 사용하던 패키지 중 웹 환경을 지원하지 않는 패키지도 있었고, 앱에서 제공하는 기능이 웹에서 제공하기에는 적절치 않은 경우도 있었기 때문입니다. 따라서 동일하게 작동하도록 만들기보다 는 인앱 웹뷰, 결제 흐름 등 일부 기능 사용에 제약이 있다는 사실을 공유하고 활용하는 방향으로 결정했습니다.
- 웹 실행 환경을 팀에 최적화된 혹은 적합한 환경으로 구성하는 것은 목표가 아닙니다. 현재 ConsumerApp에는 6개의 개발 환경이 있는데요. 우선 그중 하나의 환경에서 ConsumerApp을 웹 환경으로 실행해 활용할 수 있는 수준으로 준비하는 것을 목표로 했습니다. 추후 웹 환경을 활용하고 싶다는 니즈가 많아질 경우 상황에 따라 웹 빌드 및 배포 환경을 개선하기로 결정했습니다.
3. 빌드 오류가 발생하는 참조 패키지를 웹 지원 버전으로 업데이트
웹 환경에서 빌드할 때 기존에 사용하고 있던 패키지에서 오류가 발생해 어쩔 수 없이 버전을 올려야 하는 경우가 있었습니다. 예를 들어 newrelic_mobile 패키지에서 아래와 같은 오류가 발생했습니다.
Launching lib/main.dart on Chrome in debug mode...
main.dart:1
: Error: Dart library 'dart:ffi' is not available on this platform.
newrelic_mobile.dart:7
import 'dart:ffi';
^
Context: The unavailable library 'dart:ffi' is imported through these packages:
web_entrypoint.dart => package:consumer_app => package:newrelic_mobile => dart:ffi
Detailed import paths for (some of) the these imports:
...
Failed to compile application.
Exited
Dart의 FFI(Foreign Function Interface) 패키지를 이용하면 C와 Objective-C & Swift API를 사용할 수 있다고 알려져 있지만, 웹 빌드를 실행할 경우 FFI 패키지의 import
구문에서 오류가 발생했는데요. 원인은 ConsumerApp 프로젝트에서는 FFI 패키지를 사용하지 않지만, ConsumerApp에서 참조하는 패키지가 FFI 패키지를 참조했기 때문입니다. Recode 프로젝트를 진행하면서 사용했던 newrelic_mobile 패키지의 1.0.1 버전에서 발생한 이슈로, ConsumerApp에서는 참조하는 newrelic_mobile 패키지의 버전을 1.0.6으로 업데이트해서 이슈를 해결했습니다.
아래와 같이 Firebase 패키지들에서도 오류가 발생했으며, 이를 해결하기 위해 버전을 업데이트해야 했습니다.
패키지 이름 | 업데이트 전 | 업데이트 후 |
---|---|---|
firebase_core | 2.27.2 | 2.32.0 |
firebase_auth | 4.18.0 | 4.19.4 |
firebase_analytics | 10.9.0 | 10.10.4 |
firebase_crashlytics | 3.4.20 | 3.5.4 |
firebase_messaging | 14.7.21 | 14.9.1 |
firebase_remote_config | 4.3.19 | 4.4.7 |
4. 웹 미지원 기능 및 패키지 사용 코드 리팩토링
Flutter 패키지를 활용해 구현한 기능 중 일부는 웹 환경을 제공하지 않아서 기능 관점에서 구현하기 어려운 것들이 있었습니다. 예를 들어 앱 배지 아이콘 표시나 외부 서비스로 로그인 기능, 외부 서비스와 계정 연동 기능 등이 있습니다. 이와 같은 기능들을 구현하기 위해 사용하고 있는 패키지들이 웹 환경을 지원하면 좋겠지만 아직 그렇지 않았기 때문에 기능을 제공할 수가 없었습니다.
아래 코드는 앱 배지를 표시하기 위해 사용하고 있는 flutter_app_badger 패키지의 인터페이스를 래핑한 예시입니다. 웹 환경에서 기능을 지원하지 않는 경우, 이와 같이 패키지 단위로 각 패키지에서 제공하는 API를 래핑하는 형태로 만들었습니다.
// app_badger.dart
// note) _AppBadgerWeb, _AppBadgerImpl are an implementation of AppBadger.
AppBadger get $appBadger => PlatformHelper.isWeb ? _AppBadgerWeb() : _AppBadgerImpl();
abstract class AppBadger {
Future<void> updateBadgeCount(int count);
Future<void> removeBadge();
}
// AS-IS
FlutterAppBadger.updateBadgeCount(count);
// TO-BE
$appBadger.updateBadgeCount(count);
Firebase의 경우는 웹 환경도 지원하지만(참조), 푸시의 경우 웹과 앱 작동에 차이가 있을 수 있고, 프로젝트에서 웹 앱 등록이 필요하며, 이후 관리 범위가 늘어나기 때문에 웹 환경에서는 Firebase 기능을 지원하지 않기로 결정했습니다.
아래는 패키지 사용 코드를 리팩토링한 목록입니다.
패키지 이름 | 리팩토링 전 | 리팩토링 후 |
---|---|---|
adjust_sdk | Adjust.*** | $adjustUtil.*** |
flutter_inappweb | ChromeSafariBrowser() | $chromeSafariBrowser |
flutter_app_badger | FlutterAppBadger.*** | $appBadger |
firebase_core | Firebase.*** | FirebaseUtil.*** |
firebase_analytics | FirebaseAnalytics.instance.*** | $firebaseAnalytics.*** |
firebase_auth | FirebaseAuth.instance.*** | $firebaseAuth.*** |
firebase_crashlytics | FirebaseCrashlytics.instance.*** | $firebaseCrashlytics.*** |
firebase_messaging | FirebaseMessaging.instance.*** | $firebaseMessaging.*** |
firebase_remote_config | FirebaseRemoteConfig.instance.*** | $firebaseRemoteConfig.*** |
... |
위와 같이 패키지 사용 부분을 리팩토링했고, 이후 다른 분이 작업할 때 혼동하지 않도록 커스텀 린트도 함께 작업했습니다. 플랫폼별 동작 분기 처리 및 Flutter에서 커스텀 린트를 활용하는 자세한 내용은 저의 이전 글 Flutter에서 커스텀 린트 활용하기를 참고해 주세요.
5. 로컬 개발 환경에서 CORS 이슈 대응
이제 로컬 개발 환경에서 웹 실행 시 기본적인 기능이 정상 작동하는지 확인할 필요가 있는데요. ConsumerApp을 웹 환경에서 실행 후 메인 페이지로 진입했더니 오류 메시지가 표시됐으며, 메인 페이지뿐 아니라 각 탭(메인, 검색, 주문 상세, 마이페이지)별 화면에서도 마찬가지였습니다. 원인은 CORS(cross-origin resource sharing)로, 웹 개발할 때 반드시 한 번은 마주치게 되는 괴로운 이슈이며, Flutter Web 실행 환경에서도 피할 수 없었습니다.
CORS 이슈가 발생하는 주요 상황은 아래와 같습니다(배포 환경에서는 DevOps 팀의 도움으로 이 문제를 해결했습니다).
- 각 도메인별 BFF(backend for frontend) API를 호출하는 경우
- 화면에서 이미지를 URL로 표시하는 경우
이를 해결하는 방법은 다음과 같습니다. Flutter를 로컬에서 빌드해 웹을 실행하면 보통 Chrome 브라우저에서 실행되는데요. 이때 {flutter_sdk_설치경로}/packages/flutter_tools/lib/src/web/chrome.dart
에서 구체적으로 어떻게 Chrome 브라우저가 구동되는지 확인할 수 있습니다. 이 chrome.dart에 아래 그림과 같이 '--disable-web-security'
파라 미터를 추가하거나 flutter run
명령어 실행 시 이 옵션을 추가로 전달해서 실행하면 이슈를 해결할 수 있습니다(참고).
로컬 환경에서 CORS를 조금 더 쉽게 확인할 수 있는 도구로 flutter_cors가 있습니다. 만약 IDE에서 웹 실행을 위한 커스텀 설정을 구성하는 것이 번거롭거나, 여러 버전의 Flutter SDK를 이용해 개발하고 있는 경우라면 이 도구 사용을 추천합니다. 설치 및 사용 방법은 아래와 같습니다.
/// Install
dart pub global activate flutter cors
// To disabling CORS checks
fluttercors -d -p {flutter_sdk_install_path}
// To enabling CORS checks
fluttercors -e -p {flutter_sdk_install_path}
6. 웹에서 인앱 웹뷰 표시하기
ConsumerApp에서는 인앱 웹뷰를 구현하기 위해 flutter_inappweb 패키지를 사용하고 있습니다. 이 패키지는 6.0.0 버전부터 웹 환경을 지원하며(참고), 내부적으로는 iframe을 이용해서 화면 표시를 요청하는 URL을 표시해 줍니다.
아래 그림과 같이 인앱 웹뷰는 웹 환경에서 잘 표시됐습니다. 줌 인/아웃과 길게 누르기 이벤트 등의 사용성이 기존 웹 서비스보다 조금 떨어지긴 했지만, 앱 내 기능이 작동하는 것을 확인할 수 있도록 웹에서 인앱 웹뷰가 표현되는 것만으로도 충분히 만족했습니다.
그런데 로컬 개발 환경에서는 정상 작동했지만 빌드 결과를 배포했을 때에는 오류가 발생했습니다. ConsumerApp 빌드 시 실제 배포 환경의 경로를 설정하기 위해 --base-href
옵션을 사용하는데요. 이 옵션이 설정될 경우 빌드 후 생성된 index.html 내 web_support.js 경로가 맞지 않게 되는 것이 원인이었습니다.
이 문제는 웹 빌드 및 배포 스크립트에서 빌드 완료 후 생성된 index.html 파일 내용 중 위 내용을 수정하는 방법으로 해결했습니다. 아래는 오류 대응 코드입니다(ConsumerApp에서 빌드 및 배포 스크립트는 Deno로 작성하고 있습니다).
// Work-around: `flutter_inappwebview_web` doesn't support `--base-href` option.
// Therefore, the script replaces a javascript path in `index.html`.
// If the library supports that, we can remove below work-around.
const htmlPath = './build/web/index.html'
const originalHtml = Deno.readTextFileSync(htmlPath)
const replacedHtml = originalHtml.replaceAll(
'/assets/packages/flutter_inappwebview_web/assets/web/web_support.js',
'assets/packages/flutter_inappwebview_web/assets/web/web_support.js',
)
Deno.writeTextFileSync(htmlPath, replacedHtml)
7. 웹에서 지도 뷰 표시하기
ConsumerApp은 배송지 주소 설정이나 주문 전 위치 정보 확인, 배송 현황 화면 등에서 지도를 이용하는데요. 아래 그림과 같이 웹 환경에서는 지도가 표시되지 않는 이슈가 있었습니다.
iOS에서는 AppleMapView를, Android에서는 GoogleMapView로 표시하는 것이 요구 사항입니다. 이를 위해 Flutter에서는 각 플랫폼에 맞춰 지도 뷰를 표현할 수 있는 platform_maps_flutter 패키지를 사용하고 있는데요. 이 패키지 내에서 플랫폼별 분기를 위해 Platform.***
을 사용하기 때문에 웹 환경에서 오류가 발생했습니다.
지도 표시는 ConsumerApp의 중요한 기능이기 때문에 웹 환경에서도 표현되게 만들기로 결정하고, 아래 그림과 같이 모바일 앱 환경에서는 platform_maps_fluter, 웹 환경에서는 google_maps_flutter 패키지를 이용해 지도 뷰를 표시하는 리팩토링을 진행했습니다.
다행히 지도 뷰를 표현하기 위한 Controller
나 Location
, Marker
, Camera
등의 인터페이스 사용법이 기존과 거의 차이가 없었기에 리팩토링은 그리 어렵지 않았습니다. 이렇게 해두면 추후 iOS와 Android 모두 Google MapView를 사용하는 것으로 변경해야 할 경우, 혹은 다른 패키지로 교체하는 작업이 있을 때 손쉽게 패키지를 교체할 수 있다는 장점도 있습니다(Flutter에서는 AppleMapView 사용이 쉽지 않아 제가 희망하고 있는 부분입니다 🙂).
index.html 헤더를 설정(참고)하면서, ConsumerApp의 Flutter Web 버전은 내부 개발 확인 목적으로 사용량이 많지 않을 것으로 예상돼 이미 사용 중인 API 키를 사용했습니다. 참고로 앱과 웹 제품에서 사용하고 있는 지도 뷰 API 키는 시스템 어뷰징 때문에 리퍼러(referrer) 설정 시 로컬 호스트를 사용할 수 없게 돼 있습니다. 이에 실제 웹 서버에 업로드 후 지도가 잘 표시되는지 확인했습니다.
실제 웹 환경에서 테스트해 본 결과 지도 뷰에서 사용자 인터렉션이나 회전 등의 기능을 사용하기에는 조금 아쉬움이 있었습니다. 이는 google_maps_flutter_web에서도 제약 사항으로 미리 안내하고 있는 부분이기도 한데요. 지도가 표시되는 모든 화면의 초기 진입 시에는 잘 작동하기 때문에 앱의 동작을 웹에서 확인하겠다는 목적은 달성한 것이므로 지도 뷰 사용에 어느 정도 제약이 있음을 감안하고 그대로 활용하기로 결정했습니다.
마치며
웹 환경에서 ConsumerApp 작동 확인이 가능해지면서 프로덕트 메이커 분들의 커뮤니케이션 효율이 굉장히 좋아진 것을 느꼈습니다. 온라인 미팅에서는 앱 작동을 확인할 때 웹 환경의 화면을 공유하면서 커뮤니케이션할 수 있게 됐고, 화면 캡처도 훨씬 편해졌습니다. 다른 기기나 환경에서 앱이 어떻게 작동하는지 확인할 수 있는 device_preview 패키지를 활용하면 보다 상세하게 실행 환경을 설정해서 확인하는 것도 가능합니다. 저는 이번 작업을 진행하면서 Flutter Web 개발 환경에서 사용할 수 있는 패키지들이 점점 더 많아지고 있다고 느꼈는데요. 덕분에 기존 요건을 큰 무리 없이 충족할 수 있었다 고 생각합니다.
아래 그림과 같이 한 모니터에 두 개의 브라우저를 실행해서 웹과 앱의 동작 차이를 확인한다거나, ConsumerApp - MerchantApp - DeliveryApp 간 연계 테스트할 때에도 조금 더 편하게 작업할 수 있게 됐습니다.
과제의 진척 상황을 확인하고 개선이 필요한 부분에 대한 피드백을 받아 보완해 나가는 사이클이 빨라질수록 과제의 완성도가 높아지고 개선된 제품을 사용자에게 더욱 빠르게 전달할 수 있게 됩니다. 현재 과제 단위로 기능과 디자인이 잘 구현됐는지 확인하기 위한 웹 배포 요청 사례가 늘어나고 있는데요. 그 덕분에 제품 개선의 효율이 높아지고 있다고 생각합니다.
이번 작업은 함께 제품을 만드는 동료를 사용자로 확장할 수 있었던 의미 있는 시도였습니다. 개인적으로 이 작업 내용을 공유했을 때 일본 기획자분들께서 너무 좋아해 주셔서 보람을 느꼈습니다. 🙂 Flutter를 사용하고 있다면 제품 개선 과정에서 Flutter Web을 효과적으로 사용할 수 있는 방법을 한 번 고민해 보시기를 추천하며 이만 마치겠습니다.