[TIL] Day 46 Const Class & Clean Architecture & ChatBot App (UI, Gemini AI ์—ฐ๋™)

ํ˜„์„œยท2026๋…„ 1์›” 28์ผ

[TIL] Flutter 9๊ธฐ

๋ชฉ๋ก ๋ณด๊ธฐ
58/65
post-thumbnail

๐Ÿ“ ํšจ์œจ์„ฑ์„ ์œ„ํ•œ Const Class

โœ๏ธ const class๋ž€?

Dart/Flutter์—์„œ ์ปดํŒŒ์ผ ํƒ€์ž„ ์ƒ์ˆ˜ ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค๊ธฐ ์œ„ํ•œ ํด๋ž˜์Šค
์ƒ์„ฑ์ž ์•ž์— const๋ฅผ ๋ถ™์ด๊ณ , ํ•ด๋‹น ํด๋ž˜์Šค๋กœ ๋งŒ๋“  ๊ฐ์ฒด๋„ const๋กœ ์ƒ์„ฑํ•ด์•ผ ํ•จ
๋Œ€ํ‘œ์ ์œผ๋กœ Text, Icon ๊ฐ™์€ Flutter ์œ„์ ฏ๋“ค์ด ์ด์— ํ•ด๋‹น

โœ๏ธ Text ์œ„์ ฏ์— ๋œจ๋Š” ํŒŒ๋ž€ ์ค„์˜ ์˜๋ฏธ

๊ณ ์ •๋œ ๊ฐ’์œผ๋กœ ์œ„์ ฏ์„ ์ƒ์„ฑํ–ˆ์ง€๋งŒ const๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์•˜์„ ๋•Œ ๋ฐœ์ƒ
IDE ๊ฒฝ๊ณ  ๋ฉ”์‹œ์ง€:

Use 'const' with the constructor to improve performance.

์˜๋ฏธ: const๋ฅผ ์“ฐ๋ฉด ์„ฑ๋Šฅ์ด ๋” ์ข‹์•„์งˆ ์ˆ˜ ์žˆ๋‹ค

โœ๏ธ ์™œ const๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ• ๊นŒ?

๋ถˆ๋ณ€ ๊ฐ์ฒด (Immutable Object)

๊ฐ์ฒด ์ƒ์„ฑ ์ดํ›„ ์†์„ฑ ๋ณ€๊ฒฝ์ด ๋ถˆ๊ฐ€๋Šฅํ•œ ๊ฐ์ฒด
๋ชจ๋“  ํ•„๋“œ๊ฐ€ final

class A {
  final String id;
  A(this.id);
}

์ƒ์ˆ˜ ๊ฐ์ฒด (Const Object)

๋ถˆ๋ณ€ ๊ฐ์ฒด ์ค‘์—์„œ๋„
์ƒ์„ฑ์ž์— const๊ฐ€ ๋ถ™์–ด ์žˆ๊ณ 
๊ฐ์ฒด ์ƒ์„ฑ ์‹œ const๋กœ ์ƒ์„ฑํ•œ ๊ฒฝ์šฐ
๋ฉ”๋ชจ๋ฆฌ์— ์บ์‹ฑ๋˜์–ด ์žฌ์‚ฌ์šฉ๋จ

class A {
  final String id;
  const A(this.id);
}
const a1 = A('data');
const a2 = A('data');
print(identical(a1, a2)); // true

identical() โ†’ ๋‘ ๊ฐ์ฒด๊ฐ€ ์™„์ „ํžˆ ๊ฐ™์€ ๋ฉ”๋ชจ๋ฆฌ ์ฃผ์†Œ์ธ์ง€ ๋น„๊ต

โœ๏ธ Text ์œ„์ ฏ๊ณผ const

Text ์œ„์ ฏ์€ ๋ถˆ๋ณ€ ๊ฐ์ฒด
๋”ฐ๋ผ์„œ ์กฐ๊ฑด๋งŒ ๋งž์œผ๋ฉด const Text() ์‚ฌ์šฉ ๊ฐ€๋Šฅ

const Text('Hello');

โœ๏ธ const๊ฐ€ ์„ฑ๋Šฅ์— ๋ฏธ์น˜๋Š” ์˜ํ–ฅ

ํ•ต์‹ฌ ๊ฐœ๋…

Flutter๋Š” ์œ„์ ฏ ํŠธ๋ฆฌ๋ฅผ ์ž์ฃผ rebuildํ•จ
ํ•˜์ง€๋งŒ ์ƒ์ˆ˜ ๊ฐ์ฒด(const) ๋Š”

  • rebuild ๊ณผ์ •์—์„œ ๋‹ค์‹œ ์ƒ์„ฑ๋˜์ง€ ์•Š์Œ
  • build() ํ˜ธ์ถœ ์ž์ฒด๊ฐ€ ์ƒ๋žต๋จ

โœ๏ธ DevTools๋กœ rebuild ํšŸ์ˆ˜ ํ™•์ธํ•˜๊ธฐ

์„ค์ • ๋ฐฉ๋ฒ•

  1. ์•ฑ ์‹คํ–‰
  2. Command Palette ์—ด๊ธฐ
  3. Open DevTools Performance Page
  4. Track widget build counts ํ™œ์„ฑํ™”

๊ฒฐ๊ณผ ๋น„๊ต

๋ฒ„ํŠผ 10๋ฒˆ ํด๋ฆญ ์‹œ

  • โœ… const ์‚ฌ์šฉ โ†’ build ํšŸ์ˆ˜ ๊ฑฐ์˜ ์—†์Œ
  • โŒ const ๋ฏธ์‚ฌ์šฉ โ†’ build ๋งค๋ฒˆ ํ˜ธ์ถœ

โœ๏ธ ์–ธ์ œ const๋ฅผ ์จ์•ผ ํ• ๊นŒ?

์‚ฌ์šฉํ•˜๋ฉด ์ข‹์€ ๊ฒฝ์šฐ

  • ๊ฐ’์ด ๊ณ ์ •๋œ ์œ„์ ฏ
  • StatelessWidget ๋‚ด๋ถ€
  • ์•„์ด์ฝ˜, ํ…์ŠคํŠธ, ํŒจ๋”ฉ ๋“ฑ UI ๊ตฌ์„ฑ ์š”์†Œ
const Icon(Icons.home);
const SizedBox(height: 16);

์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋Š” ๊ฒฝ์šฐ

  • ๋Ÿฐํƒ€์ž„์— ๊ฐ’์ด ๋ฐ”๋€Œ๋Š” ๊ฒฝ์šฐ
  • ๋ณ€์ˆ˜, ์ƒํƒœ(State), context ์˜์กด ๊ฐ’ ์‚ฌ์šฉ ์‹œ

โœ๏ธ ์ •๋ฆฌ

const๋Š” ๋ถˆ๋ณ€ + ์บ์‹ฑ = ๋ถˆํ•„์š”ํ•œ rebuild ์ œ๊ฑฐ โ†’ ์„ฑ๋Šฅ ํ–ฅ์ƒ

Flutter์—์„œ๋Š” "์“ธ ์ˆ˜ ์žˆ์œผ๋ฉด ๋ฌด์กฐ๊ฑด const"๊ฐ€ ๊ธฐ๋ณธ


๐Ÿ“ Clean Architecture

โœ๏ธ Clean Architecture๋ž€?

  • ์ œ์•ˆ์ž: Robert C. Martin(Uncle Bob)
  • ํ•ต์‹ฌ ์•„์ด๋””์–ด: ์†Œํ”„ํŠธ์›จ์–ด๋ฅผ ๊ด€์‹ฌ์‚ฌ(Responsibility) ๋ณ„๋กœ ๊ณ„์ธต ๋ถ„๋ฆฌ
  • ์˜์กด์„ฑ ๊ทœ์น™: ํ•ญ์ƒ ๋ฐ”๊นฅ โ†’ ์•ˆ์ชฝ ๋‹จ๋ฐฉํ–ฅ
  • ๋‚ด๋ถ€ ๊ณ„์ธต์€ ์™ธ๋ถ€ ๊ณ„์ธต์„ ๋ชจ๋ฅธ๋‹ค

๋ชฉ์ 

  • ์œ ์ง€๋ณด์ˆ˜: ํŠน์ • ๋ถ€๋ถ„(์˜ˆ: DB)์„ ๋ฐ”๊ฟ”๋„ ๋‹ค๋ฅธ ๊ณณ์€ ๋ฉ€์ฉกํ•จ
  • ํ…Œ์ŠคํŠธ ์šฉ์ด์„ฑ(Mock/๊ฐ€์งœ ๋ฐ์ดํ„ฐ๋กœ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ)
  • ํ”„๋ ˆ์ž„์›Œํฌ/ํ”Œ๋žซํผ ๋ณ€ํ™”์— ๊ฐ•ํ•จ

โœ๏ธ ํด๋ฆฐ ์•„ํ‚คํ…์ฒ˜์˜ 4๊ณ„์ธต (๋™์‹ฌ์›)

Entities

๋น„์ฆˆ๋‹ˆ์Šค์˜ ํ•ต์‹ฌ ๊ฐœ๋…
์ˆœ์ˆ˜ Dart ํด๋ž˜์Šค

์˜ˆ: User, Movie

Use Cases

๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™ + ํ๋ฆ„
Entity๋ฅผ ์‚ฌ์šฉํ•ด ๋ฌด์—‡์„ ํ• ์ง€ ์ •์˜

์˜ˆ: ์˜ํ™” ๋ชฉ๋ก ์กฐํšŒ, ํšŒ์›๊ฐ€์ž…

Interface Adapters

์™ธ๋ถ€ โ†” ๋‚ด๋ถ€ ๋ณ€ํ™˜ ๊ณ„์ธต
Controller / Presenter / ViewModel
๋ฐ์ดํ„ฐ ํ˜•ํƒœ ๋ณ€ํ™˜ ๋‹ด๋‹น

Frameworks & Drivers

UI, DB, API, Flutter ํ”„๋ ˆ์ž„์›Œํฌ
์–ธ์ œ๋“  ๊ต์ฒด ๊ฐ€๋Šฅํ•œ ์˜์—ญ

โœ๏ธ Flutter์—์„œ์˜ ๊ณ„์ธต ๋งคํ•‘

Presentation Layer

UI ๋ฐ ์ƒํƒœ ๊ด€๋ฆฌ, ์‚ฌ์šฉ์ž์™€ ๋งŒ๋‚˜๋Š” ๊ณณ

  • Widget (ui) : ์ƒํƒœ๋ฅผ ๊ตฌ๋…(Watch)ํ•˜์—ฌ ํ™”๋ฉด์— ๊ทธ๋ฆผ
  • ViewModel : UseCase๋ฅผ ์‹คํ–‰ํ•˜๊ณ  ๊ทธ ๊ฒฐ๊ณผ๋กœ ์ƒํƒœ(State)๋ฅผ ์—…๋ฐ์ดํŠธ

Domain Layer

๊ฐ€์žฅ ์•ˆ์ชฝ, ์ˆœ์ˆ˜ํ•จ

  • Entity : ๋น„์ฆˆ๋‹ˆ์Šค์˜ ํ•ต์‹ฌ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ (์˜ˆ: Movie)
  • UseCase : ์•ฑ์ด ์ˆ˜ํ–‰ํ•ด์•ผ ํ•  ๊ตฌ์ฒด์ ์ธ ๊ธฐ๋Šฅ ๋‹จ์œ„ (์˜ˆ: FetchMoviesUseCase)
  • Repository Interface : "๋‚œ ์ด๋Ÿฐ ๋ฐ์ดํ„ฐ๊ฐ€ ํ•„์š”ํ•ด"๋ผ๊ณ  ์ •์˜๋งŒ ํ•ด๋‘” ๋ช…์„ธ์„œ

Data Layer

๋ฐ”๊นฅ์ชฝ, ๊ตฌํ˜„ ๋‹ด๋‹น

  • Repository Impl : Interface๋ฅผ ์‹ค์ œ๋กœ ๊ตฌํ˜„ํ•˜์—ฌ UseCase์— ๋ฐ์ดํ„ฐ ์ „๋‹ฌ
  • DataSource : ์‹ค์ œ ๋ฐ์ดํ„ฐ์— ์ ‘๊ทผ (๋„คํŠธ์›Œํฌ ํ†ต์‹ , ๋กœ์ปฌ DB)
  • DTO (Data Transfer Object) : ์„œ๋ฒ„์—์„œ ์˜ค๋Š” ๋ฐ์ดํ„ฐ ํ˜•ํƒœ ๊ทธ๋Œ€๋กœ๋ฅผ ๋‹ด๋Š” ๊ฐ์ฒด (Entity์™€ ๋ถ„๋ฆฌํ•˜์—ฌ ์„œ๋ฒ„ ๋ณ€๊ฒฝ์— ๋Œ€์‘)

์ค‘์š”: Domain์€ Data๋ฅผ ๋ชจ๋ฅธ๋‹ค

โœ๏ธ ์˜์กด์„ฑ ๋ฌธ์ œ์™€ ํ•ด๊ฒฐ

๋ฌธ์ œ

UseCase๊ฐ€ Repository ๊ตฌํ˜„์ฒด๋ฅผ ์ง์ ‘ ์ฐธ์กฐ
โ†’ ์˜์กด์„ฑ ์—ญ์ „ ์œ„๋ฐ˜

ํ•ด๊ฒฐ

Interface ๋„์ž…
Repository๋Š” Domain์— ์ธํ„ฐํŽ˜์ด์Šค๋กœ ์ •์˜
Data Layer์—์„œ ๊ตฌํ˜„

DTO ์‚ฌ์šฉ ์ด์œ 

  • DataSource๊ฐ€ Entity์— ์˜์กดํ•˜์ง€ ์•Š๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•จ
  • API/JSON ๊ตฌ์กฐ ๋ณ€๊ฒฝ ์‹œ Domain ๋ณดํ˜ธ

์˜ํ™” ์•ฑ ์˜ˆ์ œ ๊ตฌ์กฐ

lib/
 โ”œโ”€ data/
 โ”‚   โ”œโ”€ data_source/
 โ”‚   โ”œโ”€ dto/
 โ”‚   โ””โ”€ repository/
 โ”œโ”€ domain/
 โ”‚   โ”œโ”€ entity/
 โ”‚   โ”œโ”€ repository/
 โ”‚   โ””โ”€ usecase/
 โ””โ”€ presentation/
     โ”œโ”€ pages/
     โ”‚   โ””โ”€ movie_list/
     โ””โ”€ widgets/

โœ๏ธ ์‹คํ–‰ ํ๋ฆ„ (์ •๋ฐฉํ–ฅ)

  1. ์‚ฌ์šฉ์ž๊ฐ€ ๋ฒ„ํŠผ ํด๋ฆญ
  2. View โ†’ ViewModel ํ˜ธ์ถœ
  3. ViewModel โ†’ UseCase
  4. UseCase โ†’ Repository(Interface)
  5. Repository Impl โ†’ DataSource
  6. DTO โ†’ Entity ๋ณ€ํ™˜
  7. ๊ฒฐ๊ณผ๋ฅผ ๋‹ค์‹œ ViewModel โ†’ View

โœ๏ธ ํ…Œ์ŠคํŠธ ์ „๋žต

์™œ Mock์„ ์“ฐ๋‚˜?

์„œ๋ฒ„/API ์—†์ด๋„ ๋กœ์ง ๊ฒ€์ฆ ๊ฐ€๋Šฅ
๊ณ„์ธต๋ณ„ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ

ํ…Œ์ŠคํŠธ ๋Œ€์ƒ

  • DTO ๋ณ€ํ™˜ ํ…Œ์ŠคํŠธ
  • DataSource ํ…Œ์ŠคํŠธ (Mock AssetBundle)
  • Repository ํ…Œ์ŠคํŠธ (Mock DataSource)
  • UseCase ํ…Œ์ŠคํŠธ (Mock Repository)
  • ViewModel ํ…Œ์ŠคํŠธ (Mock UseCase)

โœ๏ธ Riverpod + Clean Architecture

  • ViewModel = Notifier
  • UseCase๋Š” Provider๋กœ ์ฃผ์ž…
  • ํ…Œ์ŠคํŠธ ์‹œ Provider override๋กœ Mock ์ฃผ์ž… ๊ฐ€๋Šฅ

โœ๏ธ ์–ธ์ œ ์“ฐ๋ฉด ์ข‹์„๊นŒ?

๐Ÿ‘ ์ถ”์ฒœ

  • ์ค‘/๋Œ€ํ˜• ํ”„๋กœ์ ํŠธ
  • ํŒ€ ํ˜‘์—…
  • ํ…Œ์ŠคํŠธ ์ค‘์š” ํ”„๋กœ์ ํŠธ

๐Ÿ‘Ž ๋น„์ถ”์ฒœ

  • ์ž‘์€ ํ† ์ด ํ”„๋กœ์ ํŠธ
  • ๋‹จ๊ธฐ MVP

์ž‘์€ ํ”„๋กœ์ ํŠธ์—์„  ์˜ค๋ฒ„์—”์ง€๋‹ˆ์–ด๋ง์ด ๋  ์ˆ˜ ์žˆ์Œ

์ •๋ฆฌ

ํด๋ฆฐ ์•„ํ‚คํ…์ณ ์“ฐ๋Š” ์ด์œ 

ํด๋ฆฐ ์•„ํ‚คํ…์ฒ˜๊ฐ€ ์—†์„ ๋•Œ (์ŠคํŒŒ๊ฒŒํ‹ฐ ์ฝ”๋“œ)
์†๋‹˜์ด ์˜ค๋ฉด ์›จ์ดํ„ฐ๊ฐ€ ์ฃผ๋ฌธ์„ ๋ฐ›๊ณ , ์ง์ ‘ ์ฃผ๋ฐฉ์— ๋“ค์–ด๊ฐ€์„œ ์žฌ๋ฃŒ๋ฅผ ๋ƒ‰์žฅ๊ณ ์—์„œ ๊บผ๋‚ด๊ณ , ์š”๋ฆฌ๊นŒ์ง€ ํ•ด์„œ ์„œ๋น™
์›จ์ดํ„ฐ ํ•œ ๋ช…์ด ๋ชจ๋“  ๊ฑธ ๋‹ค ์•Œ์•„์•ผ ํ•˜๊ณ , ๋ƒ‰์žฅ๊ณ  ์œ„์น˜๊ฐ€ ๋ฐ”๋€Œ๋ฉด ์›จ์ดํ„ฐ๊ฐ€ ์ผ์„ ๋ชป ํ•˜๊ฒŒ ๋จ -> ์—‰๋ง์ง„์ฐฝ

ํด๋ฆฐ ์•„ํ‚คํ…์ฒ˜๋ฅผ ์ ์šฉํ–ˆ์„ ๋•Œ

์—ญํ• ์„ ํ™•์‹คํžˆ ๋‚˜๋ˆŒ ์ˆ˜ ์žˆ์Œ

  • ํ™€ ์„œ๋น™ (Presentation Layer): ์†๋‹˜ ์ฃผ๋ฌธ๋งŒ ๋ฐ›๊ณ  ์Œ์‹๋งŒ ๋‚ด์คŒ. ์š”๋ฆฌ๋ฒ•์€ ๋ชจ๋ฆ„
  • ์…ฐํ”„ (Domain Layer): ์š”๋ฆฌ ๋ ˆ์‹œํ”ผ(๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง)๋งŒ ์ „๋ฌธ์œผ๋กœ ์•Œ๊ณ  ์žˆ์Œ. ์žฌ๋ฃŒ๊ฐ€ ์–ด๋””์„œ ์˜ค๋Š”์ง€ ์‹ ๊ฒฝ ์•ˆ ์”€
  • ์žฌ๋ฃŒ ๋‹ด๋‹น (Data Layer): ๋งˆํŠธ์—์„œ ์žฌ๋ฃŒ๋ฅผ ์‚ฌ ์˜ค๊ณ  ๋‹ค๋“ฌ์–ด์„œ ์…ฐํ”„์—๊ฒŒ ๋„˜๊ฒจ์คŒ

์ด๋ ‡๊ฒŒ ๋‚˜๋ˆ„๋ฉด ๋งˆํŠธ๊ฐ€ ๋ฐ”๋€Œ์–ด๋„ ์…ฐํ”„๋Š” ์ƒ๊ด€์—†๊ณ , ๋ฉ”๋‰ดํŒ ๋””์ž์ธ์ด ๋ฐ”๋€Œ์–ด๋„ ์š”๋ฆฌ ๋ง›์€ ๋ณ€ํ•˜์ง€ ์•Š์Œ!

์ธํ„ฐํŽ˜์ด์Šค์™€ ์˜์กด์„ฑ ์—ญ์ „

์ธํ„ฐํŽ˜์ด์Šค

๋„ˆ๋ž‘ ๋‚˜๋ž‘ ์ง€์ผœ์•ผ ํ•  ์•ฝ์†
์ธํ„ฐํŽ˜์ด์Šค๋Š” ๊ตฌ์ฒด์ ์ธ ๊ธฐ๋Šฅ์ด ์•„๋‹ˆ๋ผ "์–ด๋–ค ๊ธฐ๋Šฅ์ด ์žˆ์–ด์•ผ ํ•œ๋‹ค"๋ผ๋Š” ํ‘œ์ค€ ๊ทœ๊ฒฉ

์ฝ˜์„ผํŠธ = ์ธํ„ฐํŽ˜์ด์Šค

  • ์ฝ˜์„ผํŠธ๋Š” ์ „๊ธฐ๋ฅผ ์ค€๋‹ค๋Š” ์•ฝ์†๋งŒ ์ง€ํ‚ด
  • ๊ฑฐ๊ธฐ์— ์„ ํ’๊ธฐ๋ฅผ ๊ฝ‚๋“ , ํ† ์Šคํ„ฐ๋ฅผ ๊ฝ‚๋“  ์ฝ˜์„ผํŠธ๋Š” ์ƒ๊ด€ํ•˜์ง€ ์•Š์Œ
  • ํ”Œ๋Ÿฌ๊ทธ ๋ชจ์–‘(๊ทœ๊ฒฉ)๋งŒ ๋งž์œผ๋ฉด ๋ฌด์—‡์ด๋“  ์—ฐ๊ฒฐํ•  ์ˆ˜ ์žˆ์Œ

DTO vs Entity

์™œ ๊ฐ™์€ ๋ฐ์ดํ„ฐ๋ฅผ ๋‘ ๋ฒˆ ์ •์˜ํ• ๊นŒ?

DTO (Data Transfer Object)

๋ฐญ์—์„œ ๋ง‰ ์บ์˜จ ํ™ ๋ฌป์€ ๋‹น๊ทผ
์„œ๋ฒ„(API)์—์„œ ์ฃผ๋Š” ๋‚ ๊ฒƒ ๊ทธ๋Œ€๋กœ์˜ ๋ฐ์ดํ„ฐ
์šฐ๋ฆฌ ์•ฑ์—๋Š” ํ•„์š” ์—†๋Š” ์ •๋ณด(ํ™)๊ฐ€ ๋ฌป์–ด์žˆ์„ ์ˆ˜๋„ ์žˆ๊ณ , ํ˜•์‹์ด ์ง€์ €๋ถ„ํ•  ์ˆ˜๋„ ์žˆ์Œ

์—”ํ‹ฐํ‹ฐ (Entity)

์…ฐํ”„๊ฐ€ ์š”๋ฆฌ์— ์“ฐ๊ธฐ ์ข‹๊ฒŒ ๊นจ๋—์ด ์”ป๊ณ  ๋‹ค๋“ฌ์€ ๋‹น๊ทผ
์šฐ๋ฆฌ ์•ฑ์˜ ํ•ต์‹ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๊น”๋”ํ•œ ๋ฐ์ดํ„ฐ

๋ฐ์ดํ„ฐ ๊ณ„์ธต(์ฐฝ๊ณ )์—์„œ๋Š” ํ™ ๋ฌป์€ ๋‹น๊ทผ(DTO)์„ ๋ฐ›์•„์„œ,
๊นจ๋—ํ•œ ๋‹น๊ทผ(Entity)์œผ๋กœ ์”ป์–ด์„œ ๋„๋ฉ”์ธ ๊ณ„์ธต(์ฃผ๋ฐฉ)์œผ๋กœ ๋„˜๊ฒจ์คŒ

ํด๋ฆฐ ์•„ํ‚คํ…์ฒ˜๋Š” โ€˜๋ณ€๊ฒฝ์— ๊ฐ•ํ•œ ๊ตฌ์กฐโ€™๋ฅผ ๋งŒ๋“œ๋Š” ์„ค๊ณ„ ๋ฐฉ๋ฒ•


๐Ÿ“ ChatBot App ๋งŒ๋“ค๊ธฐ - UI

โœ๏ธ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ

lib/
 โ”œ main.dart
 โ”” home_page/
    โ”œ home_page.dart
    โ”” widgets/
       โ”” chat_item.dart

โœ๏ธ ChatItem ์œ„์ ฏ

๐Ÿ“ lib/home_page/widgets/chat_item.dart

์ฑ„ํŒ… ํ™”๋ฉด์—์„œ ๋ฉ”์‹œ์ง€ ๋งํ’์„  1๊ฐœ๋ฅผ ๋‹ด๋‹นํ•˜๋Š” ์œ„์ ฏ
์ „๋‹ฌ๋ฐ›์€ ๊ฐ’์œผ๋กœ UI๋งŒ ๊ทธ๋ฆผ

import 'package:flutter/material.dart';

class ChatItem extends StatelessWidget {
  const ChatItem({
    super.key,
    required this.content,
    required this.isReceived,
  });

  final String content;
  final bool isReceived;

  
  Widget build(BuildContext context) {
    return Align(
      alignment: isReceived ? Alignment.centerLeft : Alignment.centerRight,
      child: Container(
        decoration: BoxDecoration(
          color: isReceived ? Colors.purple[200] : Colors.grey[200],
          borderRadius: BorderRadius.circular(16),
        ),
        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
        child: Text(
          content,
          style: TextStyle(
            fontSize: 16,
            color: isReceived ? Colors.white : Colors.black,
          ),
        ),
      ),
    );
  }
}

โœ๏ธ HomePage

๐Ÿ“ lib/home_page/home_page.dart

import 'package:flutter/material.dart';
import 'package:flutter_chatbot/home_page/widgets/chat_item.dart';

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        FocusScope.of(context).unfocus();
      },
      child: Scaffold(
        body: SafeArea(
          child: Column(
            children: [
              Expanded(
                child: ListView.separated(
                  padding: const EdgeInsets.all(20),
                  itemCount: 20,
                  separatorBuilder: (context, index) =>
                      const SizedBox(height: 10),
                  itemBuilder: (context, index) {
                    return ChatItem(
                        content: 'content', isReceived: index.isEven);
                  },
                ),
              ),
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 20),
                child: TextField(
                  decoration: InputDecoration(
                    border: MaterialStateOutlineInputBorder.resolveWith(
                      (states) {
                        return OutlineInputBorder(
                            borderRadius: BorderRadius.circular(10),
                            borderSide: BorderSide(color: Colors.purple[100]!));
                      },
                    ),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

๐Ÿ“ asChatBot App - Gemini AI ์—ฐ๋™ ๋ฐ API ํ‚ค ๊ด€๋ฆฌ

โœ๏ธ Gemini API ํ‚ค ๋ฐœ๊ธ‰

๋ฐœ๊ธ‰ ๊ฒฝ๋กœ

API ํ‚ค ๋ฐœ๊ธ‰ ๋ฐ›๊ธฐ

๋ฐœ๊ธ‰ ์ˆœ์„œ

  1. Get API Key ํด๋ฆญ
  2. ์•ฝ๊ด€ ๋™์˜
  3. API ํ‚ค ๋งŒ๋“ค๊ธฐ
  4. ๋ฐœ๊ธ‰๋œ ํ‚ค ๋ณต์‚ฌ ๋ฐ ๋ณด๊ด€

โœ๏ธ ํŒจํ‚ค์ง€ ์ถ”๊ฐ€

Gemini SDK

flutter pub add google_generative_ai

์ƒํƒœ ๊ด€๋ฆฌ (ViewModel์šฉ)

flutter pub add flutter_riverpod

โœ๏ธ API Key ๊ด€๋ฆฌ ์ „๋žต

API ํ‚ค๋ฅผ ์†Œ์Šค์ฝ”๋“œ์— ์ง์ ‘ ์ž‘์„ฑ โŒ

API ํ‚ค๋ฅผ ๋ถ„๋ฆฌํ•˜๋Š” ์ด์œ 

GitHub ์—…๋กœ๋“œ ๋ฐฉ์ง€
ํ‚ค ๋ณ€๊ฒฝ ์‹œ ์ฝ”๋“œ ์ˆ˜์ • ๋ถˆํ•„์š”
๊ฐœ๋ฐœ / ๋ฐฐํฌ ํ™˜๊ฒฝ๋ณ„ ํ‚ค ๋ถ„๋ฆฌ ๊ฐ€๋Šฅ

์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•

.env ํŒŒ์ผ + dart-define ๋ฐฉ์‹

.env ํŒŒ์ผ ์ƒ์„ฑ

GEMINI_API_KEY=๋ฐœ๊ธ‰๋ฐ›์€_API_KEY

VSCode ์‹คํ–‰ ์„ค์ • (.vscode/launch.json)

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "flutter_env",
      "request": "launch",
      "type": "dart",
      "toolArgs": ["--dart-define-from-file", "./.env"]
    }
  ]
}

์ฝ”๋“œ์—์„œ API ํ‚ค ๊ฐ€์ ธ์˜ค๊ธฐ

String.fromEnvironment('GEMINI_API_KEY');

โœ๏ธ HomeViewModel ๊ตฌํ˜„

์ฑ„ํŒ… ์ƒํƒœ ๊ด€๋ฆฌ
Gemini AI์™€ ํ†ต์‹  ๋‹ด๋‹น

Chat ๋ชจ๋ธ

class Chat {
  final String content;
  final bool isReceived;

  const Chat({
    required this.content,
    required this.isReceived,
  });
}

content : ๋ฉ”์‹œ์ง€ ๋‚ด์šฉ
isReceived : AI ์‘๋‹ต ์—ฌ๋ถ€

HomeViewModel ๊ตฌ์กฐ

class HomeViewModel extends Notifier<List<Chat>> {
  
  List<Chat> build() => [];
}

์ƒํƒœ: List<Chat>
์ดˆ๊ธฐ ์ƒํƒœ: ๋นˆ ๋ฆฌ์ŠคํŠธ

Gemini ๋ชจ๋ธ ์ƒ์„ฑ

  final _model = GenerativeModel(
    model: 'gemini-3-flash-preview',
    apiKey: const String.fromEnvironment('GEMINI_API_KEY'),
  );

๋ฉ”์‹œ์ง€ ์ „์†ก ๋กœ์ง

void send(String text) async {
  // ์‚ฌ์šฉ์ž ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€
  state = [...state, Chat(content: text, isReceived: false)];

  // Gemini ์‘๋‹ต ์š”์ฒญ
  final result = await _model.generateContent([Content.text(text)]);

  // AI ์‘๋‹ต ์ถ”๊ฐ€
  if (result.text != null) {
    state = [...state, Chat(content: result.text!, isReceived: true)];
  }
}

Provider ์„ ์–ธ

final homeViewModel =
  NotifierProvider<HomeViewModel, List<Chat>>(() => HomeViewModel());

โœ๏ธ HomePage ์ˆ˜์ • (UI + ์ƒํƒœ ์—ฐ๋™)

StatelessWidget โ†’ ConsumerStatefulWidget
Riverpod ์ƒํƒœ ๊ตฌ๋…
TextField ์ž…๋ ฅ ์‹œ AI ํ˜ธ์ถœ

์ƒํƒœ ๊ตฌ๋…

final state = ref.watch(homeViewModel);

์ฑ„ํŒ… ๋ฆฌ์ŠคํŠธ ๋ Œ๋”๋ง

ListView.separated(
  itemCount: state.length,
  itemBuilder: (context, index) {
    final item = state[index];
    return ChatItem(
      content: item.content,
      isReceived: item.isReceived,
    );
  },
)

๋ฉ”์‹œ์ง€ ์ „์†ก ์ฒ˜๋ฆฌ

onSubmitted: (value) {
  ref.read(homeViewModel.notifier).send(value);
  textController.clear();
},

0๊ฐœ์˜ ๋Œ“๊ธ€