Flutter 튜토리얼 60편: WebAssembly와 웹 고급
개요
Flutter 웹 앱의 성능을 극대화하고 고급 기능을 활용하는 방법을 알아봅니다. WebAssembly1를 통한 네이티브 수준의 성능, 앱 초기화 커스터마이징, 기존 웹 앱에 Flutter 임베딩, 그리고 웹 이미지 처리까지 살펴보겠습니다.
1. WebAssembly (Wasm) 지원
Why: 왜 WebAssembly를 사용하나요?
WebAssembly는 JavaScript보다 훨씬 빠른 실행 속도를 제공합니다. 복잡한 애니메이션, 대량의 데이터 처리, 게임 등 성능이 중요한 웹 앱에서 큰 이점을 얻을 수 있습니다.
What: WebAssembly란 무엇인가요?
Flutter는 Dart 코드를 WebAssembly로 컴파일하여 브라우저에서 실행할 수 있습니다. 이를 통해 네이티브에 가까운 성능을 웹에서도 경험할 수 있습니다.
| 특징 | JavaScript | WebAssembly |
|---|---|---|
| 실행 속도 | 인터프리터/JIT | 네이티브에 가까움 |
| 파일 크기 | 텍스트 기반 | 바이너리 (압축) |
| 시작 시간 | 파싱 필요 | 즉시 실행 |
| 메모리 | 가비지 컬렉션 | 수동 관리 가능 |
How: WebAssembly 빌드 방법
1단계: Flutter 버전 확인
# Flutter 3.24 이상 필요flutter upgradeflutter --version2단계: 의존성 호환성 확인
WebAssembly 컴파일을 위해서는 새로운 JS interop2 라이브러리를 사용해야 합니다.
# pubspec.yaml - 호환되는 라이브러리 사용dependencies: web: ^0.5.0 # dart:html 대신 사용// 기존 (Wasm 미지원)import 'dart:html';
// 새로운 방식 (Wasm 지원)import 'package:web/web.dart';import 'dart:js_interop';3단계: index.html 업데이트
# web 디렉토리 재생성 (최신 초기화 방식 적용)flutter create . --platforms web4단계: Wasm으로 실행/빌드
# 개발 모드 실행flutter run -d chrome --wasm
# 프로덕션 빌드flutter build web --wasmWasm 실행 여부 확인
class WasmChecker extends StatelessWidget { @override Widget build(BuildContext context) { // 빌드 시점에 결정되는 환경 변수 (권장) const isWasm = bool.fromEnvironment('dart.tool.dart2wasm');
// 또는 런타임에 확인 (대안) // final isWasm = identical(double.nan, double.nan);
return Center( child: Text( isWasm ? 'WebAssembly로 실행 중' : 'JavaScript로 실행 중', style: TextStyle(fontSize: 24), ), ); }}Watch out: 주의사항
멀티스레딩을 위한 HTTP 헤더 설정
Wasm 멀티스레딩을 사용하려면 서버에서 특정 HTTP 헤더를 설정해야 합니다.
| 헤더 | 값 |
|---|---|
Cross-Origin-Embedder-Policy | credentialless 또는 require-corp |
Cross-Origin-Opener-Policy | same-origin |
브라우저 호환성
2. 웹 앱 초기화 커스터마이징
Why: 왜 초기화를 커스터마이징하나요?
기본 초기화 대신 커스텀 로직을 추가하여 로딩 화면 표시, 설정 변경, 서비스 워커 제어 등을 할 수 있습니다.
What: 초기화 토큰이란?
Flutter 빌드 시 특별한 토큰들이 실제 코드로 대체됩니다.
| 토큰 | 설명 |
|---|---|
{{flutter_js}} | FlutterLoader를 사용 가능하게 하는 JS 코드 |
{{flutter_build_config}} | 빌드 메타데이터 설정 |
{{flutter_service_worker_version}} | 서비스 워커 버전 번호 |
{{flutter_bootstrap_js}} | 부트스트랩 스크립트 전체 내용 |
How: 커스텀 초기화 구현
기본 부트스트랩 스크립트
<!DOCTYPE html><html><head> <meta charset="UTF-8"> <title>My Flutter App</title></head><body> <script> {{flutter_bootstrap_js}} </script></body></html>커스텀 부트스트랩 스크립트
{{flutter_js}}{{flutter_build_config}}
// 로딩 표시const loading = document.createElement('div');loading.id = 'loading';loading.innerHTML = '<h1>로딩 중...</h1>';document.body.appendChild(loading);
_flutter.loader.load({ onEntrypointLoaded: async function(engineInitializer) { loading.innerHTML = '<h1>엔진 초기화 중...</h1>';
const appRunner = await engineInitializer.initializeEngine();
loading.innerHTML = '<h1>앱 시작 중...</h1>'; await appRunner.runApp();
// 로딩 요소 제거 loading.remove(); }});설정 옵션
_flutter.loader.load({ config: { // 렌더러 지정 (canvaskit 또는 skwasm) renderer: 'canvaskit',
// CanvasKit 설정 canvasKitBaseUrl: '/canvaskit/', canvasKitVariant: 'auto', // auto, full, chromium canvasKitForceCpuOnly: false, canvasKitMaximumSurfaces: 8,
// Wasm 싱글스레드 강제 (호환성용) forceSingleThreadedSkwasm: true,
// 에셋 경로 설정 (CDN 사용 시) assetBase: 'https://cdn.example.com/assets/',
// 디버깅 debugShowSemanticNodes: false, }});URL 파라미터로 렌더러 선택
{{flutter_js}}{{flutter_build_config}}
const searchParams = new URLSearchParams(window.location.search);const renderer = searchParams.get('renderer');const userConfig = renderer ? { 'renderer': renderer } : {};
_flutter.loader.load({ config: userConfig,});
// 사용: https://myapp.com/?renderer=skwasm3. 기존 웹 앱에 Flutter 임베딩
Why: 왜 Flutter를 임베딩하나요?
기존 웹 사이트의 일부 섹션만 Flutter로 구현하거나, 점진적으로 Flutter로 마이그레이션할 때 유용합니다.
What: 임베딩 모드의 종류
| 모드 | 설명 | 사용 사례 |
|---|---|---|
| 전체 페이지 | 기본 모드, 전체 화면 사용 | 독립 웹 앱 |
| iframe | 별도 문서로 격리 | 위젯 형태 삽입 |
| 임베디드 | HTML 요소에 직접 렌더링 | 하이브리드 앱 |
How: 임베딩 구현 방법
iframe 임베딩 (가장 간단)
<!-- 호스트 페이지 --><iframe src="https://my-flutter-app.com/index.html" width="800" height="600" style="border: none;"></iframe>멀티뷰 임베디드 모드
JavaScript 설정:
{{flutter_js}}{{flutter_build_config}}
_flutter.loader.load({ onEntrypointLoaded: async function(engineInitializer) { let engine = await engineInitializer.initializeEngine({ multiViewEnabled: true, // 임베디드 모드 활성화 });
let app = await engine.runApp();
// 전역으로 노출하여 호스트 앱에서 사용 window.flutterApp = app; }});호스트 페이지에서 뷰 관리:
// 뷰 추가let viewId = window.flutterApp.addView({ hostElement: document.querySelector('#flutter-container'), initialData: { greeting: '안녕하세요!', userId: 12345, }});
// 뷰 제거window.flutterApp.removeView(viewId);Dart에서 멀티뷰 처리:
import 'dart:ui' show FlutterView;import 'package:flutter/widgets.dart';
void main() { runWidget( MultiViewApp( viewBuilder: (BuildContext context) => const MyApp(), ), );}
/// 각 뷰에 대해 위젯을 생성하는 래퍼class MultiViewApp extends StatefulWidget { const MultiViewApp({super.key, required this.viewBuilder});
final WidgetBuilder viewBuilder;
@override State<MultiViewApp> createState() => _MultiViewAppState();}
class _MultiViewAppState extends State<MultiViewApp> with WidgetsBindingObserver { Map<Object, Widget> _views = {};
@override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); _updateViews(); }
@override void didChangeMetrics() { _updateViews(); }
void _updateViews() { final newViews = <Object, Widget>{};
for (final view in WidgetsBinding.instance.platformDispatcher.views) { newViews[view.viewId] = _views[view.viewId] ?? View( view: view, child: Builder(builder: widget.viewBuilder), ); }
setState(() => _views = newViews); }
@override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); }
@override Widget build(BuildContext context) { return ViewCollection(views: _views.values.toList()); }}뷰 식별 및 초기 데이터
class MyWidget extends StatelessWidget { @override Widget build(BuildContext context) { // 현재 뷰 ID 가져오기 final viewId = View.of(context).viewId;
// 초기 데이터 가져오기 (JS interop 필요) // final initialData = ui_web.views.getInitialData(viewId);
return Center( child: Text('뷰 ID: $viewId'), ); }}hostElement로 단일 뷰 임베딩
// 멀티뷰 없이 특정 요소에 렌더링_flutter.loader.load({ config: { hostElement: document.getElementById('flutter_host'), }});<div id="flutter_host" style="width: 100%; height: 400px;"></div>4. 웹 이미지 표시
Why: 왜 웹 이미지 처리가 특별한가요?
웹 브라우저의 보안 정책(CORS3)으로 인해 다른 도메인의 이미지를 직접 처리하는 데 제한이 있습니다.
What: 이미지 표시 방식 비교
| 방식 | 장점 | 단점 |
|---|---|---|
<img> 태그 | 캐싱, 최적화 자동 | 픽셀 접근 불가 |
| Canvas drawImage | 픽셀 조작 가능 | CORS 제한 |
| WebGL | 최고 성능, 셰이더 | 복잡한 구현 |
CORS 문제 이해하기
How: CORS 문제 해결 방법
1. 같은 출처 이미지 사용
// 에셋 이미지Image.asset('assets/images/photo.png')
// 같은 도메인 네트워크 이미지Image.network('/images/photo.png')
// 메모리 이미지Image.memory(bytes)2. CORS 활성화된 CDN 사용
Firebase Hosting 예시:
{ "hosting": { "headers": [ { "source": "**/*.@(jpg|jpeg|png|gif|webp)", "headers": [ { "key": "Access-Control-Allow-Origin", "value": "*" } ] } ] }}3. CORS 프록시 사용
class ImageProxy { // CloudFlare Workers나 자체 서버를 통한 프록시 static String getProxiedUrl(String originalUrl) { return 'https://my-cors-proxy.com/?url=${Uri.encodeComponent(originalUrl)}'; }}
// 사용Image.network(ImageProxy.getProxiedUrl('https://external-site.com/image.jpg'))4. HTML 플랫폼 뷰 사용
import 'package:flutter/foundation.dart';import 'package:flutter/widgets.dart';
class WebImage extends StatelessWidget { final String url;
const WebImage({super.key, required this.url});
@override Widget build(BuildContext context) { if (kIsWeb) { // 웹에서는 HTML img 태그 사용 return HtmlElementView( viewType: 'img-view-$url', onPlatformViewCreated: (id) { // 플랫폼 뷰 생성 시 처리 }, ); }
// 다른 플랫폼에서는 일반 Image 사용 return Image.network(url); }}Watch out: 주의사항
- 보안: 프록시 서버를 사용할 때 신뢰할 수 있는 이미지만 프록시하세요.
- 성능: HTML 플랫폼 뷰는 Flutter 위젯보다 성능이 낮을 수 있습니다.
- 캐싱: 네트워크 이미지는
cached_network_image패키지 사용을 고려하세요.
5. 종합 예제: 고급 웹 앱 설정
import 'package:flutter/material.dart';import 'package:flutter/foundation.dart';
void main() { WidgetsFlutterBinding.ensureInitialized();
runApp(const AdvancedWebApp());}
class AdvancedWebApp extends StatelessWidget { const AdvancedWebApp({super.key});
@override Widget build(BuildContext context) { return MaterialApp( title: 'Advanced Web App', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), useMaterial3: true, ), home: const HomePage(), ); }}
class HomePage extends StatelessWidget { const HomePage({super.key});
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Flutter 웹 고급 기능'), ), body: ListView( padding: const EdgeInsets.all(16), children: [ _buildInfoCard( '실행 환경', _getExecutionEnvironment(), Icons.computer, ), const SizedBox(height: 16), _buildInfoCard( '렌더러', kIsWeb ? '웹 렌더러 사용 중' : '네이티브 렌더러', Icons.brush, ), const SizedBox(height: 16), _buildInfoCard( 'WebAssembly', _isWasm() ? '활성화됨' : '비활성화됨', Icons.speed, ), ], ), ); }
String _getExecutionEnvironment() { if (kIsWeb) { return '웹 브라우저'; } else if (defaultTargetPlatform == TargetPlatform.android) { return 'Android'; } else if (defaultTargetPlatform == TargetPlatform.iOS) { return 'iOS'; } else { return '데스크톱'; } }
bool _isWasm() { return const bool.fromEnvironment('dart.tool.dart2wasm'); }
Widget _buildInfoCard(String title, String value, IconData icon) { return Card( child: ListTile( leading: Icon(icon, size: 40), title: Text(title), subtitle: Text(value), ), ); }}마무리
이번 튜토리얼에서는 Flutter 웹의 고급 기능들을 살펴보았습니다.
핵심 요약
| 기능 | 핵심 포인트 |
|---|---|
| WebAssembly | --wasm 플래그로 빌드, 새로운 JS interop 필요 |
| 앱 초기화 | flutter_bootstrap.js 커스터마이징 |
| Flutter 임베딩 | multiViewEnabled로 멀티뷰 지원 |
| 웹 이미지 | CORS 정책 이해 및 우회 방법 |
다음 단계
- 61편에서는 데스크톱 앱 개발을 다룹니다.
- 실제 프로젝트에 WebAssembly를 적용해보세요.
- 기존 웹사이트에 Flutter 위젯을 임베딩해보세요.
참고 자료
Footnotes
공유
이 글이 도움이 되었다면 다른 사람과 공유해주세요!