πŸ“š Today I Learned 10회차 - [Flutter] U&I μ•±μœΌλ‘œ D-Day κ³„μ‚°ν•˜κΈ° πŸ’•

5 λΆ„ μ†Œμš”

πŸ“– Flutter D-Day μ•± 개발

🎯 ν•™μŠ΅ λͺ©ν‘œ

  • StatefulWidget을 μ΄μš©ν•œ μƒνƒœ 관리 읡히기
  • setState ν•¨μˆ˜ μ‚¬μš©λ²• μ™„μ „ μ΄ν•΄ν•˜κΈ°
  • Cupertino λ””μžμΈ μ‹œμŠ€ν…œ ν™œμš©ν•˜κΈ°
  • MediaQueryλ₯Ό ν†΅ν•œ λ°˜μ‘ν˜• UI κ΅¬ν˜„ν•˜κΈ°
  • ThemeDataλ₯Ό ν™œμš©ν•œ μΌκ΄€λœ λ””μžμΈ μ‹œμŠ€ν…œ κ΅¬μΆ•ν•˜κΈ°

πŸ“š μ£Όμš” λ‚΄μš©

⭐ ν”„λ‘œμ νŠΈ κ°œμš”

U&I 앱은 사귀기 μ‹œμž‘ν•œ λ‚ μ§œλ₯Ό μ„ νƒν•˜λ©΄ ν˜„μž¬κΉŒμ§€ 며칠이 μ§€λ‚¬λŠ”μ§€ κ³„μ‚°ν•΄μ£ΌλŠ” D-Day μ•±μž…λ‹ˆλ‹€.

πŸ’‘ 핡심 κΈ°λŠ₯: λ‚ μ§œ 선택, D-Day 계산, iOS μŠ€νƒ€μΌ UI κ΅¬ν˜„

이번 ν”„λ‘œμ νŠΈμ—μ„œ 집쀑할 핡심 μš”μ†Œ:

  • StatefulWidget을 μ΄μš©ν•œ μƒνƒœ 관리
  • setState ν•¨μˆ˜ μ‚¬μš© 방법
  • Cupertino (iOS μŠ€νƒ€μΌ) λ””μžμΈ μ‹œμŠ€ν…œ
    • FlutterλŠ” 2κ°€μ§€ λ””μžμΈ μ‹œμŠ€ν…œμ„ μ§€μ›ν•©λ‹ˆλ‹€
      • κ΅¬κΈ€μ˜ Material Design (Android μŠ€νƒ€μΌ)
      • μ• ν”Œμ˜ Cupertino Design (iOS μŠ€νƒ€μΌ)

⚑ 핡심 κ°œλ… 1: setState ν•¨μˆ˜

Stateλ₯Ό μƒμ†ν•˜λŠ” λͺ¨λ“  ν΄λž˜μŠ€λŠ” setState ν•¨μˆ˜λ₯Ό μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

setState ν•¨μˆ˜ μ‹€ν–‰ κ³Όμ •:

  1. Clean μƒνƒœ - λ Œλ”λ§ μ™„λ£Œ μƒνƒœ
  2. setState() - μƒνƒœ λ³€κ²½ ν•¨μˆ˜ 호좜
  3. Dirty μƒνƒœ - μž¬λ Œλ”λ§ ν•„μš” μƒνƒœ
  4. build() - μœ„μ ― λ‹€μ‹œ λΉŒλ“œ
  5. Clean μƒνƒœ - λ Œλ”λ§ μ™„λ£Œ

πŸ’‘ μ€‘μš”: StatefulWidget의 λ Œλ”λ§μ΄ λλ‚˜λ©΄ Clean μƒνƒœκ°€ λ©λ‹ˆλ‹€. μƒνƒœλ₯Ό λ³€κ²½ν•˜λ €λ©΄ λ°˜λ“œμ‹œ setState ν•¨μˆ˜λ₯Ό μ‚¬μš©ν•΄μ•Ό ν•©λ‹ˆλ‹€!

setStateλŠ” 콜백 ν•¨μˆ˜λ₯Ό λ°›μœΌλ©°, 이 콜백 ν•¨μˆ˜ λ‚΄λΆ€μ—μ„œ μƒνƒœ λ³€κ²½ μž‘μ—…μ„ μ§„ν–‰ν•©λ‹ˆλ‹€.

⚑ 핡심 κ°œλ… 2: showCupertinoDialog

iOS μŠ€νƒ€μΌμ˜ λ‹€μ΄μ–Όλ‘œκ·Έλ₯Ό μ‹€ν–‰ν•˜λŠ” ν•¨μˆ˜μž…λ‹ˆλ‹€.

showCupertinoDialog(
  context: context,// BuildContext μž…λ ₯
  barrierDismissible: true, // μ™ΈλΆ€ νƒ­ν•΄μ„œ λ‹€μ΄μ–Όλ‘œκ·Έ 닫을 수 있게 ν•˜κΈ°
  builder: (BuildContext context) { // λ‹€μ΄μ–Όλ‘œκ·Έμ— λ“€μ–΄κ°ˆ μœ„μ ―
    return Text('Dialog');
  }
);

πŸ’» μ‹€μŠ΅ 및 예제

πŸ”§ 사전 μ€€λΉ„: 폰트 등둝

폰트 등둝을 μœ„ν•΄ pubspec.yaml νŒŒμΌμ„ μˆ˜μ •ν•©λ‹ˆλ‹€:

fonts:
  - family: parisienne # family 킀에 폰트 이름을 μ§€μ •
    fonts:
      - asset: asset/font/Parisienne-Regular.ttf # 등둝할 폰트의 μœ„μΉ˜
  - family: sunflower
    fonts:
      - asset: asset/font/Sunflower-Light.ttf
      - asset: asset/font/Sunflower-Medium.ttf
        weight: 500 # 폰트의 λ‘κ»˜. FontWeight 클래슀의 κ°’κ³Ό κ°™λ‹€.
      - asset: asset/font/Sunflower-Bold.ttf
        weight: 700

πŸ’‘ 팁: WeightλŠ” 폰트의 λ‘κ»˜λ³„λ‘œ 파일이 λ”°λ‘œ 제곡되기 λ•Œλ¬Έμ— 같은 ν°νŠΈλΌλ„ λ‹€λ₯Έ λ‘κ»˜λ₯Ό ν‘œν˜„ν•˜λŠ” νŒŒμΌμ€ weight 값을 λ”°λ‘œ μ§€μ •ν•΄μ•Ό ν•©λ‹ˆλ‹€. 이후 Flutterμ—μ„œ FontWeight 클래슀의 κ°’κ³Ό λ§€μΉ­λ©λ‹ˆλ‹€ (예: FontWeight.w500)

🎨 λ ˆμ΄μ•„μ›ƒ κ΅¬μƒν•˜κΈ°

MediaQuery.of(context)λ₯Ό μ‚¬μš©ν•˜λ©΄ ν™”λ©΄ 크기와 κ΄€λ ¨λœ 각쒅 κΈ°λŠ₯을 μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€:

  • size κ²Œν„°λ‘œ ν™”λ©΄ μ „μ²΄μ˜ λ„ˆλΉ„μ™€ 높이λ₯Ό μ‰½κ²Œ κ°€μ Έμ˜¬ 수 있음
  • ν™”λ©΄ 전체 높이λ₯Ό 2둜 λ‚˜λˆ μ„œ ν™”λ©΄ λ†’μ΄μ˜ 절반만큼 이미지가 μ°¨μ§€ν•˜λ„λ‘ μ„€μ •

πŸ€” .of(context) μƒμ„±μžλž€?

.of(context) μƒμ„±μžλŠ” μ΄ˆλ³΄μžλ“€μ΄ 많이 ν—·κ°ˆλ €ν•˜λŠ” κ°œλ…μž…λ‹ˆλ‹€.

핡심 원리:

  • of(context)둜 μ •μ˜λœ λͺ¨λ“  μƒμ„±μžλŠ” BuildContextλ₯Ό λ§€κ°œλ³€μˆ˜λ‘œ λ°›μŒ
  • μœ„μ ― νŠΈλ¦¬μ—μ„œ κ°€μž₯ κ°€κΉŒμ΄μ— μžˆλŠ” 객체의 값을 찾아냄

λ™μž‘ 방식:

  1. MediaQuery.of(context) β†’ ν˜„μž¬ μœ„μ ― νŠΈλ¦¬μ—μ„œ κ°€μž₯ κ°€κΉŒμš΄ MediaQuery 값을 찾음
  2. μ•± μ‹€ν–‰ μ‹œ MaterialApp이 λΉŒλ“œλ˜λ©΄μ„œ MediaQuery도 ν•¨κ»˜ 생성
  3. μœ„μ ― 트리 μ•„λž˜μ—μ„œ ν˜ΈμΆœν•˜λ©΄ μœ„λ‘œ μ˜¬λΌκ°€λ©° κ°€μž₯ κ°€κΉŒμš΄ MediaQuery 값을 κ°€μ Έμ˜΄

πŸ’‘ λΉ„μŠ·ν•œ μ˜ˆμ‹œ: Theme.of(context), Navigator.of(context) λ“±

🎨 ν…Œλ§ˆ(Theme) μ‹œμŠ€ν…œ

Text μœ„μ ―μ˜ κΈ°λ³Έ μŠ€νƒ€μΌμ„ λ³€κ²½ν•˜κ³  싢을 λ•ŒλŠ” ν…Œλ§ˆλ₯Ό μ‚¬μš©ν•˜λ©΄ νŽΈλ¦¬ν•©λ‹ˆλ‹€!

ν…Œλ§ˆμ˜ μž₯점:

  • 13κ°€μ§€ Text μŠ€νƒ€μΌμ„ 미리 μ •μ˜ν•˜μ—¬ ν”„λ‘œμ νŠΈ μ „μ²΄μ—μ„œ μž¬μ‚¬μš© κ°€λŠ₯
  • μΌκ΄€λœ λ””μžμΈ μ‹œμŠ€ν…œ ꡬ좕

μ£Όμš” ꡬ성 μš”μ†Œ:

  • ThemeData: MaterialApp의 theme λ§€κ°œλ³€μˆ˜λ‘œ, Flutterκ°€ κΈ°λ³Έ μ œκ³΅ν•˜λŠ” λŒ€λΆ€λΆ„ μœ„μ ―μ˜ κΈ°λ³Έ μŠ€νƒ€μΌ μ§€μ •
  • TextTheme: κΈ€μž ν…Œλ§ˆλ₯Ό μ •ν•  수 μžˆλŠ” λ§€κ°œλ³€μˆ˜

μ£Όμš” ThemeData λ§€κ°œλ³€μˆ˜:

λ§€κ°œλ³€μˆ˜ μ„€λͺ…
fontFamily μ•± 전체 κΈ°λ³Έ 폰트 μ„€μ •
textTheme ν…μŠ€νŠΈ μŠ€νƒ€μΌ ν…Œλ§ˆ μ •μ˜
tabBarTheme νƒ­λ°” μŠ€νƒ€μΌ ν…Œλ§ˆ
cardTheme μΉ΄λ“œ μœ„μ ― ν…Œλ§ˆ
appBarTheme μ•±λ°” ν…Œλ§ˆ μ„€μ •
floatingActionButtonTheme FAB ν…Œλ§ˆ
elevatedButtonTheme 승격 λ²„νŠΌ ν…Œλ§ˆ
checkboxTheme μ²΄ν¬λ°•μŠ€ ν…Œλ§ˆ

πŸ’‘ νŒ¨ν„΄: μœ„μ ― 이름 + Theme κ·œμΉ™μ„ μ΄μš©ν•΄μ„œ νŠΉμ • μœ„μ ―μ˜ ν…Œλ§ˆλ₯Ό μ‰½κ²Œ μ„€μ •ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

πŸ” 심화 ν•™μŠ΅

🚨 λ‹€μ–‘ν•œ ν™”λ©΄ λΉ„μœ¨κ³Ό 해상도에 λ”°λ₯Έ μ˜€λ²„ν”Œλ‘œ ν•΄κ²°ν•˜κΈ°

문제 상황:

  • ν•Έλ“œν°λ§ˆλ‹€ ν™”λ©΄μ˜ λΉ„μœ¨κ³Ό 해상도가 λͺ¨λ‘ 닀름
  • ν•˜λ‚˜μ˜ ν™”λ©΄ κΈ°μ€€μœΌλ‘œ UIλ₯Ό μž‘μ—…ν•˜λ©΄ λ‹€λ₯Έ 크기 ν•Έλ“œν°μ—μ„œ λ ˆμ΄μ•„μ›ƒμ΄ 깨질 수 있음

ꡬ체적인 μ˜ˆμ‹œ:

  • ν•Έλ“œν° 크기가 μž‘μ•„μ„œ 상단 κΈ€μžλ“€μ΄ ν™”λ©΄μ˜ 절반 이상을 μ°¨μ§€
  • μ•„λž˜μͺ½ 이미지가 남은 곡간보닀 더 λ§Žμ€ 높이λ₯Ό μš”κ΅¬
  • 남은 곡간이 λΆ€μ‘±ν•œλ° 이미지 크기λ₯Ό ν™”λ©΄μ˜ 절반으둜 κ³ μ •ν–ˆκΈ° λ•Œλ¬Έ

🚨 Flutterμ—μ„œλŠ” 이λ₯Ό μ˜€λ²„ν”Œλ‘œ(Overflow)라고 ν•©λ‹ˆλ‹€

ν•΄κ²° 방법:

  1. κΈ€μžλ‚˜ μ΄λ―Έμ§€μ˜ 크기λ₯Ό λ™μ μœΌλ‘œ 쑰절
  2. 이미지가 남은 κ³΅κ°„λ§ŒνΌλ§Œ μ°¨μ§€ν•˜λ„λ‘ κ΅¬ν˜„ ⭐

πŸ’‘ ꢌμž₯ 방법: 이미지가 남은 κ³΅κ°„λ§ŒνΌ μ°¨μ§€ν•˜λ„λ‘ Expanded μœ„μ ―μ„ μ‚¬μš©ν•˜λŠ” 것이 μΌλ°˜μ μž…λ‹ˆλ‹€.

πŸ€” μΆ”κ°€ κ°œλ…λ“€

GestureTapCallback:

  • Material νŒ¨ν‚€μ§€μ—μ„œ κΈ°λ³Έ μ œκ³΅ν•˜λŠ” Typedef
  • λ²„νŠΌμ˜ onPressed λ˜λŠ” onTap 콜백 ν•¨μˆ˜λ“€μ΄ GestureTapCallback νƒ€μž…μœΌλ‘œ μ •μ˜λ¨

Align μœ„μ ―:

  • μžμ‹ μœ„μ ―μ˜ μœ„μΉ˜λ₯Ό μ •ν•  수 μžˆλŠ” μœ„μ ―
  • μ˜ˆμ‹œ: Alignment.bottomCenter β†’ μ•„λž˜ μ€‘κ°„μœΌλ‘œ μ •λ ¬

λ‹€μ΄μ–Όλ‘œκ·Έ μ˜΅μ…˜:

  • barrierDismissible: true β†’ μ™ΈλΆ€ νƒ­ν•  경우 λ‹€μ΄μ–Όλ‘œκ·Έ λ‹«κΈ°

Alignment의 μ •λ ¬ κ°’:

속성 μœ„μΉ˜
Alignment.topRight μœ„ 였λ₯Έμͺ½
Alignment.topCenter μœ„ 쀑앙
Alignment.topLeft μœ„ μ™Όμͺ½
Alignment.centerRight 쀑앙 였λ₯Έμͺ½
Alignment.center 쀑앙
Alignment.centerLeft 쀑앙 μ™Όμͺ½
Alignment.bottomRight μ•„λž˜ 였λ₯Έμͺ½
Alignment.bottomCenter μ•„λž˜ 쀑앙
Alignment.bottomLeft μ•„λž˜ μ™Όμͺ½

πŸ’» μ™„μ„±λœ μ†ŒμŠ€ μ½”λ“œ

main.dart - μ•±μ˜ μ§„μž…μ κ³Ό ν…Œλ§ˆ μ„€μ •:

import 'package:flutter/material.dart';
import 'package:u_and_i/screen/home_screen.dart';

void main() {
  runApp(
    MaterialApp(
      theme: ThemeData(
        // ν…Œλ§ˆλ₯Ό μ§€μ •ν•  수 μžˆλŠ” 클래슀
        fontFamily: 'sunflower', // κΈ°λ³Έ 글씨체
        textTheme: TextTheme(
          // κΈ€μž ν…Œλ§ˆλ₯Ό μ μš©ν•  수 μžˆλŠ” 클래슀
          displayLarge: TextStyle(
            // headline1 μŠ€νƒ€μΌ μ •μ˜
            color: Colors.white, // κΈ€ 색상
            fontSize: 80.0, // κΈ€ 크기
            fontWeight: FontWeight.w700, // κΈ€ λ‘κ»˜
            fontFamily: 'parisienne', // 글씨체
          ),
          displayMedium: TextStyle(
            color: Colors.white,
            fontSize: 50.0,
            fontWeight: FontWeight.w700,
          ),
          bodyLarge: TextStyle(color: Colors.white, fontSize: 30.0),
          bodyMedium: TextStyle(color: Colors.white, fontSize: 20.0),
        ),
      ),
      home: HomeScreen(),
    ),
  );
}

home_screen.dart - 메인 ν™”λ©΄κ³Ό μƒνƒœ 관리:

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  DateTime firstDay = DateTime.now();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.pink[100],
      body: SafeArea(
        top: true,
        bottom: false,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceBetween, // μœ„ μ•„λž˜ 끝에 μœ„μ ― 배치
          crossAxisAlignment: CrossAxisAlignment.stretch, // λ°˜λŒ€μΆ• μ΅œλŒ€ 크기둜 늘리기
          children: [
            _DDay(onHeartPressed: onHeartPressed, firstDay: firstDay),
            _CoupleImage(),
          ],
        ),
      ),
    );
  }

  void onHeartPressed() {
    showCupertinoDialog(
      context: context,
      builder: (BuildContext context) {
        return Align(
          alignment: Alignment.bottomCenter,
          child: Container(
            color: Colors.white,
            height: 300,
            child: CupertinoDatePicker(
              mode: CupertinoDatePickerMode.date,
              onDateTimeChanged: (DateTime date) {
                setState(() {
                  firstDay = date;
                });
              },
            ),
          ),
        );
      },
      barrierDismissible: true, // μ™ΈλΆ€ νƒ­ν•  경우 λ‹€μ΄μ–Όλ‘œκ·Έ λ‹«κΈ°
    );
  }
}

class _DDay extends StatelessWidget {
  final GestureTapCallback onHeartPressed;
  final DateTime firstDay;

  _DDay({required this.onHeartPressed, required this.firstDay});

  @override
  Widget build(BuildContext context) {
    final textTheme = Theme.of(context).textTheme;
    final now = DateTime.now();
    return Column(
      children: [
        const SizedBox(height: 16.0),
        Text('U&I', style: textTheme.displayLarge),
        const SizedBox(height: 16.0),
        Text('우리 처음 λ§Œλ‚œ λ‚ ', style: textTheme.bodyLarge),
        Text(
          '${firstDay.year}.${firstDay.month}.${firstDay.day}',
          style: textTheme.bodyMedium,
        ),
        const SizedBox(height: 16.0),
        IconButton(
          iconSize: 60,
          onPressed: onHeartPressed,
          icon: Icon(Icons.favorite, color: Colors.red),
        ),
        const SizedBox(height: 16.0),
        Text(
          'D+${DateTime(now.year, now.month, now.day).difference(firstDay).inDays + 1}',
          style: textTheme.displayMedium,
        ),
      ],
    );
  }
}

class _CoupleImage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Expanded(
      child: Center(
        child: Image.asset(
          'asset/img/middle_image.png',
          // ν™”λ©΄μ˜ 절반만큼 높이 κ΅¬ν˜„
          height: MediaQuery.of(context).size.height / 2,
        ),
      ),
    );
  }
}

πŸ“ 마무리

βœ… 였늘 배운 것

  • StatefulWidgetκ³Ό setState: Flutterμ—μ„œ μƒνƒœλ₯Ό κ΄€λ¦¬ν•˜λŠ” 핡심 κ°œλ…
  • Cupertino λ””μžμΈ: iOS μŠ€νƒ€μΌ UI μ»΄ν¬λ„ŒνŠΈ ν™œμš©λ²•
  • MediaQuery: ν™”λ©΄ 크기에 λ”°λ₯Έ λ°˜μ‘ν˜• UI κ΅¬ν˜„
  • ThemeData: μΌκ΄€λœ λ””μžμΈ μ‹œμŠ€ν…œ ꡬ좕 방법
  • μ˜€λ²„ν”Œλ‘œ ν•΄κ²°: Expanded μœ„μ ―μ„ ν†΅ν•œ μœ μ—°ν•œ λ ˆμ΄μ•„μ›ƒ

λŒ“κΈ€λ‚¨κΈ°κΈ°