Ver3.0 ๐ Flutter๋ก ํ ๋ฒ์ ๋๋ด๋ ํฌ๋ก์คํ๋ซํผ ๋ชจ๋ฐ์ผ ์ฑ ๊ฐ๋ฐ ์๋ฒฝ ๊ฐ์ด๋

๐ Flutter๋ก ํ ๋ฒ์ ๋๋ด๋ ํฌ๋ก์คํ๋ซํผ ๋ชจ๋ฐ์ผ ์ฑ ๊ฐ๋ฐ ์๋ฒฝ ๊ฐ์ด๋
iOS์ Android๋ฅผ ๋์์ ์ ๋ณตํ๋ ๊ฐ์ฅ ์ค๋งํธํ ๋ฐฉ๋ฒ
์๋ , ์น๊ตฌ! ๐ ํน์ ์ฑ ํ๋ ๋ง๋ค๋ ค๊ณ ํ๋๋ฐ iOS์ฉ์ผ๋ก Swift ๋ฐฐ์ฐ๊ณ , Android์ฉ์ผ๋ก Kotlin๋ ๋ฐฐ์์ผ ํ๋ค๋ ์ฌ์ค์ ๋จธ๋ฆฌ๊ฐ ์ง๋๊ฑฐ๋ฆฐ ์ ์์ด? ๋๋ ๊ทธ๋ฌ์ด. ๊ทธ๋ฐ๋ฐ ๋ง์ด์ผ, Flutter๋ผ๋ ๋ ์์ ๋ง๋๊ณ ๋์ ๋ด ๊ฐ๋ฐ ์ธ์์ด ์์ ํ ๋ฐ๋์๊ฑฐ๋ . ์ค๋์ ์ด ๋ง๋ฒ ๊ฐ์ ํ๋ ์์ํฌ์ ๋ํด ์น๊ตฌํํ ์๊ธฐํ๋ฏ์ด ํธํ๊ฒ ํ์ด๋ณผ๊ฒ! ๐ฏ
๐จ Flutter๊ฐ ๋ญ๋ฐ? ์ ์ด๋ ๊ฒ ๋๋ฆฌ์ผ?
์, ์ผ๋จ Flutter๊ฐ ๋ญ์ง๋ถํฐ ์์๋ณด์. Flutter๋ ๊ตฌ๊ธ์ด 2017๋
์ ์ธ์์ ๋ด๋์ ์คํ์์ค UI ํ๋ ์์ํฌ์ผ. ์ฝ๊ฒ ๋งํ๋ฉด, ์ฝ๋ ํ ๋ฒ ์์ฑํ๋ฉด iOS, Android, ์น, ๋ฐ์คํฌํฑ๊น์ง ๋ชจ๋ ๋์๊ฐ๋ ์ฑ์ ๋ง๋ค ์ ์๋ ๋๊ตฌ๋ผ๋ ๊ฑฐ์ง! ๐
"์์ด, ๊ทธ๋ฐ ๊ฑฐ ์์ ์๋ ์์์์?" ๋ผ๊ณ ์๊ฐํ ์ ์์ด. ๋ง์, React Native๋ Xamarin ๊ฐ์ ์ ๋ฐฐ๋ค์ด ์์์ง. ๊ทธ๋ฐ๋ฐ Flutter๋ ์ข ๋ฌ๋ผ. ์ด๋ป๊ฒ ๋ค๋ฅด๋๊ณ ? ๋ฐ๋ก ๋ ๋๋ง ๋ฐฉ์์์ ์ฐจ์ด๊ฐ ๋๊ฑฐ๋ .
๐ Flutter์ ํต์ฌ ์ฐจ๋ณ์
1. ์์ฒด ๋ ๋๋ง ์์ง (Skia)
Flutter๋ ๋ค์ดํฐ๋ธ ์ปดํฌ๋ํธ๋ฅผ ์ฌ์ฉํ์ง ์๊ณ , ์์ฒด ๊ทธ๋ํฝ ์์ง์ธ Skia๋ฅผ ์ฌ์ฉํด์ ํ๋ฉด์ ์ง์ ๊ทธ๋ ค. ๋ง์น ๊ฒ์ ์์ง์ฒ๋ผ ๋ง์ด์ผ. ๊ทธ๋์ iOS์์ ๋ณด์ด๋ ๋ฒํผ์ด๋ Android์์ ๋ณด์ด๋ ๋ฒํผ์ด ์์ ํ ๋๊ฐ์! ํฝ์
๋จ์๋ก ์ผ์นํ๋ UI๋ฅผ ๋ง๋ค ์ ์๋ค๋ ๊ฑฐ์ง.
2. Dart ์ธ์ด
Flutter๋ Dart๋ผ๋ ์ธ์ด๋ฅผ ์ฌ์ฉํด. "๋ ์๋ก์ด ์ธ์ด๋ฅผ ๋ฐฐ์์ผ ํด?" ๊ฑฑ์ ๋ง! Dart๋ JavaScript๋ Java๋ฅผ ์จ๋ดค๋ค๋ฉด ๊ธ๋ฐฉ ์ตํ ์ ์์ด. ๊ฒ๋ค๊ฐ JIT(Just-In-Time)์ AOT(Ahead-Of-Time) ์ปดํ์ผ์ ๋ ๋ค ์ง์ํด์ ๊ฐ๋ฐํ ๋๋ ๋น ๋ฅด๊ฒ ํ
์คํธํ๊ณ , ๋ฐฐํฌํ ๋๋ ๋ค์ดํฐ๋ธ ์์ค์ ์ฑ๋ฅ์ ๋ผ ์ ์์ด. ๐
3. Hot Reload
์ด๊ฒ ์ง์ง ๊ฐ๋ฐ์๋ค์ด ํ์ฅํ๋ ๊ธฐ๋ฅ์ด์ผ. ์ฝ๋ ์์ ํ๊ณ ์ ์ฅํ๋ฉด 1์ด๋ ์ ๋ผ์ ์ฑ์ ๋ฐ๋ก ๋ฐ์๋ผ. ์ฑ์ ๋ค์ ๋น๋ํ๊ฑฐ๋ ์ฌ์์ํ ํ์๊ฐ ์์ด! ๋ฒํผ ์๊น ๋ฐ๊พธ๊ณ ์ถ์ด? ์ฝ๋ ๊ณ ์น๊ณ ์ ์ฅ. ๋! ๋ ์ด์์ ์กฐ์ ํ๊ณ ์ถ์ด? ์ฝ๋ ๊ณ ์น๊ณ ์ ์ฅ. ๋! ์ด ์๋๊ฐ์ ํ ๋ฒ ๋ง๋ค์ด๋ฉด ๋ค๋ฅธ ๊ฑธ๋ก ๋์๊ฐ ์ ์์ด. ๐
๐ก ์ ํฌ๋ก์คํ๋ซํผ์ด ๋์ธ๊ฐ ๋์๊น?
์์ ์๋ ์ฑ ํ๋ ๋ง๋ค๋ ค๋ฉด ํ์ ๋ ๊ฐ๋ก ๋๋ ์ผ ํ์ด. iOS ํ์ Swift๋ก ๊ฐ๋ฐํ๊ณ , Android ํ์ Kotlin์ผ๋ก ๊ฐ๋ฐํ๊ณ . ๊ฐ์ ๊ธฐ๋ฅ์ธ๋ฐ ์ฝ๋๋ ๋ ๋ฒ ์์ฑํ๋ ๊ฑฐ์ง. ๋ฒ๊ทธ ์์ ๋ ๋ ๋ฒ, ๊ธฐ๋ฅ ์ถ๊ฐ๋ ๋ ๋ฒ. ์๊ฐ๋ ๋ ๋ฐฐ, ๋น์ฉ๋ ๋ ๋ฐฐ! ๐ฑ
๊ทธ๋ฐ๋ฐ ํฌ๋ก์คํ๋ซํผ ๊ฐ๋ฐ์ด ๋ฑ์ฅํ๋ฉด์ ํ๋๊ฐ ์์ ํ ๋ฐ๋์์ด. ํนํ ์คํํธ์
์ด๋ ์ค์๊ธฐ์
์
์ฅ์์๋ ๊ฒ์ ์ฒด์ธ์ ์์ง. ๊ฐ๋ฐ์ ํ ๋ช
์ด ์์ชฝ ํ๋ซํผ์ ๋ค ์ปค๋ฒํ ์ ์์ผ๋๊น ๋ง์ด์ผ.
๐ ์ค์ ํต๊ณ๋ก ๋ณด๋ ํฌ๋ก์คํ๋ซํผ์ ์๋ ฅ
โข ๊ฐ๋ฐ ์๊ฐ ๋จ์ถ: ํ๊ท 30-40% ๊ฐ์
โข ๊ฐ๋ฐ ๋น์ฉ ์ ๊ฐ: ๋ค์ดํฐ๋ธ ๋๋น 50-70% ์ ๊ฐ
โข ์ ์ง๋ณด์ ํจ์จ: ๋จ์ผ ์ฝ๋๋ฒ ์ด์ค๋ก ๊ด๋ฆฌ ์ฉ์ด
โข ์ถ์ ์๋: ๋ ํ๋ซํผ ๋์ ์ถ์ ๊ฐ๋ฅ
โข ์ธ๋ ฅ ์ด์ฉ: ์๊ท๋ชจ ํ์ผ๋ก๋ ์ถฉ๋ถํ ๊ฐ๋ฅ
์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์๋ Flutter ๊ฐ๋ฐ์๋ค์ ๋ง์ด ์ฐพ๋ ์ด์ ๊ฐ ๋ฐ๋ก ์ด๊ฑฐ์ผ. ํ ๋ช ์ด ์์ชฝ ํ๋ซํผ์ ๋ค ๊ฐ๋ฐํ ์ ์์ผ๋๊น ํจ์จ์ ์ด์์? ๐ฏ
๐ ๏ธ Flutter ๊ฐ๋ฐ ํ๊ฒฝ ์ธํ ํ๊ธฐ
์, ์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก Flutter ๊ฐ๋ฐ์ ์์ํด๋ณผ๊น? ๋จผ์ ๊ฐ๋ฐ ํ๊ฒฝ๋ถํฐ ์ธํ ํด์ผ ํด. ์๊ฐ๋ณด๋ค ์ด๋ ต์ง ์์ผ๋๊น ์ฐจ๊ทผ์ฐจ๊ทผ ๋ฐ๋ผ์๋ด!
๐ฅ 1๋จ๊ณ: Flutter SDK ์ค์น
Flutter ๊ณต์ ์น์ฌ์ดํธ(flutter.dev)์ ๊ฐ๋ฉด ์ด์์ฒด์ ๋ณ๋ก ์ค์น ๊ฐ์ด๋๊ฐ ์ ๋์ ์์ด. Windows, macOS, Linux ๋ชจ๋ ์ง์ํด.
Windows ์ฌ์ฉ์๋ผ๋ฉด:
1. Flutter SDK๋ฅผ ๋ค์ด๋ก๋๋ฐ์
2. ์ํ๋ ์์น์ ์์ถ์ ํ๊ณ (์: C:\src\flutter)
3. ํ๊ฒฝ ๋ณ์ PATH์ flutter\bin ๊ฒฝ๋ก๋ฅผ ์ถ๊ฐํด์ฃผ๋ฉด ๋ผ
macOS ์ฌ์ฉ์๋ผ๋ฉด:
ํฐ๋ฏธ๋์์ ๊ฐ๋จํ๊ฒ ์ค์นํ ์ ์์ด:
git clone https://github.com/flutter/flutter.git -b stable
export PATH="$PATH:`pwd`/flutter/bin"
์ค์น๊ฐ ๋๋ฌ์ผ๋ฉด ํฐ๋ฏธ๋์ด๋ ๋ช ๋ น ํ๋กฌํํธ์์ flutter doctor ๋ช ๋ น์ด๋ฅผ ์คํํด๋ด. ์ด ๋ช ๋ น์ด๋ Flutter ๊ฐ๋ฐ์ ํ์ํ ๋ชจ๋ ๊ฒ๋ค์ด ์ ๋๋ก ์ค์น๋๋์ง ์ฒดํฌํด์ฃผ๋ ์น์ ํ ์์ฌ ์ ์๋ ๊ฐ์ ๊ฑฐ์ผ. ๐ฉบ
๐ง 2๋จ๊ณ: IDE ์ค์
Flutter ๊ฐ๋ฐ์๋ ์ฃผ๋ก ๋ ๊ฐ์ง IDE๋ฅผ ์ฌ์ฉํด:
1. Android Studio
๋ฌด๊ฒ๊ธด ํ์ง๋ง ๊ธฐ๋ฅ์ด ์ ๋ง ๊ฐ๋ ฅํด. ํนํ Android ์๋ฎฌ๋ ์ดํฐ๊ฐ ๋ด์ฅ๋์ด ์์ด์ ํธ๋ฆฌํ์ง. Flutter์ Dart ํ๋ฌ๊ทธ์ธ๋ง ์ค์นํ๋ฉด ๋ฐ๋ก ์ฌ์ฉํ ์ ์์ด.
2. Visual Studio Code
๊ฐ๋ณ๊ณ ๋น ๋ฅด๋ค๋ ๊ฒ ์ต๊ณ ์ฅ์ ์ด์ผ. ๋๋ ๊ฐ์ธ์ ์ผ๋ก VS Code๋ฅผ ์ ํธํ๋๋ฐ, ํ์ฅ ํ๋ก๊ทธ๋จ ๋ง์ผํ๋ ์ด์ค์์ "Flutter"๋ "Dart"๋ฅผ ๊ฒ์ํด์ ์ค์นํ๋ฉด ๋! ๋ฉ๋ชจ๋ฆฌ๋ ๋ ๋จน๊ณ ์๋๋ ๋นจ๋ผ์ ์ข์. ๐จ
๐ฏ ์ถ์ฒ VS Code ํ์ฅ ํ๋ก๊ทธ๋จ
โข Flutter: ํ์! Flutter ๊ฐ๋ฐ์ ๋ชจ๋ ๊ฒ
โข Dart: ์ญ์ ํ์! Dart ์ธ์ด ์ง์
โข Awesome Flutter Snippets: ์ฝ๋ ์๋์์ฑ ๋์ฐ๋ฏธ
โข Flutter Widget Snippets: ์์ ฏ ์ฝ๋ ๋น ๋ฅด๊ฒ ์์ฑ
โข Pubspec Assist: ํจํค์ง ๊ด๋ฆฌ ๋์ฐ๋ฏธ
โข Error Lens: ์๋ฌ๋ฅผ ๋ฐ๋ก๋ฐ๋ก ๋ณด์ฌ์ค์ ํธํด
๐ฑ 3๋จ๊ณ: ์๋ฎฌ๋ ์ดํฐ ๋๋ ์ค์ ๊ธฐ๊ธฐ ์ค๋น
์ฑ์ ๋ง๋ค์์ผ๋ฉด ํ
์คํธํด๋ด์ผ๊ฒ ์ง? ๋ ๊ฐ์ง ๋ฐฉ๋ฒ์ด ์์ด:
์๋ฎฌ๋ ์ดํฐ ์ฌ์ฉ:
โข Android: Android Studio์์ AVD Manager๋ก ๊ฐ์ ๊ธฐ๊ธฐ ์์ฑ
โข iOS: macOS์์๋ง ๊ฐ๋ฅ, Xcode์ Simulator ์ฌ์ฉ
์ค์ ๊ธฐ๊ธฐ ์ฌ์ฉ:
๊ฐ์ธ์ ์ผ๋ก๋ ์ค์ ๊ธฐ๊ธฐ๋ฅผ ์ถ์ฒํด. ์ฑ๋ฅ๋ ๋ ์ ํํ๊ฒ ํ
์คํธํ ์ ์๊ณ , ํฐ์น ๊ฐ๊ฐ์ด๋ ์ค์ ์ฌ์ฉ์ฑ์ ํ์ธํ๊ธฐ์ ์ข๊ฑฐ๋ . USB๋ก ์ฐ๊ฒฐํ๊ณ ๊ฐ๋ฐ์ ๋ชจ๋๋ง ํ์ฑํํ๋ฉด ๋ผ!
๐ฏ ์ฒซ Flutter ์ฑ ๋ง๋ค์ด๋ณด๊ธฐ
์, ์ด์ ์ง์ง ์ฝ๋ฉ์ ํด๋ณผ ์๊ฐ์ด์ผ! ๊ฑฑ์ ๋ง, ์ด๋ ต์ง ์์. Flutter์ ์ฒ ํ์ "๋ชจ๋ ๊ฒ์ด ์์ ฏ(Widget)"์ด์ผ. ํ๋ฉด์ ๋ณด์ด๋ ๋ชจ๋ ๊ฒ, ๋ฒํผ๋ ์์ ฏ, ํ ์คํธ๋ ์์ ฏ, ๋ ์ด์์๋ ์์ ฏ! ๋ ๊ณ ๋ธ๋ก ์กฐ๋ฆฝํ๋ฏ์ด ์์ ฏ์ ์กฐํฉํด์ ์ฑ์ ๋ง๋๋ ๊ฑฐ์ง. ๐งฉ
๐ ํ๋ก์ ํธ ์์ฑ
ํฐ๋ฏธ๋์ด๋ ๋ช ๋ น ํ๋กฌํํธ๋ฅผ ์ด๊ณ ๋ค์ ๋ช ๋ น์ด๋ฅผ ์ ๋ ฅํด๋ด:
flutter create my_first_app
cd my_first_app
flutter run
์ง์! ๐ ์ด๋ ๊ฒ ๊ฐ๋จํ ๋ช ๋ น์ด ์ธ ์ค๋ก ์ฑ์ด ์คํ๋์ด. ๊ธฐ๋ณธ ์นด์ดํฐ ์ฑ์ด ๋ํ๋ ๊ฑฐ์ผ. ํ๋ฉด ์ค์์ ์ซ์๊ฐ ์๊ณ , ์ค๋ฅธ์ชฝ ์๋์ + ๋ฒํผ์ด ์์ง? ๋ฒํผ์ ๋๋ฅด๋ฉด ์ซ์๊ฐ ์ฌ๋ผ๊ฐ๋ ๊ฐ๋จํ ์ฑ์ด์ผ.
๐ ์ฝ๋ ๊ตฌ์กฐ ์ดํดํ๊ธฐ
lib/main.dart ํ์ผ์ ์ด์ด๋ด. ์ด๊ฒ ์ฑ์ ์ง์ ์ ์ด์ผ. ์ฝ๋๋ฅผ ๋ณด๋ฉด ์ข ๊ธธ์ด ๋ณด์ด์ง๋ง, ํต์ฌ๋ง ๋ฝ์์ ์ค๋ช ํด์ค๊ฒ:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
main() ํจ์: ๋ชจ๋ Dart ํ๋ก๊ทธ๋จ์ ์์์ ์ด์ผ. runApp()์ ํธ์ถํด์ ์ฑ์ ์คํ์ํค์ง.
MyApp ํด๋์ค: StatelessWidget์ ์์๋ฐ์. "Stateless"๋ ์ํ๊ฐ ์๋ค๋ ๋ป์ด์ผ. ์ฆ, ํ ๋ฒ ๊ทธ๋ ค์ง๋ฉด ๋ณํ์ง ์๋ ์์ ฏ์ด๋ผ๋ ๊ฑฐ์ง.
MaterialApp: Material Design์ ๋ฐ๋ฅด๋ ์ฑ์ ๋ฃจํธ ์์ ฏ์ด์ผ. ํ
๋ง, ๋ผ์ฐํ
, ํ์ดํ ๋ฑ์ ์ค์ ํ ์ ์์ด.
๐จ StatelessWidget vs StatefulWidget
Flutter์๋ ๋ ๊ฐ์ง ์ฃผ์ ์์ ฏ ํ์
์ด ์์ด:
StatelessWidget (์ํ ์๋ ์์ ฏ):
โข ํ ๋ฒ ์์ฑ๋๋ฉด ๋ณํ์ง ์์
โข ์ ์ ์ธ ์ฝํ
์ธ ์ ์ฌ์ฉ
โข ์: ๋ก๊ณ , ์์ด์ฝ, ๊ณ ์ ๋ ํ
์คํธ
StatefulWidget (์ํ ์๋ ์์ ฏ):
โข ์๊ฐ์ ๋ฐ๋ผ ๋ณํ ์ ์์ด
โข ์ฌ์ฉ์ ์ธํฐ๋์
์ ๋ฐ์
โข ์: ์ฒดํฌ๋ฐ์ค, ์ฌ๋ผ์ด๋, ํ
์คํธ ์
๋ ฅ ํ๋
์นด์ดํฐ ์ฑ์ ์ซ์๊ฐ ๋ณํ๋ ๋ถ๋ถ์ StatefulWidget์ ์ฌ์ฉํด์ผ ํด!
๐ Hot Reload ์ฒดํํ๊ธฐ
์ด์ Flutter์ ๊ฐ์ฅ ๋ฉ์ง ๊ธฐ๋ฅ์ ์ฒดํํด๋ณผ ์๊ฐ์ด์ผ! ์ฑ์ด ์คํ๋ ์ํ์์ ์ฝ๋๋ฅผ ์์ ํด๋ด. ์๋ฅผ ๋ค์ด, ํ์ดํ์ ๋ฐ๊ฟ๋ณด์:
home: MyHomePage(title: '๋ด ์ฒซ Flutter ์ฑ! ๐'),
์ ์ฅํ๊ณ (Ctrl+S ๋๋ Cmd+S) ํฐ๋ฏธ๋์์ r ํค๋ฅผ ๋๋ฌ๋ด. ๋๋ VS Code์์๋ ์๋์ผ๋ก Hot Reload๊ฐ ์คํ๋ผ. ์ฑ์ด ์ฌ์์๋์ง ์๊ณ ํ๋ฉด๋ง ๋ฐ๋๋ ๊ฒ ๋ณด์ด์ง? ์ด๊ฒ ๋ฐ๋ก Hot Reload์ ๋ง๋ฒ์ด์ผ! โจ
์ํ๊น์ง ์ ์ง๋๋๊น, ์นด์ดํฐ๋ฅผ 10๊น์ง ์ฌ๋ ค๋๊ณ ์ฝ๋๋ฅผ ์์ ํด๋ ์ซ์๋ ๊ทธ๋๋ก 10์ด์ผ. ๊ฐ๋ฐ ์๋๊ฐ ์์ฒญ๋๊ฒ ๋นจ๋ผ์ง๋ ์ด์ ๊ฐ ๋ฐ๋ก ์ด๊ฑฐ์ผ!
๐งฉ Flutter ์์ ฏ ์์ ์ ๋ณต
Flutter์ ํต์ฌ์ ์์ ฏ์ด์ผ. ์์ ฏ์ ์ ๋ค๋ฃจ๋ฉด ์ด๋ค UI๋ ๋ง๋ค ์ ์์ด. ์์ฃผ ์ฌ์ฉํ๋ ์์ ฏ๋ค์ ์นดํ ๊ณ ๋ฆฌ๋ณ๋ก ์ ๋ฆฌํด๋ดค์ด!
๐ฆ ๋ ์ด์์ ์์ ฏ
Container
๊ฐ์ฅ ๊ธฐ๋ณธ์ ์ธ ์์ ฏ์ด์ผ. ๋ฐ์ค ํ๋๋ผ๊ณ ์๊ฐํ๋ฉด ๋ผ. ํจ๋ฉ, ๋ง์ง, ํ ๋๋ฆฌ, ๋ฐฐ๊ฒฝ์ ๋ฑ์ ์ค์ ํ ์ ์์ด.
Container(
padding: EdgeInsets.all(16.0),
margin: EdgeInsets.symmetric(vertical: 8.0),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(12.0),
),
child: Text('์๋
ํ์ธ์!'),
)
Row์ Column
์์ ฏ์ ๊ฐ๋ก(Row) ๋๋ ์ธ๋ก(Column)๋ก ๋ฐฐ์นํ ๋ ์ฌ์ฉํด. ์น์ flexbox๋ ๋น์ทํ ๊ฐ๋ ์ด์ผ.
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('์ฒซ ๋ฒ์งธ ์ค'),
Text('๋ ๋ฒ์งธ ์ค'),
Text('์ธ ๋ฒ์งธ ์ค'),
],
)
Stack
์์ ฏ์ ๊ฒน์ณ์ ๋ฐฐ์นํ ๋ ์ฌ์ฉํด. ์ด๋ฏธ์ง ์์ ํ ์คํธ๋ฅผ ์ฌ๋ฆฌ๊ฑฐ๋ ํ ๋ ์ ์ฉํ์ง.
Stack(
children: [
Image.network('https://example.com/image.jpg'),
Positioned(
bottom: 16,
right: 16,
child: Text('์ด๋ฏธ์ง ์ค๋ช
', style: TextStyle(color: Colors.white)),
),
],
)
๐จ UI ์์ ฏ
Text
ํ ์คํธ๋ฅผ ํ์ํ๋ ์์ ฏ. ์คํ์ผ๋ง ์ต์ ์ด ์ ๋ง ๋ค์ํด!
Text(
'๋ฉ์ง ํ
์คํธ!',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.purple,
letterSpacing: 1.5,
),
)
Image
์ด๋ฏธ์ง๋ฅผ ํ์ํ๋ ์์ ฏ. ๋คํธ์ํฌ, ๋ก์ปฌ ํ์ผ, ์์ ๋ฑ ๋ค์ํ ์์ค๋ฅผ ์ง์ํด.
// ๋คํธ์ํฌ ์ด๋ฏธ์ง
Image.network('https://example.com/image.jpg')
// ๋ก์ปฌ ์์
์ด๋ฏธ์ง
Image.asset('assets/images/logo.png')
// ํฌ๊ธฐ ์กฐ์
Image.network(
'https://example.com/image.jpg',
width: 200,
height: 200,
fit: BoxFit.cover,
)
Icon
Material Design ์์ด์ฝ์ ์ฝ๊ฒ ์ฌ์ฉํ ์ ์์ด. ์์ฒ ๊ฐ์ ์์ด์ฝ์ด ๊ธฐ๋ณธ ์ ๊ณต๋ผ!
Icon(
Icons.favorite,
color: Colors.red,
size: 36.0,
)
๐ฏ ์ธํฐ๋์ ์์ ฏ
Button ๊ณ์ด
Flutter์๋ ์ฌ๋ฌ ์ข ๋ฅ์ ๋ฒํผ์ด ์์ด. ๊ฐ๊ฐ ๋ค๋ฅธ ์คํ์ผ๊ณผ ์ฉ๋๋ฅผ ๊ฐ์ง๊ณ ์์ง.
// ElevatedButton (์
์ฒด๊ฐ ์๋ ๋ฒํผ)
ElevatedButton(
onPressed: () {
print('๋ฒํผ ํด๋ฆญ!');
},
child: Text('ํด๋ฆญํ์ธ์'),
)
// TextButton (ํ
์คํธ๋ง ์๋ ๋ฒํผ)
TextButton(
onPressed: () {},
child: Text('ํ
์คํธ ๋ฒํผ'),
)
// IconButton (์์ด์ฝ ๋ฒํผ)
IconButton(
icon: Icon(Icons.thumb_up),
onPressed: () {},
)
TextField
์ฌ์ฉ์ ์ ๋ ฅ์ ๋ฐ๋ ์์ ฏ. ๋ก๊ทธ์ธ ํผ์ด๋ ๊ฒ์์ฐฝ ๋ง๋ค ๋ ํ์์ผ!
TextField(
decoration: InputDecoration(
labelText: '์ด๋ฆ์ ์
๋ ฅํ์ธ์',
hintText: 'ํ๊ธธ๋',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.person),
),
onChanged: (value) {
print('์
๋ ฅ๋ ๊ฐ: $value');
},
)
๐จ ์ํ ๊ด๋ฆฌ ๋ง์คํฐํ๊ธฐ
์ฑ์ด ๋ณต์กํด์ง๋ฉด ์ํ ๊ด๋ฆฌ๊ฐ ์ ๋ง ์ค์ํด์ ธ. "์ํ"๋ผ๋ ๊ฒ ๋ญ๋๊ณ ? ๊ฐ๋จํ ๋งํ๋ฉด ์ฑ์ ๋ฐ์ดํฐ์ผ. ์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธํ๋์ง, ์ฅ๋ฐ๊ตฌ๋์ ๋ญ๊ฐ ๋ด๊ฒผ๋์ง, ๋คํฌ๋ชจ๋๊ฐ ์ผ์ ธ์๋์ง ๋ฑ๋ฑ. ์ด๋ฐ ์ ๋ณด๋ค์ ์ด๋ป๊ฒ ๊ด๋ฆฌํ๋๋์ ๋ฐ๋ผ ์ฑ์ ํ์ง์ด ๊ฒฐ์ ๋ผ! ๐ฏ
๐ setState - ๊ฐ์ฅ ๊ธฐ๋ณธ์ ์ธ ๋ฐฉ๋ฒ
StatefulWidget์์ ์ฌ์ฉํ๋ ๊ฐ์ฅ ๊ธฐ๋ณธ์ ์ธ ์ํ ๊ด๋ฆฌ ๋ฐฉ๋ฒ์ด์ผ. ๊ฐ๋จํ ์ฑ์๋ ์ด๊ฒ๋ง์ผ๋ก๋ ์ถฉ๋ถํด!
class CounterWidget extends StatefulWidget {
@override
_CounterWidgetState createState() => _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('์นด์ดํฐ: $_counter'),
ElevatedButton(
onPressed: _incrementCounter,
child: Text('์ฆ๊ฐ'),
),
],
);
}
}
setState()๋ฅผ ํธ์ถํ๋ฉด Flutter๊ฐ "์, ์ํ๊ฐ ๋ฐ๋์๊ตฌ๋!" ํ๊ณ ํด๋น ์์ ฏ์ ๋ค์ ๊ทธ๋ ค์ค. ๊ฐ๋จํ์ง? ๐
๐ฏ Provider - ์ค๊ธ ๊ฐ๋ฐ์์ ์ ํ
Provider๋ Flutter ํ์ด ๊ณต์์ ์ผ๋ก ์ถ์ฒํ๋ ์ํ ๊ด๋ฆฌ ํจํค์ง์ผ. ๋ฐฐ์ฐ๊ธฐ ์ฝ๊ณ ์ฑ๋ฅ๋ ์ข์์ ๋ง์ ๊ฐ๋ฐ์๋ค์ด ์ฌ์ฉํด.
๋จผ์ pubspec.yaml์ ํจํค์ง๋ฅผ ์ถ๊ฐํด:
dependencies:
flutter:
sdk: flutter
provider: ^6.0.0
๊ทธ๋ฆฌ๊ณ ๊ฐ๋จํ ์นด์ดํฐ ๋ชจ๋ธ์ ๋ง๋ค์ด๋ณด์:
import 'package:flutter/foundation.dart';
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners(); // ๋ฆฌ์ค๋๋ค์๊ฒ ๋ณ๊ฒฝ ์๋ฆผ
}
}
์ด์ ์ฑ์์ ์ฌ์ฉํด๋ณด์:
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CounterModel(),
child: MyApp(),
),
);
}
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Provider ์์ ')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('์นด์ดํฐ ๊ฐ:'),
Consumer<CounterModel>(
builder: (context, counter, child) {
return Text(
'${counter.count}',
style: TextStyle(fontSize: 48),
);
},
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
context.read<CounterModel>().increment();
},
child: Icon(Icons.add),
),
);
}
}
โข prop drilling ๋ฌธ์ ํด๊ฒฐ
โข ์ฝ๋๊ฐ ๊น๋ํ๊ณ ์ ์ง๋ณด์ํ๊ธฐ ์ข์
โข ์ฑ๋ฅ ์ต์ ํ๊ฐ ์ฌ์ (ํ์ํ ๋ถ๋ถ๋ง ๋ฆฌ๋น๋)
๐ Riverpod, Bloc, GetX - ๊ณ ๊ธ ์ต์ ๋ค
์ฑ์ด ๋ ๋ณต์กํด์ง๋ฉด ๋ค๋ฅธ ์ํ ๊ด๋ฆฌ ์๋ฃจ์
๋ ๊ณ ๋ คํด๋ณผ ์ ์์ด:
Riverpod: Provider์ ๊ฐ์ ๋ฒ์ . ์ปดํ์ผ ํ์ ์์ ์ฑ์ด ๋ ์ข์.
Bloc: ๋น์ฆ๋์ค ๋ก์ง์ ์์ ํ ๋ถ๋ฆฌํ๊ณ ์ถ์ ๋. ์ข ๋ณต์กํ์ง๋ง ๊ฐ๋ ฅํด.
GetX: ์ํ ๊ด๋ฆฌ๋ฟ๋ง ์๋๋ผ ๋ผ์ฐํ
, ์์กด์ฑ ์ฃผ์
๊น์ง ํด๊ฒฐ. ์ฌ์ธ์ ์๋ฃจ์
!
๊ฐ์ธ์ ์ผ๋ก๋ ์ฒ์์๋ Provider๋ก ์์ํ๊ณ , ํ์์ ๋ฐ๋ผ ๋ค๋ฅธ ๊ฑธ๋ก ๋์ด๊ฐ๋ ๊ฑธ ์ถ์ฒํด. ๐
๐ ๋คํธ์ํน๊ณผ API ์ฐ๋
์์ฆ ์ฑ์ ๊ฑฐ์ ๋ค ์๋ฒ์ ํต์ ํ์์? ๋ ์จ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค๊ฑฐ๋, ์ฌ์ฉ์ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๊ฑฐ๋, ์์ ๋ก๊ทธ์ธ์ ํ๊ฑฐ๋. Flutter์์ ๋คํธ์ํนํ๋ ๋ฐฉ๋ฒ์ ์์๋ณด์! ๐
๐ก http ํจํค์ง ์ฌ์ฉํ๊ธฐ
๊ฐ์ฅ ๊ธฐ๋ณธ์ ์ธ HTTP ์์ฒญ์ http ํจํค์ง๋ก ํ ์ ์์ด. ๋จผ์ ํจํค์ง๋ฅผ ์ถ๊ฐํ์:
dependencies:
http: ^1.1.0
๊ฐ๋จํ GET ์์ฒญ ์์ ๋ฅผ ๋ณผ๊น?
import 'package:http/http.dart' as http;
import 'dart:convert';
Future<Map<String, dynamic>> fetchUserData() async {
final response = await http.get(
Uri.parse('https://api.example.com/user/123'),
);
if (response.statusCode == 200) {
return json.decode(response.body);
} else {
throw Exception('๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ค๋๋ฐ ์คํจํ์ต๋๋ค');
}
}
POST ์์ฒญ๋ ๋น์ทํด:
Future<void> createUser(String name, String email) async {
final response = await http.post(
Uri.parse('https://api.example.com/users'),
headers: {'Content-Type': 'application/json'},
body: json.encode({
'name': name,
'email': email,
}),
);
if (response.statusCode == 201) {
print('์ฌ์ฉ์ ์์ฑ ์ฑ๊ณต!');
} else {
print('์ค๋ฅ ๋ฐ์: ${response.statusCode}');
}
}
๐ฏ FutureBuilder๋ก ๋น๋๊ธฐ ๋ฐ์ดํฐ ํ์ํ๊ธฐ
API์์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ๋์ ๋ก๋ฉ ํ๋ฉด์ ๋ณด์ฌ์ฃผ๊ณ , ๋ฐ์ดํฐ๊ฐ ๋์ฐฉํ๋ฉด ํ์ํ๊ณ , ์๋ฌ๊ฐ ๋๋ฉด ์๋ฌ ๋ฉ์์ง๋ฅผ ๋ณด์ฌ์ฃผ๋ ๊ฒ ์ผ๋ฐ์ ์ด์ง? Flutter์ FutureBuilder๊ฐ ์ด๊ฑธ ์ ๋ง ์ฝ๊ฒ ๋ง๋ค์ด์ค!
class UserProfile extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FutureBuilder<Map<String, dynamic>>(
future: fetchUserData(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
// ๋ก๋ฉ ์ค
return Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
// ์๋ฌ ๋ฐ์
return Center(child: Text('์ค๋ฅ: ${snapshot.error}'));
} else if (snapshot.hasData) {
// ๋ฐ์ดํฐ ๋์ฐฉ
final user = snapshot.data!;
return Column(
children: [
Text('์ด๋ฆ: ${user['name']}'),
Text('์ด๋ฉ์ผ: ${user['email']}'),
],
);
} else {
return Center(child: Text('๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค'));
}
},
);
}
}
๐ฅ Dio - ๋ ๊ฐ๋ ฅํ HTTP ํด๋ผ์ด์ธํธ
๋ณต์กํ ๋คํธ์ํน์ด ํ์ํ๋ค๋ฉด Dio ํจํค์ง๋ฅผ ์ถ์ฒํด! http ํจํค์ง๋ณด๋ค ๊ธฐ๋ฅ์ด ํจ์ฌ ๋ง์:
โข ์ธํฐ์
ํฐ (์์ฒญ/์๋ต ๊ฐ๋ก์ฑ๊ธฐ)
โข ํ์ผ ์
๋ก๋/๋ค์ด๋ก๋
โข ํ์์์ ์ค์
โข ์๋ ์ฌ์๋
โข ์์ฒญ ์ทจ์
โข FormData ์ง์
์ค๋ฌด์์๋ Dio๋ฅผ ๋ ๋ง์ด ์ฌ์ฉํ๋ ํธ์ด์ผ!
import 'package:dio/dio.dart';
final dio = Dio();
Future<void> uploadFile(String filePath) async {
FormData formData = FormData.fromMap({
'file': await MultipartFile.fromFile(filePath),
'description': 'ํ๋กํ ์ฌ์ง',
});
try {
Response response = await dio.post(
'https://api.example.com/upload',
data: formData,
onSendProgress: (sent, total) {
print('์
๋ก๋ ์งํ๋ฅ : ${(sent / total * 100).toStringAsFixed(0)}%');
},
);
print('์
๋ก๋ ์ฑ๊ณต: ${response.data}');
} catch (e) {
print('์
๋ก๋ ์คํจ: $e');
}
}
๐พ ๋ก์ปฌ ๋ฐ์ดํฐ ์ ์ฅํ๊ธฐ
์ฑ์์ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํด์ผ ํ ๋๊ฐ ๋ง์. ์ฌ์ฉ์ ์ค์ , ๋ก๊ทธ์ธ ํ ํฐ, ์บ์ ๋ฐ์ดํฐ ๋ฑ๋ฑ. Flutter์์ ๋ก์ปฌ ์ ์ฅํ๋ ๋ฐฉ๋ฒ๋ค์ ์์๋ณด์! ๐ฆ
๐ SharedPreferences - ๊ฐ๋จํ ํค-๊ฐ ์ ์ฅ
๊ฐ๋จํ ๋ฐ์ดํฐ(์ค์ ๊ฐ, ํ ํฐ ๋ฑ)๋ฅผ ์ ์ฅํ ๋๋ SharedPreferences๊ฐ ์ต๊ณ ์ผ. ์ฌ์ฉ๋ฒ๋ ์ ๋ง ์ฌ์!
import 'package:shared_preferences/shared_preferences.dart';
// ๋ฐ์ดํฐ ์ ์ฅ
Future<void> saveUserSettings(bool isDarkMode, String language) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('darkMode', isDarkMode);
await prefs.setString('language', language);
}
// ๋ฐ์ดํฐ ๋ถ๋ฌ์ค๊ธฐ
Future<Map<String, dynamic>> loadUserSettings() async {
final prefs = await SharedPreferences.getInstance();
return {
'darkMode': prefs.getBool('darkMode') ?? false,
'language': prefs.getString('language') ?? 'ko',
};
}
// ๋ฐ์ดํฐ ์ญ์
Future<void> clearSettings() async {
final prefs = await SharedPreferences.getInstance();
await prefs.clear();
}
๐๏ธ SQLite - ๋ณต์กํ ๋ฐ์ดํฐ ๊ด๋ฆฌ
์ข ๋ ๋ณต์กํ ๋ฐ์ดํฐ ๊ตฌ์กฐ๊ฐ ํ์ํ๋ค๋ฉด? ์๋ฅผ ๋ค์ด ํ ์ผ ๋ชฉ๋ก ์ฑ์ ๋ง๋ ๋ค๋ฉด? SQLite ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ฌ์ฉํ๋ฉด ๋ผ! sqflite ํจํค์ง๊ฐ ๋์์ค ๊ฑฐ์ผ.
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
class DatabaseHelper {
static Database? _database;
Future<Database> get database async {
if (_database != null) return _database!;
_database = await initDatabase();
return _database!;
}
Future<Database> initDatabase() async {
String path = join(await getDatabasesPath(), 'todo.db');
return await openDatabase(
path,
version: 1,
onCreate: (db, version) {
return db.execute(
'CREATE TABLE todos(id INTEGER PRIMARY KEY, title TEXT, done INTEGER)',
);
},
);
}
Future<void> insertTodo(Map<String, dynamic> todo) async {
final db = await database;
await db.insert('todos', todo);
}
Future<List<Map<String, dynamic>>> getTodos() async {
final db = await database;
return await db.query('todos');
}
Future<void> updateTodo(int id, Map<String, dynamic> todo) async {
final db = await database;
await db.update('todos', todo, where: 'id = ?', whereArgs: [id]);
}
Future<void> deleteTodo(int id) async {
final db = await database;
await db.delete('todos', where: 'id = ?', whereArgs: [id]);
}
}
๐ฆ Hive - ๋น ๋ฅด๊ณ ๊ฐ๋ฒผ์ด NoSQL
SQLite๋ ์ข์ง๋ง ์ข ๋ฌด๊ฒ๊ณ ๋ณต์กํ ์ ์์ด. ๋ ๊ฐ๋จํ๊ณ ๋น ๋ฅธ ๊ฑธ ์ํ๋ค๋ฉด Hive๋ฅผ ์จ๋ด! NoSQL ๋ฐ์ดํฐ๋ฒ ์ด์ค์ธ๋ฐ, ์ ๋ง ๋น ๋ฅด๊ณ ์ฌ์ฉํ๊ธฐ ์ฌ์.
import 'package:hive/hive.dart';
// ๋ฐ์ดํฐ ๋ชจ๋ธ ์ ์
@HiveType(typeId: 0)
class Todo extends HiveObject {
@HiveField(0)
late String title;
@HiveField(1)
late bool done;
}
// ์ด๊ธฐํ
await Hive.initFlutter();
Hive.registerAdapter(TodoAdapter());
var box = await Hive.openBox<Todo>('todos');
// ๋ฐ์ดํฐ ์ถ๊ฐ
var todo = Todo()
..title = '์ฅ๋ณด๊ธฐ'
..done = false;
box.add(todo);
// ๋ฐ์ดํฐ ์ฝ๊ธฐ
var todos = box.values.toList();
// ๋ฐ์ดํฐ ์์
todo.done = true;
todo.save();
// ๋ฐ์ดํฐ ์ญ์
todo.delete();
โข SQLite: ๋ณต์กํ ์ฟผ๋ฆฌ๊ฐ ํ์ํ ๊ด๊ณํ ๋ฐ์ดํฐ
โข Hive: ๋น ๋ฅธ ์ฑ๋ฅ์ด ํ์ํ ์ค๊ฐ ๊ท๋ชจ ๋ฐ์ดํฐ
โข Firebase: ํด๋ผ์ฐ๋ ๋๊ธฐํ๊ฐ ํ์ํ ๋
๋๋ถ๋ถ์ ๊ฒฝ์ฐ SharedPreferences + Hive ์กฐํฉ์ด๋ฉด ์ถฉ๋ถํด!
๐จ ๋ค๋น๊ฒ์ด์ ๊ณผ ๋ผ์ฐํ
์ฑ์์ ํ๋ฉด ์ ํ์ ์ ๋ง ์ค์ํ์ง! ๋ก๊ทธ์ธ ํ๋ฉด์์ ํ ํ๋ฉด์ผ๋ก, ๋ชฉ๋ก์์ ์์ธ ํ๋ฉด์ผ๋ก. Flutter์ ๋ค๋น๊ฒ์ด์ ์์คํ ์ ๋ง์คํฐํด๋ณด์! ๐บ๏ธ
๐ ๊ธฐ๋ณธ ๋ค๋น๊ฒ์ด์
Flutter์ ๋ค๋น๊ฒ์ด์ ์ ์คํ(Stack) ๊ตฌ์กฐ์ผ. ํ๋ฉด์ ์์๋ค๊ฐ(push) ๋นผ๋(pop) ๋ฐฉ์์ด์ง.
// ์ ํ๋ฉด์ผ๋ก ์ด๋
Navigator.push(
context,
MaterialPageRoute(builder: (context) => DetailScreen()),
);
// ์ด์ ํ๋ฉด์ผ๋ก ๋์๊ฐ๊ธฐ
Navigator.pop(context);
// ๋ฐ์ดํฐ์ ํจ๊ป ์ด๋
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailScreen(id: 123),
),
);
// ๊ฒฐ๊ณผ๊ฐ ๋ฐ์์ค๊ธฐ
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => SelectionScreen()),
);
print('์ ํ๋ ๊ฐ: $result');
๐ฏ Named Routes - ๋ ๊น๋ํ ๋ฐฉ๋ฒ
์ฑ์ด ์ปค์ง๋ฉด Named Routes๋ฅผ ์ฌ์ฉํ๋ ๊ฒ ์ข์. ๋ผ์ฐํธ์ ์ด๋ฆ์ ๋ถ์ฌ์ ๊ด๋ฆฌํ๋ ๊ฑฐ์ง.
// main.dart์์ ๋ผ์ฐํธ ์ ์
MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => HomeScreen(),
'/detail': (context) => DetailScreen(),
'/settings': (context) => SettingsScreen(),
},
);
// ํ๋ฉด ์ด๋
Navigator.pushNamed(context, '/detail');
// ๋ฐ์ดํฐ ์ ๋ฌ
Navigator.pushNamed(
context,
'/detail',
arguments: {'id': 123, 'title': '์์ธ ์ ๋ณด'},
);
// ๋ฐ์ดํฐ ๋ฐ๊ธฐ
class DetailScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final args = ModalRoute.of(context)!.settings.arguments as Map;
return Scaffold(
appBar: AppBar(title: Text(args['title'])),
body: Text('ID: ${args['id']}'),
);
}
}
๐ฅ ๊ณ ๊ธ ๋ค๋น๊ฒ์ด์ - go_router
๋ ๋ณต์กํ ๋ค๋น๊ฒ์ด์ ์ด ํ์ํ๋ค๋ฉด go_router ํจํค์ง๋ฅผ ์ถ์ฒํด! ๋ฅ๋งํฌ, ๋ฆฌ๋ค์ด๋ ์ , ์ค์ฒฉ ๋ผ์ฐํ ๋ฑ์ ์ฝ๊ฒ ๊ตฌํํ ์ ์์ด.
import 'package:go_router/go_router.dart';
final router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => HomeScreen(),
),
GoRoute(
path: '/user/:id',
builder: (context, state) {
final id = state.params['id']!;
return UserScreen(userId: id);
},
),
GoRoute(
path: '/settings',
builder: (context, state) => SettingsScreen(),
routes: [
GoRoute(
path: 'profile',
builder: (context, state) => ProfileScreen(),
),
],
),
],
);
// ์ฌ์ฉ
MaterialApp.router(
routerConfig: router,
);
// ํ๋ฉด ์ด๋
context.go('/user/123');
context.push('/settings/profile');
๐จ ํ๋ฉด ์ ํ ์ ๋๋ฉ์ด์
๊ธฐ๋ณธ ํ๋ฉด ์ ํ๋ ์ข์ง๋ง, ์ปค์คํ ์ ๋๋ฉ์ด์ ์ ์ถ๊ฐํ๋ฉด ์ฑ์ด ํจ์ฌ ์ธ๋ จ๋์ด ๋ณด์ฌ!
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => DetailScreen(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
const begin = Offset(1.0, 0.0);
const end = Offset.zero;
const curve = Curves.easeInOut;
var tween = Tween(begin: begin, end: end).chain(
CurveTween(curve: curve),
);
return SlideTransition(
position: animation.drive(tween),
child: child,
);
},
),
);
๐ฑ ํ๋ซํผ๋ณ ์ต์ ํ์ ๋ค์ดํฐ๋ธ ๊ธฐ๋ฅ
Flutter๋ ํฌ๋ก์คํ๋ซํผ์ด์ง๋ง, ๊ฐ ํ๋ซํผ์ ํน์ฑ์ ์ด๋ฆด ์๋ ์์ด์ผ ํด. iOS์ Android๋ ๋์์ธ ๊ฐ์ด๋๋ผ์ธ๋ ๋ค๋ฅด๊ณ , ์ฌ์ฉ์๋ค์ด ๊ธฐ๋ํ๋ UX๋ ๋ฌ๋ผ. ์ด๊ฑธ ์ด๋ป๊ฒ ์ฒ๋ฆฌํ๋์ง ์์๋ณด์! ๐ฏ
๐จ ํ๋ซํผ๋ณ UI ๋ถ๊ธฐ
๋๋ก๋ ํ๋ซํผ์ ๋ฐ๋ผ ๋ค๋ฅธ UI๋ฅผ ๋ณด์ฌ์ค์ผ ํ ๋๊ฐ ์์ด. Flutter๋ ์ด๊ฑธ ์ ๋ง ์ฝ๊ฒ ๋ง๋ค์ด์ค!
import 'dart:io';
Widget build(BuildContext context) {
return Platform.isIOS
? CupertinoButton(
child: Text('iOS ์คํ์ผ ๋ฒํผ'),
onPressed: () {},
)
: ElevatedButton(
child: Text('Android ์คํ์ผ ๋ฒํผ'),
onPressed: () {},
);
}
๋๋ Theme.of(context).platform์ ์ฌ์ฉํ ์๋ ์์ด:
Widget build(BuildContext context) {
final platform = Theme.of(context).platform;
return platform == TargetPlatform.iOS
? CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text('iOS ์คํ์ผ'),
),
child: Center(child: Text('iOS UI')),
)
: Scaffold(
appBar: AppBar(title: Text('Android ์คํ์ผ')),
body: Center(child: Text('Android UI')),
);
}
๐ ๋ค์ดํฐ๋ธ ๊ธฐ๋ฅ ์ฌ์ฉํ๊ธฐ
์นด๋ฉ๋ผ, ์์น, ์๋ฆผ ๊ฐ์ ๋ค์ดํฐ๋ธ ๊ธฐ๋ฅ๋ค์ ์ด๋ป๊ฒ ์ฌ์ฉํ ๊น? Flutter์๋ ์ด๋ฏธ ๋ง๋ค์ด์ง ํ๋ฌ๊ทธ์ธ๋ค์ด ์์ฒญ ๋ง์! pub.dev์์ ๊ฒ์ํ๋ฉด ๊ฑฐ์ ๋ค ์์ด. ๐
๐ธ ์นด๋ฉ๋ผ ์ฌ์ฉ ์์
image_picker ํจํค์ง๋ก ์นด๋ฉ๋ผ๋ ๊ฐค๋ฌ๋ฆฌ์์ ์ด๋ฏธ์ง๋ฅผ ๊ฐ์ ธ์ฌ ์ ์์ด.
import 'package:image_picker/image_picker.dart';
final ImagePicker _picker = ImagePicker();
Future<void> takePhoto() async {
final XFile? photo = await _picker.pickImage(
source: ImageSource.camera,
);
if (photo != null) {
print('์ฌ์ง ๊ฒฝ๋ก: ${photo.path}');
// ์ด๋ฏธ์ง ์ฒ๋ฆฌ ๋ก์ง
}
}
Future<void> pickFromGallery() async {
final XFile? image = await _picker.pickImage(
source: ImageSource.gallery,
);
if (image != null) {
print('์ ํํ ์ด๋ฏธ์ง: ${image.path}');
}
}
๐ ์์น ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ
geolocator ํจํค์ง๋ก GPS ์์น๋ฅผ ๊ฐ์ ธ์ฌ ์ ์์ด.
import 'package:geolocator/geolocator.dart';
Future<Position> getCurrentLocation() async {
// ์์น ๊ถํ ํ์ธ
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
}
// ํ์ฌ ์์น ๊ฐ์ ธ์ค๊ธฐ
Position position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
);
print('์๋: ${position.latitude}, ๊ฒฝ๋: ${position.longitude}');
return position;
}
๐ ํธ์ ์๋ฆผ
firebase_messaging์ผ๋ก ํธ์ ์๋ฆผ์ ๊ตฌํํ ์ ์์ด.
import 'package:firebase_messaging/firebase_messaging.dart';
Future<void> setupPushNotifications() async {
FirebaseMessaging messaging = FirebaseMessaging.instance;
// ๊ถํ ์์ฒญ (iOS)
NotificationSettings settings = await messaging.requestPermission(
alert: true,
badge: true,
sound: true,
);
// FCM ํ ํฐ ๊ฐ์ ธ์ค๊ธฐ
String? token = await messaging.getToken();
print('FCM ํ ํฐ: $token');
// ํฌ๊ทธ๋ผ์ด๋ ๋ฉ์์ง ์ฒ๋ฆฌ
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
print('๋ฉ์์ง ๋์ฐฉ: ${message.notification?.title}');
});
}
โข share_plus: ์ฝํ ์ธ ๊ณต์ ํ๊ธฐ
โข connectivity_plus: ๋คํธ์ํฌ ์ํ ํ์ธ
โข device_info_plus: ๊ธฐ๊ธฐ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ
โข package_info_plus: ์ฑ ๋ฒ์ ์ ๋ณด
โข path_provider: ํ์ผ ์์คํ ๊ฒฝ๋ก
โข permission_handler: ๊ถํ ๊ด๋ฆฌ
pub.dev์์ ๊ฒ์ํ๋ฉด ์ ๋ง ๋ง์ ํ๋ฌ๊ทธ์ธ์ ์ฐพ์ ์ ์์ด!
๐ ์ฑ๋ฅ ์ต์ ํ ํ
Flutter๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์ฑ๋ฅ์ด ์ข์ง๋ง, ์ฑ์ด ๋ณต์กํด์ง๋ฉด ์ต์ ํ๊ฐ ํ์ํด. ์ค๋ฌด์์ ์ ๋ง ์ ์ฉํ ์ต์ ํ ํ๋ค์ ๊ณต์ ํ ๊ฒ! โก
๐ฏ ๋ถํ์ํ ๋ฆฌ๋น๋ ์ค์ด๊ธฐ
Flutter๋ ์ํ๊ฐ ๋ณ๊ฒฝ๋๋ฉด ์์ ฏ์ ๋ค์ ๊ทธ๋ ค. ๊ทธ๋ฐ๋ฐ ๋ชจ๋ ์์ ฏ์ ๋ค์ ๊ทธ๋ฆด ํ์๋ ์์์? const ํค์๋๋ฅผ ์ ๊ทน ํ์ฉํ์!
// โ ๋์ ์ - ๋งค๋ฒ ์๋ก ์์ฑ๋จ
Widget build(BuildContext context) {
return Container(
child: Text('๊ณ ์ ๋ ํ
์คํธ'),
);
}
// โ
์ข์ ์ - ํ ๋ฒ๋ง ์์ฑ๋จ
Widget build(BuildContext context) {
return const Container(
child: Text('๊ณ ์ ๋ ํ
์คํธ'),
);
}
๐ ListView.builder ์ฌ์ฉํ๊ธฐ
๊ธด ๋ชฉ๋ก์ ํ์ํ ๋๋ ์ ๋ ListView๋ฅผ ์ง์ ์ฌ์ฉํ์ง ๋ง! ListView.builder๋ฅผ ์ฌ์ฉํ๋ฉด ํ๋ฉด์ ๋ณด์ด๋ ํญ๋ชฉ๋ง ๋ ๋๋งํด์ ๋ฉ๋ชจ๋ฆฌ๋ฅผ ์ ์ฝํ ์ ์์ด.
// โ ๋์ ์ - ๋ชจ๋ ํญ๋ชฉ์ ํ ๋ฒ์ ์์ฑ
ListView(
children: List.generate(1000, (index) => ListTile(title: Text('Item $index'))),
)
// โ
์ข์ ์ - ํ์ํ ํญ๋ชฉ๋ง ์์ฑ
ListView.builder(
itemCount: 1000,
itemBuilder: (context, index) {
return ListTile(title: Text('Item $index'));
},
)
๐ผ๏ธ ์ด๋ฏธ์ง ์ต์ ํ
์ด๋ฏธ์ง๋ ์ฑ ์ฑ๋ฅ์ ํฐ ์ํฅ์ ์ค. ๋ช ๊ฐ์ง ํ์ ์๋ ค์ค๊ฒ:
๐จ ์ด๋ฏธ์ง ์ต์ ํ ์ฒดํฌ๋ฆฌ์คํธ
1. ์ ์ ํ ํฌ๊ธฐ ์ฌ์ฉ:
ํ๋ฉด์ ํ์๋ ํฌ๊ธฐ์ ๋ง์ถฐ ์ด๋ฏธ์ง๋ฅผ ๋ฆฌ์ฌ์ด์งํด. 1000x1000 ์ด๋ฏธ์ง๋ฅผ 100x100์ผ๋ก ํ์ํ๋ฉด ๋ญ๋น์ผ!
2. ์บ์ฑ ํ์ฉ:
cached_network_image ํจํค์ง๋ฅผ ์ฌ์ฉํ๋ฉด ๋คํธ์ํฌ ์ด๋ฏธ์ง๋ฅผ ์๋์ผ๋ก ์บ์ฑํด์ค.
3. ์ ์ ํ ํฌ๋งท ์ ํ:
โข PNG: ํฌ๋ช
๋๊ฐ ํ์ํ ๊ฒฝ์ฐ
โข JPEG: ์ฌ์ง ๊ฐ์ ๋ณต์กํ ์ด๋ฏธ์ง
โข WebP: ๋ ์์ ํ์ผ ํฌ๊ธฐ (Flutter๊ฐ ์ง์ํด!)
4. ์ง์ฐ ๋ก๋ฉ:
ํ๋ฉด์ ๋ณด์ด์ง ์๋ ์ด๋ฏธ์ง๋ ๋์ค์ ๋ก๋ํ๋๋ก ํด.
import 'package:cached_network_image/cached_network_image.dart';
CachedNetworkImage(
imageUrl: 'https://example.com/image.jpg',
placeholder: (context, url) => CircularProgressIndicator(),
errorWidget: (context, url, error) => Icon(Icons.error),
fit: BoxFit.cover,
memCacheWidth: 500, // ๋ฉ๋ชจ๋ฆฌ ์บ์ ํฌ๊ธฐ ์ ํ
)
โก ์ ๋๋ฉ์ด์ ์ต์ ํ
์ ๋๋ฉ์ด์ ์ 60fps๋ฅผ ์ ์งํด์ผ ๋ถ๋๋ฌ์ ๋ณด์ฌ. ๋ช ๊ฐ์ง ์ฃผ์์ฌํญ:
// โ
์ข์ ์ - RepaintBoundary ์ฌ์ฉ
RepaintBoundary(
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
// ์ ๋๋ฉ์ด์
์์ฑ๋ค
),
)
// โ
Transform ์ฌ์ฉ (๋ ํจ์จ์ )
Transform.translate(
offset: Offset(10, 0),
child: MyWidget(),
)
// โ ๋์ ์ - setState๋ก ์์น ๋ณ๊ฒฝ (๋นํจ์จ์ )
Container(
margin: EdgeInsets.only(left: position),
child: MyWidget(),
)
๐ ์ค์ ํ๋ก์ ํธ: ๊ฐ๋จํ ํ ์ผ ์ฑ ๋ง๋ค๊ธฐ
์ด๋ก ์ ์ด์ ์ถฉ๋ถํด! ์ค์ ๋ก ๋์ํ๋ ์ฑ์ ๋ง๋ค์ด๋ณด์. ๊ฐ๋จํ ํ ์ผ ๊ด๋ฆฌ ์ฑ์ ์ฒ์๋ถํฐ ๋๊น์ง ๋ง๋ค์ด๋ณผ ๊ฑฐ์ผ. ์ด ํ๋ก์ ํธ๋ฅผ ํตํด ์ง๊ธ๊น์ง ๋ฐฐ์ด ๊ฑธ ๋ค ์จ๋จน์ ์ ์์ด! ๐ช
๐ ํ๋ก์ ํธ ๊ตฌ์กฐ
lib/
โโโ main.dart
โโโ models/
โ โโโ todo.dart
โโโ screens/
โ โโโ home_screen.dart
โ โโโ add_todo_screen.dart
โโโ widgets/
โโโ todo_item.dart
1๏ธโฃ ๋ชจ๋ธ ์ ์ (models/todo.dart)
class Todo {
final String id;
final String title;
final String description;
bool isDone;
final DateTime createdAt;
Todo({
required this.id,
required this.title,
this.description = '',
this.isDone = false,
required this.createdAt,
});
void toggleDone() {
isDone = !isDone;
}
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'description': description,
'isDone': isDone,
'createdAt': createdAt.toIso8601String(),
};
}
factory Todo.fromJson(Map<String, dynamic> json) {
return Todo(
id: json['id'],
title: json['title'],
description: json['description'] ?? '',
isDone: json['isDone'] ?? false,
createdAt: DateTime.parse(json['createdAt']),
);
}
}
2๏ธโฃ ํ ํ๋ฉด (screens/home_screen.dart)
import 'package:flutter/material.dart';
class HomeScreen extends StatefulWidget {
@override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
List<Todo> todos = [];
void _addTodo(String title, String description) {
setState(() {
todos.add(Todo(
id: DateTime.now().toString(),
title: title,
description: description,
createdAt: DateTime.now(),
));
});
}
void _toggleTodo(int index) {
setState(() {
todos[index].toggleDone();
});
}
void _deleteTodo(int index) {
setState(() {
todos.removeAt(index);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('๋ด ํ ์ผ ๋ชฉ๋ก'),
backgroundColor: Colors.deepPurple,
),
body: todos.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.inbox, size: 100, color: Colors.grey),
SizedBox(height: 16),
Text(
'ํ ์ผ์ด ์์ต๋๋ค!',
style: TextStyle(fontSize: 20, color: Colors.grey),
),
],
),
)
: ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
final todo = todos[index];
return Dismissible(
key: Key(todo.id),
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: EdgeInsets.only(right: 20),
child: Icon(Icons.delete, color: Colors.white),
),
direction: DismissDirection.endToStart,
onDismissed: (direction) {
_deleteTodo(index);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${todo.title} ์ญ์ ๋จ')),
);
},
child: Card(
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: ListTile(
leading: Checkbox(
value: todo.isDone,
onChanged: (value) => _toggleTodo(index),
),
title: Text(
todo.title,
style: TextStyle(
decoration: todo.isDone
? TextDecoration.lineThrough
: null,
),
),
subtitle: todo.description.isNotEmpty
? Text(todo.description)
: null,
trailing: Icon(Icons.chevron_right),
),
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => AddTodoScreen()),
);
if (result != null) {
_addTodo(result['title'], result['description']);
}
},
child: Icon(Icons.add),
backgroundColor: Colors.deepPurple,
),
);
}
}
3๏ธโฃ ํ ์ผ ์ถ๊ฐ ํ๋ฉด (screens/add_todo_screen.dart)
import 'package:flutter/material.dart';
class AddTodoScreen extends StatefulWidget {
@override
_AddTodoScreenState createState() => _AddTodoScreenState();
}
class _AddTodoScreenState extends State<AddTodoScreen> {
final _titleController = TextEditingController();
final _descriptionController = TextEditingController();
final _formKey = GlobalKey<FormState>();
@override
void dispose() {
_titleController.dispose();
_descriptionController.dispose();
super.dispose();
}
void _saveTodo() {
if (_formKey.currentState!.validate()) {
Navigator.pop(context, {
'title': _titleController.text,
'description': _descriptionController.text,
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('์ ํ ์ผ ์ถ๊ฐ'),
backgroundColor: Colors.deepPurple,
),
body: Padding(
padding: EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
controller: _titleController,
decoration: InputDecoration(
labelText: '์ ๋ชฉ',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.title),
),
validator: (value) {
if (value == null || value.isEmpty) {
return '์ ๋ชฉ์ ์
๋ ฅํ์ธ์';
}
return null;
},
),
SizedBox(height: 16),
TextFormField(
controller: _descriptionController,
decoration: InputDecoration(
labelText: '์ค๋ช
(์ ํ์ฌํญ)',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.description),
),
maxLines: 3,
),
SizedBox(height: 24),
ElevatedButton(
onPressed: _saveTodo,
child: Padding(
padding: EdgeInsets.all(16),
child: Text('์ ์ฅ', style: TextStyle(fontSize: 18)),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.deepPurple,
),
),
],
),
),
),
);
}
}
โข ์ํ ๊ด๋ฆฌ (setState)
โข ๋ฆฌ์คํธ ๋ ๋๋ง (ListView.builder)
โข ํ๋ฉด ์ ํ (Navigator)
โข ํผ ์ ํจ์ฑ ๊ฒ์ฌ
โข ์ค์์ดํ ์ญ์ (Dismissible)
โข ์ฒดํฌ๋ฐ์ค ์ธํฐ๋์
๋ฑ ์ค๋ฌด์์ ์์ฃผ ์ฐ๋ ๊ธฐ๋ฅ๋ค์ด ๋ค ๋ค์ด์์ด! ์ด๊ฑธ ๊ธฐ๋ฐ์ผ๋ก ๋ ๋ฐ์ ์์ผ๋ด. ์๋ฅผ ๋ค์ด SharedPreferences๋ก ๋ฐ์ดํฐ ์ ์ฅํ๊ธฐ, ์นดํ ๊ณ ๋ฆฌ ์ถ๊ฐํ๊ธฐ, ๊ฒ์ ๊ธฐ๋ฅ ๋ฃ๊ธฐ ๋ฑ๋ฑ!
๐ ๋ฐฐํฌํ๊ธฐ: ์ฑ์คํ ์ด์ ์ฌ๋ฆฌ๋ ๋ฒ
์ฑ์ ๋ง๋ค์์ผ๋ฉด ์ธ์์ ๊ณต๊ฐํด์ผ์ง! iOS App Store์ Google Play Store์ ์ฑ์ ๋ฐฐํฌํ๋ ๊ณผ์ ์ ์์๋ณด์. ์๊ฐ๋ณด๋ค ๋ณต์กํ์ง๋ง, ์ฐจ๊ทผ์ฐจ๊ทผ ๋ฐ๋ผํ๋ฉด ํ ์ ์์ด! ๐ฑ
๐ค Android ๋ฐฐํฌ
Android๊ฐ ์๋์ ์ผ๋ก ๋ ์ฌ์. ๋จ๊ณ๋ณ๋ก ์์๋ณด์:
1๋จ๊ณ: ์ฑ ์๋ช ์ค์
์ฑ์ ๋ฐฐํฌํ๋ ค๋ฉด ๋์งํธ ์๋ช ์ด ํ์ํด. ํค์คํ ์ด ํ์ผ์ ์์ฑํ์:
keytool -genkey -v -keystore ~/my-release-key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias my-key-alias
๊ทธ๋ฆฌ๊ณ android/key.properties ํ์ผ์ ๋ง๋ค์ด:
storePassword=๋น๋ฐ๋ฒํธ
keyPassword=๋น๋ฐ๋ฒํธ
keyAlias=my-key-alias
storeFile=/Users/์ฌ์ฉ์๋ช
/my-release-key.jks
2๋จ๊ณ: build.gradle ์ค์
android/app/build.gradle์์ ์๋ช ์ค์ ์ ์ถ๊ฐํด:
android {
...
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
release {
signingConfig signingConfigs.release
}
}
}
3๋จ๊ณ: APK/AAB ๋น๋
์ด์ ๋ฆด๋ฆฌ์ฆ ๋ฒ์ ์ ๋น๋ํ์:
# AAB ํ์ผ ์์ฑ (Play Store ๊ถ์ฅ)
flutter build appbundle
# APK ํ์ผ ์์ฑ
flutter build apk --release
์์ฑ๋ ํ์ผ์ build/app/outputs/์ ์์ด!
๐ iOS ๋ฐฐํฌ
iOS๋ ์ข ๋ ๊น๋ค๋ก์. Apple Developer ๊ณ์ (์ฐ $99)์ด ํ์ํ๊ณ , macOS์์๋ง ๊ฐ๋ฅํด.
1๋จ๊ณ: Xcode ์ค์
ios/Runner.xcworkspace๋ฅผ Xcode๋ก ์ด์ด์:
โข Bundle Identifier ์ค์ (์: com.yourcompany.appname)
โข Team ์ ํ (Apple Developer ๊ณ์ )
โข ๋ฒ์ ๋ฐ ๋น๋ ๋ฒํธ ์ค์
2๋จ๊ณ: ์์ด์ฝ๊ณผ ์คํ๋์ ์คํฌ๋ฆฐ
์ฑ ์์ด์ฝ์ ์ฌ๋ฌ ํฌ๊ธฐ๋ก ์ค๋นํด์ผ ํด. flutter_launcher_icons ํจํค์ง๋ฅผ ์ฌ์ฉํ๋ฉด ์๋์ผ๋ก ์์ฑํ ์ ์์ด!
# pubspec.yaml์ ์ถ๊ฐ
dev_dependencies:
flutter_launcher_icons: ^0.13.0
flutter_launcher_icons:
android: true
ios: true
image_path: "assets/icon/app_icon.png"
# ์คํ
flutter pub run flutter_launcher_icons
3๋จ๊ณ: ๋น๋ ๋ฐ ์ ๋ก๋
Archive๋ฅผ ์์ฑํ๊ณ App Store Connect์ ์ ๋ก๋:
flutter build ios --release
# ๋๋ Xcode์์
# Product > Archive > Distribute App
โข ์คํฌ๋ฆฐ์ท ์ฌ๋ฌ ๊ธฐ๊ธฐ ์ฌ์ด์ฆ๋ณ๋ก ์ค๋น
โข ์ฑ ์ค๋ช ๋ฐ ํค์๋ ์ต์ ํ
โข ์ฌ์ฌ ๊ฐ์ด๋๋ผ์ธ ๊ผญ ํ์ธ (ํนํ iOS)
โข ์ฒซ ์ฌ์ฌ๋ ๋ณดํต 1-2์ฃผ ์์
๐ผ ์ค๋ฌด ํ๊ณผ ๋ฒ ์คํธ ํ๋ํฐ์ค
์ค์ ํ๋ก์ ํธ์์ Flutter๋ฅผ ์ฌ์ฉํ๋ฉด์ ๋ฐฐ์ด ์์คํ ๊ตํ๋ค์ ๊ณต์ ํ ๊ฒ. ์ด๋ฐ ๊ฑธ ๋ฏธ๋ฆฌ ์์๋๋ผ๋ฉด ์ํ์ฐฉ์ค๋ฅผ ๋ง์ด ์ค์ผ ์ ์์์ ํ ๋ฐ! ๐
๐ ํ๋ก์ ํธ ๊ตฌ์กฐ
ํ๋ก์ ํธ๊ฐ ์ปค์ง์๋ก ๊ตฌ์กฐ๊ฐ ์ค์ํด์ ธ. ๋ด๊ฐ ์ ํธํ๋ ๊ตฌ์กฐ๋ ์ด๋:
lib/
โโโ main.dart
โโโ core/
โ โโโ constants/
โ โโโ themes/
โ โโโ utils/
โโโ data/
โ โโโ models/
โ โโโ repositories/
โ โโโ services/
โโโ presentation/
โ โโโ screens/
โ โโโ widgets/
โ โโโ providers/
โโโ routes/
โโโ app_routes.dart
๐จ ํ ๋ง ๊ด๋ฆฌ
์ฑ ์ ์ฒด์ ๋์์ธ์ ์ผ๊ด๋๊ฒ ์ ์งํ๋ ค๋ฉด ํ ๋ง๋ฅผ ์ ์ค์ ํด์ผ ํด:
class AppTheme {
static ThemeData lightTheme = ThemeData(
primarySwatch: Colors.blue,
brightness: Brightness.light,
scaffoldBackgroundColor: Colors.white,
appBarTheme: AppBarTheme(
elevation: 0,
centerTitle: true,
backgroundColor: Colors.blue,
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 32, vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
fillColor: Colors.grey[100],
),
);
static ThemeData darkTheme = ThemeData(
primarySwatch: Colors.blue,
brightness: Brightness.dark,
scaffoldBackgroundColor: Colors.grey[900],
// ๋คํฌ ๋ชจ๋ ์ค์ ...
);
}
๐ ์๋ฌ ์ฒ๋ฆฌ
์๋ฌ ์ฒ๋ฆฌ๋ฅผ ์ ๋๋ก ํ์ง ์์ผ๋ฉด ์ฑ์ด ์์ฃผ ํฌ๋์ ๋. ํญ์ try-catch๋ฅผ ์ฌ์ฉํ๊ณ , ์ฌ์ฉ์์๊ฒ ์น์ ํ ๋ฉ์์ง๋ฅผ ๋ณด์ฌ์ค:
Future<void> loadData() async {
try {
setState(() => isLoading = true);
final data = await apiService.fetchData();
setState(() {
this.data = data;
isLoading = false;
});
} on NetworkException catch (e) {
_showError('์ธํฐ๋ท ์ฐ๊ฒฐ์ ํ์ธํด์ฃผ์ธ์');
} on ServerException catch (e) {
_showError('์๋ฒ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค');
} catch (e) {
_showError('์ ์ ์๋ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค');
} finally {
setState(() => isLoading = false);
}
}
void _showError(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.red,
action: SnackBarAction(
label: '์ฌ์๋',
textColor: Colors.white,
onPressed: loadData,
),
),
);
}
๐งช ํ ์คํธ ์์ฑ
ํ ์คํธ๋ ๊ท์ฐฎ์ง๋ง ์ ๋ง ์ค์ํด. ํนํ ํ ํ๋ก์ ํธ์์๋ ํ์์ผ!
// test/widget_test.dart
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Counter increments test', (WidgetTester tester) async {
await tester.pumpWidget(MyApp());
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}
โข Git ํ์ฉ: ์์ ๊ธฐ๋ฅ ๋จ์๋ก ์์ฃผ ์ปค๋ฐํ๊ธฐ
โข ํจํค์ง ๋ฒ์ : pubspec.yaml์์ ๋ฒ์ ์ ๊ณ ์ ํ๋ฉด ๋์ค์ ๋ฌธ์ ์์ด
โข ๋น๋ ์๋ํ: CI/CD ํ์ดํ๋ผ์ธ ๊ตฌ์ถํ๋ฉด ๋ฐฐํฌ๊ฐ ์ ๋ง ํธํด์ ธ
โข ๋ถ์ ๋๊ตฌ: Firebase Analytics, Crashlytics ๊ฐ์ ๋๊ตฌ๋ก ์ฌ์ฉ์ ํ๋ ์ถ์
โข A/B ํ ์คํ : ์ ๊ธฐ๋ฅ์ ์ผ๋ถ ์ฌ์ฉ์์๊ฒ๋ง ๋จผ์ ๊ณต๊ฐํด๋ณด๊ธฐ
์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ Flutter ์ ๋ฌธ๊ฐ๋ฅผ ์ฐพ์ ๋ฉํ ๋ง์ ๋ฐ๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์ผ!
๐ Flutter์ ๋ฏธ๋์ ์ ๋ง
Flutter๋ ๊ณ์ ๋ฐ์ ํ๊ณ ์์ด. ์ต๊ทผ ํธ๋ ๋์ ์์ผ๋ก์ ์ ๋ง์ ์ดํด๋ณด์! ๐ฎ
๐ Flutter 3.0 ์ดํ์ ๋ณํ
Flutter 3.0๋ถํฐ๋ ์ ๋ง ๋ง์ ๊ฒ ๋ฐ๋์์ด:
โข ๋ฉํฐํ๋ซํผ ์ง์ ๊ฐํ: macOS, Windows, Linux, ์น๊น์ง ๋ชจ๋ ์์ ํ๋์ด. ์ด์ ์ง์ง "ํ ๋ฒ ์์ฑํ๋ฉด ์ด๋์๋ ์คํ"์ด ๊ฐ๋ฅํด์ง ๊ฑฐ์ง!
โข Material 3 (Material You): ๋ ํ๋์ ์ด๊ณ ๊ฐ์ธํ๋ ๋์์ธ ์์คํ
. ์ฌ์ฉ์์ ๋ฐฐ๊ฒฝํ๋ฉด ์์์ ๋ง์ถฐ ์ฑ ํ
๋ง๊ฐ ์๋์ผ๋ก ์กฐ์ ๋ผ.
โข ์ฑ๋ฅ ๊ฐ์ : ๋ ๋๋ง ์์ง ์ต์ ํ๋ก ๋ ๋ถ๋๋ฌ์ด ์ ๋๋ฉ์ด์
๊ณผ ๋น ๋ฅธ ์์ ์๊ฐ.
โข Impeller ๋ ๋๋ง ์์ง: iOS์์ ๋ ๋์ ์ฑ๋ฅ์ ์ํ ์๋ก์ด ๋ ๋๋ง ์์ง. ์
ฐ์ด๋ ์ปดํ์ผ ์งํฐ(jank)๊ฐ ์ฌ๋ผ์ก์ด!
๐ผ ์ทจ์ ์์ฅ์์์ Flutter
Flutter ๊ฐ๋ฐ์ ์์๋ ๊ณ์ ์ฆ๊ฐํ๊ณ ์์ด. ํนํ:
โข ์คํํธ์
: ๋น ๋ฅธ MVP ๊ฐ๋ฐ์ด ํ์ํ ์คํํธ์
์์ Flutter๋ฅผ ์ ํธํด
โข ์ค์๊ธฐ์
: ๊ฐ๋ฐ ๋น์ฉ ์ ๊ฐ์ ์ํด ํฌ๋ก์คํ๋ซํผ ์ ํ
โข ๋๊ธฐ์
: Google, Alibaba, BMW ๋ฑ ๋๊ธฐ์
๋ค๋ Flutter ์ฌ์ฉ ์ค
โข ํ๋ฆฌ๋์: ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ Flutter ํ๋ก์ ํธ ๋ง์
ํ๊ท ์ฐ๋ด๋ ๋ค๋ฅธ ๋ชจ๋ฐ์ผ ๊ฐ๋ฐ์๋ค๊ณผ ๋น์ทํ๊ฑฐ๋ ๋ ๋์ ํธ์ด์ผ. ํนํ ํฌ๋ก์คํ๋ซํผ ๊ฐ๋ฐ ๋ฅ๋ ฅ์ ํฐ ์ฅ์ ์ด ๋์ง!
๐ Flutter vs ๊ฒฝ์์๋ค
React Native:
โข ์ฅ์ : JavaScript ์ํ๊ณ, ํฐ ์ปค๋ฎค๋ํฐ
โข ๋จ์ : ๋ค์ดํฐ๋ธ ๋ธ๋ฆฟ์ง๋ก ์ธํ ์ฑ๋ฅ ์ด์
โข Flutter๊ฐ ์ ๋ฆฌํ ์ : ๋ ๋์ ์ฑ๋ฅ, ์ผ๊ด๋ UI
Kotlin Multiplatform:
โข ์ฅ์ : ๋ค์ดํฐ๋ธ ์ฝ๋ ๊ณต์ , JetBrains ์ง์
โข ๋จ์ : UI๋ ๊ฐ ํ๋ซํผ๋ณ๋ก ์์ฑํด์ผ ํจ
โข Flutter๊ฐ ์ ๋ฆฌํ ์ : UI๊น์ง ์์ ํ ๊ณต์ ๊ฐ๋ฅ
๋ค์ดํฐ๋ธ ๊ฐ๋ฐ:
โข ์ฅ์ : ์ต๊ณ ์ ์ฑ๋ฅ, ํ๋ซํผ ์ต์ ๊ธฐ๋ฅ ์ฆ์ ์ฌ์ฉ
โข ๋จ์ : ๊ฐ๋ฐ ์๊ฐ๊ณผ ๋น์ฉ 2๋ฐฐ
โข Flutter๊ฐ ์ ๋ฆฌํ ์ : ๊ฐ๋ฐ ์๋, ๋น์ฉ ํจ์จ์ฑ
๐ ๊ณ์ ํ์ตํ๊ธฐ
Flutter๋ ๊ณ์ ๋ฐ์ ํ๋๊น ๊พธ์คํ ๊ณต๋ถํด์ผ ํด. ์ถ์ฒํ๋ ๋ฆฌ์์ค๋ค:
๊ณต์ ๋ฌธ์: flutter.dev - ๊ฐ์ฅ ์ ํํ๊ณ ์ต์ ์ ๋ณด
YouTube: Flutter ๊ณต์ ์ฑ๋, The Net Ninja, Reso Coder
์ปค๋ฎค๋ํฐ: Flutter Korea, Stack Overflow, Reddit r/FlutterDev
ํจํค์ง: pub.dev์์ ์ ์ฉํ ํจํค์ง ํ์
์คํ์์ค: GitHub์์ ๋ค๋ฅธ ์ฌ๋๋ค์ ์ฝ๋ ์ฝ์ด๋ณด๊ธฐ
๋ฉํ ๋ง: ์ฌ๋ฅ๋ท์์ ๊ฒฝํ ๋ง์ ๊ฐ๋ฐ์์๊ฒ ๋ฐฐ์ฐ๊ธฐ
๊ฐ์ฅ ์ค์ํ ๊ฑด ์ง์ ๋ง๋ค์ด๋ณด๋ ๊ฑฐ์ผ. ์์ ํ๋ก์ ํธ๋ถํฐ ์์ํด์ ์ ์ ๋ณต์กํ ๊ฑธ ๋ง๋ค์ด๋ด!
๐ฌ ๋ง๋ฌด๋ฆฌํ๋ฉฐ
์, ์ฌ๊ธฐ๊น์ง ์ฝ์๋ค๋ ์ ๋ง ๋๋จํด! ๐ Flutter์ ๋ํด ์ ๋ง ๋ง์ ๊ฑธ ๋ค๋ค์ง? ๊ธฐ์ด๋ถํฐ ์ค์ ํ๋ก์ ํธ, ๋ฐฐํฌ๊น์ง. ์ด์ ๋๋ Flutter๋ก ๋ฉ์ง ์ฑ์ ๋ง๋ค ์ค๋น๊ฐ ๋์ด!
๊ธฐ์ตํด, ์ฒ์์๋ ๋๊ตฌ๋ ์ด๋ ค์. ๋๋ ์ฒ์ Flutter๋ฅผ ์์ํ์ ๋ "์ด๊ฒ ๋ญ์ผ?" ํ๋ฉด์ ํค๋งธ๊ฑฐ๋ . ํ์ง๋ง ๊ณ์ ๋ง๋ค๋ค ๋ณด๋ฉด ์ด๋์ ์ต์ํด์ ธ ์์ ๊ฑฐ์ผ. ์๋ฌ๊ฐ ๋๋ ๋นํฉํ์ง ๋ง๊ณ , ๊ตฌ๊ธ๋งํ๊ณ , ๊ณต์ ๋ฌธ์ ์ฝ๊ณ , ์ปค๋ฎค๋ํฐ์ ๋ฌผ์ด๋ด. ๊ฐ๋ฐ์๋ค์ ๋ค๋ค ์น์ ํด! ๐
Flutter์ ๊ฐ์ฅ ํฐ ์ฅ์ ์ ๋น ๋ฅธ ๊ฐ๋ฐ ์๋์ ํฌ๋ก์คํ๋ซํผ ์ง์์ด์ผ. ํ ๋ฒ์ ์ฝ๋ฉ์ผ๋ก iOS, Android, ์น, ๋ฐ์คํฌํฑ๊น์ง ๋ชจ๋ ์ปค๋ฒํ ์ ์๋ค๋ ๊ฑด ์ ๋ง ๊ฐ๋ ฅํ ๋ฌด๊ธฐ์ง. ํนํ ์คํํธ์
์ด๋ ๊ฐ์ธ ๊ฐ๋ฐ์์๊ฒ๋ ๊ฒ์ ์ฒด์ธ์ ์ผ!
์์ผ๋ก Flutter๋ ๋์ฑ ๋ฐ์ ํ ๊ฑฐ์ผ. Google์ ์ ํญ์ ์ธ ์ง์๊ณผ ํ๋ฐํ ์ปค๋ฎค๋ํฐ๊ฐ ์์ผ๋๊น. ์ง๊ธ Flutter๋ฅผ ๋ฐฐ์ฐ๋ ๊ฑด ์ ๋ง ์ข์ ํฌ์์ผ. ๋ชจ๋ฐ์ผ ์ฑ ๊ฐ๋ฐ์ ๋ฏธ๋๋ผ๊ณ ํด๋ ๊ณผ์ธ์ด ์๋๊ฑฐ๋ .
ํน์ ๋งํ๋ ๋ถ๋ถ์ด ์๊ฑฐ๋ ๋ ๊น์ด ๋ฐฐ์ฐ๊ณ ์ถ๋ค๋ฉด, ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ์ ๋ฌธ๊ฐ์ ๋์์ ๋ฐ๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์ผ. ํผ์ ์ฝ์งํ๋ ์๊ฐ์ ์ค์ด๊ณ , ์ค๋ฌด ๋
ธํ์ฐ๋ฅผ ๋ฐฐ์ธ ์ ์๊ฑฐ๋ !
์, ์ด์ VS Code๋ Android Studio๋ฅผ ์ด๊ณ ์ฒซ Flutter ํ๋ก์ ํธ๋ฅผ ์์ํด๋ด! ์์ ๊ฒ๋ถํฐ ์์ํด์ ์ ์ ํค์๋๊ฐ๋ฉด ๋ผ. ํ ์ผ ์ฑ, ๋ ์จ ์ฑ, ๊ฐ๋จํ ๊ฒ์... ๋ญ๋ ์ข์. ์ค์ํ ๊ฑด ์ง์ ๋ง๋ค์ด๋ณด๋ ๊ฒ์ด์ผ!
Flutter๋ก ๋ฉ์ง ์ฑ ๋ง๋ค์ด์ ์ฑ์คํ ์ด์ ์ฌ๋ฆฌ๋ ๊ทธ๋ ์ ๊ธฐ๋ํ ๊ฒ! ํ์ดํ
! ๐โจ
๐ฏ ํต์ฌ ์์ฝ
โ
Flutter๋ ๊ตฌ๊ธ์ด ๋ง๋ ํฌ๋ก์คํ๋ซํผ UI ํ๋ ์์ํฌ
โ
Dart ์ธ์ด ์ฌ์ฉ, ๋ฐฐ์ฐ๊ธฐ ์ฌ์
โ
Hot Reload๋ก ์ด๊ณ ์ ๊ฐ๋ฐ ๊ฐ๋ฅ
โ
ํ ๋ฒ์ ์ฝ๋ฉ์ผ๋ก iOS, Android, ์น, ๋ฐ์คํฌํฑ ๋ชจ๋ ์ง์
โ
์์ฒด ๋ ๋๋ง ์์ง์ผ๋ก ์ผ๊ด๋ UI ์ ๊ณต
โ
ํ๋ถํ ์์ ฏ๊ณผ ํจํค์ง ์ํ๊ณ
โ
ํ๋ฐํ ์ปค๋ฎค๋ํฐ์ ์ง์์ ์ธ ๋ฐ์
โ
๊ฐ๋ฐ ๋น์ฉ๊ณผ ์๊ฐ์ ํฌ๊ฒ ์ ์ฝ
โ
์ทจ์
์์ฅ์์ ์์ ์ฆ๊ฐ ์ค
โ
์ง๊ธ ์์ํ๊ธฐ ๋ฑ ์ข์ ํ์ด๋ฐ!
๐ ํ๋ณตํ ์ฝ๋ฉ ๋์ธ์! Happy Fluttering! ๐
"The best way to predict the future is to create it."
- ๋ฏธ๋๋ฅผ ์์ธกํ๋ ๊ฐ์ฅ ์ข์ ๋ฐฉ๋ฒ์ ์ง์ ๋ง๋๋ ๊ฒ์ด๋ค -
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.