Flutter 튜토리얼 59편: 웹 앱 개발
이번 튜토리얼에서는 Flutter로 웹 애플리케이션을 개발하는 방법을 배워보겠습니다. 웹 지원 설정부터 개발, 빌드까지의 전체 과정을 다룹니다.
학습 목표
이번 튜토리얼을 완료하면 다음을 할 수 있습니다:
- Flutter 웹 개발 환경을 설정할 수 있습니다
- 웹 프로젝트를 생성하고 실행할 수 있습니다
- 웹 렌더러1의 종류와 특징을 이해할 수 있습니다
- 웹 앱을 빌드하고 배포를 준비할 수 있습니다
Chapter 1: Flutter 웹 지원 이해하기
1.1 Flutter 웹의 특징
Flutter는 하나의 코드베이스로 iOS, Android, 웹, 데스크톱 앱을 만들 수 있습니다.
왜 Flutter 웹인가요?Flutter 웹을 사용하면 다음과 같은 장점이 있습니다:
- 코드 재사용: 모바일 앱과 동일한 코드를 웹에서도 사용할 수 있습니다
- 일관된 UI: 모든 플랫폼에서 동일한 사용자 경험을 제공합니다
- WebAssembly2 지원: 네이티브에 가까운 성능을 제공합니다
- Hot Reload 지원: 빠른 개발 사이클을 유지할 수 있습니다
1.2 Flutter 웹이 적합한 앱 유형
Flutter 웹은 다음과 같은 유형의 앱에 적합합니다:
| 앱 유형 | 설명 | 적합도 |
|---|---|---|
| SPA3 | 그래픽이 풍부하고 인터랙티브한 웹 앱 | 매우 적합 |
| 기존 모바일 앱의 웹 버전 | 모바일 앱을 웹으로 확장 | 매우 적합 |
| 대시보드 | 데이터 시각화 및 관리 도구 | 적합 |
| 콘텐츠 중심 웹사이트 | 블로그, 뉴스 등 텍스트 중심 | 부적합 |
주의하세요!텍스트 중심의 정적 콘텐츠 웹사이트에는 Flutter 웹이 적합하지 않습니다. 이러한 경우 전통적인 웹 기술(HTML, CSS, JavaScript)을 사용하는 것이 좋습니다.
Chapter 2: 웹 개발 환경 설정
2.1 필수 요구 사항
Flutter 웹 개발을 위해 다음이 필요합니다:
# Flutter SDK 설치 확인flutter --version
# 웹 개발이 활성화되어 있는지 확인flutter devicesChrome 브라우저가 설치되어 있어야 합니다.
확인해보세요!
flutter devices명령어를 실행하면 Chrome이 사용 가능한 기기 목록에 표시되어야 합니다.
2.2 새 웹 프로젝트 생성
새 Flutter 프로젝트를 생성하면 기본적으로 웹 지원이 포함됩니다:
# 새 프로젝트 생성 (웹 포함)flutter create my_web_app
# 프로젝트 디렉토리로 이동cd my_web_app2.3 기존 프로젝트에 웹 지원 추가
기존 Flutter 프로젝트에 웹 지원을 추가하려면:
# 기존 프로젝트 디렉토리에서 실행flutter create . --platforms web이 명령어는 web/ 디렉토리를 생성합니다:
my_app/├── web/│ ├── favicon.png│ ├── icons/│ │ ├── Icon-192.png│ │ ├── Icon-512.png│ │ └── Icon-maskable-192.png│ ├── index.html│ └── manifest.json├── lib/│ └── main.dart└── pubspec.yamlChapter 3: 웹 앱 실행과 개발
3.1 개발 서버 실행
Chrome에서 웹 앱을 실행합니다:
# Chrome에서 실행flutter run -d chrome
# Edge에서 실행 (Windows)flutter run -d edge
# 웹 서버 모드로 실행 (브라우저 선택 가능)flutter run -d web-serverHot Reload 지원Flutter 3.35부터 웹에서도 Hot Reload가 기본적으로 활성화됩니다.
r키를 눌러 Hot Reload를,R키를 눌러 Hot Restart4를 실행할 수 있습니다.
3.2 Hot Reload 비활성화
특정 상황에서 Hot Reload를 비활성화할 수 있습니다:
# 명령줄에서 비활성화flutter run -d chrome --no-web-experimental-hot-reloadVS Code에서 비활성화하려면 launch.json을 수정합니다:
{ "configurations": [ { "name": "Flutter for web (hot reload disabled)", "type": "dart", "request": "launch", "program": "lib/main.dart", "args": [ "-d", "chrome", "--no-web-experimental-hot-reload" ] } ]}3.3 웹 전용 코드 작성
플랫폼별 코드를 작성할 때는 kIsWeb 상수를 사용합니다:
import 'package:flutter/foundation.dart';import 'package:flutter/material.dart';
class PlatformAwareWidget extends StatelessWidget { const PlatformAwareWidget({super.key});
@override Widget build(BuildContext context) { if (kIsWeb) { return const WebSpecificWidget(); } else { return const MobileSpecificWidget(); } }}
class WebSpecificWidget extends StatelessWidget { const WebSpecificWidget({super.key});
@override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(20), child: const Text( '웹 브라우저에서 실행 중입니다.', style: TextStyle(fontSize: 24), ), ); }}
class MobileSpecificWidget extends StatelessWidget { const MobileSpecificWidget({super.key});
@override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(20), child: const Text( '모바일 기기에서 실행 중입니다.', style: TextStyle(fontSize: 24), ), ); }}Chapter 4: 웹 렌더러 이해하기
4.1 렌더러 종류
Flutter 웹은 두 가지 렌더러를 지원합니다:
| 렌더러 | 설명 | 다운로드 크기 |
|---|---|---|
| CanvasKit | Skia5를 WebAssembly로 컴파일 | 약 1.5MB |
| Skwasm | 더 컴팩트한 Skia 버전, 멀티스레드 지원 | 약 1.1MB |
4.2 빌드 모드와 렌더러
빌드 모드 사용 가능한 렌더러─────────────────────────────────────────기본 모드 CanvasKit만 사용WebAssembly 모드 Skwasm (우선) → CanvasKit (폴백)렌더러 선택 기준
- CanvasKit: 모든 최신 브라우저와 호환, 안정적인 성능
- Skwasm: 더 빠른 시작 시간, 더 나은 프레임 성능 (WasmGC6 지원 브라우저에서)
4.3 멀티스레드 렌더링
Skwasm 렌더러는 멀티스레드 렌더링을 지원합니다. 이를 활성화하려면 서버가 다음 보안 헤더를 제공해야 합니다:
Cross-Origin-Opener-Policy: same-originCross-Origin-Embedder-Policy: require-corp주의하세요!멀티스레드 렌더링을 위해서는 SharedArrayBuffer7 보안 요구 사항을 충족해야 합니다. 이 요구 사항을 충족하지 않으면 단일 스레드 모드로 실행됩니다.
Chapter 5: 웹 앱 빌드
5.1 기본 빌드
릴리스용 웹 앱을 빌드합니다:
# 기본 빌드 (CanvasKit 렌더러 사용)flutter build web빌드 결과는 build/web/ 디렉토리에 생성됩니다:
build/web/├── assets/│ ├── AssetManifest.json│ ├── FontManifest.json│ ├── fonts/│ └── packages/├── canvaskit/├── favicon.png├── flutter.js├── flutter_bootstrap.js├── flutter_service_worker.js├── icons/├── index.html├── main.dart.js└── manifest.json5.2 WebAssembly 빌드
더 나은 성능을 위해 WebAssembly 모드로 빌드합니다:
# WebAssembly 빌드flutter build web --wasmWebAssembly 빌드 요구 사항WebAssembly 빌드를 사용하려면:
- 새로운 JS Interop 사용:
dart:js_interop라이브러리 사용- 새로운 Web API 사용:
package:web패키지 사용 (기존dart:html대신)- 숫자 호환성: Dart VM과 동일한 숫자 동작 보장
5.3 빌드 최적화 옵션
# 트리 쉐이킹 아이콘 사용flutter build web --tree-shake-icons
# 소스 맵 생성 비활성화 (파일 크기 감소)flutter build web --no-source-maps
# 복합 최적화flutter build web --wasm --tree-shake-icons --no-source-mapsChapter 6: 웹 앱 초기화 커스터마이징
6.1 index.html 수정
web/index.html 파일을 수정하여 앱 초기화를 커스터마이징할 수 있습니다:
<!DOCTYPE html><html><head> <meta charset="UTF-8"> <meta content="IE=Edge" http-equiv="X-UA-Compatible"> <meta name="description" content="My Flutter Web App">
<!-- iOS 지원 --> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-mobile-web-app-title" content="My App">
<!-- Favicon --> <link rel="apple-touch-icon" href="icons/Icon-192.png"> <link rel="icon" type="image/png" href="favicon.png"/>
<title>My Flutter Web App</title> <link rel="manifest" href="manifest.json"></head><body> <!-- 로딩 인디케이터 --> <div id="loading"> <style> #loading { display: flex; justify-content: center; align-items: center; height: 100vh; background-color: #ffffff; } .spinner { width: 50px; height: 50px; border: 3px solid #f3f3f3; border-top: 3px solid #3498db; border-radius: 50%; animation: spin 1s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } </style> <div class="spinner"></div> </div>
<script src="flutter_bootstrap.js" async></script></body></html>6.2 렌더러 수동 선택
특정 렌더러를 강제로 사용하도록 설정할 수 있습니다:
<body> <script> {{flutter_js}} {{flutter_build_config}}
// 렌더러 선택 로직 const useCanvasKit = !navigator.userAgent.includes('Chrome');
const config = { renderer: useCanvasKit ? "canvaskit" : "skwasm", };
_flutter.loader.load({ config: config, }); </script></body>6.3 스플래시 화면 구현
앱 로딩 중 스플래시 화면을 표시합니다:
<body> <div id="splash" style="position: fixed; inset: 0; display: flex; justify-content: center; align-items: center; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);"> <img src="icons/Icon-192.png" alt="Loading..." style="width: 100px; height: 100px;"> </div>
<script> {{flutter_js}} {{flutter_build_config}}
_flutter.loader.load({ onEntrypointLoaded: function(engineInitializer) { engineInitializer.initializeEngine().then(function(appRunner) { // 스플래시 화면 제거 document.getElementById('splash').remove(); appRunner.runApp(); }); } }); </script></body>Chapter 7: 웹 디버깅
7.1 Flutter DevTools 사용
Flutter DevTools를 사용하여 웹 앱을 디버깅합니다:
# DevTools 실행flutter run -d chrome# 실행 후 'd' 키를 눌러 DevTools 열기DevTools에서 사용할 수 있는 기능:
- Widget Inspector: 위젯 트리 검사
- Performance: 프레임 분석
- Memory: 메모리 사용량 모니터링
- Logging: 로그 메시지 확인
7.2 Chrome DevTools 활용
Chrome DevTools도 함께 사용할 수 있습니다:
// 디버그 로그 출력import 'dart:developer' as developer;
void logMessage(String message) { developer.log(message, name: 'MyApp');}
// 브레이크포인트 설정void someFunction() { debugger(); // Chrome DevTools에서 중단점으로 작동 // 코드 계속...}7.3 프로파일 빌드
성능 분석을 위한 프로파일 빌드:
# 프로파일 모드로 실행flutter run -d chrome --profile
# 프로파일 빌드flutter build web --profile성능 분석 팁Chrome DevTools의 Performance 탭을 사용하여:
- 프레임 드롭 분석
- 렌더링 성능 측정
- 메모리 누수 감지
를 수행할 수 있습니다.
Chapter 8: 테스트
8.1 위젯 테스트
웹 환경을 고려한 위젯 테스트:
import 'package:flutter/material.dart';import 'package:flutter_test/flutter_test.dart';import 'package:my_web_app/main.dart';
void main() { testWidgets('웹 앱 초기 화면 테스트', (WidgetTester tester) async { await tester.pumpWidget(const MyApp());
expect(find.text('Welcome'), findsOneWidget); expect(find.byType(ElevatedButton), findsAtLeast(1)); });
testWidgets('반응형 레이아웃 테스트', (WidgetTester tester) async { // 웹 화면 크기 설정 tester.view.physicalSize = const Size(1920, 1080); tester.view.devicePixelRatio = 1.0;
await tester.pumpWidget(const MyApp());
// 데스크톱 레이아웃 확인 expect(find.byType(Row), findsAtLeast(1));
// 모바일 화면 크기로 변경 tester.view.physicalSize = const Size(375, 812); await tester.pump();
// 모바일 레이아웃 확인 expect(find.byType(Column), findsAtLeast(1));
// 리소스 정리 tester.view.resetPhysicalSize(); });}8.2 통합 테스트
브라우저에서 통합 테스트를 실행합니다:
import 'package:flutter_test/flutter_test.dart';import 'package:integration_test/integration_test.dart';import 'package:my_web_app/main.dart' as app;
void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('전체 앱 플로우 테스트', (WidgetTester tester) async { app.main(); await tester.pumpAndSettle();
// 버튼 클릭 await tester.tap(find.byType(ElevatedButton).first); await tester.pumpAndSettle();
// 결과 확인 expect(find.text('Clicked!'), findsOneWidget); });}# 웹에서 통합 테스트 실행flutter test integration_test --device-id chromeChapter 9: 반응형 웹 디자인
9.1 반응형 레이아웃 구현
화면 크기에 따라 레이아웃을 조정합니다:
import 'package:flutter/material.dart';
class ResponsiveLayout extends StatelessWidget { final Widget mobile; final Widget tablet; final Widget desktop;
const ResponsiveLayout({ super.key, required this.mobile, required this.tablet, required this.desktop, });
static bool isMobile(BuildContext context) => MediaQuery.of(context).size.width < 600;
static bool isTablet(BuildContext context) => MediaQuery.of(context).size.width >= 600 && MediaQuery.of(context).size.width < 1200;
static bool isDesktop(BuildContext context) => MediaQuery.of(context).size.width >= 1200;
@override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { if (constraints.maxWidth >= 1200) { return desktop; } else if (constraints.maxWidth >= 600) { return tablet; } else { return mobile; } }, ); }}9.2 반응형 위젯 예제
class MyHomePage extends StatelessWidget { const MyHomePage({super.key});
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('반응형 웹 앱'), ), body: ResponsiveLayout( mobile: const MobileLayout(), tablet: const TabletLayout(), desktop: const DesktopLayout(), ), ); }}
class DesktopLayout extends StatelessWidget { const DesktopLayout({super.key});
@override Widget build(BuildContext context) { return Row( children: [ // 사이드바 NavigationRail( destinations: const [ NavigationRailDestination( icon: Icon(Icons.home), label: Text('Home'), ), NavigationRailDestination( icon: Icon(Icons.settings), label: Text('Settings'), ), ], selectedIndex: 0, ), const VerticalDivider(thickness: 1, width: 1), // 메인 콘텐츠 Expanded( child: Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 1200), child: const ContentArea(), ), ), ), ], ); }}
class TabletLayout extends StatelessWidget { const TabletLayout({super.key});
@override Widget build(BuildContext context) { return Row( children: [ NavigationRail( destinations: const [ NavigationRailDestination( icon: Icon(Icons.home), label: Text('Home'), ), NavigationRailDestination( icon: Icon(Icons.settings), label: Text('Settings'), ), ], selectedIndex: 0, labelType: NavigationRailLabelType.none, ), const Expanded(child: ContentArea()), ], ); }}
class MobileLayout extends StatelessWidget { const MobileLayout({super.key});
@override Widget build(BuildContext context) { return Scaffold( body: const ContentArea(), bottomNavigationBar: BottomNavigationBar( items: const [ BottomNavigationBarItem( icon: Icon(Icons.home), label: 'Home', ), BottomNavigationBarItem( icon: Icon(Icons.settings), label: 'Settings', ), ], ), ); }}
class ContentArea extends StatelessWidget { const ContentArea({super.key});
@override Widget build(BuildContext context) { return ListView.builder( padding: const EdgeInsets.all(16), itemCount: 20, itemBuilder: (context, index) { return Card( margin: const EdgeInsets.only(bottom: 12), child: ListTile( leading: CircleAvatar(child: Text('${index + 1}')), title: Text('Item ${index + 1}'), subtitle: const Text('This is a sample item'), ), ); }, ); }}마무리
이번 튜토리얼에서 Flutter 웹 앱 개발의 기초를 배웠습니다.
핵심 정리
- 프로젝트 설정:
flutter create로 웹 지원 프로젝트를 쉽게 생성할 수 있습니다 - 개발 서버:
flutter run -d chrome으로 Hot Reload와 함께 개발할 수 있습니다 - 렌더러: CanvasKit(기본)과 Skwasm(WebAssembly) 중 선택할 수 있습니다
- 빌드:
flutter build web또는flutter build web --wasm으로 배포용 빌드를 생성합니다 - 반응형 디자인: LayoutBuilder와 MediaQuery를 활용하여 반응형 레이아웃을 구현합니다
다음 단계
- WebAssembly 심화 학습 (튜토리얼 60편)
- 웹 앱 배포 방법 (튜토리얼 64편)
참고 자료
- Web support for Flutter 공식 문서
- Building a web application with Flutter
- Web renderers
- Set up web development for Flutter
Footnotes
-
렌더러(Renderer)는 UI를 화면에 그리는 역할을 담당하는 구성 요소입니다. ↩
-
WebAssembly(Wasm)는 웹 브라우저에서 실행할 수 있는 저수준 바이너리 명령어 형식으로, JavaScript보다 빠른 실행 속도를 제공합니다. ↩
-
SPA(Single Page Application)는 페이지 전환 없이 동적으로 콘텐츠를 업데이트하는 웹 애플리케이션입니다. ↩
-
Hot Restart는 앱 상태를 초기화하면서 전체 앱을 다시 시작하는 기능입니다. Hot Reload보다 더 많은 변경 사항을 반영할 수 있습니다. ↩
-
Skia는 Google이 개발한 오픈소스 2D 그래픽 라이브러리로, Chrome, Android, Flutter 등에서 사용됩니다. ↩
-
WasmGC(WebAssembly Garbage Collection)는 WebAssembly에서 가비지 컬렉션을 지원하는 확장 기능입니다. ↩
공유
이 글이 도움이 되었다면 다른 사람과 공유해주세요!