📝최근 Trend 기술 Ranking
OSS Insight
https://ossinsight.io/collections/static-site-generator
📝문자 대행 서비스
Twilio
📝메일 대행 서비스
sendgrid
📝이미지 클라우드 서비스
cloudflare
📝Flutter 화면 디자인 코드로 변환
FlutterFlow
OSS Insight
https://ossinsight.io/collections/static-site-generator
Twilio
sendgrid
cloudflare
FlutterFlow
import 'dart:convert'; // JSON 디코딩을 위해 필요
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http; // HTTP 패키지
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('HTTP API Example with FutureBuilder'),
),
body: PostScreen(),
),
);
}
}
class PostScreen extends StatelessWidget {
// 데이터를 가져오는 비동기 함수
Future<Map<String, dynamic>> fetchPost() async {
final response =
await http.get(Uri.parse('https://jsonplaceholder.typicode.com/posts/1'));
if (response.statusCode == 200) {
// API 응답이 성공적이면 데이터를 파싱합니다.
return json.decode(response.body);
} else {
// 응답이 실패하면 에러 처리
throw Exception('Failed to load post');
}
}
@override
Widget build(BuildContext context) {
return FutureBuilder<Map<String, dynamic>>(
future: fetchPost(), // 비동기 데이터를 가져오는 Future
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
// 데이터가 로딩 중일 때
return Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
// 에러가 발생한 경우
return Center(child: Text('Failed to load data'));
} else if (snapshot.hasData) {
// 데이터를 성공적으로 가져온 경우
final postData = snapshot.data!;
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Title: ${postData['title']}',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
SizedBox(height: 16),
Text(
'Body: ${postData['body']}',
style: TextStyle(fontSize: 16),
),
],
),
);
} else {
// 데이터를 못 가져온 경우 대비
return Center(child: Text('No data found'));
}
},
);
}
}
FutureBuilder를 통해 데이터를 받아서 화면에 출력할 수 있습니다. snapshot을 이용해 데이터 로딩 등을 처리할 수 있습니다.
final TextEditingController _usernameController = TextEditingController();
// 메모리 누수 잡기 [필수]
@override
void dispose() {
_usernameController.dispose(); // 컨트롤러 dispose를 제때 사용 안하면 메모리 누수 발생
super.dispose(); // 컨트롤러 관한 걸 제거한 후에 상위 것들도 dispose 한다 [ 일반적으로 이렇게 많이 사용함]
}
@override
void initState() {
super.initState();
// 텍스트 쓸때마다 감지한다 [ 상태관리 비슷한 개념 ]
_usernameController.addListener(() {
setState(() {
_username = _usernameController.text; // 텍스트 필드의 값을 가져온다
});
});
}
TextField(
controller: _usernameController, // 컨트롤러
...
)
// main.dart
final preferences = await SharedPreferences.getInstance();
final repository = PlaybackConfigRepository(preferences);
runApp(MultiProvider(
providers: [ // 프로바이더에 쓰일 것들...
ChangeNotifierProvider( // 프로바이더 지원 함수 [데이터 변경 감지 및 데이터 관리 해줌 + UI업데이트 등]
create: (context) => PlaybackConfigViewModel(repository), // 해당 뷰모델을 프로바이더로 쓰겠다
)
],
child: const TikTokApp(),
));
// ViewModel 선언
...
class PlaybackConfigViewModel extends ChangeNotifier
// View에서 ViewModel을 통해 로직 실행및 가져오기
SwitchListTile.adaptive(
value: context.watch<PlaybackConfigViewModel>().muted,
onChanged: (value) =>
context.read<PlaybackConfigViewModel>().setMuted(value),
title: const Text("Mute video"),
subtitle: const Text("Video will be muted by default."),
),
MVVM 기반으로 개발하며 Proivder를 이용해 데이터를 관리할 수 있다. main에서 provider를 사용하고 viewmodel을 연결해서 감지시키고 모든 곳에서 접근해서 사용이 가능하다. Riverpod이 더 많은 기능을 제공하고 더 잘 분리되어있다.
Animation 효과 줄 수 있는 위젯 (setState 필수) [비추] → 전체 랜더링 비효율적
void _onTap() {
setState(() {
_isSelected = !_isSelected;
});
}
GestureDetector(
onTap: _onTap,
child: AnimatedContainer( // 애니메이션 위젯 컨테이너
duration: const Duration(milliseconds: 300), // 실행 시간
padding: const EdgeInsets.symmetric( // 컨테이너 패딩
vertical: Sizes.size16,
horizontal: Sizes.size24,
),
decoration: BoxDecoration( // 컨테이너 꾸미기
color: _isSelected
? Theme.of(context).primaryColor
: isDarkMode(context)
? Colors.grey.shade700
: Colors.white,
borderRadius: BorderRadius.circular(
Sizes.size32,
),
border: Border.all(
color: Colors.black.withOpacity(0.1),
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 5,
spreadRadius: 5,
),
],
),
child: Text( // 컨테이너 안에 들어갈 텍스트
widget.interest,
style: TextStyle(
fontWeight: FontWeight.bold,
color: _isSelected ? Colors.white : Colors.black87),
),
),
)
Animation 효과 줄 수 있는 위젯 (부분 랜더링 가능) [추천]
class _VideoPostState extends State<VideoPost>
with SingleTickerProviderStateMixin {
_animationController = AnimationController(
vsync: this, // 프레임마다 SingleTickerProviderStateMixin의 ticker가 상태를 체크해준다
lowerBound: 1.0, // 작아지는 배율
upperBound: 1.5, // 커지는 배율
value: 1.5, // 초기 크기 (upperBound를 넘지 못한다)
duration: _animationDuration,
);
void _onTogglePause() {
if (_videoPlayerController.value.isPlaying) {
_animationController.reverse(); // 애니메이션 역진행 만약 lower Bounce 값으로 애니메이션 실행
} else {
_animationController.forward(); // 애니메이션 정상진행 만약 upper Bounce 값으로 애니메이션 실행
}
setState(() {
_isPaused = !_isPaused;
});
}
AnimatedBuilder
animation: _animationController, // 컨트롤러 매핑을 이용해 부분 렌더링
builder: (context, child) {
return Transform.scale(
scale: _animationController.value,
child: child, // 밑에 child값을 실행시킨다
);
},
child: AnimatedOpacity(
opacity: _isPaused ? 1 : 0,
duration: _animationDuration,
child: const FaIcon(
FontAwesomeIcons.play,
color: Colors.white,
size: Sizes.size52,
),
),
),
}
import 'package:flutter/material.dart';
// main method
void main() {
runApp(App());
}
// Root (whatever you want, you draw it here)
class App extends StatelessWidget {
// build method hepls your UI render
@override
Widget build(BuildContext context) {
return MaterialApp(
);
}
}
Flutter를 이용해 정적인 화면을 시뮬레이터 화면을 띄우려면 StatelessWidget을 상속받고 build라는 함수를 구현해야한다.
Flutter의 실행과정을 간단히 이야기하자면 main.dart 페이지에서 실행이 되며 runApp을 통해 앱이 실행된다.
const CurrencyCard(
name: 'Euro',
code: 'EUR',
amount: '6 428',
icon: Icons.euro_rounded,
isInverted: false,
),
/** -------- Currecy Card --------- **/
class CurrencyCard extends StatelessWidget {
final String name, code, amount;
final IconData icon;
final bool isInverted;
final _blackColor = const Color(0xFF1F2123);
const CurrencyCard({
super.key,
required this.name,
required this.code,
required this.amount,
required this.icon,
required this.isInverted,
});
@override
Widget build(BuildContext context) {
return Container( ....);
}
}
생성자를 만들어서 나만의 위젯을 만들어 재활용 할 수 있다.
import 'package:flutter/material.dart';
void main() {
runApp(App());
}
class App extends StatefulWidget {
const App({super.key});
@override
State<App> createState() => _AppState();
}
class _AppState extends State<App> {
int counter = 0;
void onClicked() {
setState(() {
counter = counter + 1;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
backgroundColor: const Color(0xFFF4EDDB),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Click Count',
style: TextStyle(fontSize: 30),
),
Text(
'$counter',
style: const TextStyle(fontSize: 30),
),
IconButton(
iconSize: 40,
onPressed: onClicked,
icon: const Icon(
Icons.add_box_rounded,
),
),
],
),
),
),
);
}
}
데이터가 동적으로 변해야 할 때 쓴다. 예를 들자면 "클릭시 카운트가 올라간다."가 이에 해당한다. 동적페이지를 만들기 위해서는 StatefulWidget을 상속받아 사용해야한다.
StatefulWidget을 사용할 때 주의점들은 아래와 같다.
동작 방식
대부분의 경우는 key를 사용하지 않는다. 하지만 상태를 유지하고 있는 같은 종류의 위젯을 컬렉션에 더하거나, 제거하거나, 정렬할 때 key가 필요하다. 예를 들면 ToDoList를 보면 List안에 내용들은 똑같은 내용 즉, 똑같은 위젯을 재활용한 것이기 때문에 이것들을 관리하기 위해서는 key가 필요합니다. 이건 동적인 데이터이기 때문에 key가 쓰이는 경우는 StatefulWidget에 해당합니다.
자세한 내용은 아래 블로그를 참고하시길 바랍니다.
Key는 LocalKey와 GlobalKey 두개의 종류가 있습니다.
LocalKey
Widget 안에서 사용되는 유니크한 키입니다. 다양한 LocalKey 종류가 있는데 특징은 아래와 같습니다.
GlobalKey
전체 위젯 트리에서 고유한 전역적인 식별자로 어디에서나 전역적으로 해당 위젯을 식별하고 액세스할 수 있다.
StatefulWidget을 사용하는 위젯에서 상태를 관리하거나 위젯 트리 외부에서 상태에 액세스해야 할 때 사용됩니다.
GlobalKey 사용되는 곳
final key = GlobalKey<MyWidgetState>();
MyWidget(key: key);
key.currentState.someMethod();
final formKey = GlobalKey<FormState>();
if (formKey.currentState.validate()) {
formKey.currentState.save();
// ... 폼 저장 처리
}
import 'package:flutter/material.dart';
void main() {
runApp(App());
}
class App extends StatefulWidget {
@override
State<App> createState() => _AppState();
}
class _AppState extends State<App> {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
textTheme: const TextTheme(
titleLarge: TextStyle(
color: Colors.red,
),
),
),
home: const Scaffold(
backgroundColor: Color(0xFFF4EDDB),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
MyLargeTitle(),
],
),
),
),
);
}
}
class MyLargeTitle extends StatelessWidget {
const MyLargeTitle({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Text(
'My Large Title',
style: TextStyle(
fontSize: 30,
color: Theme.of(context).textTheme.titleLarge?.color,
),
);
}
}
하위 위젯인 MyLargeTitle에서 상위에 있는 위젯을 사용하려면 상위 위젯에 대한 문맥(context)를 알아야하는데 BuildContext가 그러한 역할을 해준다. 그래서 Theme.of(context)를 이용해 context에 있는 Theme을 가져오고 그 안에 textTheme 그 안에 titleLarge를 가져오면 된다.
Widget이란 앱에 있는 요소를 의미한다 (버튼, 글자, 스크롤 박스 등…)
어떤 위젯이 부모의 크기를 비율로 가져가는 경우 부모의 크기가 이론상 무한대로 확장 가능할 때 제한해야한다 예를 들면 Row가 부모 위젯인 경우 Width가 이론상 무한이고 Column의 경우 Height가 이론상 무한이기 때문에 이 두개가 있을 경우 Expanded따위로 제한해야한다 참고로 Row 안에 Column 안에 있는 위젯의 경우 둘다 Expanded로 제한해줘야한다
// Size 예시 [Gaps, FontSize, Color 등…]
class Gaps {
static const v1 = SizedBox(height: 1);
...
}
Size 표가 있으면 통일성이 생긴다
포커스 해제하기 (다른 곳 터치 시 키보드 내리기)
Widget build(BuildContext context) {
return GestureDetector(
onTap: _onScaffoldTap,
...
)
)
void _onScaffoldTap() {
FocusManager.instance.primaryFocus?.unfocus(); // 현재 포커스 제거
}
다크모드 설정
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:tiktok_clone/constants/sizes.dart';
import 'package:tiktok_clone/features/authentication/sign_up_screen.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await SystemChrome.setPreferredOrientations(
[
DeviceOrientation.portraitUp,
],
);
runApp(const TikTokApp());
}
class TikTokApp extends StatelessWidget {
const TikTokApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'TikTok Clone',
themeMode: ThemeMode.system, // 시스템에 따라 다르다
// themeMode: ThemeMode.dark, // 다크모드 강제
// themeMode: ThemeMode.light, // 라이트모드 강제
theme: ThemeData(
// 라이트 모드일 때 사용
brightness: Brightness.light,
scaffoldBackgroundColor: Colors.white,
primaryColor: const Color(0xFFE9435A),
textSelectionTheme: const TextSelectionThemeData(
cursorColor: Color(0xFFE9435A),
),
splashColor: Colors.transparent,
appBarTheme: const AppBarTheme(
foregroundColor: Colors.black,
backgroundColor: Colors.white,
elevation: 0,
titleTextStyle: TextStyle(
color: Colors.black,
fontSize: Sizes.size16 + Sizes.size2,
fontWeight: FontWeight.w600,
),
),
tabBarTheme: TabBarTheme( // 탭바에서 사용하는 다크 모드 내용들
labelColor: Colors.black, // 선택 글자
unselectedLabelColor: Colors.grey.shade500, // 노 선택 글자
indicatorColor: Colors.black, // 아래 글자
),
),
darkTheme: ThemeData(
// 다크모드일 때 설정
brightness: Brightness.dark, // 글자색상??
scaffoldBackgroundColor: Colors.black, // scafoold 색상
bottomAppBarTheme: BottomAppBarTheme(
// 바텀앱바 색상
color: Colors.grey.shade900,
),
primaryColor: const Color(0xFFE9435A),
),
home: const SignUpScreen(),
);
}
}
회전 막기
void main() async {
WidgetsFlutterBinding.ensureInitialized(); // 플러터의 화면이 나오기전에 아래 설정을 사용하겠습니다라는 뜻
await SystemChrome.setPreferredOrientations(
[
DeviceOrientation.portraitUp, // 핸드폰 회전 막기
],
);
runApp(const TikTokApp());
}
Text Theme
// 방법1 - 직접 Theme 정의하기
theme: ThemeData(
// 라이트 모드일 때 사용
brightness: Brightness.light,
textTheme: TextTheme(
// <https://m2.material.io/design/typography/the-type-system.html#type-scale> 해당 사이트에서 폰트에 따른 크기를
// css 또는 flutter전용 textTheme형식을 폰트체만 정해주면 코드를 자동 생성해준다 (Material2)
// textTheme에서 제공하는 것들 displayLarge(이름만 제공)
displayLarge: GoogleFonts.openSans(
fontSize: 95, fontWeight: FontWeight.w300, letterSpacing: -1.5),
displayMedium: GoogleFonts.openSans(
fontSize: 59, fontWeight: FontWeight.w300, letterSpacing: -0.5),
displaySmall:
GoogleFonts.openSans(fontSize: 48, fontWeight: FontWeight.w400),
headlineMedium: GoogleFonts.openSans(
fontSize: 34, fontWeight: FontWeight.w400, letterSpacing: 0.25),
headlineSmall:
GoogleFonts.openSans(fontSize: 24, fontWeight: FontWeight.w400),
titleLarge: GoogleFonts.openSans(
fontSize: 20, fontWeight: FontWeight.w500, letterSpacing: 0.15),
titleMedium: GoogleFonts.openSans(
fontSize: 16, fontWeight: FontWeight.w400, letterSpacing: 0.15),
titleSmall: GoogleFonts.openSans(
fontSize: 14, fontWeight: FontWeight.w500, letterSpacing: 0.1),
bodyLarge: GoogleFonts.roboto(
fontSize: 16, fontWeight: FontWeight.w400, letterSpacing: 0.5),
bodyMedium: GoogleFonts.roboto(
fontSize: 14, fontWeight: FontWeight.w400, letterSpacing: 0.25),
labelLarge: GoogleFonts.roboto(
fontSize: 14, fontWeight: FontWeight.w500, letterSpacing: 1.25),
bodySmall: GoogleFonts.roboto(
fontSize: 12, fontWeight: FontWeight.w400, letterSpacing: 0.4),
labelSmall: GoogleFonts.roboto(
fontSize: 10, fontWeight: FontWeight.w400, letterSpacing: 1.5),
),
listTileTheme: const ListTileThemeData(
iconColor: Colors.black,
),
scaffoldBackgroundColor: Colors.white,
primaryColor: const Color(0xFFE9435A),
textSelectionTheme: const TextSelectionThemeData(
cursorColor: Color(0xFFE9435A),
),
splashColor: Colors.transparent,
appBarTheme: const AppBarTheme(
foregroundColor: Colors.black,
backgroundColor: Colors.white,
elevation: 0,
titleTextStyle: TextStyle(
color: Colors.black,
fontSize: Sizes.size16 + Sizes.size2,
fontWeight: FontWeight.w600,
),
),
),
// 방법2 - 만들어져있는 폰트 Theme 가져오기
// GoogleFonts의 itim이라는 폰트체 내용 전체 가져오기
textTheme: GoogleFonts.itimTextTheme(
ThemeData(brightness: Brightness.dark).textTheme, // 다크모드 테마 가져오기
),
// 상세가 먼저 적용되기 때문에 부분적으로 적용시켜도 된다
Text(
"Sign up for TikTok",
style: GoogleFonts.abrilFatface(
textStyle: const TextStyle(
fontSize: Sizes.size24,
fontWeight: FontWeight.w700,
),
),
)
// theme 데이터 가져오기 예제
style: Theme.of(context)
.textTheme
.headlineSmall!
.copyWith(color: Colors.red), // Theme에서 가져오지만 변형이 필요하면 copyWith을 사용한다
// 방법3 - Typography 사용 (Flutter 자체 내제된 Font Style)
theme: ThemeData(
textTheme: Typography.blackMountainView,
...
darkTheme: ThemeData(
textTheme: Typography.whiteMountainView, // ?
...
// 일부 항목 커스텀 가능
static const TextTheme blackMountainView = TextTheme(
displayLarge: TextStyle(debugLabel: 'blackMountainView displayLarge', fontFamily: 'Roboto', color: Colors.black54, decoration: TextDecoration.none),
displayMedium: TextStyle(debugLabel: 'blackMountainView displayMedium', fontFamily: 'Roboto', color: Colors.black54, decoration: TextDecoration.none),
displaySmall: TextStyle(debugLabel: 'blackMountainView displaySmall', fontFamily: 'Roboto', color: Colors.black54, decoration: TextDecoration.none),
...
) // 예시
// 색상지정 및 투명도
Colors.white.withOpacity(0.5)
Colors.white
Color(0xFF181818)
// 폰트 두께
FontWeight.w800
// 보더형태
BorderRadius.circular(25)
// 위젯 경계 넘어갈시 자름
clipBehavior: Clip.hardEdge
// 좌우 패딩
EdgeInsets.symmetric(horizontal: 20,)
// Column에서 가로 정렬, Row에서 세로 정렬
crossAxisAlignment: CrossAxisAlignment.start
crossAxisAlignment: CrossAxisAlignment.end
crossAxisAlignment: CrossAxisAlignment.spaceBetween
// Column에서 수직 정렬, Row에서 가로 정렬
mainAxisAlignment: MainAxisAlignment.start
mainAxisAlignment: MainAxisAlignment.end
mainAxisAlignment: MainAxisAlignment.spaceBetween
Widget 명 역할 키 : 설명 → 값 사용 예) 활용 예제
Widget명 | 역할 | 키 : 설명 → 값 | 사용 예) | 활용 예제 |
MaterialApp | 구글 기본앱같은 디자인에 쓰인다 → 지원 위젯이 많다 [커스텀디자인을 많이 만들기 때문에 해당 위젯 사용] | home : 위젯 넣을 곳 (일반적 Scaffold 많이 사용) → Widget | MaterialApp(home: Scaffold(..)) | |
아이폰 기본앱같은 디자인에 쓰인다 → 하위 지원 위젯이 별로 없어서 개발하기 힘듬 | ||||
Scaffold | 앱은 대부분 상 / 중 / 하로 나누어져있는데 쉽게 구성하기 위한 위젯 | appBar : 상단에 넣을 위젯 → PreferredSizeWidget body : 중단에 넣을 위젯 -> Widget bottomNavigationBar : 하단에 넣을 위젯 → Widget backgroundColor: 배경색 → Color |
Scaffold(appBar: Padding(..)) | |
Padding | 패딩을 주는 위젯 | padding : 패딩옵션 → EdgeInsetschild : 패딩 안에 들어갈 Widget → Widget | ||
Color | 색을 반환해주는 위젯 | 색 값 → int | Color(0xFF181818) | |
EdgeInsets | 패딩 옵션 위젯 | symmetric → 좌우패딩, 상하패딩 | all → 상하좌우 패딩 | vertical : 상하 패딩 → doublehorizental : 좌우 패딩 → double |
Column | Children 안에 항목을 세로로 배치한다 (이론상 무한정 배치 가능하지만 화면 벗어난다고 에러 발생) [width 제한 : 상위 위젯크기 (없으면 화면 크기), height 제한 : 무한 (화면 벗어날시 에러)]- 화면 세로줄 하나를 다 차지한다 (넓이만 없어) | children : 세로 박스 안에 들어갈 위젯 리스트 → Widget crossAxisAlignment : 수직 정렬 옵션 →CrossAxisAlignment |
children: [const SizedBox(..)] | |
SizedBox | 빈 박스 생성 (Not Div) [마진 주는 용도로 자주 쓰인다] | height : 높이 → int | SizedBox ( height : 80 ) | |
Row | Children 안에 항목을 가로로 배치한다 (이론상 무한정 배치 가능하지만 화면 벗어난다고 에러 발생) [width 제한 : 무한 (화면 벗어날시 에러), height 제한 : 상위 위젯크기 (없으면 화면 크기), ]- 화면 가로줄 하나를 다 차지한다 (높이만 없어) | mainAxisAlignment : 수평 정렬 → MainAxisAlignment children : 가로 박스 안에 들어갈 위젯 리스트 → Widget |
Row(children : [ …]) | |
Text | 텍스트 위젯 | data[생략 가능(필수)] : 텍스트 → String style : 텍스트 스타일 → TextStyle |
Text( 'Hey, Selena', style: TextStyle( ... ), | |
TextStyle | 텍스트 스타일 위젯 | color : 색상 → Color fontSize : 글자크기 → int fontWeight : 폰트 두께 -> FontWeight |
TextStyle( color: Colors.white, fontSize: 28, fontWeight: FontWeight.w800, ), | |
AppBar | 상단 레이아웃 (헤더) 위젯 | title : 헤더에 적을 내용 → String | ||
Center | 수평 및 수직 가운데 정렬된 상태로 정중앙에 위치한다 | child : Center 박스 안에 들어갈 Widget → Widget | Center( child: Text(”hello word”)) | |
SingleChildScrollView | 스크롤 바 위젯 | child : 스크롤바 안에 들어갈 Widget → Widget | ||
Container | child에 있는 위젯 크기만큼 크기가 정해진다 | clipBehavior : 내용물 크기가 커서 넘치는 경우 처리 방식 → Clip decoration : 컨테이너에 설정할 색상 등 옵션 → Decoration |
Container(clipBehavior: Clip.hardEdge, …) | |
BoxDecoration (This is not Widget) | 색상과 둥근 정도 등을 조정한다 | color : 박스 색상 → Color borderRadius : 박스 테두리 둥근 정도 → BorderRadiusGeometry |
BoxDecoration(borderRadius: BorderRadius.circular(25)) | |
Icon | 아이콘 위젯 | icon[생략 가능] : 사용할 아이콘 → IconData color : 아이콘 색상 → Color size : 아이콘 크기 → int |
Icon( icon, color: isInverted ? _blackColor : Colors.white, size: 88, ), | |
Transform.scale | Child에 적은 위젯의 크기 및 색상 등을 조정한다 | scale : 크기 → double child : Scale 조절할 위젯 → Widget |
Transform.scale( scale: 2.2, ... ) | |
Transform.translate | Child에 적은 위젯의 위치를 이동시킨다(scale과 같이 적용시키는 경우 Transform.scale( … child : Transform.translate( .. child : 적용시킬 위젯) 순으로 적용시킨다 | offset : 위치 → Offset child : 회전 시킬 위젯 → Widget |
Transform.translate( offset: const Offset(..)) | |
Offset | 현재 위치 x, y축 이동 위젯 | dx[생략 가능] : 현재 위치에서 x축 이동 → doubledy[생략 가능] : 현재 위치에서 y축 이동 → double | Offset(-5,12) | |
IconButton | 버튼 이벤트가 있는 아이콘 위젯 | icon : 아이콘 종류 → Icon onPressed : 눌렀을 때 동작 시키기 → Function:void iconSize : 아이콘 사이즈 → int |
onPressed: onClicked | |
ThemeData | 공통 스타일을 적용시킬 Data를 설정한다 (이름은 이미 정해져있다 textTheme, focusColor 등..) → 결국 그냥 이름일 뿐이고 반환값만 신경쓰고 문맥에 따른 것만 잘 생각해서 정하면 된다 | textTheme : 텍스트 테마 → TextTheme | textTheme: const TextTheme(...) | |
TextTheme | 텍스트 스타일 적용시킬 Theme을 정한다 이름은 이미 정해져있다 titleLarge 등..) → 결국 그냥 이름일 뿐이고 반환값만 신경쓰고 문맥에 따른 것만 잘 생각해서 정하면 된다 | titleLarge : → TextStyle | textTheme: const TextTheme( titleLarge: TextStyle( color: Colors.red, ), | |
Flexible | Flex박스를 만든다 → 각각의 박스를 비율로 줄 수 있어서 어떤 핸드폰이든 %이기 때문에 깨지지 않는다 | flex : 비율 → int child : FlexBox에 넣을 위젯 → Widget |
Flexible( flex: 1,… | |
Expanded | 사용 가능한 크기만큼 가져갑니다 (최대 화면의 크기) | child : 확장된 Row안에 들어갈에 넣을 위젯 → Widget flex : 나눌 비율 크기 → int |
Expanded( child: Container( | |
리스트의 형태로 데이터를 보여준다 → 많은 데이터 보여줄 때 사용 하지만… 모든 데이터를 가져오기 때문에 메모리 누수 현상 발생 가능성 존재 | ||||
ListView.builder | 리스트 형태로 데이터를 보여주고 화면에 보이는 부분만 렌더링 하기 때문에 메모리 누수 걱정이 없다 | scrollDirection : 리스트뷰 스크롤 방향 → ScrollWidget itemCount : 전체 아이템 수 → int padding : 패딩 옵션 → EdgeInsets itemBuilder : 새로운 인덱스 감지시 itemBuilder함수를 호출해 렌더링할 위젯을 설정한다 추후 스크롤 시 안 보이는 부분은 사라지고 나중에 다시 보일 때 재활용하게 된다 → 일반적으로 itemBuilder : (context, index) 이런형식으로 이용된다 |
||
ListView.seperator | ListView.builder와 동일하며 리스트 사이에 위젯을 넣을 수 있다 | 위와 동일seperatorBuilder : 리스트 사이에 들어갈 위젯 → 일반적으로 speratorBuilder : (BuildContext context, int index) ⇒ 위젯.. | ||
Image.network | 이미지를 가져온다 | |||
CircularProgressIndicator | 로딩바 | X | CircularProgressIndicator() | |
SafeArea | 와이파이, 배터리 등 위에 있는 부분을 제외한 위젯 박스 | child : 모든 위젯 [일반적으로 Column, Container 등..] → Widget | SafeArea(child: Column...) | |
FractionallySizedBox | 부모 위젯의 전체 크기를 비율로 가져가는 위젯 박스 | widthFactor : 0.5 → double heightFactor : 0.5 → double child : 비율로 가져가는 하위 위젯 → Widget |
FractionallySizedBox(widthFactor:0.5, heightFactor:0.5, child: Column…) | |
Positioned.fill | 화면을 꽉채울 수 있는 위젯 | child : 화면에 꽉 채울 위젯 → Widget | Positioned.fill(child:…) | |
Stack | 화면 위에 쌓을 수 있는 위젯 (absolute처럼 위젯 위에 올릴 수 있다) | children : 쌓을 위젯들 → Widget[] | Stack(children: […]) | 박스 안에 [아이콘 |
Positioned | 위젯을 상하좌우 값을 줘서 위치시킬 수 있다 | left : left 위젯 위치 값 → int right : right 위젯 위치 값 → int top : top 위젯 위치 값 → int bottom : bottom 위젯 위치 값 → int |
Positioned(left:20,top:40,child:...) | |
Wrap | Wrap은 가로로 Children Widget들을 채우는데 더 이상 못 채우면 알아서 줄바꿈이 된다 | runSpacing : 줄 간격 → int spacing : 요소들 사이 가로 간격 → int children : 적용시킬 위젯들 → Widget[] |
Wrap(runSpacing: 15, spacing: 15, children: ...) | |
TextField | 인풋박스를 제공해준다. 유효성 검사 등 다양한 기능 제공 | https://api.flutter.dev/flutter/material/TextField-class.html | ||
TabBar | 앱 하단에 3개나 5개 아이콘 등에 쓰이는 Tab들을 의미한다. | https://api.flutter.dev/flutter/material/TabBar-class.html | ||
TabPageSelector | 슬라이드해서 페이지를 넘어가는 탭들의 경우 현재 어떤 탭 위치에 있는지 알려주는 역할을 한다. | https://api.flutter.dev/flutter/material/TabPageSelector-class.html | ||
InputDecoration | TextField 또는 TextFormField와 같은 입력 필드를 꾸미는데 사용되는 위젯입니다. | https://api.flutter.dev/flutter/material/InputDecoration-class.html | ||
UnderlineInputBorder | TextField 또는 TextFormField의 밑줄 스타일로 설정할 때 사용됩니다. | https://api.flutter.dev/flutter/material/UnderlineInputBorder-class.html | ||
BorderSide | 경계선(border)의 색상, 두께 및 스타일을 정의하는 위젯입니다. | https://api.flutter.dev/flutter/painting/BorderSide-class.html | ||
TabBarView | 여러 페이지를 탭 전환을 통해 스와이프할 수 있는 뷰를 제공. | https://api.flutter.dev/flutter/material/TabBarView-class.html | ||
AspectRatio | 자식 위젯의 너비와 높이 비율을 일정하게 유지시켜주는 위젯입니다 | https://api.flutter.dev/flutter/widgets/AspectRatio-class.html | ||
TextFormField | TextField와 비슷하지만 Form 위젯과 함께 사용됩니다. | https://api.flutter.dev/flutter/material/TextFormField-class.html | ||
NavigationDestination | 네비게이션에 보여줄 항목을 정의해주는 위젯입니다. | https://api.flutter.dev/flutter/material/NavigationDestination-class.html | ||
PageView | 수평 또는 수직으로 페이지를 스와이프할 수 있게 해주는 위젯입니다. | https://api.flutter.dev/flutter/widgets/PageView-class.html | ||
FadeInImage.assetNetwork | 이미지가 로드되는 동안 보여줄 Placeholder 이미지입니다. (로딩개념) | https://api.flutter.dev/flutter/widgets/FadeInImage/FadeInImage.assetNetwork.html | ||
NavigationBar | 3.0에서 새롭게 도입된 신개념으로 BottomNavigationBar보다 선호된다. TabBar의 경우 하단이 아니여도 붙일 수 있지만 NavigationBar의 경우는 하단에 붙는다. | https://api.flutter.dev/flutter/material/NavigationBar-class.html | ||
ListTile | List형식인데 알림으로 오는 형식처럼 앞뒤에 붙여서 리스트를 쉽게 만들수 있게 도와준다. | https://api.flutter.dev/flutter/material/ListTile-class.html | ||
Dismissible | List형식인데 왼쪽이나 오른쪽 슬라이드로 삭제하거나 할 수 있는 기능이다. | https://api.flutter.dev/flutter/widgets/Dismissible-class.html | ||
GoRoute | 라우팅, 중첩라우팅기능, 애니메이션전환 등을 제공한다. | https://pub.dev/packages/go_router | ||
VerticalDivider | 위젯들 사이에 수직선을 넣어 구분할 수 있는 위젯 | https://api.flutter.dev/flutter/material/VerticalDivider-class.html | ||
RichText | 위젯을 배열로 가지기 때문에 TextSpan위젯을 사용시 첫글자는 다른 스타일 중간 글자는 다른 스타일을 넣기가 편하다 | https://api.flutter.dev/flutter/widgets/RichText-class.html | ||
DefaultTextStyle | 텍스트 위젯을 여러 개 써야할 때 최상단 스타일 고정하고 여러 항목을 받을 수 있다. | https://api.flutter.dev/flutter/widgets/DefaultTextStyle-class.html | ||
NestedScrollView | 상단의 헤더 영역과 본문 영역이 서로 독립적인 스크롤을 처리하면서도, 상호작용하는 방식으로 동작하는 게 특징입니다. 예를 들어, 상단의 헤더가 먼저 스크롤되고, 그 이후에 본문 영역이 스크롤되는 방식입니다. | https://api.flutter.dev/flutter/widgets/NestedScrollView-class.html |
모달창
// 모달창 (Yes / No) iOS
onTap: () {
showCupertinoDialog(
context: context,
builder: (context) => CupertinoAlertDialog(
// iOS 모달창
title: const Text("Are you sure?"),
content: const Text("Plx dont go"),
actions: [
CupertinoDialogAction(
// iOS 모달 No 버튼
onPressed: () => Navigator.of(context).pop(),
child: const Text("No"),
),
CupertinoDialogAction(
// iOS 모달 Yes 버튼
onPressed: () => Navigator.of(context).pop(),
isDestructiveAction: true,
child: const Text("Yes"),
),
],
),
);
},
// 모달창 (Yes / No) AOS
onTap: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
// AOS 모달창
icon: const FaIcon(FontAwesomeIcons.skull),
title: const Text("Are you sure?"), // 제목 내용
content: const Text("Plx dont go"), // 제목 안에 내용
actions: [
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const FaIcon(FontAwesomeIcons.car),
), // 왼쪽
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text("Yes"),
), // 오른쪽
],
),
);
}
// 모달창 하단형 (iOS)
onTap: () {
showCupertinoModalPopup(
// 모달창 하단형
context: context,
builder: (context) => CupertinoActionSheet(
// 모달창 하단형에 들어갈 내용
title: const Text("Are you sure?"),
message: const Text("Please dooooont gooooo"),
actions: [
CupertinoActionSheetAction(
isDefaultAction: true, // OK (파란글씨)
onPressed: () => Navigator.of(context).pop(),
child: const Text("Not log out"),
),
CupertinoActionSheetAction(
isDestructiveAction: true, // 취소 (빨간 글씨)
onPressed: () => Navigator.of(context).pop(),
child: const Text("Yes plz."), // 들어갈 문구
)
],
),
);
},
스크롤 (sliver)
import 'package:flutter/material.dart';
class UserProfileScreen extends StatefulWidget {
const UserProfileScreen({super.key});
@override
State createState() => _UserProfileScreenState();
}
class _UserProfileScreenState extends State {
@override
Widget build(BuildContext context) {
return CustomScrollView(
slivers: [ // 앱의 스크롤에따라 모양이 변하는 것들을 말한다
// <https://api.flutter.dev/flutter/material/SliverAppBar-class.html> 샌드박스 화면으로 보면 이해가 쉽다.
SliverAppBar(
snap:
true, // floating가 ture일 때 동작하며 SliverFixedExtentList에 리스트들을 살짝 위로 스크롤하면 서서히 나오는게 아니라 한번에 짠 하고 나온다
floating:
true, // SliverFixedExtentList에 있는 리스트를 위로 스크롤할 때 올릴 때 해당 앱바가 출현한다(끝까지 올리지 않는 이상 안 나옴 일반적)
stretch: true, // 해당 앱바를 아래로 당길 수 있냐 (그래서 늘어나냐 안 늘어나냐)
pinned: true, // 내렸을 때 앱바를 고정시킬지 여부
backgroundColor: Colors.teal, // 위로 당겼을 때 보여줄 것
collapsedHeight: 80, // 높이 (언제부터 위에 bacground color teal을 실행시킬거냐에 대한)
expandedHeight: 200
flexibleSpace: FlexibleSpaceBar(
stretchModes: const [
StretchMode.blurBackground, // -> 아래로 당길시 뿌얘짐
StretchMode.zoomBackground, // 밑에 선언한 백그라운드를 중심으로 당겨진다
StretchMode.fadeTitle, // Title이 뿌얘진다
],
background: Image.asset(
"assets/images/placeholder.jpg",
fit: BoxFit.cover,
),
title: const Text("Hello!"),
),
),
// 스크롤이 가능한 위젯 생성
const SliverToBoxAdapter(
child: Column(
children: [
CircleAvatar(
backgroundColor: Colors.red,
radius: 20,
),
],
),
),
// 스크롤이 가능한 리스트 위젯 생성
SliverFixedExtentList(
// Body값 (List)
delegate: SliverChildBuilderDelegate(
childCount: 50,
(context, index) => Container(
color: Colors.amber[100 * (index % 9)],
child: Align(
alignment: Alignment.center,
child: Text("Item $index"),
),
),
),
itemExtent: 100, // 높이 차이
),
// SliverAppBar에 비해 사용자 정의가 더 자유롭다.
SliverPersistentHeader(
delegate: CustomDelegate(),
pinned: true,
floating: true,
),
// 스크롤이 가능한 그리드 위젯 생성
SliverGrid(
delegate: SliverChildBuilderDelegate(
childCount: 50,
(context, index) => Container(
color: Colors.blue[100 * (index % 9)],
child: Align(
alignment: Alignment.center,
child: Text("Item $index"),
),
),
),
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 50, // 정사각형 크기
mainAxisSpacing: Sizes.size20, // 세로 마진
crossAxisSpacing: Sizes.size20, // 가로 마진
childAspectRatio: 1, // 정사각형들 비율
),
)
],
);
}
}
// SliverPersistentHeader에 사용하기 위해 따로 정의 필요
class CustomDelegate extends SliverPersistentHeaderDelegate {
@override
Widget build(
BuildContext context,
double shrinkOffset,
bool overlapsContent,
) {
return Container(
color: Colors.indigo,
child: const FractionallySizedBox(
heightFactor: 1, // 차지하는 높이 퍼센티지
child: Center(
child: Text(
'Title!!!!!',
style: TextStyle(
color: Colors.white,
),
),
),
),
);
}
@override
double get maxExtent => 150; // 높이
@override
double get minExtent => 120; // pin사용시 유지되는 높이 영역
@override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
return false; // 필요 시 다시 빌드 여부
}
}
Sliver는 Scroll안에 일반적으로 사용되며 스크롤함에 따라 보여지는 게 다른 걸 의미한다.
에러 위젯
void showFirebaseErrorSnack(
// 하단에 에러 메세지 알림 처럼 튀어나옴
BuildContext context,
Object? error,
) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
showCloseIcon: true, // 닫기 버튼
content: Text( // 보여줄 메시지
(error as FirebaseException).message ?? "Something wen't wrong.",
),
),
);
}
Widget 명 역할 키 : 설명 → 값 사용 예) 활용 예제
Widget 명 | 역할 | 키 : 설명 → 값 | 사용 예) | 활용 예제 |
FutureBuilder | Http통신 후에 데이터 정보를 담는 위젯 | future : Future타입의 데이터 → Future<T> builder : ****future에서 받은 데이터 정보 → AsyncWidgetBuilder -> 일반적으로 (context, snapshot) 사용 |
||
GestureDetector | 동작 이벤트를 제공해준다 | onTap : 탭 눌렀을 때 → funciton onPanUpdate : 드래깅 감지 → function(DragUpdateDetails details) onPanEnd : 드래깅 끝냈을 때 → function(DragEndDetails details) |
- onPageUpdatedetails.delta.dx > 0 → 오른쪽에서 왼쪽으로 스왑 | |
Navigator.push | 데이터를 다른 위젯으로 보내고 Router로 해당 위젯을 띄운다 (뒤로가기가 쌓인다) | 일반적으로 **(context, Router)**받고 위젯을 Router 객체로 변환하기 위해서MaterialPageRoute를 사용한다 | Navigator.of(context).push(MaterialPageRoute(builder: (context) => const LoginScreen(),),); | |
Navigator.pop | 뒤로가기 | Navigator.*of*(context).pop() | ||
MaterialPageRoute | 위젯을 Router형태로 바꾸는 역할 | builder : 어떤 context를 읽을 지 → buildContext fullscreenDialog : 페이지 이동 효과 → boolean |
MaterialPageRoute( builder: (context) => DetailScreen( title: title, thumb: thumb, id: id, ) → [DetailScreen 위젯을 Router로] |
Widget 명 역할 키 : 설명 → 값 사용 예) 활용 예제
Widget 명 | 역할 | 키 : 설명 → 값 | 사용 예) | 활용 예제 |
Hero | 페이지 Route 애니메이션 효과 (히어로 처럼 슝 나옴) | 필요하면 알아서 사용하세요 | https://docs.flutter.dev/ui/animations/hero-animations | |
AnimatedOpacity | 불투명 애니메이션 위젯 | opacity : 불투명도 → double (0~1) duration : 불투명도 애니메이션 적용 시간 → Duration child : 적용시킬 위젯 → Widget |
AnimatedOpacity(opacity: 1,duration: const Duration(milliseconds: 300), child: …) | |
AnimatedCrossFade | FadeIn FadeOut 전환 애니메이션 위젯 | firstChild : 위젯 → Widget secondChild : 위젯 → Widget duration : 애니메이션 적용 시간 → Duration crossFadeState : 보여줄 위젯 → CrossFadeState.showFirst, CrossFadeState.showSecond |
AnimatedCrossFade(fistChild:…, secondChild:…, duration:…, crossFadeState: …) | |
AnimatedBuilder | 상태 변화를 감지해 화면을 업데이트할 수 있습니다. (컨트롤러로 로직처리를 하며 Builder를 통해 그려줍니다. | https://api.flutter.dev/flutter/widgets/AnimatedBuilder-class.html | ||
AnimatedDefaultTextStyle | 텍스트에 애니메이션을 줄수 있는 위젯입니다. | https://api.flutter.dev/flutter/widgets/AnimatedDefaultTextStyle-class.html | ||
SizeTransition | 위젯의 크기(가로 또는 세로 방향)를 애니메이션을 통해 변환하는 위젯입니다. | https://api.flutter.dev/flutter/widgets/SizeTransition-class.html | ||
SlideTransition | 슬라이드하는 애니메이션을 적용한 위젯입니다. | https://api.flutter.dev/flutter/widgets/SlideTransition-class.html | ||
AnimatedModalBarrier | 모달로서 화면을 가리는 반투명한 배경(Barrier) 위젯을 만듭니다. | https://api.flutter.dev/flutter/widgets/AnimatedModalBarrier-class.html | ||
AnimatedList | 리스트의 항목들이 동적으로 추가되거나 제거될 때 애니메이션을 적용할 수 있는 위젯 | https://api.flutter.dev/flutter/widgets/AnimatedList-class.html | ||
ColorTween | 애니메이션 전환시 색상의 변화되는 과정을 애니메이션으로 보여줍니다. | https://api.flutter.dev/flutter/animation/ColorTween-class.html | ||
Tween | 시작과 종료값을 줄 수 있는 애니메이션기능을 줍니다. | https://api.flutter.dev/flutter/animation/Tween-class.html |
저장할 때 마다 trailing comma 바로 코드 적용 (auto prettier)
Format code on save를 통해 저장 시 Prettier가 바로 동작해 정리해준다.
trailing comma에 대한 Weak Warning Message 표시 및 출력
linter:
rules:
require_trailing_commas: true
analysis_options.yaml 파일에 require_trailing_commas: true를 통해 해당 기능을 사용할 수 있다
Flutter Widget Method 또는 Widget으로 Extract
코드 내 Error 및 다양한 메시지 출력하기
Flutter Outline
위젯 구조를 한눈에 알 수 있다.
Flutter Inspector
위젯과 레이아웃을 한 눈에 볼 수 있다.
초록색 동그라미를 눌러 에뮬레이터의 레이아웃을 정밀하게 볼 수 있습니다.
하위 위젯 한번에 감싸기 (Wrap with Widget)
Widget이 엄청 많아지면 상위 Widget으로 감싸려고 하는게 매우 힘들다 IDE에서 Widget을 클릭해놓으면 전구(Code Acation)이 나오는데 여기에서 원하는 걸 선택하면 된다
Http
Logging
상태관리
그 외 라이브러리
https://mondaymonday2.tistory.com/1007
SnakeCase
CamelCase
PascalCase
Tip
// 좋은 예시
typedef Comparison<T> = int Function(T a, T b);
// 나쁜 예시
typedef int Comparison<T>(T a, T b); // 구식 문법
// wrong code
class Person {
String name = '';
int age = 0;
Person setName(String name) {
this.name = name;
return this; // `this`를 반환
}
Person setAge(int age) {
this.age = age;
return this; // `this`를 반환
}
}
void main() {
var person = Person()
.setName('Alice') // 메서드 체이닝
.setAge(30); // 메서드 체이닝
print('Name: ${person.name}, Age: ${person.age}');
}
// right code
class Person {
String name = '';
int age = 0;
void setName(String name) {
this.name = name;
}
void setAge(int age) {
this.age = age;
}
}
void main() {
var person = Person()
..setName('Alice') // 메서드 캐스케이드
..setAge(30); // 메서드 캐스케이드
print('Name: ${person.name}, Age: ${person.age}');
}
// 좋은 예시
var desserts = <List<Ingredient>>[];
// 나쁜 예시
List<List<Ingredient>> desserts = <List<Ingredient>>[];
// ─────────────────────────────────────────────────────────────
// 좋은 예시
var playerScores = <String, int>{}; // 명시적으로 타입 인자 지정
final events = StreamController<Event>();
// 나쁜 예시
var playerScores = {}; // 타입 인자 없음
final events = StreamController(); // 타입 인자 없음
// 간단한 방식
Point(this.x, this.y);
// 좋은 예시
set foo(Foo value) { ... }
// 나쁜 예시
void set foo(Foo value) { ... } // 불필요한 반환 타입
class ListBox {
final bool scroll;
final bool showScrollbars;
ListBox({this.scroll = false, this.showScrollbars = false});
}
void main() {
// 명명된 매개변수를 사용하여 `ListBox` 인스턴스를 생성
var listBox = ListBox(scroll: true, showScrollbars: true);
}
// 명명된 매개변수를 가진 함수
void createUser({required String name, int age = 18, String? email}) {
print('Name: $name, Age: $age, Email: $email');
}
https://dart.dev/effective-dart/style
https://dart.dev/effective-dart/usage
https://dart.dev/effective-dart/design
lib/
├─ model
├─ my_menu
├─ my_menu_model.dart
├─ repository
├─ my_menu
├─ my_menu_repository.dart
├─ view_model
├─ my_menu
├─ my_menu_view_model.dart
├─ screen
├─ my_menu
├─ my_menu_screen.dart
├─ common
├─ enum
├─ exception
├─ logger
├─ util
├─ widget
├─ my_menu
├─ profile.dart
다양한 구조가 있지만 MVVM 구조가 앱개발할 때 가장 적합한 스타일이라고한다. (iOS / AOS에도 적극 채용중) Rivderpod을 선택했고 Riverpod의 경우 MVVM에 맞게 개발되었기 때문에 MVVM 프로젝트 구조가 적합하다.
dart라는 구글에서 개발한 언어로 크로스 플랫폼이 가능한 강력한 프레임워크로 Android, iOS, Web 등 하나의 언어로 개발이 가능하다 C, C++ 로 구현된 엔진을 사용한다
Framework는 사용자가 직접 개발하는 부분으로 이 계층은 주로 UI와 관련된 로직과 기능을 제공합니다.
C++로 작성된 Flutter 엔진으로 다양한 플랫폼에서 UI를 동일하게 렌더링하기 위해 필수적인 기능을 제공합니다.
각 플랫폼에 맞게 Flutter 애플리케이션을 통합하고 실행할 수 있게 하는 역할을 합니다.
Dart언어, Flutter 엔진, 그래픽디자인툴이 들어간 녀석을 Android 또는 iOS와 연결시켜 빌드 파일을 만든다. 빌드 파일안에는 Dart언어, Flutter 엔진, 그래픽디자인툴 등이 들어가있다.
🔗 참고 및 출처
이미지와 원하는 도형 형태를 같이 올려둔다
도형을 뒤로 보낸다
도형하고 이미지를 둘다 선택하고 Use as Mask를 누른다 (2개 밖에 없을 땐 Ctrl + A로 가능)
완성
커스텀훅은 개발자가 직접 정의한 재사용 가능한 함수입니다.
useOnlineStatus.js
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
function handleOnline() {
setIsOnline(true);
}
function handleOffline() {
setIsOnline(false);
}
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return isOnline;
}
App.js
import { useOnlineStatus } from './useOnlineStatus.js';
function StatusBar() {
const isOnline = useOnlineStatus();
return <h1>{isOnline ? '✅ 온라인' : '❌ 연결 안 됨'}</h1>;
}
function SaveButton() {
const isOnline = useOnlineStatus();
function handleSaveClick() {
console.log('✅ 진행사항 저장됨');
}
return (
<button disabled={!isOnline} onClick={handleSaveClick}>
{isOnline ? '진행사항 저장' : '재연결 중...'}
</button>
);
}
export default function App() {
return (
<>
<SaveButton />
<StatusBar />
</>
);
}
위 예제를 보면 연결이 됐냐 안 됐냐에 대한 상태관리를 하는데 이걸 이부분만 아니라 다른 곳에서도 사용한 경우 훅으로 만들어서 연결 됐는지 안 됐는지에 대한 처리를 간결하게 처리할 수 있고 유지보수가 용이해집니다.
커스텀훅의 경우 use를 prefix로 붙여야 동작합니다.
App.js (또다른 예제)
import { useState } from 'react';
export default function Form() {
const [firstName, setFirstName] = useState('Mary');
const [lastName, setLastName] = useState('Poppins');
function handleFirstNameChange(e) {
setFirstName(e.target.value);
}
function handleLastNameChange(e) {
setLastName(e.target.value);
}
return (
<>
<label>
First name:
<input value={firstName} onChange={handleFirstNameChange} />
</label>
<label>
Last name:
<input value={lastName} onChange={handleLastNameChange} />
</label>
<p><b>Good morning, {firstName} {lastName}.</b></p>
</>
);
}
Form을 이용한 또 다른 예제를 살펴보겠습니다. 여기에선 기존에 e.target.value를 이용해 상태값에 넣는 동일 역할을 하는 함수가 있습니다. 이걸 커스텀훅을 이용해 줄여보도록 하겠습니다.
useFormInput.js (또다른 예제)
import { useState } from 'react';
export function useFormInput(initialValue) {
const [value, setValue] = useState(initialValue);
function handleChange(e) {
setValue(e.target.value);
}
const inputProps = {
value: value,
onChange: handleChange
};
return inputProps;
}
App.js (또다른 예제)
import { useFormInput } from './useFormInput.js';
export default function Form() {
const firstNameProps = useFormInput('Mary');
const lastNameProps = useFormInput('Poppins');
return (
<>
<label>
First name:
<input {...firstNameProps} />
</label>
<label>
Last name:
<input {...lastNameProps} />
</label>
<p><b>Good morning, {firstNameProps.value} {lastNameProps.value}.</b></p>
</>
);
}
useFormInput을 두개를 호출했는데 이럴 경우 state가 1개로 공유하는 게 아니라 각각 들어가게 됩니다. state를 공유하는 게 아닌 state 저장 로직을 공유함으로 독립적이다.
커스텀 Hook 안의 코드는 컴포넌트가 재렌더링될 때마다 다시 돌아갈 겁니다. 이게 바로 커스컴 Hook이 (컴포넌트처럼) 순수해야하는 이유 입니다.
import { useState } from 'react';
export default function Accordion() {
const [activeIndex, setActiveIndex] = useState(0);
return (
<>
<h2>Almaty, Kazakhstan</h2>
<Panel
title="About"
isActive={activeIndex === 0}
onShow={() => setActiveIndex(0)}
>
With a population of about 2 million, Almaty is Kazakhstan's largest city. From 1929 to 1997, it was its capital city.
</Panel>
<Panel
title="Etymology"
isActive={activeIndex === 1}
onShow={() => setActiveIndex(1)}
>
The name comes from <span lang="kk-KZ">алма</span>, the Kazakh word for "apple" and is often translated as "full of apples". In fact, the region surrounding Almaty is thought to be the ancestral home of the apple, and the wild <i lang="la">Malus sieversii</i> is considered a likely candidate for the ancestor of the modern domestic apple.
</Panel>
</>
);
}
function Panel({
title,
children,
isActive,
onShow
}) {
return (
<section className="panel">
<h3>{title}</h3>
{isActive ? (
<p>{children}</p>
) : (
<button onClick={onShow}>
Show
</button>
)}
</section>
);
}
만약 state를 공유해야하면 위 예제 처럼 부모 state를 정의하고 그 state를 props로 전달하면 된다.
또다른 예제
function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
// 이 Effect는 나라별 도시를 불러옵니다.
useEffect(() => {
let ignore = false;
fetch(`/api/cities?country=${country}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setCities(json);
}
});
return () => {
ignore = true;
};
}, [country]);
const [city, setCity] = useState(null);
const [areas, setAreas] = useState(null);
// 이 Effect 선택된 도시의 구역을 불러옵니다.
useEffect(() => {
if (city) {
let ignore = false;
fetch(`/api/areas?city=${city}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setAreas(json);
}
});
return () => {
ignore = true;
};
}
}, [city]);
// ...
/** ──── custom hook으로 변환 ────**/
function useData(url) {
const [data, setData] = useState(null);
useEffect(() => {
if (url) {
let ignore = false;
fetch(url)
.then(response => response.json())
.then(json => {
if (!ignore) {
setData(json);
}
});
return () => {
ignore = true;
};
}
}, [url]);
return data;
}
커스텀 훅이라는 건 간단하게 설명하면 Hook이 들어간 함수라고 생각하면 된다.
다양한 상황이 존재한다.