OS๋ณ SDK ์ค์น๋ถํฐ Android/iOS ํด์ฒด์ธ ์ค์ , ํ์ CLI, ์์ ฏ ์๋ช
์ฃผ๊ธฐ, ๋ ์ด์์/์คํ์ผ๋ง, ์ํ ๊ด๋ฆฌ(Provider, Riverpod), ๋ด๋น๊ฒ์ด์
(GoRouter), ๋น๋๊ธฐ ํต์ , ์ ๋๋ฉ์ด์
๋ฐ ๋น๋ ๋ฐฐํฌ๊น์ง ๋จ๊ณ๋ณ ํต ๋ ํผ๋ฐ์ค
## 1. [Step 1] ์ด์์ฒด์ ๋ณ Flutter SDK ์ค์น & ํ๊ฒฝ ๋ณ์ ์ค์
```bash
# =============================================================
# ๐ช Windows ํ๊ฒฝ ์ค์น (๊ฐ์ฅ ํํ ๊ถํ/๊ฒฝ๋ก ์ค์ ๋ฐฉ์ง)
# =============================================================
# 1. ์ฌ์ ํ์ ์๊ตฌ์ฌํญ: Git for Windows ์ค์น (https://git-scm.com)
# 2. ๊ณต์ ํํ์ด์ง(docs.flutter.dev)์์ flutter_windows_x.x.x-stable.zip ๋ค์ด๋ก๋
# โ ๏ธ ์ฃผ์: 'C:\Program Files\' ๊ฐ์ ๊ด๋ฆฌ์ ๊ถํ ํ์ ํด๋๊ฐ ์๋ 'C:\src\flutter' ๋๋ 'C:\flutter'์ ์์ถ ํด์ !
# ๋๋ Git ๋ช
๋ น์ด๋ก ์ง์ ์ํ๋ ์์น์ ํด๋ก :
git clone https://github.com/flutter/flutter.git -b stable C:\flutter
# 3. ํ๊ฒฝ ๋ณ์ (PATH)์ Flutter bin ๊ฒฝ๋ก ๋ฑ๋ก:
# [์์] -> '์์คํ
ํ๊ฒฝ ๋ณ์ ํธ์ง' ๊ฒ์ -> [ํ๊ฒฝ ๋ณ์] -> [์ฌ์ฉ์ ๋ณ์] ๋๋ [์์คํ
๋ณ์]์ 'Path' ํธ์ง
# -> '์๋ก ๋ง๋ค๊ธฐ' -> 'C:\flutter\bin' ์ถ๊ฐ ํ ํ์ธ
# (PowerShell ๊ด๋ฆฌ์ ๊ถํ์ผ๋ก ํ ์ค ์ค์ ์):
[Environment]::SetEnvironmentVariable("Path", $env:Path + ";C:\flutter\bin", [EnvironmentVariableTarget]::User)
# 4. ์ ํฐ๋ฏธ๋(cmd ๋๋ powershell) ์ด๊ณ ํ์ธ:
flutter --version
# =============================================================
# ๐ macOS ํ๊ฒฝ ์ค์น (Apple Silicon M1/M2/M3 vs Intel)
# =============================================================
# Homebrew๋ฅผ ํตํ ์ด๊ฐํธ ์ค์น (๊ถ์ฅ):
brew install --cask flutter
# ๋๋ ์๋ ์์ถ ํด์ ๋ฐฉ์:
# 1. ์ํคํ
์ฒ์ ๋ง๋ zip ๋ค์ด๋ก๋ (Apple Silicon: arm64 / Intel: x64)
mkdir -p ~/development
unzip ~/Downloads/flutter_macos_arm64_*.zip -d ~/development/
# 2. ํ๊ฒฝ ๋ณ์(PATH) ์ถ๊ฐ (~/.zshrc ๋๋ ~/.bashrc):
echo 'export PATH="$PATH:$HOME/development/flutter/bin"' >> ~/.zshrc
source ~/.zshrc
# 3. ์ค์น ํ์ธ:
flutter --version
# =============================================================
# ๐ง Linux (Ubuntu/Debian) ํ๊ฒฝ ์ค์น
# =============================================================
# Snap์ ํตํ ์ํด๋ฆญ ์ค์น:
sudo snap install flutter --classic
```
## 2. [Step 2] ๊ฐ๋ฐ ํด์ฒด์ธ ์ค์ & ๋ผ์ด์ ์ค ๋์ (Android / iOS / VS Code)
```bash
# -------------------------------------------------------------
# ๐ค 1. Android ๊ฐ๋ฐ ํ๊ฒฝ ๊ตฌ์ถ (Android Studio)
# -------------------------------------------------------------
# 1) Android Studio ๋ค์ด๋ก๋ ๋ฐ ์ค์น (https://developer.android.com/studio)
# 2) Setup Wizard๋ฅผ ํตํด Android SDK, Platform-tools ์๋ ์ค์น ์งํ
# 3) ๐ก [๋งค์ฐ ์ค์!]: Command-line Tools ์ค์น (๋ฏธ์ค์น ์ doctor ์๋ฌ ๋ฐ์)
# - Android Studio ์คํ -> [More Actions] ๋๋ [Settings] -> [Languages & Frameworks]
# - [Android SDK] -> [SDK Tools] ํญ ํด๋ฆญ
# - 'Android SDK Command-line Tools (latest)' ์ฒดํฌ๋ฐ์ค ์ ํ ํ [Apply] ํด๋ฆญํ์ฌ ์ค์น!
# 4) Android ๋ผ์ด์ ์ค ์ ์ฒด ์ผ๊ด ์น์ธ (ํฐ๋ฏธ๋์์ ์คํ):
flutter doctor --android-licenses
# ํ๋ฉด์ ๋ผ์ด์ ์ค ๋์ ์ง๋ฌธ์ด ๋์ฌ ๋๋ง๋ค 'y'๋ฅผ ์
๋ ฅํ์ฌ ๋ชจ๋ ์น์ธ ์๋ฃ
# -------------------------------------------------------------
# ๐ 2. iOS ๊ฐ๋ฐ ํ๊ฒฝ ๊ตฌ์ถ (macOS ํ์)
# -------------------------------------------------------------
# 1) Mac App Store์์ 'Xcode' ์ต์ ๋ฒ์ ์ค์น
# 2) ํฐ๋ฏธ๋์์ Xcode ๋ช
๋ น์ด ๋ผ์ธ ๋๊ตฌ ์ฐ๊ฒฐ:
sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
sudo xcodebuild -runFirstLaunch
# 3) CocoaPods (iOS ์ธ๋ถ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋งค๋์ ) ์ค์น:
brew install cocoapods
# ๋๋: sudo gem install cocoapods
# -------------------------------------------------------------
# ๐ป 3. ์๋ํฐ(VS Code) ํ์ฅ ํ๋ก๊ทธ๋จ ์ค์น
# -------------------------------------------------------------
# VS Code [Extensions] (Ctrl+Shift+X)์์ ๊ฒ์ ๋ฐ ์ค์น:
# - 'Flutter' (Dart ํ์ฅ์ด ์๋์ผ๋ก ํจ๊ป ์ค์น๋จ)
# - 'Awesome Flutter Snippets' (๋จ์ถ ํค์๋๋ก ์์ ฏ ํ
ํ๋ฆฟ ์์ฑ)
# - 'Error Lens' (์ฝ๋ ์๋ฌ๋ฅผ ์ธ๋ผ์ธ์ผ๋ก ์ฆ์ ์๊ฐํ)
```
## 3. [Step 3] ํ๊ฒฝ ์ง๋จ (flutter doctor) & ์ฒซ ํ๋ก์ ํธ ์คํ
```bash
# 1. ์ ์ฒด ๊ฐ๋ฐ ํ๊ฒฝ ์ข
ํฉ ์ง๋จ
flutter doctor -v
# ๐ฏ ๋ชจ๋ ํญ๋ชฉ์ด ์๋์ฒ๋ผ [โ] ๋
น์ ์ฒดํฌ๊ฐ ๋์ค๋ฉด ์๋ฒฝํ ์ค๋น ์๋ฃ!
# [โ] Flutter (Channel stable, 3.x.x, on Microsoft Windows ...)
# [โ] Windows Version (Installed version of Windows is version 10 or higher)
# [โ] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
# [โ] Chrome - develop for the web
# [โ] Android Studio (version 2024.x)
# [โ] VS Code (version 1.9x)
# [โ] Connected device (2 available)
# -------------------------------------------------------------
# ๐ 2. ์ฒซ ํ๋ก์ ํธ ์์ฑ & ์๋ฎฌ๋ ์ดํฐ์์ ์คํํ๊ธฐ
# -------------------------------------------------------------
# ์ ํ๋ก์ ํธ ์์ฑ (my_first_app)
flutter create my_first_app
cd my_first_app
# ์คํ ๊ฐ๋ฅํ ๋๋ฐ์ด์ค ํ์ธ (Chrome, ์ฐ๊ฒฐ๋ ํฐ, ์๋ฎฌ๋ ์ดํฐ)
flutter devices
# A. ์น ๋ธ๋ผ์ฐ์ ๋ก ์ด๊ณ ์ 1์ด ๋ง์ ์คํํด๋ณด๊ธฐ:
flutter run -d chrome
# B. Android ์๋ฎฌ๋ ์ดํฐ ์คํํ๊ธฐ:
# Android Studio -> [Device Manager] -> [Create Device] (Pixel 8 ์ ํ)
# -> ์์คํ
์ด๋ฏธ์ง(API 34/35) ๋ค์ด๋ก๋ ํ ์์ฑ -> ์ฌ์(โถ) ๋ฒํผ ํด๋ฆญ
# ์๋ฎฌ๋ ์ดํฐ๊ฐ ์ผ์ง ์ํ์์:
flutter run
# C. iOS ์๋ฎฌ๋ ์ดํฐ ์คํํ๊ธฐ (macOS):
open -a Simulator
flutter run
```
## 4. [Step 4] ํซ ๋ฆฌ๋ก๋ ์ฒดํ & Flutter CLI ํ์ ๋ช
๋ น์ด
```bash
# =============================================================
# โก ํซ ๋ฆฌ๋ก๋(Hot Reload) ์ค์ ๊ฐ๋ฐ ๋ฃจํด
# =============================================================
# ์ฑ์ด ์คํ ์ค์ธ ์ํ์ ํฐ๋ฏธ๋์์ ๋จ์ถํค:
# [r] : ํซ ๋ฆฌ๋ก๋ (Hot Reload) - ์ฑ ์ํ(์
๋ ฅ๊ฐ, ํ๋ฉด ์คํ)๋ฅผ ๋ณด์กดํ๋ฉด์ UI ๋ณ๊ฒฝ์ 0.5์ด ์ฆ๊ฐ ๋ฐ์
# [R] : ํซ ๋ฆฌ์คํํธ (Hot Restart) - ์ํ๋ฅผ ์ด๊ธฐํํ๊ณ ์ฑ์ ์ฒ์๋ถํฐ ์ฌ์์
# [v] : ๋ธ๋ผ์ฐ์ ๋ก Flutter DevTools ์คํ (์์ ฏ ํธ๋ฆฌ ๊ฒ์ฌ, ๋ฉ๋ชจ๋ฆฌ/CPU ํ๋กํ์ผ๋ฌ, ๋คํธ์ํฌ ๋ชจ๋ํฐ)
# [h] : ๋จ์ถํค ๋์๋ง ๋ณด๊ธฐ
# [q] : ์ฑ ์คํ ์ ์ ์ข
๋ฃ
# =============================================================
# ๐ ๏ธ ํ๋ก์ ํธ ์ ์ง๋ณด์ & ํจํค์ง ๊ด๋ฆฌ CLI
# =============================================================
# ์์กด์ฑ ํจํค์ง ๋ค์ด๋ก๋ (pubspec.yaml ๊ธฐ์ค)
flutter pub get
# ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ถ๊ฐ (pubspec.yaml ์๋ ์์ ๋จ)
flutter pub add dio provider flutter_riverpod go_router
# ๊ฐ๋ฐ ์ ์ฉ ๋๊ตฌ ์ถ๊ฐ (์ฝ๋ ์์ฑ๊ธฐ ๋ฑ)
flutter pub add dev:build_runner dev:freezed
# ํจํค์ง ์ต์ ๋ฒ์ ๋ถ์ ๋ฐ ์
๊ทธ๋ ์ด๋
flutter pub outdated
flutter pub upgrade
# ์ฝ๋ ์ ์ ๋ถ์ ๋ฐ ํฌ๋งคํ
flutter analyze # ๋ฌธ๋ฒ ์ค๋ฅ, ๊ฒฝ๊ณ , ๋ฏธ์ฌ์ฉ ๋ณ์ ๊ฒ์ฌ
dart format lib/ # ๊ณต์ ์ฝ๋ฉ ์ปจ๋ฒค์
์ ๋ง์ถฐ ์๋ ์ ๋ ฌ (๋ค์ฌ์ฐ๊ธฐ 2์นธ)
# ๐ก ๋น๋๊ฐ ๊ผฌ์ด๊ฑฐ๋ ์บ์ ์ถฉ๋์ด ๋ฐ์ํ ๋ ๋ง๋ณํต์น์ฝ:
flutter clean # build/ ๋ฐ .dart_tool/ ์บ์ ์์ ์ญ์
flutter pub get # ํจํค์ง ๋ค์ ๋ฐ๊ธฐ
```
## 5. ํ๋ก์ ํธ ๋๋ ํ ๋ฆฌ ๊ตฌ์กฐ & pubspec.yaml ์๋ฒฝ ๊ฐ์ด๋
```text
my_flutter_app/
โโโ android/ โ ์๋๋ก์ด๋ ๋ค์ดํฐ๋ธ ์ค์ (Gradle, AndroidManifest.xml)
โโโ ios/ โ iOS ๋ค์ดํฐ๋ธ ์ค์ (Podfile, Info.plist, Xcode ํ๋ก์ ํธ)
โโโ web/ โ ์น ๋น๋์ฉ ์ํธ๋ฆฌ (index.html, manifest.json)
โโโ assets/ โ ์ด๋ฏธ์ง, ํฐํธ, ๋ก์ปฌ JSON ๋ฑ ์ ์ ๋ฆฌ์์ค (์์ฑ ํ์)
โโโ lib/ โ ๐ก Dart/Flutter ๋ฉ์ธ ์์ค ์ฝ๋ ๋๋ ํ ๋ฆฌ
โ โโโ main.dart โ ์ฑ ์ง์
์ ์ํธ๋ฆฌํฌ์ธํธ (runApp)
โ โโโ models/ โ ๋ฐ์ดํฐ ๋ชจ๋ธ (DTO, Entity)
โ โโโ services/ โ API ํต์ ๋ฐ ์ธ๋ถ ์๋น์ค ๊ณ์ธต
โ โโโ providers/ โ ์ํ ๊ด๋ฆฌ ๋ก์ง
โ โโโ screens/ โ ๊ฐ๋ณ ํ์ด์ง (Page / Screen)
โ โโโ widgets/ โ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ์ปค์คํ
์์ ฏ
โโโ test/ โ ๋จ์ ๋ฐ ์์ ฏ ํ
์คํธ ์ฝ๋
โโโ pubspec.yaml โ ํ๋ก์ ํธ ๋ฉํ๋ฐ์ดํฐ, ์์กด์ฑ ๋ผ์ด๋ธ๋ฌ๋ฆฌ, ์์
์ ์ ํ์ผ
```
```yaml
# pubspec.yaml ํต์ฌ ์ค์ ์์
name: my_flutter_app
description: "A modern Flutter mobile application."
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: '>=3.0.0 <4.0.0'
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8
dio: ^5.7.0 # ๊ฐ๋ ฅํ HTTP ๋คํธ์ํน ํด๋ผ์ด์ธํธ
flutter_riverpod: ^2.6.1 # ๋ชจ๋ ๋ฆฌ์กํฐ๋ธ ์ํ ๊ด๋ฆฌ
go_router: ^14.6.0 # ์ ์ธํ URL ๋ผ์ฐํ
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
flutter:
uses-material-design: true
assets:
- assets/images/ # ํด๋ ๋ด ๋ชจ๋ ์ด๋ฏธ์ง ์์
์๋ ๋ฑ๋ก
- assets/config.json
```
## 6. Dart 3 ํต์ฌ ๋ฌธ๋ฒ ๊ฐ์ด๋ (Dart 3 for Flutter)
```dart
// [1] Null Safety (์๋ฒฝํ ๋ ์์ ์ฑ)
String name = "Flutter"; // ๋ ๋ถ๊ฐ
String? nickname = null; // ๋ ๊ฐ๋ฅ (?)
int len = nickname?.length ?? 0; // ์์ ํธ์ถ(?.) & ๋ ๋ณํฉ ์ฐ์ฐ์(??)
nickname ??= "Guest"; // ๋์ผ ๋๋ง ๊ฐ ํ ๋น(??=)
// [2] Dart 3 Records (๋ค์ค ๋ฐํ ๋ ์ฝ๋) & Patterns ๊ตฌ์กฐ ๋ถํด
(String, int) getUserInfo() => ("Lucky", 29);
final (userName, userAge) = getUserInfo(); // ํํ ๊ตฌ์กฐ ๋ถํด ํ ๋น
// ๋ช
๋ช
๋ ํ๋๋ฅผ ๊ฐ์ง ๋ ์ฝ๋
({String name, int score}) getResult() => (name: "Dart", score: 100);
final result = getResult();
print("${result.name}: ${result.score}");
// [3] Dart 3 Switch ํํ์ (Pattern Matching)
String getRoleBadge(String role) => switch (role) {
'ADMIN' => '๐ ๊ด๋ฆฌ์',
'MANAGER' || 'LEAD' => 'โญ ๋งค๋์ ',
'USER' => '๐ค ์ผ๋ฐ ํ์',
_ => 'โ ๋ฏธ์ง์ ' // default
};
// [4] Cascade ์ฐ์ฐ์ (..) ๋ฐ ์ปฌ๋ ์
if/for
final numbers = [
1, 2, 3,
if (true) 4, // ์กฐ๊ฑด๋ถ ์์ ์ถ๊ฐ
for (var i = 5; i <= 6; i++) i, // ๋ฐ๋ณต๋ฌธ ์์ ์ถ๊ฐ
];
// [5] ์ต์คํ
์
(Extension Methods)
extension StringExt on String {
bool get isValidEmail => contains('@') && contains('.');
}
// bool valid = "test@example.com".isValidEmail;
```
## 7. ์์ ฏ ๊ธฐ์ด & ์๋ช
์ฃผ๊ธฐ (Widget & Lifecycle)
```dart
import 'package:flutter/material.dart';
// [1] ์ฑ ์ง์
์
void main() => runApp(const MyApp());
// [2] StatelessWidget: ์ํ๊ฐ ๋ณํ์ง ์๋ ์ ์ UI ์ปดํฌ๋ํธ
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
),
home: const CounterScreen(),
);
}
}
// [3] StatefulWidget: ์ฌ์ฉ์ ์ํธ์์ฉ์ผ๋ก ํ๋ฉด์ด ๊ฐฑ์ ๋๋ ๋์ ์ปดํฌ๋ํธ
class CounterScreen extends StatefulWidget {
const CounterScreen({super.key});
@override
State<CounterScreen> createState() => _CounterScreenState();
}
class _CounterScreenState extends State<CounterScreen> {
int _counter = 0;
// 1. ์์ ฏ์ด ํธ๋ฆฌ ์์ฑ ์ ๋จ 1ํ ํธ์ถ (์ด๊ธฐํ, ๋ฆฌ์ค๋ ๋ฑ๋ก)
@override
void initState() {
super.initState();
print("initState: ๋ฆฌ์์ค ์ด๊ธฐํ");
}
// 2. ๋ถ๋ชจ ์์ ฏ์ InheritedWidget/Theme์ด ๋ณ๊ฒฝ๋ ๋ ํธ์ถ
@override
void didChangeDependencies() {
super.didChangeDependencies();
}
// 3. UI ๋ ๋๋ง (setState ํธ์ถ ์๋ง๋ค ๋ค์ ์คํ)
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('์นด์ดํฐ ์์ ')),
body: Center(
child: Text(
'ํด๋ฆญ ํ์: $_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
_counter++; // ์ํ ๋ณ๊ฒฝ ํ UI ์ฌ๋ ๋๋ง ํธ๋ฆฌ๊ฑฐ
});
},
child: const Icon(Icons.add),
),
);
}
// 4. ์์ ฏ์ด ํธ๋ฆฌ์์ ์๊ตฌ ์ ๊ฑฐ๋ ๋ ํธ์ถ (์ปจํธ๋กค๋ฌ, ํ์ด๋จธ ํด์ )
@override
void dispose() {
print("dispose: ๋ฉ๋ชจ๋ฆฌ ๋์ ๋ฐฉ์ง ๋ฆฌ์์ค ํด์ ");
super.dispose();
}
}
```
## 8. ํต์ฌ ๋ ์ด์์ & ์ ๋ ฌ ์์ ฏ (Layout & Sizing)
```dart
// [1] Container & SizedBox: ํฌ๊ธฐ, ๋ง์ง, ํจ๋ฉ, ํ
๋๋ฆฌ, ๋ฐฐ๊ฒฝ ์ง์
Container(
width: 200,
height: 100,
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: const Center(child: Text("์คํ์ผ๋ ์ปจํ
์ด๋")),
);
// ๋จ์ ์ฌ๋ฐฑ/๊ณต๋ฐฑ ์ค ๋๋ ๊ฐ๋ฒผ์ด SizedBox ๊ถ์ฅ
const SizedBox(height: 24); // ์ธ๋ก ๊ณต๋ฐฑ
// [2] Column & Row: ์์ง/์ํ ๋ฐฐ์น
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, // ์ฃผ์ถ(๊ฐ๋ก) ์ ๋ ฌ
crossAxisAlignment: CrossAxisAlignment.center, // ๊ต์ฐจ์ถ(์ธ๋ก) ์ ๋ ฌ
children: [
const Icon(Icons.star, color: Colors.amber),
// ๋จ์ ๊ณต๊ฐ์ ๋น์จ๋๋ก ์ฑ์ฐ๊ธฐ
Expanded(
child: Text("๊ธธ์ด๊ฐ ๊ธธ์ด์ ธ๋ ํ
์คํธ๊ฐ ์ค๋ฐ๊ฟ ์ฒ๋ฆฌ๋ฉ๋๋ค."),
),
const Text("์ฐ์ธก ๋"),
],
);
// [3] Stack & Positioned: Z-Index ๊ฒน์นจ ๋ ์ด์์
Stack(
children: [
Image.asset('assets/images/bg.jpg', fit: BoxFit.cover),
Positioned(
top: 16,
right: 16,
child: Badge(label: const Text('NEW'), child: const Icon(Icons.notifications)),
),
],
);
// [4] Wrap: ์ค๋ฐ๊ฟ์ด ์ผ์ด๋๋ ํ๊ทธ/์นฉ ๋ชฉ๋ก
Wrap(
spacing: 8.0, // ๊ฐ๋ก ๊ฐ๊ฒฉ
runSpacing: 4.0, // ์ธ๋ก(์ค๋ฐ๊ฟ) ๊ฐ๊ฒฉ
children: [
Chip(label: Text('Flutter')),
Chip(label: Text('Dart')),
Chip(label: Text('CrossPlatform')),
],
);
```
## 9. ๊ณ ์ฑ๋ฅ ์คํฌ๋กค & ๋ฆฌ์คํธ (Scrolling & Slivers)
```dart
// [1] ListView.builder: ํ๋ฉด์ ๋ณด์ด๋ ์์ดํ
๋ง ๊ฐ์ํ(Virtualization) ๋ ๋๋ง
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return ListTile(
leading: CircleAvatar(child: Text('${index + 1}')),
title: Text(item.title),
subtitle: Text(item.desc),
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
onTap: () => print('ํด๋ฆญ: ${item.id}'),
);
},
);
// [2] ListView.separated: ์์ดํ
์ฌ์ด์ ๊ตฌ๋ถ์ (Divider) ์๋ ์ฝ์
ListView.separated(
itemCount: 20,
separatorBuilder: (context, index) => const Divider(height: 1),
itemBuilder: (context, index) => ListTile(title: Text("ํญ๋ชฉ $index")),
);
// [3] GridView.builder: ๋ฐ์ํ ๊ฒฉ์ ๋ ์ด์์
GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2, // ํ ์ค์ 2์ด
crossAxisSpacing: 12, // ๊ฐ๋ก ๊ฐ๊ฒฉ
mainAxisSpacing: 12, // ์ธ๋ก ๊ฐ๊ฒฉ
childAspectRatio: 0.85, // ๊ฐ๋ก/์ธ๋ก ๋น์จ
),
itemCount: 10,
itemBuilder: (context, index) => Card(child: Center(child: Text("์นด๋ $index"))),
);
// [4] Pull-to-Refresh (๋น๊ฒจ์ ์๋ก๊ณ ์นจ)
RefreshIndicator(
onRefresh: () async {
await Future.delayed(const Duration(seconds: 1)); // API ์ฌ์กฐํ
},
child: ListView(children: [...]),
);
// [5] CustomScrollView & Sliver: ์ ํ๋ ํค๋(Collapsible Appbar)
CustomScrollView(
slivers: [
SliverAppBar(
expandedHeight: 180.0,
floating: false,
pinned: true, // ์๋จ ๊ณ ์
flexibleSpace: FlexibleSpaceBar(
title: const Text('ํ๋กํ ์์ธ'),
background: Image.asset('assets/images/banner.jpg', fit: BoxFit.cover),
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(title: Text("ํผ๋ ์์ดํ
$index")),
childCount: 30,
),
),
],
);
```
## 10. Material 3 UI ์ปดํฌ๋ํธ & ์
๋ ฅ ํผ (UI & Forms)
```dart
// [1] ๋ฒํผ ์ข
๋ฅ ๋ชจ์
FilledButton(onPressed: () {}, child: const Text("์ฃผ์ ์์
"));
ElevatedButton(onPressed: () {}, child: const Text("๊ธฐ๋ณธ ๋ฒํผ"));
OutlinedButton(onPressed: () {}, child: const Text("์ธ๊ณฝ์ ๋ฒํผ"));
TextButton(onPressed: () {}, child: const Text("ํ
์คํธ ๋งํฌ"));
IconButton(icon: const Icon(Icons.favorite), onPressed: () {});
// [2] ๋ชจ๋ฌ & ๋ฐํ
์ํธ ํธ์ถ
// SnackBar ํ์
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('์ ์ฅ๋์์ต๋๋ค.'),
action: SnackBarAction(label: '์คํ์ทจ์', onPressed: () {}),
),
);
// Alert Dialog ๋ค์ด์ผ๋ก๊ทธ
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('์ญ์ ํ์ธ'),
content: const Text('์ ๋ง๋ก ์ด ํญ๋ชฉ์ ์ญ์ ํ์๊ฒ ์ต๋๊น?'),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('์ทจ์')),
FilledButton(onPressed: () => Navigator.pop(context), child: const Text('์ญ์ ')),
],
),
);
// Modal Bottom Sheet ๋ฐํ
์ํธ
showModalBottomSheet(
context: context,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20))),
builder: (context) => Container(
padding: const EdgeInsets.all(20),
child: const Text('ํ๋จ ์ํธ ์ฝํ
์ธ '),
),
);
// [3] ์ ํจ์ฑ ๊ฒ์ฆ(Validation) ํผ ์์
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: '์ด๋ฉ์ผ ์ฃผ์',
prefixIcon: Icon(Icons.email),
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) return '์ด๋ฉ์ผ์ ์
๋ ฅํด์ฃผ์ธ์.';
if (!value.contains('@')) return '์ฌ๋ฐ๋ฅธ ์ด๋ฉ์ผ ํ์์ด ์๋๋๋ค.';
return null; // ํต๊ณผ
},
),
const SizedBox(height: 16),
FilledButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
print("ํผ ๊ฒ์ฆ ์ฑ๊ณต: ${_emailController.text}");
}
},
child: const Text('์ ์ถํ๊ธฐ'),
),
],
),
);
```
## 11. ๋ด๋น๊ฒ์ด์
& GoRouter ๋ผ์ฐํ
(Navigation)
```dart
// ๐ก [1] ์ ํต์ ์ธ Navigator 1.0 (๊ฐ๋จํ ํ๋ฉด ์ด๋)
// ํ์ด์ง ์ด๋
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const DetailScreen(id: 42)),
);
// ์ด์ ํ๋ฉด์ผ๋ก ๋์๊ฐ๊ธฐ & ๊ฒฐ๊ณผ ๋ฐํ
Navigator.pop(context, "๊ฒฐ๊ณผ ๋ฐ์ดํฐ");
// ๋ค๋ก๊ฐ๊ธฐ ๋ถ๊ฐ๋ฅํ ๋์ฒด ์ด๋ (๋ก๊ทธ์ธ โ ํ ์ด๋ ์ ํ์)
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const HomeScreen()),
);
// -------------------------------------------------------------
// ๐ [2] ํ๋์ ์ธ ์ ์ธํ ๋ผ์ฐํ
: GoRouter (๊ณต์ ๊ถ์ฅ, ๋ฅ๋งํฌ ์๋ฒฝ ์ง์)
// -------------------------------------------------------------
import 'package:go_router/go_router.dart';
final goRouter = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomeScreen(),
routes: [
// ์ค์ฒฉ ๋ผ์ฐํธ: /product/:id
GoRoute(
path: 'product/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
final query = state.uri.queryParameters['ref'] ?? '';
return ProductDetailScreen(productId: id);
},
),
],
),
GoRoute(
path: '/login',
builder: (context, state) => const LoginScreen(),
),
],
// ์ธ์ฆ ๊ฐ๋ ๋ฆฌ๋ค์ด๋ ํธ
redirect: (context, state) {
final bool isLoggedIn = AuthService.isLoggedIn;
final bool isLoggingIn = state.matchedLocation == '/login';
if (!isLoggedIn && !isLoggingIn) return '/login';
if (isLoggedIn && isLoggingIn) return '/';
return null;
},
);
// MaterialApp์ ์ฐ๊ฒฐ:
// MaterialApp.router(routerConfig: goRouter);
// ์ฝ๋์์ ๋ผ์ฐํธ ์ด๋:
// context.go('/product/100'); // ์คํ ๊ต์ฒด
// context.push('/product/100'); // ์์ ์คํ ์๊ธฐ
// context.pop(); // ๋ค๋ก ๊ฐ๊ธฐ
```
## 12. ์ํ ๊ด๋ฆฌ ํต์ฌ ํจํด (Riverpod & Provider)
```dart
// =============================================================
// [1] Flutter Riverpod (์ต์ ์ค๋ฌด ๋์ธ & ํ์
์์ ์ฑ)
// =============================================================
import 'package:flutter_riverpod/flutter_riverpod.dart';
// 1. ๋จ์ ์ํ ํ๋ก๋ฐ์ด๋ ์ ์
final counterProvider = StateProvider<int>((ref) => 0);
// 2. ๋น์ฆ๋์ค ๋ก์ง์ ๊ฐ์ง Notifier ์ ์
class TodoNotifier extends StateNotifier<List<String>> {
TodoNotifier() : super([]);
void add(String text) => state = [...state, text];
void remove(int index) => state = state.whereIndexed((i, _) => i != index).toList();
}
final todoProvider = StateNotifierProvider<TodoNotifier, List<String>>(
(ref) => TodoNotifier(),
);
// 3. UI์์ ๊ตฌ๋
(ConsumerWidget ์ฌ์ฉ)
class RiverpodCounterView extends ConsumerWidget {
const RiverpodCounterView({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// ref.watch: ์ํ ๋ณ๊ฒฝ ์ ์๋ ์์ ฏ ๋ฆฌ๋น๋
final count = ref.watch(counterProvider);
return Scaffold(
body: Center(child: Text("Riverpod Count: $count")),
floatingActionButton: FloatingActionButton(
onPressed: () {
// ref.read: ์ด๋ฒคํธ ์ฝ๋ฐฑ์์ ์ํ ๊ฐฑ์
ref.read(counterProvider.notifier).state++;
},
child: const Icon(Icons.add),
),
);
}
}
// =============================================================
// [2] Provider (์ ํต์ ํจํด)
// =============================================================
class UserProvider extends ChangeNotifier {
String _name = 'Guest';
String get name => _name;
void updateName(String newName) {
_name = newName;
notifyListeners(); // ๊ตฌ๋
์ค์ธ UI์ ๊ฐฑ์ ์๋ฆผ
}
}
// UI์์:
// context.watch<UserProvider>().name; // ๊ฐ ๋ณ๊ฒฝ ์ ๋ฆฌ๋น๋
// context.read<UserProvider>().updateName('Lucky'); // ์ฝ๋ฐฑ์์ ๋ฉ์๋ ํธ์ถ
```
## 13. ๋น๋๊ธฐ ํต์ & API ๋คํธ์ํน (Async & Dio)
```dart
import 'dart:convert';
import 'package:dio/dio.dart';
// [1] ๋ชจ๋ธ ์ ์ (fromJson / toJson)
class Post {
final int id;
final String title;
Post({required this.id, required this.title});
factory Post.fromJson(Map<String, dynamic> json) {
return Post(
id: json['id'] as int,
title: json['title'] as String,
);
}
}
// [2] Dio๋ฅผ ์ฌ์ฉํ REST API ํธ์ถ ์๋น์ค
class ApiService {
final Dio _dio = Dio(
BaseOptions(
baseUrl: 'https://jsonplaceholder.typicode.com',
connectTimeout: const Duration(seconds: 5),
receiveTimeout: const Duration(seconds: 3),
headers: {'Content-Type': 'application/json'},
),
);
Future<List<Post>> getPosts() async {
try {
final res = await _dio.get('/posts');
final list = res.data as List;
return list.map((json) => Post.fromJson(json)).toList();
} on DioException catch (e) {
throw Exception('API ์๋ฌ: ${e.response?.statusCode} - ${e.message}');
}
}
}
// [3] FutureBuilder๋ฅผ ์ด์ฉํ ๋ก๋ฉ/์๋ฌ/์ฑ๊ณต ์ํ ์๋ UI ๋ ๋๋ง
class PostListScreen extends StatelessWidget {
final ApiService api = ApiService();
PostListScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('๊ฒ์๊ธ ๋ชฉ๋ก')),
body: FutureBuilder<List<Post>>(
future: api.getPosts(),
builder: (context, snapshot) {
// 1. ๋ก๋ฉ ์ํ
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
// 2. ์๋ฌ ์ํ
if (snapshot.hasError) {
return Center(child: Text('์ค๋ฅ ๋ฐ์: ${snapshot.error}'));
}
// 3. ์ฑ๊ณต ์ํ
final posts = snapshot.data ?? [];
return ListView.builder(
itemCount: posts.length,
itemBuilder: (context, i) => ListTile(
title: Text(posts[i].title),
),
);
},
),
);
}
}
```
## 14. ์ ์ค์ฒ, ์ ๋๋ฉ์ด์
& ํ
๋ง (Animations & Theme)
```dart
// [1] ํฐ์น ๊ฐ์ง: GestureDetector vs InkWell
InkWell(
onTap: () => print("๋ฌผ๊ฒฐ ๋ฆฌํ ํจ๊ณผ ํฐ์น"),
borderRadius: BorderRadius.circular(8),
child: const Padding(padding: EdgeInsets.all(12), child: Text("ํด๋ฆญ ๊ฐ๋ฅ ์์ญ")),
);
// [2] ์์์ ์ ๋๋ฉ์ด์
(Implicit Animations - ๊ฐํธํ๊ณ ๋ถ๋๋ฌ์ด ์ ํ)
AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
width: isExpanded ? 200 : 100,
height: isExpanded ? 200 : 100,
decoration: BoxDecoration(
color: isExpanded ? Colors.teal : Colors.blueGrey,
borderRadius: BorderRadius.circular(isExpanded ? 30 : 8),
),
);
AnimatedOpacity(
opacity: isVisible ? 1.0 : 0.0,
duration: const Duration(milliseconds: 250),
child: const Text("ํ์ด๋ ์ธ/์์ ํ
์คํธ"),
);
// [3] Hero ์ ๋๋ฉ์ด์
(ํ๋ฉด ๊ฐ ์ด๋ฏธ์ง ์ ํ ๋ง๋ฒ)
// ํ๋ฉด 1:
Hero(
tag: 'product-image-42',
child: Image.asset('assets/images/item.png'),
);
// ํ๋ฉด 2์์๋ ๋์ผ tag ๋ถ์ฌ ์ ํ๋ฉด ์ ํ ๊ฐ ๋ถ๋๋ฝ๊ฒ ํฌ๊ธฐ/์์น ์ด๋ ์ ๋๋ฉ์ด์
๋ฐ์!
// [4] ๋คํฌ ๋ชจ๋ & ํ
๋ง ์ค์
MaterialApp(
themeMode: ThemeMode.system, // ์์คํ
์ค์ (๋ผ์ดํธ/๋คํฌ) ์๋ ์ถ์ข
theme: ThemeData(
useMaterial3: true,
brightness: Brightness.light,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo, brightness: Brightness.light),
),
darkTheme: ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo, brightness: Brightness.dark),
),
);
```
## 15. ๋น๋, ํ
์คํธ & ๋ฐฐํฌ ๋ช
๋ น์ด (Build & Deployment)
```bash
# [1] ์ฝ๋ ๋ถ์ ๋ฐ ์ ์ ๊ฒ์ฌ
flutter analyze # Dart ๋ฆฐํธ ๋ฐ ๊ฒฝ๊ณ ๊ฒ์ฌ
dart format lib/ # ์ฝ๋ ์คํ์ผ ์๋ ํฌ๋งคํ
(2์นธ ๋ค์ฌ์ฐ๊ธฐ)
# [2] ํ
์คํธ ์คํ
flutter test # ์ ์ฒด ๋จ์/์์ ฏ ํ
์คํธ ์คํ
flutter test test/widget_test.dart # ํน์ ํ
์คํธ ํ์ผ ์คํ
# [3] Android ๋ฆด๋ฆฌ์ค ๋น๋
flutter build apk --release # ๋จ์ผ ํตํฉ APK ์์ฑ (build/app/outputs/flutter-apk/)
flutter build apk --split-per-abi # CPU ์ํคํ
์ฒ๋ณ ๊ฒฝ๋ํ APK(v7a, v8a, x86_64) ๋ถํ ๋น๋ (์ฉ๋ ์ต์ ํ!)
flutter build appbundle --release # ๊ตฌ๊ธ ํ๋ ์ด์คํ ์ด ๋ฐฐํฌ์ฉ AAB ํ์ผ ์์ฑ (๊ถ์ฅ)
# [4] iOS ๋ฆด๋ฆฌ์ค ๋น๋ (macOS & Xcode ํ์)
flutter build ipa --release # App Store ๋ฐฐํฌ์ฉ ์์นด์ด๋ธ ์์ฑ
# ์์ฑ๋ .ipa ํ์ผ์ Transporter ์ฑ ๋๋ Xcode Organizer๋ฅผ ํตํด App Store Connect ์
๋ก๋
# [5] Web ๋ฆด๋ฆฌ์ค ๋น๋
flutter build web --release # ์น ํธ์คํ
์ฉ ๋น๋ (build/web/ ํด๋ ์์ฑ)
flutter build web --wasm # WebAssembly ์ต์ ๊ฐ์ ๋น๋ (์คํ์ ๊ณ ์ฑ๋ฅ)
# [6] ๋น๋ ์ ํ๊ฒฝ ๋ณ์ ์ ๋ฌ (--dart-define)
flutter run --dart-define=API_URL=https://api.mycompany.com --dart-define=ENV=dev
flutter build apk --release --dart-define=API_URL=https://prod.mycompany.com
# Dart ์ฝ๋์์ ์ฐธ์กฐ:
# const apiUrl = String.fromEnvironment('API_URL', defaultValue: 'https://dev.api.com');
```
์๊ฒฌ ๋ฐ ์ง๋ฌธ
0์์ง ๋ฑ๋ก๋ ์๊ฒฌ์ด ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ๋๊ธ์ ๋จ๊ฒจ๋ณด์ธ์!
๋๊ธ ์์
๋๊ธ ์ญ์