Flutter 튜토리얼 59편: 웹 앱 개발

이번 튜토리얼에서는 Flutter로 웹 애플리케이션을 개발하는 방법을 배워보겠습니다. 웹 지원 설정부터 개발, 빌드까지의 전체 과정을 다룹니다.

학습 목표#

이번 튜토리얼을 완료하면 다음을 할 수 있습니다:

  1. Flutter 웹 개발 환경을 설정할 수 있습니다
  2. 웹 프로젝트를 생성하고 실행할 수 있습니다
  3. 웹 렌더러1의 종류와 특징을 이해할 수 있습니다
  4. 웹 앱을 빌드하고 배포를 준비할 수 있습니다

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 웹 개발을 위해 다음이 필요합니다:

Terminal window
# Flutter SDK 설치 확인
flutter --version
# 웹 개발이 활성화되어 있는지 확인
flutter devices

Chrome 브라우저가 설치되어 있어야 합니다.

확인해보세요!

flutter devices 명령어를 실행하면 Chrome이 사용 가능한 기기 목록에 표시되어야 합니다.

2.2 새 웹 프로젝트 생성#

새 Flutter 프로젝트를 생성하면 기본적으로 웹 지원이 포함됩니다:

Terminal window
# 새 프로젝트 생성 (웹 포함)
flutter create my_web_app
# 프로젝트 디렉토리로 이동
cd my_web_app

2.3 기존 프로젝트에 웹 지원 추가#

기존 Flutter 프로젝트에 웹 지원을 추가하려면:

Terminal window
# 기존 프로젝트 디렉토리에서 실행
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.yaml

Chapter 3: 웹 앱 실행과 개발#

3.1 개발 서버 실행#

Chrome에서 웹 앱을 실행합니다:

Terminal window
# Chrome에서 실행
flutter run -d chrome
# Edge에서 실행 (Windows)
flutter run -d edge
# 웹 서버 모드로 실행 (브라우저 선택 가능)
flutter run -d web-server
Hot Reload 지원

Flutter 3.35부터 웹에서도 Hot Reload가 기본적으로 활성화됩니다. r 키를 눌러 Hot Reload를, R 키를 눌러 Hot Restart4를 실행할 수 있습니다.

3.2 Hot Reload 비활성화#

특정 상황에서 Hot Reload를 비활성화할 수 있습니다:

Terminal window
# 명령줄에서 비활성화
flutter run -d chrome --no-web-experimental-hot-reload

VS 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 웹은 두 가지 렌더러를 지원합니다:

렌더러설명다운로드 크기
CanvasKitSkia5를 WebAssembly로 컴파일약 1.5MB
Skwasm더 컴팩트한 Skia 버전, 멀티스레드 지원약 1.1MB

4.2 빌드 모드와 렌더러#

빌드 모드 사용 가능한 렌더러
─────────────────────────────────────────
기본 모드 CanvasKit만 사용
WebAssembly 모드 Skwasm (우선) → CanvasKit (폴백)
렌더러 선택 기준
  • CanvasKit: 모든 최신 브라우저와 호환, 안정적인 성능
  • Skwasm: 더 빠른 시작 시간, 더 나은 프레임 성능 (WasmGC6 지원 브라우저에서)

4.3 멀티스레드 렌더링#

Skwasm 렌더러는 멀티스레드 렌더링을 지원합니다. 이를 활성화하려면 서버가 다음 보안 헤더를 제공해야 합니다:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
주의하세요!

멀티스레드 렌더링을 위해서는 SharedArrayBuffer7 보안 요구 사항을 충족해야 합니다. 이 요구 사항을 충족하지 않으면 단일 스레드 모드로 실행됩니다.


Chapter 5: 웹 앱 빌드#

5.1 기본 빌드#

릴리스용 웹 앱을 빌드합니다:

Terminal window
# 기본 빌드 (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.json

5.2 WebAssembly 빌드#

더 나은 성능을 위해 WebAssembly 모드로 빌드합니다:

Terminal window
# WebAssembly 빌드
flutter build web --wasm
WebAssembly 빌드 요구 사항

WebAssembly 빌드를 사용하려면:

  1. 새로운 JS Interop 사용: dart:js_interop 라이브러리 사용
  2. 새로운 Web API 사용: package:web 패키지 사용 (기존 dart:html 대신)
  3. 숫자 호환성: Dart VM과 동일한 숫자 동작 보장

5.3 빌드 최적화 옵션#

Terminal window
# 트리 쉐이킹 아이콘 사용
flutter build web --tree-shake-icons
# 소스 맵 생성 비활성화 (파일 크기 감소)
flutter build web --no-source-maps
# 복합 최적화
flutter build web --wasm --tree-shake-icons --no-source-maps

Chapter 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를 사용하여 웹 앱을 디버깅합니다:

Terminal window
# 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 프로파일 빌드#

성능 분석을 위한 프로파일 빌드:

Terminal window
# 프로파일 모드로 실행
flutter run -d chrome --profile
# 프로파일 빌드
flutter build web --profile
성능 분석 팁

Chrome DevTools의 Performance 탭을 사용하여:

  • 프레임 드롭 분석
  • 렌더링 성능 측정
  • 메모리 누수 감지

를 수행할 수 있습니다.


Chapter 8: 테스트#

8.1 위젯 테스트#

웹 환경을 고려한 위젯 테스트:

test/widget_test.dart
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 통합 테스트#

브라우저에서 통합 테스트를 실행합니다:

integration_test/app_test.dart
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);
});
}
Terminal window
# 웹에서 통합 테스트 실행
flutter test integration_test --device-id chrome

Chapter 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 웹 앱 개발의 기초를 배웠습니다.

핵심 정리#

  1. 프로젝트 설정: flutter create로 웹 지원 프로젝트를 쉽게 생성할 수 있습니다
  2. 개발 서버: flutter run -d chrome으로 Hot Reload와 함께 개발할 수 있습니다
  3. 렌더러: CanvasKit(기본)과 Skwasm(WebAssembly) 중 선택할 수 있습니다
  4. 빌드: flutter build web 또는 flutter build web --wasm으로 배포용 빌드를 생성합니다
  5. 반응형 디자인: LayoutBuilder와 MediaQuery를 활용하여 반응형 레이아웃을 구현합니다

다음 단계#

  • WebAssembly 심화 학습 (튜토리얼 60편)
  • 웹 앱 배포 방법 (튜토리얼 64편)

참고 자료#


Footnotes#

  1. 렌더러(Renderer)는 UI를 화면에 그리는 역할을 담당하는 구성 요소입니다.

  2. WebAssembly(Wasm)는 웹 브라우저에서 실행할 수 있는 저수준 바이너리 명령어 형식으로, JavaScript보다 빠른 실행 속도를 제공합니다.

  3. SPA(Single Page Application)는 페이지 전환 없이 동적으로 콘텐츠를 업데이트하는 웹 애플리케이션입니다.

  4. Hot Restart는 앱 상태를 초기화하면서 전체 앱을 다시 시작하는 기능입니다. Hot Reload보다 더 많은 변경 사항을 반영할 수 있습니다.

  5. Skia는 Google이 개발한 오픈소스 2D 그래픽 라이브러리로, Chrome, Android, Flutter 등에서 사용됩니다.

  6. WasmGC(WebAssembly Garbage Collection)는 WebAssembly에서 가비지 컬렉션을 지원하는 확장 기능입니다.

  7. SharedArrayBuffer는 여러 웹 워커 간에 메모리를 공유할 수 있게 해주는 JavaScript 객체입니다.

공유

이 글이 도움이 되었다면 다른 사람과 공유해주세요!

Flutter 튜토리얼 59편: 웹 앱 개발
https://moodturnpost.net/posts/flutter/flutter-platform-web/
작성자
Moodturn
게시일
2026-01-08
Moodturn

목차