최근 사내 레거시 어드민 페이지를 Flutter로 마이그레이션 하는 작업을 진행했습니다. 앱 개발 경험이 있는 팀원들이 많아 선택한 기술 스택이었지만, 막상 Flutter web을 프로덕션 환경에 배포하니 예상치 못한 문제들이 쏟아졌습니다. 특히 로컬 호스트(Localhost)에서는 쾌적하던 스크롤이 배포 서버에서는 뚝뚝 끊기거나, 초기 로딩 시 main.dart.js 다운로드에만 5초 이상 소요되어 사용자 이탈이 발생하는 심각한 성능 병목이 발생했습니다.
Flutter Web의 이중 렌더링 엔진 이해하기
많은 개발자가 Flutter web을 시작할 때 간과하는 것이 바로 렌더링 엔진의 차이입니다. 네이티브 앱은 Skia 엔진을 탑재하여 GPU를 직접 제어하지만, 웹 환경은 다릅니다. Flutter 3.x 버전 기준으로 웹 빌드 시 두 가지 렌더러 중 하나를 선택하거나, 자동으로 분기 처리할 수 있습니다.
저희 프로젝트는 처음에 아무런 옵션 없이 flutter build web 명령어를 사용했습니다. 이 경우 auto 모드로 동작하는데, 데스크톱 브라우저에서는 CanvasKit을, 모바일 브라우저에서는 HTML 렌더러를 사용합니다. 문제는 여기서 발생했습니다. 사내 와이파이 환경이 아닌 4G 네트워크를 사용하는 영업팀 태블릿에서 초기 로딩이 10초 가까이 걸리는 현상이 목격되었습니다.
반면 HTML 렌더러는 용량이 가볍고 DOM 요소를 활용하지만, 캔버스 드로잉이 많은 복잡한 UI에서는 프레임 드랍이 심하게 발생합니다. 우리는 고해상도 차트와지도 연동이 필수였기 때문에 HTML 렌더러로는 UX를 만족시킬 수 없었습니다.
실패 사례: 무조건적인 CanvasKit 강제 적용
초기 로딩 속도보다는 렌더링 품질이 우선이라는 판단하에, 모바일에서도 강제로 CanvasKit을 사용하도록 빌드 옵션을 수정했습니다. 이는 명백한 오판이었습니다.
// 처음 시도했던 빌드 명령어
flutter build web --web-renderer canvaskit --release
이 방식으로 배포하자마자 아이폰(iOS) 사파리 사용자들로부터 "사이트가 멈췄다"는 제보가 빗발쳤습니다. 원인을 분석해보니, iOS의 WebKit 브라우저 정책상 대용량 Wasm 메모리 할당에 제한이 있었고, CanvasKit이 로드되다가 메모리 부족으로 탭이 리로드되거나 크래시가 나는 현상이었습니다.
또한, Flutter 특유의 해시(#) URL 방식(`/#/home`)을 제거하기 위해 url_strategy 패키지를 적용했는데, 이 과정에서 웹 서버(Nginx)의 History API Fallback 설정을 누락하여 새로고침 시 404 에러가 발생하는 기초적인 실수도 범했습니다.
CORS 이슈와 이미지 렌더링 최적화
또 다른 난관은 외부 이미지 서버(AWS S3)에서 프로필 이미지를 불러올 때 발생한 CORS(Cross-Origin Resource Sharing) 문제였습니다. 앱에서는 문제없이 보이던 Image.network가 웹에서는 엑박(Broken Image)으로 표시되었습니다.
이는 Flutter web의 HTML 렌더러가 <img> 태그를 사용하는 반면, CanvasKit은 WebGL 텍스처로 이미지를 처리하기 때문에 브라우저의 보안 정책(CORS)을 훨씬 엄격하게 적용받기 때문입니다. 이를 해결하기 위해 백엔드 설정뿐만 아니라, 클라이언트 코드에서도 이미지 렌더링 방식을 플랫폼별로 분기 처리해야 했습니다.
최적화된 플랫폼 별 이미지 로더 구현
다음은 제가 프로덕션에 적용한 조건부 이미지 위젯의 핵심 로직입니다. 웹(Web)일 때와 앱(App)일 때, 그리고 웹 내부에서도 렌더러 특성에 따라 다르게 동작하도록 설계했습니다.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'dart:html' as html; // 웹 전용 패키지 주의
import 'dart:ui' as ui;
class OptimizedNetworkImage extends StatelessWidget {
final String imageUrl;
const OptimizedNetworkImage({Key? key, required this.imageUrl}) : super(key: key);
@override
Widget build(BuildContext context) {
// 웹 환경이면서 HTML 렌더러를 사용할 때의 우회 처리
if (kIsWeb) {
// 1. 단순 Image.network 사용 시 CORS 에러 발생 가능성 높음
// 2. HTML ElementView를 사용하여 브라우저 네이티브 img 태그 활용
final String viewType = 'img-view-${imageUrl.hashCode}';
// 플랫폼 뷰 등록 (최초 1회만 등록되도록 로직 보강 필요)
// 실제 구현 시에는 별도 레지스트리 관리 권장
ui.platformViewRegistry.registerViewFactory(
viewType,
(int viewId) => html.ImageElement()
..src = imageUrl
..style.width = '100%'
..style.height = '100%'
..style.objectFit = 'cover'
);
return SizedBox(
width: 100,
height: 100,
child: HtmlElementView(viewType: viewType),
);
}
// 네이티브 앱 환경
return Image.network(
imageUrl,
errorBuilder: (context, error, stackTrace) {
return const Icon(Icons.error); // 에러 처리 필수
},
);
}
}
위 코드에서 ui.platformViewRegistry는 Dart 컴파일러가 인식하지 못해 빨간 줄이 뜰 수 있습니다. 이를 해결하려면 analysis_options.yaml에서 해당 규칙을 무시하도록 설정하거나, 별도의 shim 파일을 만들어야 합니다. 이 방식은 Flutter web에서 외부 이미지를 다룰 때 CORS 문제를 우회하는 가장 확실한 방법 중 하나입니다.
렌더러 별 성능 비교 및 결과
최종적으로 우리는 Deferred Loading(지연 로딩)과 Icon Font Tree Shaking을 적용하고, 모바일 웹에서는 HTML 렌더러를, 데스크톱에서는 CanvasKit을 사용하는 하이브리드 전략을 채택했습니다. 아래는 최적화 전후의 성능 지표 비교입니다.
| 지표 (Metric) | 기본 설정 (Default) | 최적화 후 (Optimized) |
|---|---|---|
| 초기 번들 크기 (main.dart.js) | 4.2 MB | 1.8 MB (Gzipped) |
| LCP (Largest Contentful Paint) | 4.8s | 1.2s |
| 메모리 사용량 (iOS Safari) | 250 MB+ (Crash 빈번) | 80 MB (안정적) |
| 스크롤 FPS (Desktop) | 60 FPS (CanvasKit) | 60 FPS 유지 |
표에서 볼 수 있듯이, 무조건적인 고성능 렌더러 사용보다는 타겟 디바이스에 맞는 전략적인 선택이 핵심입니다. 특히 LCP가 4.8초에서 1.2초로 줄어든 것은 사용자 체감 성능에 지대한 영향을 미쳤습니다. 이는 Flutter 프레임워크가 제공하는 deferred 키워드를 사용하여 무거운 라이브러리를 필요할 때만 로드하도록 코드를 분할(Code Splitting)한 덕분입니다.
주의사항 및 SEO 한계
모든 문제가 해결된 것은 아닙니다. Flutter web은 본질적으로 SPA(Single Page Application)이며, 캔버스 기반 렌더링을 할 경우 텍스트가 이미지가 아닌 픽셀로 그려지기 때문에 검색 엔진 로봇(Crawler)이 내용을 읽어가기 어렵습니다.
하지만 로그인 후 사용하는 어드민 대시보드, 사내 도구, 혹은 복잡한 인터랙션이 필요한 웹 앱(Web App)의 경우에는 Flutter가 제공하는 생산성과 크로스 플랫폼의 이점이 SEO의 단점을 충분히 상쇄하고도 남습니다.
결론
Flutter web은 아직 '은탄환(Silver Bullet)'이 아닙니다. 모바일 앱을 그대로 웹으로 옮길 수 있다는 마케팅 문구만 믿고 접근했다가는 렌더러 이슈와 번들 사이즈 문제로 고생할 수 있습니다. 하지만 본문에서 다룬 렌더러 분기 전략, 이미지 최적화, 그리고 지연 로딩을 적절히 활용한다면, 웹에서도 충분히 네이티브급의 사용자 경험을 제공할 수 있습니다. 특히 기존 Flutter 개발 인력이 있는 팀에게는 웹 개발을 위한 최고의 가속기가 될 것입니다.
Post a Comment