Flutter - ynjch97/YNJCH_WIKI GitHub Wiki

1. Flutter

  • ์ฐธ๊ณ  : https://nomadcoders.co/flutter-for-beginners
  • Dart ์–ธ์–ด์™€ Flutter ํ”„๋ ˆ์ž„์›Œํฌ ์‚ฌ์šฉ
  • ๋ฉ€ํ‹ฐ ํ”Œ๋žซํผ ์ง€์›
    • ์›น ์‚ฌ์ดํŠธ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜
    • iOS, Android ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜
    • Mac OS, Windows, Linux
    • IoT ์™€ ๊ฐ™์€ ์ž„๋ฒ ๋””๋“œ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜

1-1. Flutter ์‚ฌ์šฉ ์˜ˆ์‹œ

1-2. Flutter ๋™์ž‘ ๋ฐฉ์‹

  • Swift(iOS), Java(Android) ๋กœ ๋„ค์ดํ‹ฐ๋ธŒ ์•ฑ ๊ฐœ๋ฐœ ์‹œ, OS ์™€ ์ง์ ‘์ ์œผ๋กœ ๋Œ€ํ™”ํ•˜๊ฒŒ ๋จ (๋„ค์ดํ‹ฐ๋ธŒ ํ”„๋ ˆ์ž„์›Œํฌ ๋™์ž‘ ๋ฐฉ์‹)
  • Flutter/Dart ์ฝ”๋“œ๋Š” ์šด์˜์ฒด์ œ์™€ ์ง์ ‘ ์†Œํ†ตํ•˜์ง€ ์•Š์Œ
    • https://docs.flutter.dev/resources/architectural-overview
    • Engine ์„ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‚ด๋ถ€์— ๋„ฃ๊ณ , Dart ์ฝ”๋“œ๋ฅผ ์ปดํŒŒ์ผํ•จ
    • C or C++ ๋กœ ๋งŒ๋“ค์–ด์ง„ Engine ์ด ํ™”๋ฉด ์ƒ์— ์š”์†Œ๋“ค์„ ๊ทธ๋ ค์คŒ (ex. Unity ์—”์ง„)
    • ์•ฑ ์‹คํ–‰ ์‹œ, Flutter ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๊ณ  ์ฝ”๋“œ๋ฅผ ๋™์ž‘์‹œ์ผœ ๋ชจ๋“  UI ๋ฅผ ๋ Œ๋”๋ง ํ•ด์คŒ
    • Flutter ๋กœ ํ†ต์ œํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ๋“ค์ด ๋งŽ์•„์ง (ํ™”๋ฉด ์ƒ ๋ชจ๋“  ํ”ฝ์…€์„ ์กฐ์ ˆ > ์• ๋‹ˆ๋ฉ”์ด์…˜/๋‚ด๋น„๊ฒŒ์ด์…˜ ๋“ฑ์„ ํ†ต์ œ)
    • ๊ฒŒ์ž„ ์—”์ง„์ด ํ™”๋ฉด์˜ ๋ชจ๋“  ํ”ฝ์…€์„ ๋‹ค๋ฃจ๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ๋ชจ๋“  ๊ฒƒ์„ ๊ทธ๋ ค์คŒ (โˆด ๋ Œ๋”๋ง ์‹œ ํ˜ธ์ŠคํŠธ ํ”Œ๋žซํผ ์‚ฌ์šฉ X)
    • ๋…์ž์  ์—”์ง„์„ ๊ฐ€์ง€๋ฉฐ, ์ด๊ฒƒ์œผ๋กœ ๋ Œ๋”๋งํ•จ > ๋„ค์ดํ‹ฐ๋ธŒ ์•ฑ๋“ค์˜ ์ปดํฌ๋„ŒํŠธ, ์œ„์ ฏ๋“ค์„ ๊ฐ€์งˆ ์ˆ˜ ์—†์Œ
  • ์œ ์ €๊ฐ€ Mac OS, Windows, iOS, Android ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹คํ–‰ ์‹œ, ์—”์ง„์„ ๊ฐ€๋™์‹œํ‚ค๋Š” 'runner' ํ”„๋กœ์ ํŠธ ์‹คํ–‰
  • Embedder : ํ˜ธ์ŠคํŠธ ํ”Œ๋žซํผ ์ƒ์—์„œ ์—”์ง„์„ ๊ฐ€๋™์‹œํ‚ค๋Š” ์—ญํ•  (ํŠน์ • ํ”Œ๋žซํผ์— ํŠนํ™”) 2

1-3. Flutter VS React Native

  • Flutter
    • ์„ธ๋ฐ€ํ•œ ๋””์ž์ธ ์š”๊ตฌ์‚ฌํ•ญ์ด ์žˆ๊ฑฐ๋‚˜ 100% ์ปค์Šคํ„ฐ๋งˆ์ด์ง•ํ•˜๊ณ  ์‹ถ์€ ๊ฒฝ์šฐ
    • ์™ธ๋ถ€ ํŒจํ‚ค์ง€์— ์˜์กดํ•˜์ง€ ์•Š๊ณ  ๊ณ ์ˆ˜์ค€์˜ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๊ตฌํ˜„ํ•˜๊ณ  ์‹ถ์€ ๊ฒฝ์šฐ
  • React Native
    • ๋„ค์ดํ‹ฐ๋ธŒ ์•ฑ ์šด์˜์ฒด์ œ ์ƒ์—์„œ ๊ฐ€๋Šฅํ•œ ์œ„์ ฏ์„ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ
    • ๋””์ž์ธ์ด iOS ํ˜น์€ Android ์•ฑ์ฒ˜๋Ÿผ ๋ณด์ด๊ฒŒ๋” ๋งŒ๋“ค๊ณ  ์‹ถ์€ ๊ฒฝ์šฐ

2. Flutter ์„ค์น˜

2-1. SDK ์„ค์น˜

  • SDK ์„ค์น˜๊ฐ€ ์„ ํ–‰๋˜์–ด์•ผ ํ•จ
  • Windows Flutter SDK ์„ค์น˜
  • Mac OS Flutter SDK ์„ค์น˜
    • https://docs.flutter.dev/release/archive?tab=macos
    • flutter_macos_3.19.6-stable.zip ํŒŒ์ผ๋กœ ์„ค์น˜, path ์„ค์ •
    • M1 Mac ์ธ์ง€ ํ™•์ธํ•˜์—ฌ ๋งž๋Š” .zip ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ
  • ๋‘˜ ๋‹ค ์„ค์น˜ํ•˜๋Š” ๊ฒƒ์ด ๋ฒˆ๊ฑฐ๋กญ๋‹ค๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ์ง„ํ–‰

2-1-1. Windows SDK ๊ฐ„ํŽธ ์„ค์น˜

  • Chocolatey : ์ฝ˜์†”์„ ์ด์šฉํ•œ Windows ํŒจํ‚ค์ง€ ์„ค์น˜ ๋งค๋‹ˆ์ € (.zip ๋‹ค์šด๋กœ๋“œ ๋ฐ path ์„ค์ • ํ•„์š” X)
  • https://chocolatey.org/install#individual
  • PowerShell ์„ค์น˜ ๋ฐ ๊ด€๋ฆฌ์ž ๊ถŒํ•œ ์‹คํ–‰ (administrative shell) 3
# Get-ExecutionPolicy -> Restricted ๋ฐ˜ํ™˜ ์‹œ ๊ทธ ์•„๋ž˜ ๋ช…๋ น์–ด๋“ค ์ž…๋ ฅ
PS C:\Users\YNJCH> Get-ExecutionPolicy
Unrestricted
PS C:\Users\YNJCH> Set-ExecutionPolicy AllSigned
PS C:\Users\YNJCH> Set-ExecutionPolicy Bypass -Scope Process
PS C:\Users\YNJCH> Get-ExecutionPolicy
Bypass

# ์œ„ ๋ช…๋ น์–ด ์ดํ›„ ์‹คํ–‰
PS C:\Users\YNJCH> Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))

๊ฒฝ๊ณ : 'choco' was found at 'C:\ProgramData\chocolatey\bin\choco.exe'.
๊ฒฝ๊ณ : An existing Chocolatey installation was detected. Installation will not continue. This script will not overwrite
existing installations.
If there is no Chocolatey installation at 'C:\ProgramData\chocolatey', delete the folder and attempt the installation
again.

Please use choco upgrade chocolatey to handle upgrades of Chocolatey itself.
If the existing installation is not functional or a prior installation did not complete, follow these steps:
 - Backup the files at the path listed above so you can restore your previous installation if needed.
 - Remove the existing installation manually.
 - Rerun this installation script.
 - Reinstall any packages previously installed, if needed (refer to the lib folder in the backup).

Once installation is completed, the backup folder is no longer needed and can be deleted.
  • Chocolatey ์„ค์น˜ ํ›„, choco- ๋ช…๋ น์–ด ์‚ฌ์šฉ ๊ฐ€๋Šฅ
PS C:\Users\YNJCH> choco install flutter

Chocolatey v1.1.0
Installing the following packages:
flutter
By installing, you accept licenses for the packages.
Progress: Downloading flutter 3.19.6... 100%

flutter v3.19.6 [Approved]
flutter package files install completed. Performing other installation steps.
The package flutter wants to run 'chocolateyinstall.ps1'.
Note: If you don't run this script, the installation will fail.
Note: To confirm automatically next time, use '-y' or consider:
choco feature enable -n allowGlobalConfirmation
Do you want to run the script?([Y]es/[A]ll - yes to all/[N]o/[P]rint): y

Downloading flutter
  from 'https://storage.googleapis.com/flutter_infra_release/releases/stable/windows/flutter_windows_3.19.6-stable.zip'
Progress: 100% - Completed download of C:\Users\YNJCH\AppData\Local\Temp\flutter\3.19.6\flutter_windows_3.19.6-stable.zip (955.15 MB).
Download of flutter_windows_3.19.6-stable.zip (955.15 MB) completed.
Hashes match.
Extracting C:\Users\YNJCH\AppData\Local\Temp\flutter\3.19.6\flutter_windows_3.19.6-stable.zip to C:\tools...
C:\tools
PATH environment variable does not have C:\tools\flutter\bin in it. Adding...
Environment Vars (like PATH) have changed. Close/reopen your shell to
 see the changes (or in powershell/cmd.exe just type `refreshenv`).
 The install of flutter was successful.
  Software installed to 'C:\tools'

Chocolatey installed 1/1 packages.
 See the log for details (C:\ProgramData\chocolatey\logs\chocolatey.log).

2-1-2. Mac OS SDK ๊ฐ„ํŽธ ์„ค์น˜

  • Homebrew : Mac OS ์šฉ ํŒจํ‚ค์ง€ ๊ด€๋ฆฌ์ž (Node, Python ๋“ฑ ์„ค์น˜๋„ ๊ฐ€๋Šฅ)
  • https://brew.sh/
brew install --cask flutter

2-1-3. Flutter ์„ค์น˜ ํ™•์ธ

  • Chocolatey, Homebrew ๊ด€๊ณ„ ์—†์ด ์ฝ˜์†”์— ์•„๋ž˜ ๋ช…๋ น์–ด ์ž…๋ ฅํ•˜์—ฌ ํ™•์ธ
PS C:\Users\YNJCH> flutter
Manage your Flutter app development.

Common commands:
(์ƒ๋žต)

PS C:\Users\YNJCH> flutter --version
Flutter 3.19.6 โ€ข channel stable โ€ข https://github.com/flutter/flutter.git
Framework โ€ข revision 54e66469a9 (5 weeks ago) โ€ข 2024-04-17 13:08:03 -0700
Engine โ€ข revision c4cd48e186
Tools โ€ข Dart 3.3.4 โ€ข DevTools 2.31.1

2-2. ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ์„ค์น˜

  • Windows ์‚ฌ์šฉ์ž ๊ธฐ์ค€์œผ๋กœ Windows, Web, Android ์„ธ ๊ฐ€์ง€ ์ค‘ ์„ ํƒ
  • Web ๊ฐœ๋ฐœ > ์ด๋ฏธ ์›น ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๋ณ„๋„๋กœ ํ•  ์ผ X
  • Android ๊ฐœ๋ฐœ > https://docs.flutter.dev/get-started/install/windows/mobile ํ™•์ธ
    • Android Studio ์„ค์น˜ํ•ด์•ผ ํ•จ
    • ํœด๋Œ€ํฐ ์—ฐ๊ฒฐํ•˜์—ฌ ์‹คํ–‰ or ์• ๋ฎฌ๋ ˆ์ดํ„ฐ ์‹คํ–‰

2-2-1. Android Studio ์„ค์น˜

Android SDK Platform, API 34.0.0
Android SDK Command-line Tools
Android SDK Build-Tools
Android SDK Platform-Tools
Android Emulator
  • Android Studio ์‹คํ–‰ > Plugins > SDK Manager ํ™•์ธ ๊ฐ€๋Šฅ (ํ”„๋กœ์ ํŠธ ์—ด๊ธฐ > Tools ์—์„œ๋„ ํ™•์ธ ๊ฐ€๋Šฅ)
  • Android Studio > ํ”„๋กœ์ ํŠธ ์—ด๊ธฐ > Tools > Device Manager
    • Create Virtual Device ํด๋ฆญ > ๊ธฐ์ข… ์„ ํƒ
    • Next > x86 Images > ์›ํ•˜๋Š” ์ด๋ฏธ์ง€์˜ ์˜ค๋ฅธ์ชฝ ๋‹ค์šด๋กœ๋“œ ์•„์ด์ฝ˜ ํด๋ฆญ > SDK Quickfix ์„ค์น˜ ๋Œ€ํ™”์ƒ์ž ํ™•์ธ
    • AVD(Android Virtual Device) ์ด๋ฆ„ ๋ณ€๊ฒฝ
    • Show Advanced Settings > Emulated Performance > Graphics > Hardware - GLES 2.0.
      (ํ•˜๋“œ์›จ์–ด ๊ฐ€์†์ด ๊ฐ€๋Šฅํ•ด์ง€๊ณ  ๋ Œ๋”๋ง ์„ฑ๋Šฅ์ด ํ–ฅ์ƒ๋จ)
  • PowerShell > Run Flutter doctor
    • Android์šฉ์œผ๋กœ ๊ฐœ๋ฐœํ•˜๊ธฐ๋กœ ์„ ํƒํ–ˆ์œผ๋ฏ€๋กœ ๋ชจ๋“  ๊ตฌ์„ฑ ์š”์†Œ๊ฐ€ ํ•„์š”ํ•˜์ง€๋Š” ์•Š์Šต๋‹ˆ๋‹ค. ์ด ๊ฐ€์ด๋“œ๋ฅผ ๋”ฐ๋ฅธ ๊ฒฝ์šฐ ๋ช…๋ น ๊ฒฐ๊ณผ๋Š” ๋‹ค์Œ๊ณผ ์œ ์‚ฌํ•ฉ๋‹ˆ๋‹ค. ์™€ ๊ฐ™์€ ๊ฒฐ๊ณผ์ด๋ฏ€๋กœ ํŒจ์Šค
PS C:\Users\YNJCH> flutter doctor
Found an existing Pub cache at C:\Users\YNJCH\AppData\Local\Pub\Cache.
It can be repaired by running `dart pub cache repair`.
It can be reset by running `dart pub cache clean`.
Doctor summary (to see all details, run flutter doctor -v):
[โˆš] Flutter (Channel stable, 3.19.6, on Microsoft Windows [Version 10.0.22631.3447], locale ko-KR)
[โˆš] Windows Version (Installed version of Windows is version 10 or higher)
[!] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
    X cmdline-tools component is missing
      Run `path/to/sdkmanager --install "cmdline-tools;latest"`
      See https://developer.android.com/studio/command-line for more details.
    X Android license status unknown.
      Run `flutter doctor --android-licenses` to accept the SDK licenses.
      See https://flutter.dev/docs/get-started/install/windows#android-setup for more details.
[โˆš] Chrome - develop for the web
[X] Visual Studio - develop Windows apps
    X Visual Studio not installed; this is necessary to develop Windows apps.
      Download at https://visualstudio.microsoft.com/downloads/.
      Please install the "Desktop development with C++" workload, including all of its default components
[โˆš] Android Studio (version 2023.2)
[โˆš] VS Code (version 1.76.0)
[โˆš] Connected device (4 available)
[โˆš] Network resources

! Doctor found issues in 2 categories.

3. Flutter ์˜จ๋ผ์ธ ์—๋””ํ„ฐ

  • ๋ณ„๋„ ํ”„๋กœ๊ทธ๋žจ ์—†์ด ์‹คํ–‰ ๊ฐ€๋Šฅ
  • DartPad ์—์„œ counter ์ƒ˜ํ”Œ ํ™•์ธ (https://dartpad.dev/?sample=counter)

4. Flutter Project

  • PowerShell > Flutter Project ์ƒ์„ฑ
PS C:\Users\YNJCH> cd C:\Users\YNJCH\Flutter
PS C:\Users\YNJCH\Flutter> flutter create toonflix
Creating project toonflix...
Resolving dependencies in toonflix...
Got dependencies in toonflix.
Wrote 129 files.

All done!
You can find general documentation for Flutter at: https://docs.flutter.dev/
Detailed API documentation is available at: https://api.flutter.dev/
If you prefer video documentation, consider: https://www.youtube.com/c/flutterdev

In order to run your application, type:

  $ cd toonflix
  $ flutter run

Your application code is in toonflix\lib\main.dart.
  • ์‹คํ–‰ํ•˜๊ธฐ (๋ธŒ๋ผ์šฐ์ € ์„ ํƒ > http://localhost:11542/)
    • Flutter ๋ฅผ console ์—์„œ ์‹คํ–‰์‹œํ‚จ ๊ฒƒ์ด๋ฏ€๋กœ ์ •์ƒ ์‹คํ–‰๋œ ๊ฒƒ ํ™•์ธ ํ›„ ์ค‘์ง€
PS C:\Users\YNJCH\Flutter> cd toonflix
PS C:\Users\YNJCH\Flutter\toonflix> flutter run
# ์œ„์™€ ๊ฐ™์ด ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์Œ

4-1. Project ๊ตฌ์กฐ

  • Flutter ํŒŒ์ผ > C:\Users\YNJCH\Flutter\toonflix\lib\main.dart
  • linux, windows, macos, web, android, ios ํด๋” : ๊ฐ๊ฐ ๊ฐœ๋ฐœ์„ ์œ„ํ•œ ์„ค์ • ํŒŒ์ผ ๆœ‰

4-2. VSCode ์—์„œ ์‹คํ–‰ํ•˜๊ธฐ

4-2-1. ์ •์ƒ ์ž‘๋™ ํ™•์ธ

  • Widget Inspector ํ™•์ธ ๊ฐ€๋Šฅ 4
  • main.dart > ์ƒ‰์ƒ ๋ณ€๊ฒฝํ•˜์—ฌ ๋ฐ˜์˜๋˜๋Š” ๊ฒƒ ํ™•์ธ
    backgroundColor: Theme.of(context).colorScheme.inversePrimary,
    backgroundColor: Theme.of(context).colorScheme.inverseSurface,

5. ์ฐธ๊ณ  ๋งํฌ

5-1. pub.dev

  • pub.dev : Dart, Flutter ๊ณต์‹ ํŒจํ‚ค์ง€ ๋ณด๊ด€์†Œ
    • Node.js ์˜ npm, Python ์˜ PyPI ์™€ ๋น„์Šทํ•œ ๊ฐœ๋…
  • HTTP ๊ด€๋ จ ํŒจํ‚ค์ง€ : https://pub.dev/packages/http
    • A composable, Future-based library for making HTTP requests.
    • PLATFORM > ANDROID, IOS, LINUX, MACOS, WEB, WINDOWS
    • ์„ค์น˜ ๋ฐฉ๋ฒ• : command line ์ž…๋ ฅ, pubspec.yaml
$ dart pub add http     # Dart ์„ค์น˜
$ flutter pub add http  # Flutter ์„ค์น˜
dependencies:
  flutter:
    sdk: flutter

  http: ^1.2.1 # ๋ฒ„์ „ ์ •๋ณด ๊ธฐ์žฌ
  • pubspec.yaml : VSCode ์—์„œ ์ €์žฅ ์ฆ‰์‹œ ๋ฐ”๋กœ ํŒจํ‚ค์ง€ ์„ค์น˜ (์•„๋ž˜์™€ ๊ฐ™์ด ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ ์„ค์น˜ํ•  ์ˆ˜๋„ ์žˆ์Œ) 10

5-1-1. Font Awesome

  • Font Awesome :๋ฌด๋ฃŒ ์•„์ด์ฝ˜ (๋ธŒ๋žœ๋“œ ๋กœ๊ณ  ๆœ‰)
  • https://pub.dev/packages/font_awesome_flutter ํŒจํ‚ค์ง€ ์„ค์น˜
    • nomadcoders ๊ฐ•์˜ ๊ธฐ์ค€ ์„ค์น˜ ์‹œ, pubspec.yaml > font_awesome_flutter: 10.3.0 ์ž‘์„ฑ
  • FaIcon(FontAwesomeIcons.amazon)
    • ์•„์ด์ฝ˜ ์ด๋ฆ„์€ FontAwesome ์‚ฌ์ดํŠธ์—์„œ ๊ฒ€์ƒ‰ํ•˜์—ฌ ์‚ฌ์šฉ

5-2. UI/UX

5-3. material design

    return MaterialApp(
      theme: ThemeData(
        useMaterial3: true,

6. ๊ฐœ๋ฐœ ์‹œ ์ฐธ๊ณ 

  • Error Lens Extension ์„ค์น˜ > ์—๋Ÿฌ ๋‚ด์šฉ์„ ๋ฐ”๋กœ ๋ณผ ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์คŒ 8

6-1. Widget Inspector Page

  • Widget ๊ตฌ์กฐ ํŒŒ์•…
  • Toggle select widget mode : ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ์—์„œ Widget ์„ ํƒ ๊ฐ€๋Šฅ
  • Overlay guidelines to assist with fixing layout issues : ๊ฐ€์ด๋“œ๋ผ์ธ ํ™•์ธ ๊ฐ€๋Šฅ
  • Widget Tree : tree ๊ตฌ์กฐ๋กœ ํŒŒ์•… ๊ฐ€๋Šฅ
  • Layout Explorer : ์˜ต์…˜ ๊ฐ’์„ ๋ณ€๊ฒฝํ•˜์—ฌ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๊ฐ€๋Šฅ (์†Œ์Šค ๋ณ€๊ฒฝ X)
  • Widget Details Tree : ๋ชจ๋“  ์†์„ฑ๊ฐ’ ํ™•์ธ ๊ฐ€๋Šฅ 5

6-2. ์ฝ”๋“œ ๋ธ”๋ก ๋‹จ์œ„ ์ด๋™

  • ์„ ํƒํ•œ ๋ผ์ธ ์ขŒ์ธก์˜ ๋…ธ๋ž€ ์ „๊ตฌ ํด๋ฆญ (or Ctrl + .)
  • ํŠน์ • Widget ์ฝ”๋“œ ๋ธ”๋ก์„ ๋‹ค๋ฅธ Widget ํ•˜์œ„๋กœ ์ด๋™ ์‹œ > Wrap with ~ ํด๋ฆญ 7
    • Wrap with widget... : ์ง์ ‘ ์ž…๋ ฅํ•˜์—ฌ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ‹€๋งŒ ์ œ๊ณต
    • Remove this widget : ๊ฐ์‹ธ๊ณ  ์žˆ๋˜ Widget ์ œ๊ฑฐ ๊ฐ€๋Šฅ
  • ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ Widget ์ƒ์„ฑ ์‹œ > Extract Widget ํด๋ฆญ

7. ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ

7-1. Android Studio ์—์„œ ํ•ธ๋“œํฐ ์—ฐ๊ฒฐ

  • ์Šค๋งˆํŠธํฐ์„ ์—ฐ๊ฒฐํ•˜๋ ค๋ฉด, PC์— Samsung Android USB Driver ์„ค์น˜
  • ์Šค๋งˆํŠธํฐ ์„ค์ • > ํœด๋Œ€์ „ํ™” ์ •๋ณด > ์†Œํ”„ํŠธ์›จ์–ด ์ •๋ณด > ๋นŒ๋“œ๋ฒˆํ˜ธ 5-6ํšŒ ํด๋ฆญ > ๊ฐœ๋ฐœ์ž ๋ชจ๋“œ ํ™œ์„ฑํ™”
  • ์„ค์ • > ๊ฐœ๋ฐœ์ž ์˜ต์…˜ > USB ๋””๋ฒ„๊น… ON
  • ์Šค๋งˆํŠธํฐ์ด ๊ธฐ๊ธฐ ๋ชฉ๋ก์— ๋œจ๋ฉด ์„ฑ๊ณต > Run

7-2. VSCode ์—์„œ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ํ™œ์šฉ

  • UI ์—์„œ overflow ๋ฐœ์ƒ ์‹œ ์•„๋ž˜์™€ ๊ฐ™์ด ์•ˆ๋‚ด๋จ 9

8. main.dart ๊ฐœ๋ฐœ

8-1. ํ™”๋ฉด ์Šคํฌ๋กค

  • ํ™”๋ฉด์ด overflow ๋˜๋Š” ์ด์œ ๋Š” ์Šคํฌ๋กค์ด ๋ถˆ๊ฐ€ํ•˜๊ธฐ ๋•Œ๋ฌธ
    • SingleChildScrollView Widget ์‚ฌ์šฉ

8-2. BuildContext

  • Widget build(BuildContext context) { ...
  • theme : ์•ฑ์˜ ๋ชจ๋“  ์Šคํƒ€์ผ์„ ํ•œ ๊ณณ์—์„œ ์ง€์ •ํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ ์ œ๊ณต
    • ์ƒ‰์ƒ, ํฌ๊ธฐ, ๊ธ€์ž ๊ตต๊ธฐ ๋“ฑ์ผ ๋ณ€์ˆ˜๋กœ ์‚ฌ์šฉ
    • ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์œ„ํ•œ ์Šคํƒ€์ผ ์‹œํŠธ ์ง€์ •
    • MaterialApp > theme: ThemeData.dark() ์™€ ๊ฐ™์ด ์‚ฌ์šฉ
  • Flutter ๋ Œ๋”๋ง ์ˆœ์„œ ์˜ˆ์‹œ) Container > Row > Column > Container > Text
    • Text ๊ฐ€ ๋ถ€๋ชจ ์š”์†Œ ์ •๋ณด MaterialApp ์— ์ ‘๊ทผํ•˜๋ ค๋ฉด => context ์‚ฌ์šฉ
  • context : Text ์ด์ „์˜ ๋ชจ๋“  ์ƒ์œ„ ์š”์†Œ๋“ค์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์Œ (= ์œ„์ ฏ ํŠธ๋ฆฌ์— ๋Œ€ํ•œ ์ •๋ณด)

8-3. OrientationBuilder

  • ํ•ธ๋“œํฐ ๋ฐฉํ–ฅ ์ „ํ™˜์„ ์œ„ํ•œ Widget
  • builder: (context, orientation) {}
    • orientation : Orientation.portrait or Orientation.landscape ๋ฅผ ๋ฆฌํ„ด
  • ์•ˆ์— if ๋ฌธ์„ ์ด์šฉํ•˜์—ฌ Widget list ๋˜๋Š” Widget ์„ ๋ฐฐ์น˜ํ•˜๋„๋ก ํ•จ
if (orientation == Orientation.portrait) ...[
    AuthButton(
        text: 'Use email & password',
    ),
    Gaps.v16,
    AuthButton(
        text: 'Continue with Google',
    ),
],

8-3-1. ์„ธ๋กœ ๋ชจ๋“œ๋กœ ๊ณ ์ •

  • ์•ฑ ์‹œ์ž‘ ์ „์— ๋ฐ”๊พธ๊ณ  ์‹ถ์€ state ๊ฐ€ ์žˆ๋‹ค๋ฉด engine ์ž์ฒด์™€ engine-widget ์—ฐ๊ฒฐ์„ ํ™•์‹คํžˆ ์ดˆ๊ธฐํ™”ํ•ด์•ผ ํ•จ
  • WidgetsFlutterBinding : This is the glue that binds the framework to the Flutter engine.
    • WidgetsFlutterBinding.ensureInitialized(); : GestureBinding, SchedulerBinding, ServicesBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding ๋ฅผ ๋ชจ๋‘ ์ดˆ๊ธฐํ™”
void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await SystemChrome.setPreferredOrientations(
    [DeviceOrientation.portraitUp], // ์„ธ๋กœ ๋ชจ๋“œ๋กœ ๊ณ ์ •
  );

  SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.light); // ๋‹คํฌ ๋ชจ๋“œ

  runApp(const TikTokApp());
}

9. Stateless Widget ๊ฐœ๋ฐœ

  • Stateless Widget : build() ๋ฉ”์†Œ๋“œ๋ฅผ ํ†ตํ•ด UI ๋ฅผ ์ถœ๋ ฅํ•˜๋Š” ์—ญํ• ๋งŒ ํ•จ
class App extends StatelessWidget {
  const App({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
        home: Scaffold(
      backgroundColor: Color(0xFFF4EDDB),
      body: Container(),
    ));
  }
}
  • Scaffold
    • navigation bar, body, navbar ์‚ฌ์šฉ
    • Text ๋ฐฉํ–ฅ, ์‚ฌ์ด์ฆˆ ๋“ฑ์„ ์„ค์ •

9-1. Widget ํŒŒ์ผ ์ƒ์„ฑ

  • st ์ž๋™์™„์„ฑ Flutter Stateless Widget ๋กœ Widget ํŒŒ์ผ ๊ตฌ์กฐ ์ƒ์„ฑ ๊ฐ€๋Šฅ
  • ๋ชจ๋“  Widget ์€ ๊ฐ์ž์˜ build ๋ฉ”์†Œ๋“œ๊ฐ€ ์‹คํ–‰๋˜์–ด์•ผ ํ•จ -> @override ~ build()
  • ๋…ธ๋ž€ ์ „๊ตฌ Code Action > Create constructor for final fields ๋กœ ์ƒ์„ฑ์ž ํ•จ์ˆ˜ ์ž๋™ ์™„์„ฑ
class Button extends StatelessWidget {
  final String text;
  final Color bgColor;
  final Color textColor;

  const Button({super.key, required this.text, required this.bgColor, required this.textColor,});

9-1-1. const variables

  • _blackColor : ์ž์ฃผ ์‚ฌ์šฉํ•˜๋Š” ๊ฐ’์„ ์ƒ์ˆ˜ํ˜• ๋ณ€์ˆ˜๋กœ ๋“ฑ๋ก (_ ๋ฅผ ๋ถ™์—ฌ private ํƒ€์ž…์œผ๋กœ ์‚ฌ์šฉ)
  • isInverted : bool ๊ฐ’์œผ๋กœ ๋ฐ›์•„์„œ ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ์— ํ™œ์šฉ
  final String name, code, amount;
  final bool isInverted; // ์ƒ‰์ƒ ๋ฐ˜์ „ ์—ฌ๋ถ€

  final _blackColor = const Color(0xFF1F2123);

  (์ค‘๋žต)
  color: isInverted ? _blackColor : Colors.white,

9-2. Container

  • <div> ์™€ ๊ฐ™์€ ์—ญํ• ์„ ํ•˜๋Š” Widget
  • child ๋ฅผ ๊ฐ€์ง€๋Š” ๋‹จ์ˆœํ•œ box ๋กœ,๋ฒ„ํŠผ ๋””์ž์ธ ์‹œ ์‚ฌ์šฉํ•˜๊ฒŒ ๋จ
  • ์˜๋ฏธ ์—†์ด ์‚ฌ์šฉ ์‹œ Unnecessary instance of 'Container' ๋ฌธ๊ตฌ ๋œธ > ์Šคํƒ€์ผ ์ง€์ • ํ•„์š”

9-3. Transform

  • Transform.scale : ์•„์ด์ฝ˜์ด overflow ๋˜๋„๋ก scale ์„ค์ • ํ•„์ˆ˜
  • Transform.translate : ์•„์ด์ฝ˜์ด ์ด๋™๋˜๋„๋ก offset ์„ค์ • ํ•„์ˆ˜
    • ํ†ตํ™” ์•„์ด์ฝ˜ ์œ„์น˜ ์กฐ์ •, ์นด๋“œ ๊ฒน์ณ ๋ณด์ด๋„๋ก ์œ„์น˜ ์กฐ์ • ๋“ฑ
  Transform.scale(
    scale: 2.2,
    child: Transform.translate(
      offset: const Offset(-5, 12),

9-4. Row, Column, Stack

  • Row, Column : ๊ฐ€๋กœ, ์„ธ๋กœ๋กœ ์ฐจ๋ก€๋Œ€๋กœ ๋ฐฐ์น˜
  • Stack : ์œ„์ ฏ์„ ์œ„์— ์Œ“์„ ์ˆ˜ ์žˆ๊ฒŒ ํ•จ (๊ฒน์ณ์ง€๋„๋ก)
    • alignment ์†์„ฑ์œผ๋กœ ๋ฐฐ์น˜ํ•  ์ˆ˜ ์žˆ์Œ
child: Stack(
  alignment: Alignment.center,
  children: [
    Align(
      alignment: Alignment.centerLeft,
      child: icon,
    ),
    Expanded(
      child: Text(
        text,
      ),
    ),
  ],

10. Stateful Widget ๊ฐœ๋ฐœ

  • Stateful Widget : ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€๊ฒฝ๋จ์— ๋”ฐ๋ผ ์‹ค์‹œ๊ฐ„์œผ๋กœ UI ์— ๋ฐ˜์˜๋จ
  • ์•„๋ž˜ ๋‘ ๊ฐœ์˜ ๋ถ€๋ถ„์œผ๋กœ ๋‚˜๋‰˜์–ด์ง (ํ•˜๋‹จ ์†Œ์Šค ํ™•์ธ)
    • Stateless Widget ๊ทธ ์ž์ฒด
    • State : Widget ์— ๋“ค์–ด๊ฐˆ ๋ฐ์ดํ„ฐ์™€ UI ๋ฅผ ์ €์žฅ
  • StatelessWidget > Code Action > Convert to StatefulWidget ์œผ๋กœ ์ˆ˜์ • ๊ฐ€๋Šฅ
class App extends StatefulWidget {
  const App({super.key});

  @override
  State<App> createState() => _AppState();
}

class _AppState extends State<App> {
  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
        home: Scaffold(
      backgroundColor: Color(0xFFF4EDDB),
      body: Container(),
    ));
  }
}
  • State ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ”๊ฟ€ ๋•Œ, UI ๊ฐ€ ์ƒˆ๋กœ๊ณ ์นจ ๋˜๋ฉด์„œ ์ตœ์‹  ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด์—ฌ์คŒ
    • ๋ฐ์ดํ„ฐ = class property
  • State ๋Š” build() ํ•จ์ˆ˜๊ฐ€ ํ•„์š”
    • initState(), dispose() ๋“ฑ์˜ ํ•จ์ˆ˜๋„ ์กด์žฌ (ํ•„์ˆ˜ X)

10-1. setState

  • State ํด๋ž˜์Šค์—๊ฒŒ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ๋‹ค๊ณ  ์•Œ๋ฆฌ๋Š” ํ•จ์ˆ˜
  • State ์—๊ฒŒ ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์Œ์„ ์•Œ๋ฆฌ๊ณ  ์Šค์Šค๋กœ ์ƒˆ๋กœ๊ณ ์นจํ•˜๊ฒŒ ํ•จ
  • ํ•จ์ˆ˜ ๋‚ด๋ถ€์— ์ž‘์„ฑํ•˜๊ฒŒ ๋จ

10-2. initState

  • ์ƒํƒœ๋ฅผ ์ดˆ๊ธฐํ™” ํ•˜๊ธฐ ์œ„ํ•œ ๋ฉ”์†Œ๋“œ (Initialize)
    • ๋Œ€๋ถ€๋ถ„ ๋ณ€์ˆ˜(Variable) ์„ ์–ธ์œผ๋กœ ์ดˆ๊ธฐํ™” ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ž˜ ์“ฐ์ง€ ์•Š์Œ
    • ๋ถ€๋ชจ ์š”์†Œ์— ์˜์กดํ•˜๋Š” ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์‹œ ์‚ฌ์šฉ
    • ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™”, API ์—์„œ ์—…๋ฐ์ดํŠธ ์‹œ ์‚ฌ์šฉ
  • initState() ๋Š” build() ๋ณด๋‹ค ๋จผ์ € ํ˜ธ์ถœ๋˜์–ด์•ผ ํ•จ

10-3. dispose

  • ์œ„์ ฏ์ด ์Šคํฌ๋ฆฐ์—์„œ ์ œ๊ฑฐ๋  ๋•Œ ํ˜ธ์ถœ๋˜๋Š” ๋ฉ”์†Œ๋“œ
    • API ์—…๋ฐ์ดํŠธ, ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๋กœ๋ถ€ํ„ฐ ๊ตฌ๋… ์ทจ์†Œ ์‹œ ์‚ฌ์šฉ
    • ์œ„์ ฏ์ด ์œ„์ ฏ ํŠธ๋ฆฌ์—์„œ ์ œ๊ฑฐ๋˜๊ธฐ ์ „์— ๋ฌด์–ธ๊ฐ€ ์ทจ์†Œํ•  ๋•Œ

10-4. IconButton

  • IconButton(onPressed: onClicked, icon: icon)
    • onPressed : ํด๋ฆญํ•  ๋•Œ๋งˆ๋‹ค ์‹คํ–‰๋  ํ•จ์ˆ˜๋ฅผ ํ• ๋‹น
    • ์•„๋ฌด๊ฒƒ๋„ ํ• ๋‹นํ•˜์ง€ ์•Š์„ ๋•Œ๋Š” onPressed: () {}, ๋กœ ์ž‘์„ฑ
  • ์‹คํ–‰๋  ํ•จ์ˆ˜๋Š” _AppState ๋‚ด๋ถ€์— ์ž‘์„ฑ
class _AppState extends State<App> {
  int counter = 0;

  void onClicked() {
    setState(() {
      counter += 1;
    });
  }

  (์ค‘๋žต)
        child: IconButton(
          onPressed: onClicked,

  (์ค‘๋žต)

          Text(
            '$counter',
          ),

11. API ๊ฐœ๋ฐœ

  • import 'package:http/http.dart' as http;
  • HTTP ๊ด€๋ จ ํŒจํ‚ค์ง€ ์„ค์น˜ ํ›„ import -> as ALIAS ๋กœ ์ด๋ฆ„ ์ง€์ •ํ•˜์—ฌ ์‚ฌ์šฉ ๊ฐ€๋Šฅ

11-1. http.get()

  • return ๊ฐ’ : Future<Response>
    • Future : ๋ฏธ๋ž˜์— ๋ฐ›์„ ๊ฐ’์˜ ํƒ€์ž…
    • ๋‚˜์ค‘์— ์™„๋ฃŒ๋  ๊ฐ’์ด์ง€๋งŒ, ์™„๋ฃŒ๋˜๋ฉด Response ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค๋Š” ๋œป

11-1-1. await http.get()

  • async(๋น„๋™๊ธฐ) programming ์ฒ˜๋ฆฌ
  • http.get() ํ•จ์ˆ˜์˜ API ์š”์ฒญ์ด ์ฒ˜๋ฆฌ๋˜๊ณ  ์‘๋‹ต์ด ๋ฐ˜ํ™˜๋  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆผ
    • ์„œ๋ฒ„๊ฐ€ ์‘๋‹ตํ•  ๋•Œ๊นŒ์ง€ ํ”„๋กœ๊ทธ๋žจ์ด ๊ธฐ๋‹ค๋ฆฌ๊ฒŒ ๋จ
  • ๋น„๋™๊ธฐ ํ•จ์ˆ˜(asynchronous function) ๋‚ด์—์„œ๋งŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅ
    • ๋ณดํ†ต Future ํƒ€์ž…์— ์‚ฌ์šฉ๋จ
  void getTodaysToons() async {
    final url = Uri.parse('$baseUrl/$today');
    final response = await http.get(url);
  }
  • ํ•จ์ˆ˜ ์‹คํ–‰ ์™„๋ฃŒ๋  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฌ๊ธฐ ๋•Œ๋ฌธ์— Future ๊ฐ€ ์•„๋‹Œ Response ํƒ€์ž…์œผ๋กœ ๋ฐ”๋กœ ๋ฐ˜ํ™˜

11-2. JSON ๋ฐ์ดํ„ฐ

  • API ํ†ต์‹ ์œผ๋กœ ๋ฐ›์€ ๋ฐ์ดํ„ฐ response.body ๋Š” String ํ˜•์‹
[
  {
    "id": "208",
    "rating": "9.91",
    "date": "24.05.12"
  },
  {
    "id": "207",
    "rating": "9.96",
    "date": "24.05.05"
  },
]
  • ์ด ํ…์ŠคํŠธ๋ฅผ ํด๋ž˜์Šค๋“ค์˜ List ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ์ž‘์—… ํ•„์š” : List<dynamic>
    • String to JSON : jsonDecode(response.body)
    • return ๊ฐ’์ด dynamic ํƒ€์ž…์ด๋ฏ€๋กœ ์–ด๋–ค ํƒ€์ž…์ด๋“  ์ˆ˜์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋จ
  final List<dynamic> webtoons = jsonDecode(response.body);
  for (var webtoon in webtoons) {
    print(webtoon); // webtoon.runtimeType = _JsonMap
  }
  • named constructor(์ด๋ฆ„์ด ์žˆ๋Š” ํด๋ž˜์Šค ์ƒ์„ฑ์ž) ๋กœ ์ดˆ๊ธฐํ™”ํ•˜๊ธฐ
    • Java ์˜ VO ๊ฐ์ฒด ์ƒ์„ฑ ๊ณผ์ •๊ณผ ๋™์ผ (WebtoonModel ์ƒ์„ฑ)
  WebtoonModel.fromJson(Map<String, dynamic> json)
      : id = json['id'],
        title = json['title'],
        thumb = json['thumb'];
  • WebtoonModel ํด๋ž˜์Šค๋กœ ๋งคํ•‘ํ•˜๊ธฐ
  List<WebtoonModel> webtoonInstances = [];
  final List<dynamic> webtoons = jsonDecode(response.body);
  for (var webtoon in webtoons) {
    webtoonInstances.add(WebtoonModel.fromJson(webtoon));
  }

11-3. ๋ฐ์ดํ„ฐ ์ถœ๋ ฅ

  • API ํ†ต์‹ ์„ ํ†ตํ•ด ๋ถˆ๋Ÿฌ์˜จ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ๋ฐฉ๋ฒ•์€ ๋‘ ๊ฐ€์ง€

11-3-1. async & await

  • StatefulWidget ์—์„œ async, await ์‚ฌ์šฉ (์ž˜ ์‚ฌ์šฉํ•˜์ง€ ์•Š์Œ)
  • webtoons, isLoading ๋ณ€์ˆ˜ ์ƒ์„ฑ
  • isLoading ์ƒํƒœ ๋ณ€ํ™” ๋ฐ initState, setState ์‚ฌ์šฉ
class _HomeScreenState extends State<HomeScreen> {
  List<WebtoonModel> webtoons = [];
  bool isLoading = true;

  void waitForWebtoons() async {
    webtoons = await ApiService.getTodaysToons();
    isLoading = false;
    setState(() {});
  }

  @override
  void initState() {
    super.initState();
    waitForWebtoons();
  }

11-3-2. FutureBuilder

  • await ๋ฅผ ์‚ฌ์šฉํ•  ํ•„์š”๊ฐ€ ์—†์Œ
  • StatelessWidget ์—์„œ FutureBuilder ์‚ฌ์šฉ
    • future : Future ๊ฐ’์„ ์ „๋‹ฌ
    • builder : UI ๋ฅผ ๊ทธ๋ ค์ฃผ๋Š” ํ•จ์ˆ˜ (BuildContext, Snapshot ์„ ์ „๋‹ฌ)
  • Snapshot : Future ์˜ ์ƒํƒœ๋ฅผ ์•Œ ์ˆ˜ ์žˆ์Œ (๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•˜๋Š”์ง€, ์˜ค๋ฅ˜ ๋ฐœ์ƒํ–ˆ๋Š”์ง€)
  final Future<List<WebtoonModel>> webtoons = ApiService.getTodaysToons();
  (์ค‘๋žต)
  body: FutureBuilder(
    future: webtoons,
    builder: (context, snapshot) {
      if (snapshot.hasData) {
        return const Text('there is data');
      }
      return const Text('loading...');
    },
  ),
  • snapshot.hasData : false ์ด๋ฉด ๋กœ๋”ฉ ์ค‘ UI, true ์ด๋ฉด if ๋ฌธ ๋‚ด UI ๋กœ ์ „ํ™˜๋จ
  • snapshot.data : API ํ˜ธ์ถœ ํ›„ ๋ฐ›์€ ๋ฐ์ดํ„ฐ

11-4. Parameter ๊ฐ’ ์ „๋‹ฌ

  • getToonById() ๋Š” ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ webtoon.id ๋ฅผ ๋ฐ›์Œ
    • StatelessWidget ์—์„œ webtoonDetail ํ”„๋กœํผํ‹ฐ๋ฅผ ์ดˆ๊ธฐํ™” ์‹œ, webtoon ํ”„๋กœํผํ‹ฐ์— ์ ‘๊ทผ ๋ถˆ๊ฐ€
class DetailScreen extends StatefulWidget {
  final WebtoonModel webtoon;
  (์ค‘๋žต)
}

class _DetailScreenState extends State<DetailScreen> {
  //final Future<WebtoonDetailModel> webtoonDetail = ApiService.getToonById(widget.webtoon.id);
  late Future<WebtoonDetailModel> webtoonDetail; // initState() ์—์„œ define ์˜ˆ์ •

  @override
  void initState() {
    super.initState();
    webtoonDetail = ApiService.getToonById(widget.webtoon.id);
  }
  (์ค‘๋žต)
  • State ์˜ build method ๊ฐ€ StatefulWidget ์˜ data ๋ฅผ ๋ฐ›์•„์˜ค๋Š” ๋ฒ•
    • DetailScreen > webtoon ๋ฐ์ดํ„ฐ๋ฅผ _DetailScreenState ํด๋ž˜์Šค์—์„œ ์‚ฌ์šฉ -> widget ์„ ๋ถ™์ž„
    • late ์ฒ˜๋ฆฌํ•œ ํ›„ initState() ์—์„œ widget.webtoon.id ๋กœ ์‚ฌ์šฉ
  • ์ดˆ๊ธฐํ™”ํ•˜๊ณ  ์‹ถ์€ ๊ฐ’์ด ์žˆ์ง€๋งŒ, constructor ์—์„œ ๋ถˆ๊ฐ€๋Šฅํ•œ ๊ฒฝ์šฐ late ์‚ฌ์šฉ
  • initState() ๋Š” ํ•ญ์ƒ build() ๋ณด๋‹ค ๋จผ์ €, ๋‹จ ํ•œ ๋ฒˆ๋งŒ ์‹คํ–‰๋œ๋‹ค๋Š” ํŠน์ง•

11-5. ListView

  • ๋งŽ์€ ์–‘์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์—ฐ์†์ ์œผ๋กœ ๋‚˜์—ดํ•  ๋•Œ ์‚ฌ์šฉ
  • ์ž๋™์œผ๋กœ Scroll View ๋„ ๊ฐ€์ง (Overflow X)
return ListView(
  children: [
    for (var webtoon in snapshot.data!) Text(webtoon.title)
  ],
);
  • ํ•œ๋ฒˆ์— ๋ชจ๋“  ์•„์ดํ…œ ๋กœ๋”ฉํ•˜๋Š” ๋ฐฉ์‹
  • ์ธ์Šคํƒ€๊ทธ๋žจ
    • ํƒ€์ž„๋ผ์ธ์˜ ๋ชจ๋“  ์‚ฌ์ง„์„ ํ•œ๋ฒˆ์— ๋กœ๋”ฉํ•˜์ง€ ์•Š์Œ
    • ์‚ฌ์šฉ์ž๊ฐ€ ๋ณด๊ณ ์žˆ๋Š” ์‚ฌ์ง„์ด๋‚˜ ์„น์…˜๋งŒ ๋กœ๋”ฉํ•ด์•ผ ํ•จ

11-5-1. ListView.builder

  • ListView : ๋ฉ”๋ชจ๋ฆฌ ๋ฌธ์ œ ๋ฐœ์ƒ
  • ListView.builder : ๋งŽ์€ ์•„์ดํ…œ์— ์ตœ์ ํ™” -> ์‚ฌ์šฉ์ž๊ฐ€ ๋ณด๊ณ  ์žˆ๋Š” ์•„์ดํ…œ๋งŒ build (๋‚˜๋จธ์ง€๋Š” ๋ฉ”๋ชจ๋ฆฌ์—์„œ ์‚ญ์ œ)
    • itemCount : ๋ช‡ ๊ฐœ์˜ ์•„์ดํ…œ์„ build ํ• ์ง€ ์„ค์ • (์ „์ฒด ๊ฐœ์ˆ˜ ์„ค์ • -> ํ™”๋ฉด์— ๋ณด์ด๋Š” ๊ฒƒ๋งŒ ๋จผ์ € build ๋จ)
return ListView.builder(
  scrollDirection: Axis.vertical, // ์Šคํฌ๋กค ๋ฐฉํ–ฅ
  itemCount: snapshot.data!.length,
  itemBuilder: (context, index) {
    var webtoon = snapshot.data![index];
    return Text('$index  ${webtoon.title}');
  },
);

11-5-2. ListView.separated

  • ListView.builder + separatorBuilder ๋กœ ๋ฆฌ์ŠคํŠธ ์•„์ดํ…œ ์‚ฌ์ด ๋ Œ๋”๋ง ๋  Widget ์„ค์ •

12. ํ™”๋ฉด ์ „ํ™˜

  • *.dart ๋กœ ๋งŒ๋“  ํด๋ž˜์Šค๋Š” ๋‹จ์ˆœํ•œ Widget ์˜ ๊ฐœ๋…
  • ์œ„์ ฏ ์ „ํ™˜ ์• ๋‹ˆ๋ฉ”์ด์…˜ ํšจ๊ณผ, ๋‚ด๋น„๊ฒŒ์ด์…˜ ๋ฐ” ๋“ฑ์ด ์ค‘์š”

12-1. GestureDetector

  • ์ด๋ฒคํŠธ ๋ฐœ์ƒ์„ ๊ฐ์ง€ํ•˜๊ธฐ ์œ„ํ•œ ์œ„์ ฏ
    • onTap : ์œ ์ €๊ฐ€ ๋ฒ„ํŠผ์„ ํด๋ฆญํ–ˆ์„ ๋•Œ
return GestureDetector(
  onTap: () {
    print('objects');
  },
  child: Column(
    (์ƒ๋žต)

12-2. Navigator

  • ํŽ˜์ด์ง€ ์ด๋™์ด ํ•„์š”ํ•  ๋•Œ ์‚ฌ์šฉ
  • Navigator.push(context, route)
    • Navigator ๋กœ ์ƒˆ๋กœ์šด route ๋ฅผ push
    • route : StatelessWidget ์„ ์• ๋‹ˆ๋ฉ”์ด์…˜ ํšจ๊ณผ๋กœ ๊ฐ์‹ธ, ์Šคํฌ๋ฆฐ์ฒ˜๋Ÿผ ๋ณด์ด๋„๋ก ํ•จ
  • Navigator.push(context, MaterialPageRoute(builder: builder))
    • builder : route ๋ฅผ ๋งŒ๋“œ๋Š” ํ•จ์ˆ˜
    • StatelessWidget ์œ„์ ฏ์„ ๋ Œ๋”๋ง
  • ๊ธฐ์กด ํ™”๋ฉด์—์„œ ์ด๋™๋˜๋ฏ€๋กœ, Scaffold ๋ Œ๋”๋ง ํ•„์š”
Navigator.push(
  context,
  MaterialPageRoute(
    builder: (context) {
      DetailScreen(webtoon: webtoon);
    },
  ),
);

12-2-1. PageRouteBuilder

  • MaterialPageRoute ๋Œ€์‹  PageRouteBuilder ์‚ฌ์šฉํ•ด๋„ ๋จ
Navigator.push(
  context,
  PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation) {
      return DetailScreen(webtoon: webtoon);
    },
    fullscreenDialog: true,
  ),
);

12-2-2. fullscreenDialog

  • default : ์˜†์œผ๋กœ ์Šค์™€์ดํ”„ ๋˜๋ฉด์„œ ์ƒˆ ํŽ˜์ด์ง€ ๋“ฑ์žฅ
    • ์ขŒ์ธก ์ƒ๋‹จ ์•„์ด์ฝ˜์ด โ† ๋กœ ๋œธ
  • fullscreenDialog: true
    • ์ขŒ์ธก ์ƒ๋‹จ ์•„์ด์ฝ˜์€ X, ์• ๋‹ˆ๋ฉ”์ด์…˜ ํšจ๊ณผ๋„ ๋ณ€๊ฒฝ๋จ

12-3. Hero

  • ํ™”๋ฉด ์ „ํ™˜ ์‹œ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ œ๊ณต
// webtoon_widget.dart > ๋ชฉ๋ก ํŽ˜์ด์ง€์˜ ์ด๋ฏธ์ง€
Hero(
  tag: webtoon.id,
  child: Container(

// detail_screen.dart > ์ƒ์„ธ ํŽ˜์ด์ง€์˜ ์ด๋ฏธ์ง€
Hero(
  tag: webtoon.id,
  child: Container(
  • ํ™”๋ฉด ์ „ํ™˜ ์‹œ, ์ƒˆ๋กœ์šด ์ด๋ฏธ์ง€๋กœ ๋ฎ์–ด๋ฒ„๋ฆฌ๋Š” ๊ฒŒ ์•„๋‹Œ ๊ธฐ์กด ์ด๋ฏธ์ง€๋ฅผ ์›€์ง์ด๋Š” ํšจ๊ณผ ์ฃผ๊ธฐ
    • ๋‘ ๊ฐœ ํ™”๋ฉด์— ๊ฐ๊ฐ ์‚ฌ์šฉ, ๊ฐ๊ฐ์— ๊ฐ™์€ ํƒœ๊ทธ๋ฅผ ์คŒ

12-4. Url Launcher

  • Flutter ์—์„œ ๋ธŒ๋ผ์šฐ์ € ์—ด๊ธฐ
  • ํŒจํ‚ค์ง€ ์„ค์น˜ : https://pub.dev/packages/url_launcher
    • pubspec.yaml > url_launcher: ^6.2.6 ์ž…๋ ฅ
    • Readme ํƒญ ํ™•์ธ > iOS > Info.plist ํŒŒ์ผ ์ˆ˜์ •
    • Readme ํƒญ ํ™•์ธ > Android > AndroidManifest.xml ํŒŒ์ผ ์ˆ˜์ •
    • sms, tel ์€ ๊ฐ๊ฐ ๋ฌธ์ž, ์ „ํ™” ๊ด€๋ จ์ด๋ฏ€๋กœ https ๋ฅผ ์ถ”๊ฐ€ํ•ด์•ผ ํ•จ
onButtonTap() async {
  final url = Uri.parse("https://www.naver.com");
  await launchUrl(url);
}

13. ๊ธฐ๊ธฐ ๋‚ด ๋ฐ์ดํ„ฐ ์ €์žฅ

  • ํ•ธ๋“œํฐ ์ €์žฅ์†Œ์— ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ด์„ ์ˆ˜ ์žˆ์Œ
    • ex. ํ•˜ํŠธ ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ ํ•ธ๋“œํฐ ์ €์žฅ์†Œ์— ์ข‹์•„์š” ํ•œ ์›นํˆฐ ์ €์žฅ
  • ํŒจํ‚ค์ง€ ์„ค์น˜ : https://pub.dev/packages/shared_preferences
    • pubspec.yaml > shared_preferences: ^2.2.3 ์ž…๋ ฅ
    • Readme ํƒญ > ์ž‘๋™ ๋ฐฉ์‹ ํ™•์ธ
  • SharedPreferences ์ดˆ๊ธฐํ™” ํ•„์š” > async, await ํ•„์š”
// SharedPreferences.getInstance()
final SharedPreferences prefs = await SharedPreferences.getInstance();

// initState() ํ™œ์šฉ ์‹œ late ๋ณ€์ˆ˜ ์ง€์ •
late SharedPreferences prefs;

Future initPrefs() async {
  prefs = await SharedPreferences.getInstance();
}

@override
void initState() {
  super.initState();
  initPrefs();
}
  • ๊ฐ’์„ ์ฝ๊ฑฐ๋‚˜ ์ €์žฅํ•  ์ˆ˜ ์žˆ์Œ
await prefs.setInt('counter', 10);
final String? action = prefs.getString('action');

14. Controller

  • Code, Method ๋“ฑ์œผ๋กœ Textfield ์™€ ๊ฐ™์€ ์œ„์ ฏ์„ ์ปจํŠธ๋กคํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์คŒ
    • StatefulWidget > State ํด๋ž˜์Šค ์•ˆ์— Controller ๋ฅผ ์„ ์–ธํ•˜์—ฌ ์‚ฌ์šฉ
class _UsernameScreenState extends State<UsernameScreen> {
  final TextEditingController _usernameController = TextEditingController();

  @override
  void initState() {
    super.initState();
    _usernameController.addListener(() {
      print(_usernameController.text); // ์ž…๋ ฅ๊ฐ’์„ ๋ณด์—ฌ์คŒ
    });
  }

      (์ƒ๋žต)
      TextField(
        controller: _usernameController,
  • initState() > addListener : Textfield ์˜ ํ…์ŠคํŠธ ๋ณ€ํ™” ๋“ฑ์„ ๊ฐ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ํ•„์š”

14-1. Animated~ Widget

  • AnimatedContainer
    • decoration ์ด ๋ณ€๊ฒฝ๋˜์—ˆ์„ ๋•Œ ์• ๋‹ˆ๋ฉ”์ด์…˜ ํšจ๊ณผ๊ฐ€ ์ ์šฉ๋จ
    • AnimatedContainer ์˜ ํšจ๊ณผ๊ฐ€ ์ž์‹์—๊ฒŒ๊นŒ์ง€ ์˜ํ–ฅ์„ ์ฃผ์ง„ ์•Š์Œ
    • ํ•„์ˆ˜ ์†์„ฑ๊ฐ’ : duration: const Duration(milliseconds: 300),
  • AnimatedDefaultTextStyle
    • text ๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์„ ๋•Œ ์• ๋‹ˆ๋ฉ”์ด์…˜ ํšจ๊ณผ๊ฐ€ ์ ์šฉ๋จ
    • ํ•„์ˆ˜ ์†์„ฑ๊ฐ’ : duration, style
child: AnimatedDefaultTextStyle(
  // style : ์• ๋‹ˆ๋ฉ”์ด์…˜ํ™” ์‹œํ‚ค๊ณ  ์‹ถ์€ ๋Œ€์ƒ
  style: TextStyle(
      color: _username.isEmpty
          ? Colors.grey.shade400
          : Colors.white,
      fontWeight: FontWeight.w600,
    ),
  duration: const Duration(milliseconds: 300),
  child: const Text("Next"),

14-1-1. AnimationController

  • Widget ์ด ์•„๋‹Œ Controller ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ์‹
  late final AnimationController _animationController;

  @override
  void initState() {
    super.initState();

    _animationController = AnimationController(
      vsync: this,
      lowerBound: 1.0, // ํฌ๊ธฐ ์ง€์ •
      upperBound: 1.5, // ํฌ๊ธฐ ์ง€์ •
      value: 1.5, // default ํฌ๊ธฐ
      duration: _animationDuration,
    );

    _animationController.addListener(() {
      setState(() {});
    });
  }
  • _animationController.reverse() ์ˆ˜ํ–‰
    • 1.5 => 1.0 ์œผ๋กœ ๊ฐ’์ด ๋ฐ”๋€Œ๊ฒŒ ๋˜๋Š”๋ฐ, build() ๋Š” 1.5, 1.0 ์ผ ๋•Œ๋งŒ ์žฌ์ˆ˜ํ–‰ ๋จ
    • build() ๊ฐ€ ๊ฐ’์ด ๋ฐ”๋€Œ๋Š” ๊ฒƒ์„ ๊ฐ์ง€ํ•˜๊ฒŒ ํ•˜๋ ค๋ฉด? _animationController ์— ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ถ”๊ฐ€
    • ๋ชจ๋“  ๋‹จ๊ณ„์—์„œ build ๋ฉ”์„œ๋“œ๋ฅผ ์‹คํ–‰ํ•˜๊ธฐ ์œ„ํ•ด addListener ๋‚ด๋ถ€์— setState() ์ถ”๊ฐ€
    child: Transform.scale(
      scale: _animationController.value,
  • scale : ์‚ฌ์ด์ฆˆ ์กฐ์ ˆ์„ ์œ„ํ•ด AnimationController ์‚ฌ์šฉ

14-1-2. AnimatedBuilder

  • 14-1-1. ๋Œ€์‹  ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ฐฉ๋ฒ• (_animationController.addListener( ~ ์ฃผ์„ ์ฒ˜๋ฆฌ)
  • AnimatedBuilder ๊ฐ€ ์• ๋‹ˆ๋ฉ”์ด์…˜์˜ ๋ณ€ํ™”๋ฅผ ๊ฐ์ง€ํ•˜๊ณ , builder ๊ฐ€ ์ตœ์‹  ๊ฐ’์œผ๋กœ return ํ•˜๋„๋ก ํ•จ
child: AnimatedBuilder(
  animation: _animationController,
  builder: (context, child) {
    return Transform.scale(
      scale: _animationController.value,
      child: child,
    );
  },

14-2. dispose

  • Widget ์ด ์‚ฌ๋ผ์ง€๋ฉด Controller ๋„ ๋ฉ”๋ชจ๋ฆฌ์—์„œ ์ง€์›Œ์•ผ ํ•จ (Listener ๊ฐ€ ์ž‘๋™ํ•˜๋Š” ์ƒํƒœ)
  @override
  void dispose() {
    // _usernameController ์™€ ์—ฐ๊ด€๋œ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๋ฅผ ๋ชจ๋‘ ์ง€์›€
    _usernameController.dispose();

    super.dispose();
  }
  • super.dispose(); ๋Š” ๋ชจ๋“  ๊ฒƒ์˜ ๋’ค์—, super.initState(); ๋Š” ๋ชจ๋“  ๊ฒƒ์˜ ์•ž์— ์„ ์–ธํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅ

14-3. TextField

  • controller: _emailController : ์œ„์ ฏ์„ ์ปจํŠธ๋กคํ•˜๊ธฐ ์œ„ํ•ด Controller ์ถ”๊ฐ€
  • onSubmitted: (value) => _onSubmit : ํ‚ค๋ณด๋“œ์˜ Done ํด๋ฆญ ์‹œ์—๋„ _onSubmit ์ด ์ž‘๋™ํ•˜๋„๋ก + value ์ž…๋ ฅ๊ฐ’์„ ์ œ๊ณต
  • onEditingComplete: () => _onSubmit : ํ‚ค๋ณด๋“œ์˜ Done ํด๋ฆญ ์‹œ์—๋„ _onSubmit ์ด ์ž‘๋™ํ•˜๋„๋ก + ๋งค๊ฐœ๋ณ€์ˆ˜ ์—†์ด ์‹คํ–‰
  • keyboardType: TextInputType.emailAddress : ํ‚ค๋ณด๋“œ ํƒ€์ž… ์ง€์ •
  • autocorrect: false : ์ž๋™์™„์„ฑ ๋„๊ธฐ
  • hintText: "Email" : ํžŒํŠธ ๋ฌธ์ž์—ด
  • errorText: _isEmailValid() : ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ (์—†์œผ๋ฉด ์—๋Ÿฌ ํ‘œ์‹œ๋˜์ง€ ์•Š์Œ)

14-3-1. ํ‚ค๋ณด๋“œ unfocus

  • ์ž…๋ ฅ ํ•„๋“œ๊ฐ€ ์•„๋‹Œ ์•„๋ฌด ๊ณต๊ฐ„์ด๋‚˜ ํƒญํ•  ๊ฒฝ์šฐ, ํ‚ค๋ณด๋“œ๊ฐ€ ์‚ฌ๋ผ์ง€๋„๋ก ํ•ด์•ผํ•จ
    • focus ๋œ ๊ฒƒ์„ ๋ชจ๋‘ unfocus ์‹œํ‚ค๊ธฐ
void _onScaffoldTap() {
  FocusScope.of(context).unfocus();
}

14-3-2. function ()

  • ํ•จ์ˆ˜์— () ๋ฅผ ๋ถ™์ด๋ฉด ์ฆ‰์‹œ ์‹คํ–‰๋จ
    • _isEmailVaild() : ์ฆ‰์‹œ ์‹คํ–‰๋จ
  • ๋ถ™์ด์ง€ ์•Š์œผ๋ฉด ์ด๋ฒคํŠธ ๋ฐœ์ƒ ์‹œ์—๋งŒ Flutter ๊ฐ€ add
    • _onNextTap : ์œ ์ €๊ฐ€ ํƒญํ•œ ์ˆœ๊ฐ„์—๋งŒ () ๋ฅผ ๋ถ™์ด๊ฒŒ ๋จ

15. Form

  • ๊ณ ์œ  ์‹๋ณ„์ž ์—ญํ• ์„ ํ•˜๋Š” Global Key ํ•„์š”
    • Form ์˜ State ์— ์ ‘๊ทผ ๊ฐ€๋Šฅ / Method Trigger ์‹คํ–‰ ๊ฐ€๋Šฅ
    • Controller ๋ฅผ ์ด์šฉํ•ด ์ถ”์ ํ•  ํ•„์š”๊ฐ€ ์—†์Œ
class _LoginFormScreenState extends State<LoginFormScreen> {
  final GlobalKey<FormState> _formKey = GlobalKey<FormState>();

(์ƒ๋žต)

    child: Form(
      key: _formKey,

15-1. _formKey

  • validate() : ๊ฐ ํ•„๋“œ์˜ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์ˆ˜ํ–‰ (return bool)
  • save() : ๋ชจ๋“  ํ…์ŠคํŠธ ์ž…๋ ฅ์— onSaved ์ฝœ๋ฐฑ ํ•จ์ˆ˜ ์‹คํ–‰
  void _onSubmitTap() {
    if (_formKey.currentState != null) {
      if (_formKey.currentState!.validate()) {
        _formKey.currentState!.save();
      }
    }
  }

15-2. TextFormField

  • validator : String? ์˜ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ, ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€ String? ์„ ๋ฐ˜ํ™˜ (null ์ผ ์ˆ˜๋„ ์žˆ์Œ)
  • onSaved : _formKey.currentState!.save(); ์‹คํ–‰ ์‹œ ์ฝœ๋ฐฑ ํ•จ์ˆ˜
    • newValue : ์ €์žฅ๋œ ์ˆœ๊ฐ„์˜ ์ž…๋ ฅ๊ฐ’
  TextFormField(
    decoration: const InputDecoration( ~ ),
    validator: _chkTextField,
    onSaved: (newValue) {
      if (newValue != null) formData['email'] = newValue;
    },
  ),
  • _chkTextField() ์‹คํ–‰ ๊ฒฐ๊ณผ ๋ฆฌํ„ด๊ฐ’์ด null ์ด๋ฉด, onSaved ์— ์˜ํ•ด formData Map ์— ์ž…๋ ฅ๊ฐ’์ด ์„ธํŒ…๋จ

16. Navigation Bar

  • NavTab : Custom Widget ์ž„
    return Scaffold(
      // screens[_selectedIndex] ๋กœ๋„ ์ž‘์„ฑ ๊ฐ€๋Šฅ
      body: screens.elementAt(_selectedIndex),
      bottomNavigationBar: BottomAppBar(
        child: Row(
          children: [
            NavTab(
              icon: FontAwesomeIcons.house,
              isSelected: _selectedIndex == 0,
              onTap: () => _onNavTap(0),
            ),
            (x4 ๋ฐ˜๋ณต)
          ],
        ),
      ),
    );
  • body : ๊ต์ฒดํ•  Widget ์˜ ๋ฐฐ์—ด index
    • screens.elementAt(_selectedIndex) : NavigationBar ์„ ํƒ๋œ ํ™”๋ฉด๋งŒ์„ ๋ณด์—ฌ์ฃผ๋„๋ก build, ์„ ํƒ๋˜์ง€ ์•Š์€ ์ด์ „ ํ™”๋ฉด์€ Flutter ๊ฐ€ ์—†์• ๊ฒŒ ๋จ
    • Tab Navigation ์€ ์žˆ๋˜ ์ž๋ฆฌ๋ฅผ ๊ธฐ์–ตํ•จ
  • bottomNavigationBar > onTap : ํ˜„์žฌ index _selectedIndex ๊ฐ’์„ ๋ณ€๊ฒฝ

16-1. state

  • StfScreen Widget ์„ NavigationBar ๊ฐ ํƒญ๋งˆ๋‹ค ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋Š” ๊ฒฝ์šฐ, ๊ฐ state ๋ฅผ ํ˜ผ๋™ํ•˜๊ฒŒ ๋จ
  • StfScreen( key: GlobalKey() ) : key ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ, ์„œ๋กœ ๋‹ค๋ฅธ Widget ์ธ ๊ฒƒ์ฒ˜๋Ÿผ ๋ Œ๋”๋งํ•จ
    • ๋‹ค์‹œ ํƒญ์„ ๋Œ์•„์˜ค๋ฉด ์ƒˆ๋กœ build ํ•˜๊ฒŒ ๋จ (ํ™”๋ฉด์„ ์ƒˆ๋กœ ๊ทธ๋ฆผ)
final screens = [
  StfScreen(key: GlobalKey()),
  StfScreen(key: GlobalKey()),
  StfScreen(key: GlobalKey()),
  StfScreen(key: GlobalKey())
];

16-2. Offstage

  • ๋ณด๊ณ  ์žˆ์—ˆ๋˜ ์˜์ƒ์„ ๋‹ค์‹œ fetch ํ•˜์ง€ ์•Š๊ณ , ์‚ฌ์šฉ์ž์˜ ์Šคํฌ๋กค ์œ„์น˜๋„ ๊ธฐ์–ตํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•จ
    • screens.elementAt(0) : screens ์˜ 0๋ฒˆ์งธ ํ™”๋ฉด๋งŒ ๋ Œ๋”๋ง
  • Offstage : Widget ์ด ์•ˆ๋ณด์ด๊ฒŒ ํ•˜๋ฉด์„œ ๊ณ„์† ์กด์žฌํ•˜๊ฒŒ ํ•ด์คŒ
    • Creates a widget that visually hides its child. (์‹ค์ œ๋กœ build ๋˜์–ด render ๋˜๊ณ  ์žˆ์œผ๋‚˜, child ๋ฅผ ๋ณด์ด์ง€ ์•Š๊ฒŒ ์ˆจ๊น€)
    • offstage : bool ๊ฐ’์œผ๋กœ ํ™”๋ฉด์„ ๋ณด์—ฌ์ค„์ง€ ๊ฒฐ์ •
body: Stack(
  children: [
    Offstage(
      offstage: _selectedIndex != 0,
      child: StfScreen(), // GlobalKey ๋ฏธ์‚ฌ์šฉ
    ),
  ],
),
  • ์œ ์ €๊ฐ€ ํ™”๋ฉด์„ ๋ณด๊ณ  ์žˆ๋Š”์ง€ ํ™•์ธ ๊ฐ€๋Šฅ
    • ์‚ฌ์šฉ์ž๊ฐ€ ํ™”๋ฉด์„ ๋‚˜๊ฐ€๋ฉด, ์˜์ƒ์„ ๋ฉˆ์ถ”๊ณ  ๋ชจ๋“  fetching ์„ ์ค‘๋‹จ -> ๋Œ์•„์™”์„ ๋•Œ ์žฌ๊ฐœํ•˜๋„๋ก ํ•จ
  • ํ•œ ํ™”๋ฉด์—์„œ ๋„ˆ๋ฌด ๋งŽ์€ resource ์‚ฌ์šฉ ์‹œ, Widget ์ด ์‚ฌ๋ผ์ง€์ง€ ์•Š๊ณ  ์•ฑ์ด ๋А๋ ค์งˆ ์ˆ˜ ์žˆ์Œ

17. Video Player

  • https://pub.dev/packages/video_player ํŒจํ‚ค์ง€ ์‚ฌ์šฉ
  • pubspec.yaml
    • video_player: 2.4.10 ๋‹ค์šด๋กœ๋“œ
    • ์•„๋ž˜์™€ ๊ฐ™์ด assets ํด๋” ๋˜ํ•œ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Œ
assets:
  - assets/videos/

17-1. Visibility Detector

  • https://pub.dev/packages/visibility_detector ํŒจํ‚ค์ง€ ์‚ฌ์šฉ
  • pubspec.yaml > visibility_detector: 0.3.3 ๋‹ค์šด๋กœ๋“œ
  • ์Šคํฌ๋กค์„ ๋‚ด๋ฆฌ๋Š” ์™€์ค‘์— ์˜์ƒ์ด ์žฌ์ƒ๋˜๋Š” ๋ฌธ์ œ ํ•ด๊ฒฐ
    • ์Šคํฌ๋กค์„ ์™„์ „ํžˆ ๋‚ด๋ฆฐ ๋‹ค์Œ ์˜์ƒ์ด ์žฌ์ƒ๋˜๋„๋ก ๋ณ€๊ฒฝํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ
    • ํ•œ ๋ฒˆ์— ํ•˜๋‚˜์˜ ๋น„๋””์˜ค๋งŒ ์žฌ์ƒํ•˜๊ธฐ ์œ„ํ•ด
return VisibilityDetector(
  key: Key("${widget.index}"),
  onVisibilityChanged: (info) {
    print("${widget.index} is ${info.visibleFraction * 100} Visible");
  },
  child: Stack(
  • Key("${widget.index}") : ์ด์ „ ํ™”๋ฉด์—์„œ ๋ฐ›์€ index ๊ฐ’์„ Key ๋กœ
  • onVisibilityChanged > info.visibleFraction : Widget ์ด ์–ผ๋งˆ๋‚˜ ๋ณด์ด๋Š”์ง€ ๋‚˜ํƒ€๋‚ด๋Š” 0 ์ด์ƒ 1 ์ดํ•˜ ๋ฒ”์œ„์˜ ์ˆ˜ (%)

18. CustomScrollView

  • slivers : ์‚ฌ์šฉ์ž๊ฐ€ ์Šคํฌ๋กคํ•  ์ˆ˜ ์žˆ๋Š” ๊ตฌ์—ญ (ํŠน์ •ํ•œ sliver widget ๋ชฉ๋ก์„ ๋„ฃ์Œ)
  • Sliver* ์•ˆ์— ๋˜ ๋‹ค๋ฅธ Sliver* ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Œ

18-1. slivers

18-1-1. SliverAppBar

  • ์•„๋ž˜๋กœ ์Šคํฌ๋กคํ•ด๋„ ๋ณผ ์ˆ˜ ์žˆ๋Š” ์ƒ๋‹จ ์ œ๋ชฉ ๋ฐ”
  • snap, floating, stretch, pinned : SliverAppBar ๊ฐ€ ์ˆจ๊ฒจ์ง€๊ฑฐ๋‚˜ ๋ณด์—ฌ์ง€๋Š” ๋ฐฉ์‹
  • collapsedHeight : ํ™”๋ฉด์„ ์“ธ์–ด์˜ฌ๋ฆฌ๋ฉด 80px๊นŒ์ง€ ์ค„์–ด๋“ค์—ˆ๋‹ค๊ฐ€ ์‚ฌ๋ผ์ง
return CustomScrollView(
  slivers: [
    SliverAppBar(
      snap: true, // ์‚ด์ง๋งŒ ์˜ฌ๋ผ๊ฐ€๋„ appbar ์ „์ฒด๊ฐ€ ๋ฐ”๋กœ ๋ณด์—ฌ์ง
      floating: true, // ์Šคํฌ๋กค์„ ๋‚ด๋ ธ๋‹ค๊ฐ€ ์˜ฌ๋ผ๊ฐ€๋ฉด ๋‹ค์‹œ ๋ณด์—ฌ์ง
      stretch: true,
      pinned: true, // FlexibleSpaceBar ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ๊ฒŒ ์œ ์ง€
      collapsedHeight : 80,
      expandedHeight: 200,
      flexibleSpace: FlexibleSpaceBar(
        stretchModes: const [
          StretchMode.blurBackground,
          StretchMode.zoomBackground,
          StretchMode.fadeTitle,
        ],
        background: Image.asset(
          "assets/images/kota.jpg",
          fit: BoxFit.cover,
        ),
        title: const Text("Hello!"),
        titlePadding:
            const EdgeInsets.symmetric(horizontal: 0, vertical: 10),
        centerTitle: true,
      ),

18-1-2. SliverFixedExtentList

  • childCount : ๋ชฉ๋ก ์•„์ดํ…œ ๊ฐœ์ˆ˜
  • itemExtent : ์•„์ดํ…œ์˜ ํฌ๊ธฐ (height)
    SliverFixedExtentList(
      delegate: SliverChildBuilderDelegate(
        childCount: 20, // 20๊ฐœ ์ œํ•œ
        (context, index) => Container(
          color: Colors.amber[100 * (index % 9)],
          child: Align(
            alignment: Alignment.center,
            child: Text("Item $index"),
          ),
        ),
      ),
      itemExtent: 100, // item ์˜ height
    ),

18-1-3. SliverGrid

  • childCount : ๊ทธ๋ฆฌ๋“œ ์•„์ดํ…œ ๊ฐœ์ˆ˜
    SliverGrid(
      delegate: SliverChildBuilderDelegate(
        childCount: 50, // 50๊ฐœ ์ œํ•œ
        (context, index) => Container(
          color: Colors.blue[100 * (index % 9)],
          child: Align(
            alignment: Alignment.center,
            child: Text("Item $index"),
          ),
        ),
      ),
      // ํ—ˆ์šฉ๋œ ๋งŒํผ์˜ ๋ฌดํ•œํ•œ grid ๋ฅผ ์ƒ์„ฑ
      gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
        maxCrossAxisExtent: 100,
        mainAxisSpacing: Sizes.size20,
        crossAxisSpacing: Sizes.size20,
        childAspectRatio: 1,
      ),
    ),

18-1-4. SliverPersistentHeader

  • ์Šคํฌ๋กค์„ ๋‚ด๋ฆฌ๋Š” ์ค‘ ์ด ์˜์—ญ์— ๊ฑธ๋ฆฌ๋ฉด pinned ์— ์˜ํ•ด CustomDelegate() ์˜์—ญ์ด ๊ณ ์ •๋จ
    SliverPersistentHeader(
      delegate: CustomDelegate(),
      pinned: true,
    ),
  • CustomDelegate ํด๋ž˜์Šค๋Š” SliverPersistentHeaderDelegate ๋ฅผ extends ํ•ด์„œ ์‚ฌ์šฉํ•ด์•ผ ํ•จ
class CustomDelegate extends SliverPersistentHeaderDelegate {
  @override
  Widget build(
      BuildContext context, double shrinkOffset, bool overlapsContent) {
    return Container(
      color: Colors.lightGreen,
      // FractionallySizedBox : ๋ถ€๋ชจ๋กœ๋ถ€ํ„ฐ ์ตœ๋Œ€ํ•œ ๋งŽ์€ ๊ณต๊ฐ„์„ ์ฐจ์ง€
      child: const FractionallySizedBox(
        heightFactor: 1, // 100% (maxExtent)
        child: Center(
          child: Text(
            'SliverPersistentHeader!!!!!',
            style: TextStyle(
              color: Colors.white,
            ),
          ),
        ),
      ),
    );
  }

  @override
  double get maxExtent => 150; // ์ตœ๊ณ  ๋†’์ด

  @override
  double get minExtent => 80; // ์ตœ์ € ๋†’์ด

  // SliverPersistentHeader ๊ฐ€ ๋ณด์—ฌ์ ธ์•ผ ๋˜๋Š”์ง€ ์•Œ๋ ค์ฃผ๋Š” method
  @override
  bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
    // maxExtent, minExtent ๋ณ€๊ฒฝ ์‹œ์—๋Š” true ๋ฐ˜ํ™˜
    // build ์—์„œ ์™„์ „ํžˆ ๋‹ค๋ฅธ widget tree ๋ฆฌํ„ด ์‹œ false ๋ฐ˜ํ™˜
    return false;
  }
}

18-1-5. SliverToBoxAdapter

  • ์ผ๋ฐ˜์ ์ธ Flutter Widget ์„ render ํ•  ๋•Œ ์‚ฌ์šฉ
    SliverToBoxAdapter(
      child: Column(
        children: [
          CircleAvatar(
            backgroundColor: Colors.red,
            radius: 20,
          )
        ],
      ),
    ),

18-2. NestedScrollView

  • ์—ฌ๋Ÿฌ ๊ฐœ์˜ ์Šคํฌ๋กค ๊ฐ€๋Šฅํ•œ View ๋“ค์„ ๋„ฃ์„ ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์คŒ
  • ๋ชจ๋“  scroll position ๋“ค์„ ์—ฐ๊ฒฐํ•ด์คŒ
    • SliverAppBar, TabBar ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ

19. GoRouter

  • pubspec.yaml > go_router: 6.0.2 ์ž…๋ ฅ
    • 12. ํ™”๋ฉด ์ „ํ™˜ ๋ณด๋‹ค ๋ณต์žกํ•˜๊ณ  ๋‹ค์–‘ํ•œ ํ™œ์šฉ์ด ๊ฐ€๋Šฅํ•œ ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉ
    • ๋ฉ€ํ‹ฐ ํ”Œ๋žซํผ ํ™˜๊ฒฝ์—์„œ ์‚ฌ์šฉํ•˜๊ธฐ ์ ํ•ฉ (Navigator.pushNamed : ๋ธŒ๋ผ์šฐ์ €์—์„œ ์‚ฌ์šฉ ์‹œ ์•ž์œผ๋กœ ๊ฐ€๊ธฐ ๋ฒ„ํŠผ ์ง€์› X, ๋น„์ถ”์ฒœ)
    • /video/1 ๊ณผ ๊ฐ™์ด ํŒŒ๋ผ๋ฏธํ„ฐ๋„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•จ

19-1. GoRouter ์‚ฌ์šฉ ํด๋ž˜์Šค

  • ์•„๋ž˜์™€ ๊ฐ™์ด ํด๋ž˜์Šค๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ์‚ฌ์šฉํ•จ
  • routes ๋Š” ์ค‘์ฒฉํ•˜์—ฌ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Œ (nested routes)
    • users/:id/settings ๊ฐ™์€ ๊ฒฝ์šฐ ๋ชจ๋“  route ๋ฅผ ์ ์ง€ ์•Š๊ณ  ์ค‘์ฒฉ์‹œ์ผœ ํ•ด๊ฒฐ
final router = GoRouter(
  routes: [
    GoRoute(
      path: SignUpScreen.routeURL, // String ๊ฐ’
      builder: (context, state) => const SignUpScreen(),
      routes: [
        GoRoute(
          path: UsernameScreen.routeURL, // ์ด๋•Œ๋Š” ์Šฌ๋ž˜์‹œ ์ œ๊ฑฐ
          builder: (context, state) => const UsernameScreen(),
        ),
      ],
    ),
    GoRoute(
      path: "/users/:username", // parameter ๋ฅผ ๋ฐ›์•„์˜ด
      builder: (context, state) {
        final username = state.params['username'];
        return UserProfileScreen(username: username!);
      },
    ),
  ],
);
  • main.dart > MaterialApp.router ์‚ฌ์šฉ, routerConfig ์†์„ฑ๊ฐ’์œผ๋กœ ์—ฐ๊ฒฐ
return MaterialApp.router(
  routerConfig: router,

19-1-1. GoRouter name

  • path ๋Œ€์‹  name ์‚ฌ์šฉ ๊ฐ€๋Šฅ
    // router.dart
    GoRoute(
      name: UsernameScreen.routeName,
      path: UsernameScreen.routeURL,

    // sign_up_screen.dart
    context.pushNamed(UsernameScreen.routeName);
    context.goNamed(UsernameScreen.routeName);

19-2. push, pop, go

  • go_router ํŒจํ‚ค์ง€๊ฐ€ context ๋ฅผ ํ™•์žฅ์‹œํ‚ด
  • context.push(LoginScreen.routeURL);, context.pop();
    • ์•ž์œผ๋กœ, ๋’ค๋กœ๊ฐ€๊ธฐ ๋ฒ„ํŠผ์œผ๋กœ ์™”๋‹ค๊ฐ”๋‹คํ•  ์ˆ˜ ์žˆ์Œ (Stack ๊ตฌ์กฐ)
  • context.go(LoginScreen.routeURL);
    • Stack ์— ๊ด€๋ จ๋œ ๊ฒƒ์€ ์ „๋ถ€ ๋ฌด์‹œ (Screen, Navigation)
    • Stack ์— ๊ด€๊ณ„ ์—†์ด ๋ณ„๋„์˜ ์œ„์น˜๋กœ ์ด๋™์‹œํ‚ด (๋’ค๋กœ๊ฐ€๊ธฐ ํ•  ์ˆ˜ ์—†์Œ)
    • back ๋ฒ„ํŠผ์ด ํ•„์š” ์—†์„ ๋•Œ ์‚ฌ์šฉ

19-3. queryParams

  • :username : Path Parameter
  • ~/users/ynjch?show=likes : show ๋Š” Query Parameter
    GoRoute(
      path: "/users/:username", // parameter ๋ฅผ ๋ฐ›์•„์˜ด
      builder: (context, state) {
        final username = state.params['username'];
        final tab = state.params['show'];
        return UserProfileScreen(username: username!, tab: tab!);
      },
    )

19-4. Extra Parameter

  • URL ์— ๋‹ด์ง€ ์•Š๊ณ  ๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ์Œ
  • extra : ์–ด๋–ค ํ˜•ํƒœ์˜ ๋ฐ์ดํ„ฐ๋“  ์ „๋‹ฌ ๊ฐ€๋Šฅ
    context.push(
      EmailScreen.routeURL,
      extra: EmailScreenArgs(username: _username),
    );
  • router.dart > ๋‹ค์Œ๊ณผ ๊ฐ™์ด extra ๋ฅผ ๋ฐ›์•„์˜ด
    GoRoute(
      path: EmailScreen.routeURL,
      builder: (context, state) {
        final args = state.extra as EmailScreenArgs;
        return EmailScreen(
          username: args.username,
        );
      },
    ),

20. Video

  • ํ„ฐ๋ฏธ๋„ > flutter pub add camera ์ž…๋ ฅํ•˜์—ฌ camera ํŒจํ‚ค์ง€ ๋‹ค์šด๋กœ๋“œ (camera: ^0.11.0+1)
  • Android > Change the minimum Android sdk version to 21 (or higher) in your android/app/build.gradle file.
    • ~\tiktok_clone\android\app\build.gradle > minSdkVersion 21 ๋กœ ๋Œ€์ฒด
  • iOS > Add two rows to the ios/Runner/Info.plist
    • ios/Runner/Info.plist > ์•„๋ž˜ ๋‚ด์šฉ ์ถ”๊ฐ€
<key>NSCameraUsageDescription</key>
<string>your usage description here</string>
<key>NSMicrophoneUsageDescription</key>
<string>your usage description here</string>

20-1. permission_handler

    final cameraPermission = await Permission.camera.request();
    final micPermission = await Permission.microphone.request();

20-2. gallery_saver

  • ๋ฏธ๋””์–ด ํŒŒ์ผ ์ €์žฅ ํŒจํ‚ค์ง€ ์„ค์น˜ : https://pub.dev/packages/gallery_saver
  • pubspec.yaml > gallery_saver: 2.3.2 ์ž…๋ ฅ
  • ์‚ฌ์šฉ ์ „ ๊ถŒํ•œ ์š”์ฒญ ํ•„์š” (Readme ํƒญ ํ™•์ธ)
    • Android : ~\tiktok_clone\android\app\src\main\AndroidManifest.xml ์„ค์ •
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <application
        android:label="tiktok_clone"
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher">
(์ƒ๋žต)
  • iOS : ~\tiktok_clone\ios\Runner\Info.plist ์„ค์ •
	<key>NSPhotoLibraryUsageDescription</key>
	<string>This app required NSPhotoLibraryUsageDescription permission</string>

20-3. didChangeAppLifecycleState

  • Application ์ด ์ œ๊ฑฐ๋˜์—ˆ์„ ๋•Œ(๋น„ํ™œ์„ฑํ™”), ์œ ์ €๊ฐ€ ์•ฑ์„ ๋– ๋‚ฌ์„ ๋•Œ ์ˆ˜๋™์œผ๋กœ ๊ฐ์ง€ํ•ด์•ผ ํ•จ
  • https://docs.flutter.dev/release/breaking-changes/add-applifecyclestate-hidden
    • with WidgetsBindingObserver ์ถ”๊ฐ€
  • initState() > WidgetsBinding.instance.addObserver(this);
    • ์œ ์ €๊ฐ€ Application ์—์„œ ๋ฒ—์–ด๋‚˜๋ฉด ์•Œ ์ˆ˜ ์žˆ๋„๋ก ํ•จ
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.inactive) {
      _cameraController.dispose();
    } else if (state == AppLifecycleState.resumed) {
      initCamera();
    }
  }
  • didChangeAppLifecycleState() : ์‹œ์Šคํ…œ์ด ์•ฑ์„ background ๋กœ ๋ณด๋‚ด๊ฑฐ๋‚˜ ๋‹ค์‹œ ๋Œ์•„์˜ฌ ๋•Œ ํ˜ธ์ถœ

20-4. ImagePicker

  • https://pub.dev/packages/image_picker ํŒจํ‚ค์ง€ ๋‹ค์šด๋กœ๋“œ
  • pubspec.yaml > image_picker: 0.8.6+1 ์ž…๋ ฅ
    • ~\tiktok_clone\ios\Runner\Info.plist > iOS ๊ถŒํ•œ ์„ค์ • ํ•„์š”
    • Android ๋Š” ๋ณ„๋„ ์„ค์ •ํ•  ํ•„์š” ์—†์Œ
	<key>NSCameraUsageDescription</key>
	<string>your usage description here</string>
	<key>NSMicrophoneUsageDescription</key>
	<string>your usage description here</string>
	<key>NSPhotoLibraryUsageDescription</key>
	<string>This app required NSPhotoLibraryUsageDescription permission</string>
  • final video = await ImagePicker().pickVideo(source: ImageSource.gallery);

21. InheritedWidget (๋น„์ถ”์ฒœ)

  • State ๊ด€๋ฆฌ
    • ์•ฑ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ฒด๊ณ„์ ์œผ๋กœ ์ •๋ฆฌ, ๋ฐ์ดํ„ฐ์˜ ๋ณ€๊ฒฝ์„ ๊ฐ์ง€, ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š” ๋ฐฉ์‹
    • ์œ„์ ฏ๊ณผ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถ„๋ฆฌํ•˜๋Š” ๋ชฉ์ ์œผ๋กœ ์‚ฌ์šฉ
  • InheritedWidget : ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ๊ฐ€์žฅ ์‰ฌ์šด ๋ฐฉ๋ฒ•
  • ์œ„์ ฏ ํŠธ๋ฆฌ ๋งจ ์œ„์—์„œ ์ •์˜๋œ ๊ฐ’๋„ ์ ‘๊ทผ ๊ฐ€๋Šฅ (๋ถ€๋ชจ/์ž์‹ ๊ฐ„ ์ „๋‹ฌํ•  ํ•„์š” X)
  • ํฌ๊ณ  ์š”๊ตฌ์‚ฌํ•ญ์ด ๋งŽ์€ ์•ฑ์—๋Š” ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์„ ์ถ”์ฒœ
class VideoConfig extends InheritedWidget {
  (์ƒ๋žต)
  final bool autoMute = false;

  static VideoConfig of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<VideoConfig>()!;
  }

  @override
  bool updateShouldNotify(covariant InheritedWidget oldWidget) {
    return true;
  }
}
  • updateShouldNotify
    • ์œ„์ ฏ์„ rebuild ํ• ์ง€ ๋ง์ง€๋ฅผ ์ •ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์คŒ (์ƒ์†ํ•˜๋Š” ์œ„์ ฏ๋“ค๋„ rebuild ํ•ด์•ผํ•  ์ˆ˜๋„ ์žˆ์Œ)
    • ์ด ์œ„์ ฏ์„ ๋‹ค์‹œ render ํ•˜๋ฉด, oldWidget ์˜ state ๋ฅผ ๊ฐ€์งˆ ์ˆ˜ ์žˆ์Œ
    • ์ด ์œ„์ ฏ์„ ์ƒ์†ํ•˜๋Š” ์œ„์ ฏ๋“ค์—๊ฒŒ ์•Œ๋ ค์ค„ ๊ฒƒ์ธ๊ฐ€๋ฅผ ๊ฒฐ์ •
  • dependOnInheritedWidgetOfExactType
    • VideoConfig ๋กœ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋Š” ์ง์ ‘์ ์ธ ๋งํฌ ํ•„์š”
    • dependOnInheritedWidgetOfExactType : VideoConfig ๋ผ๋Š” ์ด๋ฆ„์˜ InheritedWidget ์„ ๊ฐ€์ ธ์˜ค๋„๋ก ํ•จ
    • of Constructor ์ƒ์„ฑํ•˜์—ฌ ๊ฐ„๋‹จํ•˜๊ฒŒ ์ ‘๊ทผํ•˜๊ฒŒ ํ•จ (ํ™”๋ฉด๋งˆ๋‹ค ๊ธธ๊ฒŒ ์ž‘์„ฑํ•  ํ•„์š” X)
theme: ThemeData(
        primaryColor: const Color(0xFFE9435A),
  • ThemeData.of(context), MediaQuery.of(context) ๋„ dependOnInheritedWidgetOfExactType ์‚ฌ์šฉ

22. ChangeNotifier

  • ํ™”๋ฉด์ด 10๊ฐœ ๋ฏธ๋งŒ + API ์—์„œ ๋ฐ›์•„์˜ฌ ๊ฒƒ์ด ๋งŽ๊ฑฐ๋‚˜, ๋ฉ”์†Œ๋“œ, ๋ฐ์ดํ„ฐ๊ฐ€ ๋งŽ์„ ๋•Œ ์ถ”์ฒœ
class VideoConfig extends ChangeNotifier {
  bool autoMute = true;

  void toggleAutoMute() {
    autoMute = !autoMute;
    notifyListeners();
  }
}
  • notifyListeners() : ํŠน์ • ๋ฐ์ดํ„ฐ ๊ฐ’์ด ๋ณ€๊ฒฝ๋˜์—ˆ๋‹ค๊ณ  ์•Œ๋ ค์ฃผ๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ

22-1. ํ™”๋ฉด์—์„œ ์‚ฌ์šฉํ•˜๊ธฐ

22-1-1. AnimatedBuilder

AnimatedBuilder(
  animation: videoConfig,
  builder: (context, child) => SwitchListTile.adaptive(
    value: videoConfig.autoMute,
    onChanged: (value) => videoConfig.toggleAutoMute(),
  • AnimatedBuilder
    • ChangeNotifier ์™€ ๊ฐ™์ด ์“ฐ์ž„ => ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณด์—ฌ์ง€๋Š” ํ™”๋ฉด์—์„œ ์‚ฌ์šฉ
    • ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ๊ฐ’ ๋ณ€๊ฒฝ ์•Œ๋ฆผ์—๋„ ์‚ฌ์šฉ๋จ
    • ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์‹œ ์ด ๋ถ€๋ถ„๋งŒ ์ƒˆ๋กœ rebuild ๋˜๊ธฐ ๋•Œ๋ฌธ์— InheritedWidget ๋ณด๋‹ค ์œ ์šฉํ•จ

22-1-2. addListener()

  bool _autoMute = videoConfig.autoMute;

  @override
  void initState() {
    super.initState();

    videoConfig.addListener(() {
      setState(() {
        _autoMute = videoConfig.autoMute;
      });
    });
  }
  • notifyListeners() ๋ฅผ ๋“ฃ๊ธฐ ์œ„ํ•ด initState() ์— ๋ฆฌ์Šค๋„ˆ ์ถ”๊ฐ€
  • ๊ฐ’์ด ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ์—…๋ฐ์ดํŠธ๋ฅผ ๋ฐ›์Œ

22-2. ValueNotifier

final videoConfig = ValueNotifier(true);
  • ๊ฐ’์ด ํ•˜๋‚˜๊ณ  true, false ๋“ฑ ๊ฐ„๋‹จํ•œ ๊ตฌ๋ถ„๋งŒ ํ•„์š”ํ•œ ๊ฒฝ์šฐ ChangeNotifier ๋ณด๋‹ค ์งง์€ ์ฝ”๋“œ๋กœ ์‚ฌ์šฉ ๊ฐ€๋Šฅ
ValueListenableBuilder(
  valueListenable: videoConfig,
  builder: (context, value, child) => SwitchListTile.adaptive(
    value: videoConfig.value,
    onChanged: (value) => videoConfig.value = !videoConfig.value,
  • AnimatedBuilder ๋Œ€์‹  ValueListenableBuilder ์‚ฌ์šฉ ๊ฐ€๋Šฅ

23. Provider (๊ถŒ์žฅ)

class VideoConfig extends ChangeNotifier {
  bool isMuted = false;

  void toggleIsMuted() {
    isMuted = !isMuted;
    notifyListeners();
  }
}
  • wrapper around InheritedWidget -> InheritedWidget ๋ฅผ ์‰ฝ๊ฒŒ ์‚ฌ์šฉํ•˜๊ณ  ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•จ
  • ์‚ฌ์šฉ์ž๊ฐ€ ๋ณด๋Š” UI ์œ„์ ฏ๋งŒ ์žˆ๋Š” ์•ฑ์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Œ => ๊ธฐ๋Šฅ์€ ChangeNotifier ๋กœ ๋ถ„๋ฆฌ

23-1. ChangeNotifierProvider

  Widget build(BuildContext context) {
    // VideoConfig ChangeNotifier ๋ฅผ ์•ฑ ์ „์ฒด์— ์ œ๊ณต
    return ChangeNotifierProvider(
      create: (context) => VideoConfig(),
      child: MaterialApp.router(
  • Provider : The most basic form of provider. It takes a value and exposes it, whatever the value is.
  • ChangeNotifierProvider : A specification of ListenableProvider for ChangeNotifier. It will automatically call ChangeNotifier.dispose when needed.
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(
          create: (context) => VideoConfig(),
        ),
      ],
      child: MaterialApp.router(
  • Provider ๊ฐ€ ๋งŽ์„ ๋•Œ๋Š” MultiProvider ์•ˆ์— ๋ฆฌ์ŠคํŠธ๋กœ ๊ธฐ์žฌ

23-2. context.watch

SwitchListTile.adaptive(
  value: context.watch<VideoConfig>().isMuted,
  onChanged: (value) => context.read<VideoConfig>().toggleIsMuted(),
  • ๊ฐ€์ ธ๋‹ค ์“ธ ๋•Œ๋Š” context.watch<VideoConfig>() ๋กœ ์ž‘์„ฑ
    • watch : ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ์žˆ์œผ๋ฉด rebuild (ํ”„๋กœํผํ‹ฐ ๊ฐ’์— ์‚ฌ์šฉ)
    • read : ๋ฉ”์†Œ๋“œ ์ ‘๊ทผ ์‹œ ์‚ฌ์šฉ

24. MVVM ์•„ํ‚คํ…์ฒ˜

  • ChangeNotifier, Provider ์™€ ์ž˜๋งž๋Š” ์•„ํ‚คํ…์ฒ˜๋กœ ์„ ์ •
  • Model, View, View Model ๋กœ ๊ตฌ์„ฑ๋จ
    • View : UI ํ‘œํ˜„, ์‚ฌ์šฉ์ž ์ž…๋ ฅ
    • Model : ๋ฐ์ดํ„ฐ
    • View Model : ํ™”๋ฉด๊ณผ ๋ฐ์ดํ„ฐ๋ฅผ ์—ฐ๊ฒฐ => ํ™”๋ฉด์œผ๋กœ๋ถ€ํ„ฐ ์ด๋ฒคํŠธ๋ฅผ ๋ฐ›์•„ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€๊ฒฝํ•˜๊ณ  ์ด๋ฅผ ํ™”๋ฉด์— ์•Œ๋ ค์คŒ (ChangeNotifier)
    • Repository : ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๊ณ  ๊ฐ€์ ธ์˜ค๋Š” ์—ญํ• 
      • ๊ธฐ๊ธฐ ์ €์žฅ์†Œ์— ์ €์žฅ => ๋ฐ์ดํ„ฐ๋ฅผ ๋””์Šคํฌ์— ์ €์žฅ(persist)ํ•˜๊ณ , ๋””์Šคํฌ์—์„œ ๊ฐ€์ ธ์˜ด(read)
      • Firebase ์™€ ํ†ต์‹ 

24-1. SharedPreferences

24-2. Model, View, View Model

24-2-1. Model

class PlaybackConfigModel {
  bool muted;

  PlaybackConfigModel({
    required this.muted,
  });
}

24-2-2. Repository

  • SharedPreferences ์ด์šฉํ•˜์—ฌ ๊ธฐ๊ธฐ ๋‚ด ์ €์žฅ์†Œ์— ๋ฐ์ดํ„ฐ ์ €์žฅ
class PlaybackConfigRepository {
  static const String _muted = "muted";

  final SharedPreferences _preferences;

  PlaybackConfigRepository(this._preferences);

  // ์Œ์†Œ๊ฑฐ ๊ด€๋ จ ๋ฐ์ดํ„ฐ ๋””์Šคํฌ์— ์ €์žฅ
  Future<void> setMuted(bool value) async {
    _preferences.setBool(_muted, value);
  }

  // ์Œ์†Œ๊ฑฐ ๊ด€๋ จ ๋ฐ์ดํ„ฐ ๋””์Šคํฌ์—์„œ ์ฝ๊ธฐ
  bool isMuted() {
    return _preferences.getBool(_muted) ?? false;
  }
}

24-2-3. View Model

  • PlaybackConfigViewModel ํด๋ž˜์Šค construct ์‹œ์ ์— Repository ์š”๊ตฌ
class PlaybackConfigViewModel extends ChangeNotifier {
  final PlaybackConfigRepository _repository;

  late final PlaybackConfigModel _model = PlaybackConfigModel(
    muted: _repository.isMuted(),
    autoplay: _repository.isAutoplay(),
  );

  PlaybackConfigViewModel(this._repository);

  // _model ์€ private ํ•˜๊ฒŒ ๋‘๊ณ  getter ์‚ฌ์šฉ
  bool get muted => _model.muted;

  void setMuted(bool value) {
    _repository.setMuted(value); // ๋””์Šคํฌ์— ์ €์žฅ(persist)
    _model.muted = value; // Model ์ˆ˜์ •
    notifyListeners(); // ๋ฐ์ดํ„ฐ๋ฅผ listen ํ•˜๊ณ  ์žˆ๋Š” screen ๋“ค์—๊ฒŒ ์ „๋‹ฌ
  }
}

24-3. Provider ์ดˆ๊ธฐํ™”

  • main.dart
    • PlaybackConfigViewModel ์‚ฌ์šฉ์„ ์œ„ํ•ด Repository ๋ฅผ ๋ณด๋‚ด์ค˜์•ผ ํ•จ
    • Repository ์‚ฌ์šฉ์„ ์œ„ํ•ด SharedPreferences ์˜ Instance ์— ์ ‘๊ทผ์ด ํ•„์š”
  // Provider ์ดˆ๊ธฐํ™”
  final preferences = await SharedPreferences.getInstance();
  final repository = PlaybackConfigRepository(preferences);

  runApp(MultiProvider(
    providers: [
      ChangeNotifierProvider(
        create: (context) => PlaybackConfigViewModel(repository),
      ),
    ],
    child: const TikTokApp(),
  ));

25. Riverpod

  • View ๋กœ์ง๊ณผ Business ๋กœ์ง์„ ๊ฐ๊ฐ ๋‹ค๋ฅธ ๊ณณ์— ์œ„์น˜ํ•˜๊ฒŒ ๋งŒ๋“ค์–ด์คŒ
  • dependency ์ฃผ์ž…ํ•˜์—ฌ Provider ๋“ค์„ ์–ด๋””์—์„œ๋‚˜ ์ฝ์„ ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์คŒ
  • ์—ฌ๋Ÿฌ ๊ฐœ์˜ Provider ๊ฐ€ ๋™์ผํ•œ type ์˜ ๊ฐ’์„ ๋…ธ์ถœํ•  ์ˆ˜ ์žˆ์Œ (๋ณต์ œ ๊ฐ€๋Šฅ)
  • ์œ„์ ฏ ํŠธ๋ฆฌ ๋ฐ–์— ์œ„์น˜ํ•  ์ˆ˜ ์žˆ์Œ (์ƒ์† ๋ฐ›์€ ์œ„์ ฏ ํŠธ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•  ํ•„์š” X)
  • Provider ์˜ ํ›„์†์ž‘ / Provider ์™€ ์œ ์‚ฌ
    • ๊ฐ์ฒด๋ฅผ ์บ์‹ฑ(cache)ํ•˜๊ณ  ์ข…๋ฃŒ(dispose)ํ•จ
    • ๊ทธ๋Ÿฌํ•œ ๊ฐ์ฒด๋“ค์„ ์œ„์ ฏ์ด listen ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ• ์ œ๊ณต
    • Provider ๊ฐ€ ๊ฐ€์ง„ ๋ฌธ์ œ์  ๋ณด์™„ (Provider ๊ฐ„์˜ ๊ฒฐํ•ฉ์„ ๊ฐ„์†Œํ™”)

25-1. Riverpod ์„ค์น˜

25-2. NotifierProvider

25-3. Notifier

25-3-1. View Model

  • PlaybackConfigViewModel > Notifier
class PlaybackConfigViewModel extends Notifier<PlaybackConfigModel> {
  final PlaybackConfigRepository _repository;

  PlaybackConfigViewModel(this._repository);

  @override
  PlaybackConfigModel build() {
    // PlaybackConfigViewModel build => ํ™”๋ฉด์ด ๊ฐ–๊ฒŒ ๋˜๋Š” ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ๋ฅผ return
    return PlaybackConfigModel(
      muted: _repository.isMuted(),
    );
  }

  void setMuted(bool value) {
    _repository.setMuted(value);
    // ์ƒํƒœ ๋ณ€๊ฒฝํ•˜๋ฉด ๋ชจ๋“  listener ๋“ค์ด ์ž๋™์œผ๋กœ ํ†ต์ง€ ๋ฐ›์Œ
    state = PlaybackConfigModel(muted: value);
  }
}
  • Notifier ์•ˆ์— expose ํ•  ๋ฐ์ดํ„ฐ๋ฅผ ๋„ฃ์Œ (ํ™”๋ฉด์ด listen ํ•˜๊ธฐ๋ฅผ ์›ํ•˜๋Š” ๋ฐ์ดํ„ฐ)
    • Notifier ๋Š” State ๋ฅผ ๊ฐ€์ง => Notifier ๋‚ด ์–ด๋””์„œ๋“  ๋ฐ์ดํ„ฐ์— ์ ‘๊ทผ ๊ฐ€๋Šฅ
    • State : ์‚ฌ์šฉ์ž์—๊ฒŒ ๋…ธ์ถœ์‹œํ‚ค๊ณ  ์‹ถ์€ ๋ฐ์ดํ„ฐ
    • ๋ชจ๋ธ ๊ฐ์ฒด ์ ‘๊ทผ ๋ฐฉ์‹ => state.muted (๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€๊ฒฝ๋˜์ง€ ์•Š์Œ)
    • ๋ชจ๋ธ ๊ฐ์ฒด ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ => State ๋ฅผ ๋Œ€์ฒดํ•ด์•ผ ํ•จ
      • state = PlaybackConfigModel(muted: value, autoplay: state.autoplay);
final playbackConfigProvider =
    NotifierProvider<PlaybackConfigViewModel, PlaybackConfigModel>(
  () => throw UnimplementedError(),
  // SharedPreferences ์ด ํ•„์š”ํ•ด์„œ PlaybackConfigViewModel(repository) ๊ฐ€ ํ•„์š”ํ•œ๋ฐ, ์—ฌ๊ธฐ์— repository ๋ฅผ ์“ธ ์ˆ˜ ์—†์–ด์„œ ์—๋Ÿฌ๋ฅผ ๋‚ด๊ณ , main.dart ์—์„œ ๋ฐ›์•„์˜ด
);

25-3-2. main.dart

final preferences = await SharedPreferences.getInstance();
final repository = PlaybackConfigRepository(preferences);

// 22.1 Riverpod ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ํ™˜๊ฒฝ์œผ๋กœ ์„ค์ •
runApp(
  ProviderScope(overrides: [
    playbackConfigProvider.overrideWith(
      () => PlaybackConfigViewModel(repository),
    ),
  ], child: const TikTokApp()),
);

25-3-2. View

  • Riverpod > ConsumerWidget
    • WidgetRef ref : Provide ๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ , ์ฝ์„ ์ˆ˜ ์žˆ๋Š” ๋ ˆํผ๋Ÿฐ์Šค
    • ref.watch : ๋ณ€ํ™”๋ฅผ listen ํ•˜๊ธฐ ์œ„ํ•จ / ref.read : ํ•œ ๋ฒˆ ์ฝ๋Š” ๊ฒƒ
      • ref : ViewModel > Notifier ๋‚ด์—์„œ๋„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Œ
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    (์ƒ๋žต)
    SwitchListTile.adaptive(
      value: ref.watch(playbackConfigProvider).muted,
      onChanged: (value) =>
          ref.read(playbackConfigProvider.notifier).setMuted(value),
      title: const Text("Mute video"),
    ),
  • Riverpod > ConsumerStatefulWidget
    • ConsumerState ์‚ฌ์šฉํ•ด์•ผ ํ•˜๋ฉฐ, ref ๋Š” ์–ด๋””์—์„œ๋‚˜ ์ด์šฉ ๊ฐ€๋Šฅ
// StatefulWidget
class VideoPost extends StatefulWidget {
  @override
  State<VideoPost> createState() => _VideoPostState();
}

class _VideoPostState extends State<VideoPost> {}

// ConsumerStatefulWidget
class VideoPost extends ConsumerStatefulWidget {
  @override
  VideoPostState createState() => VideoPostState();
}

class VideoPostState extends ConsumerState<VideoPost> {}

25-4. AsyncNotifier

25-4-1. View Model

  • TimelineViewModel > AsyncNotifier
class TimelineViewModel extends AsyncNotifier<List<VideoModel>> {
  List<VideoModel> _list = [VideoModel(title: "First video")];

  @override
  FutureOr<List<VideoModel>> build() async {
    await Future.delayed(const Duration(seconds: 3));
    return _list;
  }

  void uploadVideo() async {
    state = const AsyncValue.loading(); // ๊ฐ•์ œ๋กœ loading state ๊ฐ€ ๋˜๋„๋ก ํ•จ
    await Future.delayed(const Duration(seconds: 2));

    final newVideo = VideoModel(title: "${DateTime.now()}");
    _list = [..._list, newVideo];
    state = AsyncValue.data(_list);
  }
}

final timelineProvider =
    AsyncNotifierProvider<TimelineViewModel, List<VideoModel>>(
  () => TimelineViewModel(),
);
  • FutureOr : Future ๋˜๋Š” ๋ชจ๋ธ์„ return
  • state = AsyncValue.data(_list); : AsyncNotifier ์•ˆ์—์„œ State ๋ฅผ ๋Œ€์ฒดํ•  ๋•Œ

25-4-2. View

  • when() ์„ ์ด์šฉํ•˜์—ฌ ๊ฐ๊ธฐ ๋‹ค๋ฅธ state ๋ฅผ ์œ„ํ•œ callback ์ œ๊ณต (async ๊ฐ’์ด๋ฏ€๋กœ)
    • return ref.watch(timelineProvider).when(data: data, error: error, loading: loading)
    • ๊ฐ state ์— ๋Œ€ํ•œ ์œ„์ ฏ์„ ์„ธํŒ…
  (์ƒ๋žต)
  data: (videos) => PageView.builder( // videos : timelineProvider ๊ฐ€ expose ํ•˜๋Š” ๋ฐ์ดํ„ฐ
    controller: _pageController,
    onPageChanged: _onPageChanged,
    itemCount: videos.length,
    itemBuilder: (context, index) => VideoPost(onVideoFinished: _onVideoFinished, index: index),
  ),
  • state ์— ๋”ฐ๋ฅธ ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ
  IconButton(
    onPressed: _onUploadPressed,
    icon: ref.watch(timelineProvider).isLoading // state ์กฐํšŒ
        ? const CircularProgressIndicator()
        : const FaIcon(FontAwesomeIcons.cloudArrowUp),
  ),

25-5. GoRouter ์ˆ˜์ •

  • router.dart > GoRouter ๋ฅผ Provider ๋กœ ๊ฐ์‹ธ์„œ ์‚ฌ์šฉ
final routerProvider = Provider((ref) {
  GoRouter(
  • main.dart > ConsumerWidget ์œผ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ref.watch ์‚ฌ์šฉ
class TikTokApp extends ConsumerWidget {
  const TikTokApp({super.key});
  
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return MaterialApp.router(
      routerConfig: ref.watch(routerProvider),

26. Firebase

  • https://firebase.google.com/?hl=ko
  • ๋ฐฑ์—”๋“œ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์—ฌ๋Ÿฌ๊ฐ€์ง€ ์†”๋ฃจ์…˜ ์ œ๊ณต (๋ฐฑ์—”๋“œ๋ฅผ ๋งŒ๋“ค ํ•„์š” X)
    • Firestore DB ์ œ๊ณต
    • ํด๋ผ์šฐ๋“œ ์ €์žฅ์†Œ ์ œ๊ณต (๋น„๋””์˜ค, ์œ ์ € ์•„๋ฐ”ํƒ€ ๋“ฑ)
    • Authentication, ์†Œ์…œ๋ฏธ๋””์–ด ์ธ์ฆ, Cookie, Token, ๋ณด์•ˆ ๋“ฑ ์ œ๊ณต
    • ์•ฑ ๋ฐฐํฌ, ํ†ต๊ณ„๋ถ„์„, ํ…Œ์ŠคํŠธ, ์•Œ๋ฆผ์ฐฝ, ๋™์  ๋งํฌ ๋“ฑ
  • ์š”๊ธˆ์ œ ์ •๋ณด : https://firebase.google.com/pricing?authuser=0&hl=ko
  • ํ”„๋กœํ† ํƒ€์ž… ์ œ์ž‘, ์Šคํƒ€ํŠธ์—…์—์„œ ์‚ฌ์šฉํ•˜๊ธฐ ์ ํ•ฉ
    • SQL ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๊ณ , Firebase ์—์„œ ์ œ๊ณตํ•˜๋Š” DB ๋ฅผ ์จ์•ผ ํ•จ

26-1. Firebase ์„ค์น˜

  • Firebase CLI reference > https://firebase.google.com/docs/cli?hl=ko
  • Windows > binary ํŒŒ์ผ์„ ๋ฐ›์•„์„œ ์„ค์น˜, console ์—์„œ firebase command ์ž…๋ ฅ ๊ฐ€๋Šฅํ•œ์ง€ ํ™•์ธ
    • firebase-tools-instant-win.exe ์‹คํ–‰ > ์„ค์น˜ ๊ณผ์ •์—์„œ ๋กœ๊ทธ์ธ ์ง„ํ–‰ > "Firebase CLI Login Successful"
> firebase --version
13.11.2

11

  • macOS > curl -sL https://firebase.tools | bash ์ž…๋ ฅ

26-1-1. Flutter ์•ฑ์— Firebase ์ถ”๊ฐ€

PS C:\Users\> dart pub global activate flutterfire_cli
+ ansi_styles 0.3.2+1s... (1.0s)
(์ƒ๋žต)
+ test_api 0.7.2
You can fix that by adding that directory to your system's "Path" environment variable.
A web search for "configure windows path" will show you how.
Activated flutterfire_cli 1.0.0.
Warning: Pub installs executables into C:\Users\YNJCH\AppData\Local\Pub\Cache\bin, which is not on your path.
You can fix that by adding that directory to your system's "Path" environment variable.
A web search for "configure windows path" will show you how.
Activated flutterfire_cli 1.0.0.
  • ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ํŽธ์ง‘ > Path > ์œ„ ๋ช…๋ น์–ด ์ˆ˜ํ–‰ ๊ฒฐ๊ณผ๋กœ ๋‚˜์˜จ ๊ฒฝ๋กœ ์ถ”๊ฐ€ (C:\Users\YNJCH\AppData\Local\Pub\Cache\bin)
  • flutterfire : Flutter ์•ฑ์—์„œ Firebase ๋ฅผ ์„ค์ •ํ•˜๊ธฐ ์œ„ํ•ด ํ•„์š”
    • VSCode > Terminal > flutterfire configure
    • ์—ฌ๊ธฐ์„œ CommandNotFoundException ๋ฐœ์ƒ ์‹œ, ์•„๋ž˜ 26-1-2. ๋‚ด์šฉ ๋จผ์ € ์ˆ˜ํ–‰
PS C:\Users\YNJCH\Flutter\tiktok_clone> flutterfire configure
i Found 2 Firebase projects.
โœ” Select a Firebase project to configure your Flutter application with ยท tiktokclone-3260b (TikTokClone)
โœ” Which platforms should your configuration support (use arrow keys & space to select)? ยท web, ios, android
i Firebase android app com.example.tiktok_clone is not registered on Firebase project tiktokclone-3260b.
i Registered a new Firebase android app on Firebase project tiktokclone-3260b.
i Firebase ios app com.example.tiktokClone is not registered on Firebase project tiktokclone-3260b.
i Registered a new Firebase ios app on Firebase project tiktokclone-3260b.
i Firebase web app tiktok_clone (web) is not registered on Firebase project tiktokclone-3260b.
i Registered a new Firebase web app on Firebase project tiktokclone-3260b.

Firebase configuration file lib\firebase_options.dart generated successfully with the following Firebase apps:

Platform  Firebase App Id
web       1:[์ˆซ์ž]:web:[์ฝ”๋“œ]     // ~\tiktok_clone\firebase.json ์— ์“ฐ์—ฌ์žˆ๋Š” ๋‚ด์šฉ ํ™•์ธ!
android   1:[์ˆซ์ž]:android:[์ฝ”๋“œ]
ios       1:[์ˆซ์ž]:ios:[์ฝ”๋“œ]

Learn more about using this file and next steps from the documentation:
 > https://firebase.google.com/docs/flutter/setup

26-1-2. flutterfire CommandNotFoundException

  • C:\tools\flutter\bin > cache ํด๋” ์‚ญ์ œ > cmd ์ฐฝ์—์„œ flutter doctor ์ˆ˜ํ–‰
  • PowerShell ๊ด€๋ฆฌ์ž ๊ถŒํ•œ ์‹คํ–‰ > dart sdk ํŒŒ์ผ ์—…๊ทธ๋ ˆ์ด๋“œ choco upgrade dart-sdk
    • ~\tiktok_clone\android\app\build.gradle > minSdkVersion 23 ๋กœ ๋Œ€์ฒด
PS C:\Users\YNJCH> choco upgrade dart-sdk
Chocolatey v1.1.0
2 validations performed. 1 success(es), 1 warning(s), and 0 error(s).

Validation Warnings:
 - A pending system reboot request has been detected, however, this is
   being ignored due to the current Chocolatey configuration.  If you
   want to halt when this occurs, then either set the global feature
   using:
     choco feature enable -name=exitOnRebootDetected
   or pass the option --exit-when-reboot-detected.

Upgrading the following packages:
dart-sdk
By upgrading, you accept licenses for the packages.
dart-sdk is not installed. Installing...
Progress: Downloading dart-sdk 3.4.3... 100%
Progress: Downloading dart-sdk 3.4.3... 100%

dart-sdk v3.4.3 [Approved]
  • PowerShell ๊ด€๋ฆฌ์ž ๊ถŒํ•œ ์‹คํ–‰ > npm install -g firebase-tools
PS C:\Users\YNJCH> npm install -g firebase-tools
npm WARN deprecated [email protected]: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
npm WARN deprecated [email protected]: Rimraf versions prior to v4 are no longer supported
npm WARN deprecated [email protected]: Glob versions prior to v9 are no longer supported

added 620 packages in 27s

66 packages are looking for funding
  run `npm fund` for details
PS C:\Users\YNJCH> firebase --version
13.11.2
PS C:\Users\YNJCH> firebase login
Already logged in as [๋กœ๊ทธ์ธ ์ด๋ฉ”์ผ ์ฃผ์†Œ]
  • ์ฝ˜์†”๋กœ ์ด๋™ > https://console.firebase.google.com/
    • ํ”„๋กœ์ ํŠธ ๋งŒ๋“ค๊ธฐ > TikTokClone ํ”„๋กœ์ ํŠธ ๊ธฐ๋ณธ ์„ค์ •์œผ๋กœ ์ƒ์„ฑ
  • PC ๋‹ค์‹œ ์‹œ์ž‘ํ•˜๋‹ˆ ๋˜์—ˆ์Œ...

26-1-3. Flutter ์•ฑ์— Firebase ์ถ”๊ฐ€ 2

flutter pub add firebase_core # core ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์„ค์น˜
flutterfire configure # Flutter ์•ฑ์˜ Firebase ๊ตฌ์„ฑ์ด ์ตœ์‹  ์ƒํƒœ์ธ์ง€ ํ™•์ธ
  • ํ”Œ๋Ÿฌ๊ทธ์ธ ์ถ”๊ฐ€ ์„ค์น˜ (4๋‹จ๊ณ„: Firebase ํ”Œ๋Ÿฌ๊ทธ์ธ ์ถ”๊ฐ€)
# flutter pub add [PLUGIN_NAME] ๋กœ ์„ค์น˜ ์ˆ˜ํ–‰ => flutterfire configure ์ˆ˜ํ–‰
flutter pub add firebase_auth
flutter pub add cloud_firestore
flutterfire configure

27. Firebase ์—ฐ๋™

27-1. Firebase ์ดˆ๊ธฐํ™”

  • main.dart ์—์„œ ์ดˆ๊ธฐํ™”
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );

27-1-1. Firebase ์—ฐ๊ฒฐ

  • Firebase ์ดˆ๊ธฐํ™” + FirebaseAuth.instance ์ƒ์„ฑ => ๋ฐ”๋กœ Firebase ์™€ ์†Œํ†ตํ•  ์ˆ˜ ์žˆ์Œ
class AuthenticationRepository {
  final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;

  bool get isLoggedIn => user != null;
  User? get user => _firebaseAuth.currentUser; // ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž๋Š” Nullable
}

final authRepo = Provider((ref) => AuthenticationRepository());

27-2. Firebase Authentication

  • GoRouter > redirect ์†์„ฑ์„ ์ด์šฉํ•ด ๋กœ๊ทธ์ธ ์—ฌ๋ถ€ ํ™•์ธ ํ›„ ํŽ˜์ด์ง€ ๋ฆฌํ„ด
final routerProvider = Provider((ref) {
  return GoRouter(
    redirect: (context, state) {
      final isLoggedIn = ref.read(authRepo).isLoggedIn;
      if (!isLoggedIn) { // ํšŒ์›๊ฐ€์ž…, ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ ์ œ์™ธ ์ ‘๊ทผ ๋ถˆ๊ฐ€ํ•˜๋„๋ก
        if (state.subloc != SignUpScreen.routeURL &&
            state.subloc != LoginScreen.routeURL) {
          return SignUpScreen.routeURL;
        }
      }
      return null;
    },

27-3. Email/Password ๋กœ ๊ณ„์ • ์ƒ์„ฑํ•˜๊ธฐ

  • Firebase console > Authentication > Get Started
    • ๋กœ๊ทธ์ธ ๋ฐฉ์‹ ์„ ํƒํ•˜๊ธฐ > Email/Password > Enable ํ›„ ์ €์žฅ

27-3-1. View Model

  • signup_view_model.dart > AsyncNotifier view_model ์ƒ์„ฑ
    • ๊ณ„์ • ์ƒ์„ฑ ์‹œ ๋กœ๋”ฉ ํ™”๋ฉด ๋ณด์—ฌ์ฃผ๊ณ , ๊ณ„์ • ์ƒ์„ฑ์„ ํŠธ๋ฆฌ๊ฑฐ (ํšŒ์›๊ฐ€์ž…)
class SignUpViewModel extends AsyncNotifier<void> {
  late final AuthenticationRepository _authRepo;

  @override
  FutureOr<void> build() {
    _authRepo = ref.read(authRepo);
  }

  Future<void> signUp() async {
    state = const AsyncValue.loading();
    final form = ref.read(signUpForm);

    state = await AsyncValue.guard(
      () async => await _authRepo.signUp(form["email"], form["password"]),
    );
  }
}

final signUpForm = StateProvider((ref) => {});
final signUpProvider = AsyncNotifierProvider<SignUpViewModel, void>(
  () => SignUpViewModel(),
);
  • AsyncValue ์ƒํƒœ ๊ด€๋ จ
    • AsyncValue.loading() : ๋กœ๋”ฉ ์ค‘
    • AsyncValue.data(null) : ๋กœ๋”ฉ ์ƒํƒœ ์ œ๊ฑฐ
    • AsyncValue.guard() : ์ฝ”๋“œ ์‹คํ–‰ ํ›„ ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ๊ทธ ์—๋Ÿฌ๋ฅผ state ์— ๋ฆฌํ„ดํ•˜๊ณ , ์ •์ƒ ์‹คํ–‰ ์‹œ ๊ฒฐ๊ณผ๊ฐ’์„ state ์— ๋ฆฌํ„ด
      • (state.error as FirebaseException).message ๋กœ ์—๋Ÿฌ ๋ฉ”์„ธ์ง€ ํ™•์ธ ๊ฐ€๋Šฅ
  • signUpForm : ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ์ •๋ณด๋ฅผ ๋‹ด๋Š” ์—ญํ• 
    • StateProvider => ๋ฐ”๊นฅ์—์„œ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ๋Š” value ๋ฅผ expose ํ•˜๊ฒŒ ํ•จ
  • signUpProvider : state ํ™•์ธ / signUp() ๋“ฑ ๋ฉ”์†Œ๋“œ ์‹คํ–‰

27-3-2. View

    ref.read(signUpForm.notifier).state = {"email": _email};
  • email_screen.dart > ์ด๋ฉ”์ผ ์ฃผ์†Œ ์ •๋ณด๋ฅผ signUpForm state ์— ์ „๋‹ฌ
    final state = ref.read(signUpForm.notifier).state;
    ref.read(signUpForm.notifier).state = {...state, "password": _password};
  • password_screen.dart > ๊ธฐ์กด state ์— password ๊ฐ’ ์ „๋‹ฌ
    • print ๊ฒฐ๊ณผ : I/flutter ( 4427): {email: [์ด๋ฉ”์ผ], password: [ํŒจ์Šค์›Œ๋“œ]}
    void _onNextTap() {
      ref.read(signUpProvider.notifier).signUp();
    }
  • birthday_screen.dart > signUp() ์‹คํ–‰
    • ๋ฒ„ํŠผ ํ™œ์„ฑํ™” > disabled: ref.watch(signUpProvider).isLoading,

27-4. Stream ์ ์šฉ

  • UI์™€ ๋ฐฑ์—”๋“œ ๊ฐ„์˜ ์‹ค์‹œ๊ฐ„ ์—ฐ๊ฒฐ
    • ์‚ฌ์šฉ์ž ์ธ์ฆ ์ƒํƒœ๊ฐ€ ๋ฐ”๋€” ๋•Œ ์•ฑ์„ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ƒˆ๋กœ๊ณ ์นจ

27-4-1. Repository

  • StreamProvider : ๋ณ€ํ™”๋ฅผ ๋ฐ”๋กœ ๊ฐ์ง€ํ•  ์ˆ˜ ์žˆ์Œ => ์œ ์ € ์ธ์ฆ ์ƒํƒœ ๋ณ€๊ฒฝ์„ ๊ฐ์ง€ (ex. ๋กœ๊ทธ์ธ/๋กœ๊ทธ์•„์›ƒ)
class AuthenticationRepository {
  final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;
  (์ƒ๋žต)
  Stream<User?> authStateChanges() => _firebaseAuth.authStateChanges();
}

final authStateStream = StreamProvider((ref) {
  final repo = ref.read(authRepo);
  return repo.authStateChanges();
});
  • View > ref.watch(authStateStream); : Stream ๋ณ€ํ™”๊ฐ€ ์žˆ์œผ๋ฉด ๋ฐ”๋กœ rebuild ๋˜๋ฉฐ, router ์˜ initialLocation ๋กœ ๋Œ์•„๊ฐ

27-5. Github ๋กœ๊ทธ์ธ ์—ฐ๊ฒฐ

  • https://console.firebase.google.com/project/tiktokclone-3260b/authentication/providers
    • Firebase console > Authentication > Github > Enable ํ™œ์„ฑํ™”
    • ์ฝœ๋ฐฑ URL ๋ณต์‚ฌ https://tiktokclone-3260b.firebaseapp.com/__/auth/handler
  • Github Register a new OAuth application > https://github.com/settings/applications/new
    • Application name : TikTokClone
    • Homepage URL : http://temp.com (ํ˜„์žฌ ์•„๋ฌด๊ฑฐ๋‚˜ ์ƒ๊ด€ X)
    • Authorization callback URL : https://tiktokclone-3260b.firebaseapp.com/__/auth/handler
    • Register application ํด๋ฆญ
  • Client ID ํ™•์ธ, Generate a new client secret ํด๋ฆญํ•˜์—ฌ Client secrets ํ™•์ธ
  • Firebase console > Authentication > Github ์ด์–ด์„œ ์ž‘์—…
    • Client ID : Ov23li855FeRNA2vsRJV
    • Client secrets : 1f81dbd16227abc6bf14dbac251480268a1d0870

27-5-1. Android ์—์„œ ์—ฐ๊ฒฐ

PS C:\Users\YNJCH\Flutter\tiktok_clone\android> ./gradlew signinReport
Starting a Gradle Daemon, 1 incompatible Daemon could not be reused, use --status for details

> Task :app:signingReport
Variant: debug
Config: debug
Store: C:\Users\YNJCH\.android\debug.keystore
Alias: AndroidDebugKey
MD5: B4:80:06:00:30:25:53:E8:1C:2B:F3:58:41:71:B1:02
SHA1: C9:FD:F1:9A:FC:30:57:E5:5F:D6:C0:96:81:18:37:36:8B:9F:CC:79
SHA-256: CA:E3:44:17:BE:6F:60:8C:08:54:BA:0F:A6:ED:BB:F2:C3:0F:5E:73:30:40:A5:8E:79:F7:E7:29:E9:27:0B:4C
Valid until: 2054๋…„ 4์›” 26์ผ ์ผ์š”์ผ
----------
Variant: release
Variant: profile
Variant: debugAndroidTest
... (์ƒ๋žต)

BUILD SUCCESSFUL in 12s
16 actionable tasks: 12 executed, 4 up-to-date

28. Firestore

  • Firebase ์˜ NoSQL ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค
    • ๋ฐ์ดํ„ฐ ์ค‘๋ณต์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๊ณ , ์—…๋ฐ์ดํŠธ ๋ฐœ์ƒ ์‹œ ์—ฌ๋Ÿฌ ๊ณณ์—์„œ ํ•ด๋‹น ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ ํ•ด์ฃผ์–ด์•ผ ํ•จ
  • Firestore : https://console.firebase.google.com/project/tiktokclone-3260b/firestore
    • Create Database > ๋ณด์•ˆ ๊ทœ์น™(ํ…Œ์ŠคํŠธ ๋ชจ๋“œ) / ์„œ๋ฒ„ ์œ„์น˜(์œ ์ €์™€ ๊ฐ€๊นŒ์šด ์ง€์—ญ) ์„ ํƒ > Create

28-1. ํ…Œ์ด๋ธ” ์ƒ์„ฑ

  • Start Collection ๋กœ ํ…Œ์ด๋ธ” ์ƒ์„ฑํ•˜๊ธฐ
    • Collection ID : users
    • Document ID : Auto generate ID ํด๋ฆญํ•˜์—ฌ ์ž๋™ ์ƒ์„ฑ (user id ๊ฐœ๋…)
    • Field : name, bio, link ๋ชจ๋‘ String ํƒ€์ž…์œผ๋กœ ์ƒ์„ฑ
  • ์ƒ์„ฑ๋œ users ๋‚ด์— ์ƒˆ Collection ์ƒ์„ฑ ๊ฐ€๋Šฅ
    • Collection ID : likes
    • Document ID : Auto generate ID ํด๋ฆญํ•˜์—ฌ ์ž๋™ ์ƒ์„ฑ (video likes ๊ฐœ๋…)
    • Field : video ๋ฅผ Reference ํƒ€์ž…์œผ๋กœ ์ƒ์„ฑ, ์ฐธ์กฐ ๊ฒฝ๋กœ ์„ค์ • (videos/124)

28-2. ๋ฐ์ดํ„ฐ ์ „๋‹ฌ

  • ๋ฐ์ดํ„ฐ๋Š” Dart ํด๋ž˜์Šค๊ฐ€ ์•„๋‹Œ JSON ํƒ€์ž…์œผ๋กœ ๋ณด๋‚ด์•ผ ํ•จ
  • Map<String, String> ํ˜•ํƒœ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ Repository ์—์„œ Insert
    • await _db.collection("users").doc(user.uid).set(user.toJson());
  Map<String, String> toJson() {
    return {
      "uid": uid,
      "email": email,
      "name": name,
      "bio": bio,
      "link": link,
    };
  }
  • ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์„ ๋•Œ๋Š” JSON ํƒ€์ž…์„ Dart ํด๋ž˜์Šค๋กœ ๋ณ€ํ™˜
  UserProfileModel.fromJson(Map<String, dynamic> json)
      : uid = json["uid"],
        email = json["email"],
        name = json["name"],
        bio = json["bio"],
        link = json["link"],
        birthday = json["birthday"];
  • ์ด๋•Œ Permission ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ, Firestore Rules ํ™•์ธ
    match /{document=**} {
      allow read, write: if request.time < timestamp.date(2025, 7, 17);
    }

29. Storage

29-1. ํ”„๋กœํ•„ ์ด๋ฏธ์ง€ ๋ณ€๊ฒฝ

  • ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๋‹ด์€ ref.watch() ๋ฅผ ํ”„๋กœํ•„ ์ด๋ฏธ์ง€ ๋ณ€๊ฒฝ์—๋„ ํ™œ์šฉํ•œ๋‹ค๋ฉด ์ด๋ฏธ์ง€ ๋ณ€๊ฒฝ ์‹œ, ํ™”๋ฉด ์ „์ฒด๊ฐ€ ๋กœ๋”ฉ๋  ๊ฒƒ์ž„
    • CircleAvatar ๋ถ€๋ถ„๋งŒ ๋กœ๋”ฉ๋  ์ˆ˜ ์žˆ๋„๋ก ํ•˜๋‚˜์˜ View ๋กœ ๋ถ„๋ฆฌํ•ด์•ผ ํ•จ
  @override
  Widget build(BuildContext context) {
    return ref.watch(userProvider).when(
          data: (data) => SafeArea(
                          (์ƒ๋žต)
                          CircleAvatar(
                            radius: 50,
                            foregroundImage: const NetworkImage(
                              "https://avatars.githubusercontent.com/u/69029517",
                            ),
                            child: Text(data.name),
                          ),
  • Firebase Storage ์—์„œ๋Š” ๋Œ€๋ถ€๋ถ„ reference ๋กœ ์ž‘๋™
    • reference : ์•ฑ๊ณผ Firebase ํด๋”์™€์˜ link ๊ฐ™์€ ๊ฒƒ
  Future<void> uploadAvatar(File file, String fileName) async {
    final fileRef = _storage.ref().child("avatar/$fileName");
    await fileRef.putFile(file);
  }
  • _storage.ref() : Storage Bucket
  • child() : reference ์— ๋Œ€ํ•œ ์ƒ๋Œ€ ๊ฒฝ๋กœ์˜ reference ๋ฅผ ๋ฆฌํ„ด

30. Cloud Functions

  • Google ๋ฐฑ์—”๋“œ์— ์–ด๋–ค ์ฝ”๋“œ๋„ ๋ฐฐํฌํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์คŒ (๋ฐฑ์—”๋“œ์—์„œ ์ด๋ฒคํŠธ๋ฅผ ๊ด€์ฐฐ, ์ปค์Šคํ…€ ํ•จ์ˆ˜ ์‹คํ–‰)
    • AWS, Cloudflare, Google Cloud ๋“ฑ๊ณผ ๊ฐ™์€ ๊ธฐ๋Šฅ
  • Authentication, Database, Storage ์— ๋ณ€ํ™” ๋ฐœ์ƒ ์‹œ ์‹คํ–‰ํ•  ์ปค์Šคํ…€ ์ฝ”๋“œ ์ž‘์„ฑ ๊ฐ€๋Šฅ
  • ์•„๋ž˜ ์ฝ”๋“œ๋กœ ์„ค์น˜ํ•˜๋Š” ๊ณผ์ •์ด ํ•„์š”ํ•˜๋‚˜ ์šฐ์„  ์Šคํ‚ตํ•˜์˜€์Œ
    • Firebase ๋„๊ตฌ ์„ค์น˜ ์ฝ”๋“œ : $ npm install -g firebase-tools
    • ํ”„๋กœ์ ํŠธ ์‹œ์ž‘ : $ firebase init
    • ํ•จ์ˆ˜ ๋ฐฐํฌ : $ firebase deploy

30-1. Cloud Functions ํ™˜๊ฒฝ์„ค์ •

  • https://console.firebase.google.com/project/tiktokclone-3260b/functions ์‹œ์ž‘ํ•˜๊ธฐ
    • ์ข…๋Ÿ‰์ œ ์š”๊ธˆ์ œ๋กœ ์—…๊ทธ๋ ˆ์ด๋“œ ํ•„์š”
  • VSCode > Terminal > ์•„๋ž˜ ๋ช…๋ น์–ด ์ž…๋ ฅ
    • Firebase ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์ถ”๊ฐ€/์ œ๊ฑฐํ•  ๋•Œ๋งˆ๋‹ค flutterfire configure ์‹คํ–‰ํ•ด์ฃผ์–ด์•ผ ํ•จ
flutter pub add cloud_functions # Cloud Functions ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์„ค์น˜
  • Node.js ์„ค์น˜ ์—ฌ๋ถ€ ํ™•์ธ
    • Cloud Functions ๋Š” Typescript ๋กœ ์ž‘์„ฑ๋˜๋ฉฐ, Typescript ๋Š” Node.js ์œ„์—์„œ ๋™์ž‘
C:\Users\YNJCH>node -v
v20.10.0
  • VSCode > Terminal > ์•„๋ž˜ ๋ช…๋ น์–ด ์ž…๋ ฅ
    • ๊ธฐ์กด ํ”„๋กœ์ ํŠธ ์„ ํƒ Use an existing project > tiktokclone-3260b (TikTokClone)
    • TypeScript ์„ ํƒ ๋ฐ ESLint ๋Š” ์‚ฌ์šฉํ•˜์ง€ ์•Š์Œ
firebase init functions

? Please select an option: Use an existing project
? Select a default Firebase project for this directory: tiktokclone-3260b (TikTokClone)
? What language would you like to use to write Cloud Functions? TypeScript
? Do you want to use ESLint to catch probable bugs and enforce style? No
+  Firebase initialization complete!
  • functions ํด๋” -> ~\functions\src\index.ts ํŒŒ์ผ ์ƒ์„ฑ๋œ ๊ฒƒ ํ™•์ธ
    • ๋ฐฑ์—”๋“œ์— ๋“ค์–ด๊ฐˆ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๊ฒŒ ๋จ
    • ์ƒ์„ฑ, ์ˆ˜์ •, ์‚ญ์ œ๋ฅผ listen ํ•  ์ˆ˜ ์žˆ์Œ

30-2. TikTok Clone

  • ์˜์ƒ ์—…๋กœ๋“œ ํ–ˆ์„ ๋•Œ, ์ด๋ฅผ ๊ด€์ฐฐํ•˜๋Š” ์ปค์Šคํ…€ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑ
    • ์‚ฌ์šฉ์ž๋Š” ์˜์ƒ์„ ์˜ฌ๋ฆฌ๊ธฐ๋งŒ ํ•˜๋ฉด ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ๋ชจ๋‘ ๋™์ž‘ํ•  ๊ฒƒ์ž„
    • ์‚ฌ์šฉ์ž๊ฐ€ ๋น„๋””์˜ค document ์—…๋กœ๋“œ ํ•˜๋Š” ๊ฒƒ์„ ๊ด€์ฐฐ -> ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ, ์ธ๋„ค์ผ ์ถ”์ถœ -> Storage bucket ์— ์ธ๋„ค์ผ ํŒŒ์ผ์„ ์—…๋กœ๋“œ -> ์ธ๋„ค์ผ property ๋ฅผ ์ถ”๊ฐ€ํ•ด ์˜์ƒ ์—…๋ฐ์ดํŠธ

30-2-1. index.ts

  • Firebase ์˜ admin ์„ import (Firebase ์— ๊ด€๋ฆฌ์ž ๋„๊ตฌ ์กด์žฌ)
  • ~\tiktok_clone\functions\package.json ์— firebase-admin, firebase-functions ์žˆ๋Š” ๊ฒƒ ํ™•์ธ
  "dependencies": {
    "firebase-admin": "^12.1.0",
    "firebase-functions": "^5.0.0"
  },
  • ~\tiktok_clone\functions\src\index.ts ์•„๋ž˜์™€ ๊ฐ™์ด ์ž…๋ ฅ
import * as functions from "firebase-functions";
import * as admin from "firebase-admin";

// admin ์ดˆ๊ธฐํ™”
admin.initializeApp();

/**์ƒˆ ์˜์ƒ์ด ์žˆ๋Š”์ง€ listen -> ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ถ”๊ฐ€ -> ์ถ”๊ฐ€์ ์ธ ์ •๋ณด๋ฅผ ์˜์ƒ์— ์ถ”๊ฐ€
 * document() : listen ํ•  ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ฒฝ๋กœ ์ž…๋ ฅ
 * {videoId} : ๋ณ€์ˆ˜์ฒ˜๋Ÿผ ์ž‘๋™ -> onCreate ๋กœ ๊ฐ’์ด ์ƒ์„ฑ๋  ๋•Œ ์•Œ๋ฆผ์„ ๋ฐ›์Œ
 * onCreate() : snapshot(์ƒ์„ฑ๋œ document), context(์–ด๋–ค document ๊ฐ€ ์ƒ์„ฑ๋˜์—ˆ๋Š”์ง€ videoId ์ •๋ณด ์–ป์„ ์ˆ˜ ์žˆ์Œ) ํŒŒ๋ผ๋ฏธํ„ฐ
 */
export const onVideoCreated = functions.firestore
    .document("videos/{videoId}")
    .onCreate(async (snapshot, context) => {
        // snapshot : ๋ฐฉ๊ธˆ ๋งŒ๋“ค์–ด์ง„ ์˜์ƒ์„ ์˜๋ฏธ (ref ๋กœ document ์— ์ ‘๊ทผ ๊ฐ€๋Šฅ)
        snapshot.ref.update({ "hello": "from functions" });
});

30-2-2. firebase deploy (๊ตฌ๊ธ€ ์„œ๋ฒ„์— ๋ฐฐํฌํ•˜๊ธฐ)

  • VSCode > Terminal > ์•„๋ž˜ ๋ช…๋ น์–ด ์ž…๋ ฅ
PS C:\Users\YNJCH\Flutter\tiktok_clone> firebase deploy --only functions
(์ƒ๋žต)
+  Deploy complete!

12

30-2-3. firebase deploy (์˜ค๋ฅ˜ ํ•ด๊ฒฐํ•˜๊ธฐ)

  • ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ์•„๋ž˜์™€ ๊ฐ™์ด ํ•ด๊ฒฐ (~never read ๋‚ด์šฉ๋งŒ ์ฃผ์„ํ•˜์˜€๋”๋‹ˆ ํ•ด๊ฒฐ๋จ)
  • src/index.ts:10:1 - error TS6133: 'onRequest' is declared but its value is never read.
    • ๊ด€๋ จ ๋‚ด์šฉ ์ฃผ์„ ์ฒ˜๋ฆฌ : // import {onRequest} from "firebase-functions/v2/https";
  • ๋…ธ๋“œ ๊ด€๋ จ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ, ๋…ธ๋“œ ๋ฒ„์ „ ์ผ์น˜์‹œํ‚ค๊ธฐ : "node": "18" -> "node": "20"
  • Error: HTTP Error: 500, Failed to generate source upload url for 1st Gen function projects/tiktokclone-3260b/locations/us-central1
    • region ์ถ”๊ฐ€ํ•˜๊ธฐ : functions.region('asia-northeast3').firestore.document('videos/{videoId}')

30-2-4. ์‹คํ–‰

  • .document("videos/{videoId}") : ์ƒˆ๋กœ์šด document ๊ฐ€ ์ƒ์„ฑ๋˜๋ฉด
  • .update({ "hello": "from functions" }) : hello ๋ผ๋Š” ํ•„๋“œ ์ถ”๊ฐ€ํ•˜๋„๋ก ํ•จ
โš ๏ธ **GitHub.com Fallback** โš ๏ธ