๐Ÿ“š Today I Learned 24ํšŒ์ฐจ - Gemini AI ์ฑ„ํŒ…๋ด‡ ๋งŒ๋“ค๊ธฐ ๐Ÿค–

7 ๋ถ„ ์†Œ์š”

์˜ค๋Š˜์€ Gemini AI๋ฅผ ํ™œ์šฉํ•œ ๋‚˜๋งŒ์˜ ์ฑ„ํŒ…๋ด‡ ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค์–ด๋ดค์Šต๋‹ˆ๋‹ค! ๐Ÿค– HTTP ์š”์ฒญ๊ณผ REST API๋ฅผ ์ด์šฉํ•œ AI ํ†ต์‹ , Isar NoSQL ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ํ™œ์šฉํ•œ ์ฑ„ํŒ… ๊ธฐ๋ก ๊ด€๋ฆฌ, ๊ทธ๋ฆฌ๊ณ  StreamBuilder๋ฅผ ์‚ฌ์šฉํ•œ ์‹ค์‹œ๊ฐ„ UI ์—…๋ฐ์ดํŠธ๊นŒ์ง€ ์‹ค์ „ ํ”„๋กœ์ ํŠธ๋ฅผ ํ†ตํ•ด ๋ฐฐ์›Œ๋ณด์•˜์–ด์š”!

๐ŸŽฏ ํ•™์Šต ๋ชฉํ‘œ

  • Gemini API๋ฅผ ์‚ฌ์šฉํ•œ AI ์ฑ„ํŒ… ๋ฉ”์‹œ์ง€ ์†ก์ˆ˜์‹  ๊ตฌํ˜„ํ•˜๊ธฐ
  • Isar NoSQL ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋กœ ์ฑ„ํŒ… ๊ธฐ๋ก ์ €์žฅ ๋ฐ ์กฐํšŒํ•˜๊ธฐ
  • StreamBuilder๋ฅผ ํ™œ์šฉํ•œ ์‹ค์‹œ๊ฐ„ UI ์—…๋ฐ์ดํŠธ ๊ตฌํ˜„ํ•˜๊ธฐ
  • HTTP ์š”์ฒญ๊ณผ REST API ๊ฐœ๋… ์ดํ•ดํ•˜๊ณ  ์‹ค์Šตํ•˜๊ธฐ
  • ListView์˜ ์ž๋™ ์Šคํฌ๋กค ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ œ์–ด ๋ฐฉ๋ฒ• ์ตํžˆ๊ธฐ

๐Ÿ“Š ํ”„๋กœ์ ํŠธ ๊ฐœ์š”

๐Ÿ’ก ๊ตฌํ˜„ ๊ธฐ๋Šฅ

๊ธฐ๋Šฅ ์„ค๋ช… ์‚ฌ์šฉ ๊ธฐ์ˆ 
AI ์ฑ„ํŒ… Gemini API๋ฅผ ํ†ตํ•œ ์‹ค์‹œ๊ฐ„ ๋Œ€ํ™” REST API, HTTP ์š”์ฒญ
๋ฐ์ดํ„ฐ ์ €์žฅ ์ฑ„ํŒ… ๊ธฐ๋ก์„ ๋กœ์ปฌ์— ์˜๊ตฌ ๋ณด๊ด€ Isar NoSQL Database
์‹ค์‹œ๊ฐ„ UI ๋ฉ”์‹œ์ง€ ๋„์ฐฉ ์‹œ ์ž๋™ ํ™”๋ฉด ๊ฐฑ์‹  StreamBuilder
์ž๋™ ์Šคํฌ๋กค ์ƒˆ ๋ฉ”์‹œ์ง€ ์ˆ˜์‹  ์‹œ ์ž๋™ ์Šคํฌ๋กค ScrollController

๐Ÿ—๏ธ ํ”„๋กœ์ ํŠธ ์•„ํ‚คํ…์ฒ˜

์‚ฌ์šฉ์ž ์ž…๋ ฅ (TextField)
      โ”‚
      โ–ผ
์ฑ„ํŒ… ๋ฉ”์‹œ์ง€ ์ „์†ก (HTTP POST)
      โ”‚
      โ–ผ
Gemini API ์‘๋‹ต (Stream)
      โ”‚
      โ”œโ”€โ–ถ ์‹ค์‹œ๊ฐ„ UI ์—…๋ฐ์ดํŠธ (StreamBuilder)
      โ”‚
      โ–ผ
Isar DB ์ €์žฅ
      โ”‚
      โ–ผ
์ฑ„ํŒ… ๊ธฐ๋ก ์กฐํšŒ ๋ฐ ํ‘œ์‹œ (ListView)

๐Ÿ“š ์ฃผ์š” ๋‚ด์šฉ

โญ HTTP ์š”์ฒญ์˜ ์ดํ•ด

HTTP(HyperText Transfer Protocol)๋Š” ํด๋ผ์ด์–ธํŠธ์™€ ์„œ๋ฒ„ ๊ฐ„ ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ๊ณ ๋ฐ›๊ธฐ ์œ„ํ•œ ํ†ต์‹  ๊ทœ์•ฝ์ž…๋‹ˆ๋‹ค.

๐Ÿ” HTTP URL ๊ตฌ์กฐ ๋ถ„์„

http://www.domain.com:1234/path/to/resource?a=b&x=y
๊ตฌ์„ฑ ์š”์†Œ ์˜ˆ์‹œ ์„ค๋ช…
ํ”„๋กœํ† ์ฝœ http:// ํ†ต์‹  ๋ฐฉ์‹ (http ๋˜๋Š” https)
ํ˜ธ์ŠคํŠธ www.domain.com ์š”์ฒญํ•˜๋Š” ์„œ๋ฒ„์˜ ๋„๋ฉ”์ธ ์ฃผ์†Œ
ํฌํŠธ :1234 ์„œ๋ฒ„ ๋‚ด ํŠน์ • ํฌํŠธ ๋ฒˆํ˜ธ
- HTTP: ๊ธฐ๋ณธ 80
- HTTPS: ๊ธฐ๋ณธ 443
๊ฒฝ๋กœ /path/to/resource ์„œ๋ฒ„ ๋‚ด ๋ฆฌ์†Œ์Šค ์œ„์น˜ (API ์—”๋“œํฌ์ธํŠธ)
์ฟผ๋ฆฌ ?a=b&x=y ์ถ”๊ฐ€ ์ „๋‹ฌ ์ •๋ณด
- ?๋กœ ์‹œ์ž‘
- &๋กœ ๊ตฌ๋ถ„
- key=value ํ˜•์‹

๐Ÿ“‹ HTTP ํ—ค๋” (Header)

ํ—ค๋”๋Š” HTTP ์š”์ฒญ/์‘๋‹ต์— ๋Œ€ํ•œ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ๋‹ด๋Š” ์˜์—ญ์ž…๋‹ˆ๋‹ค.

// ํ—ค๋” ์˜ˆ์‹œ
headers: {
  'Content-Type': 'application/json',       // ๋ฐ์ดํ„ฐ ํ˜•์‹
  'Authorization': 'Bearer $token',         // ์ธ์ฆ ํ† ํฐ
  'User-Agent': 'Flutter/3.0',              // ํด๋ผ์ด์–ธํŠธ ์ •๋ณด
  'Content-Length': '1234',                 // ๋ฐ์ดํ„ฐ ํฌ๊ธฐ
}

๐Ÿ’ก ํ•ต์‹ฌ: ํ—ค๋”๋Š” ๋ฐ์ดํ„ฐ ํ˜•์‹, ์ธ์ฆ ์ •๋ณด, ํด๋ผ์ด์–ธํŠธ ์ •๋ณด ๋“ฑ์„ ์ „๋‹ฌํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค!

๐ŸŒ HTTP API ๋ฐฉ์‹์˜ ์ข…๋ฅ˜

๋ฐฉ์‹ ์„ค๋ช… ํŠน์ง•
REST API ์ž์› ์ค‘์‹ฌ์˜ ํ‘œ์ค€ HTTP ๋ฐฉ์‹ ๊ฐ„๋‹จํ•˜๊ณ  ๋ฒ”์šฉ์ , ๊ฐ€์žฅ ๋„๋ฆฌ ์‚ฌ์šฉ๋จ
GraphQL ์ฟผ๋ฆฌ ์–ธ์–ด ๊ธฐ๋ฐ˜ API ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋งŒ ์„ ํƒ์ ์œผ๋กœ ์š”์ฒญ ๊ฐ€๋Šฅ
gRPC ๊ตฌ๊ธ€์˜ ๊ณ ์„ฑ๋Šฅ RPC ํ”„๋ ˆ์ž„์›Œํฌ ๋น ๋ฅด๊ณ  ํšจ์œจ์ ์ด์ง€๋งŒ ๋ณต์žกํ•จ

โšก REST API

REST(Representational State Transfer) API๋Š” HTTP๋ฅผ ํšจ์œจ์ ์œผ๋กœ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•œ ์•„ํ‚คํ…์ฒ˜ ์Šคํƒ€์ผ์ž…๋‹ˆ๋‹ค.

๐ŸŽฏ REST API์˜ 4๊ฐ€์ง€ ํ•ต์‹ฌ ์›์น™

์›์น™ ์„ค๋ช… ์˜ˆ์‹œ
๊ท ์ผํ•œ ์ธํ„ฐํŽ˜์ด์Šค ์ผ๊ด€๋œ URL ๊ตฌ์กฐ์™€ HTTP ๋ฉ”์„œ๋“œ ์‚ฌ์šฉ GET /users/1, POST /users
๋ฌด์ƒํƒœ(Stateless) ๊ฐ ์š”์ฒญ์€ ๋…๋ฆฝ์ ์ด๋ฉฐ ์„œ๋ฒ„๋Š” ์ƒํƒœ๋ฅผ ์ €์žฅํ•˜์ง€ ์•Š์Œ ๋งค ์š”์ฒญ๋งˆ๋‹ค ์ธ์ฆ ํ† ํฐ ์ „์†ก
๊ณ„์ธตํ™” ์‹œ์Šคํ…œ ํด๋ผ์ด์–ธํŠธ-์„œ๋ฒ„ ๊ฐ„ ์ค‘๊ฐ„ ๊ณ„์ธต ํ—ˆ์šฉ ๋กœ๋“œ๋ฐธ๋Ÿฐ์„œ, ์บ์‹œ ์„œ๋ฒ„ ๋“ฑ
์บ์‹œ ๊ฐ€๋Šฅ ์‘๋‹ต ๋ฐ์ดํ„ฐ๋ฅผ ์บ์‹ฑํ•˜์—ฌ ์„ฑ๋Šฅ ํ–ฅ์ƒ Cache-Control ํ—ค๋” ํ™œ์šฉ

๐Ÿ“Œ HTTP ๋ฉ”์„œ๋“œ์™€ REST API

๋ฉ”์„œ๋“œ ์šฉ๋„ ์„ค๋ช…
GET ์กฐํšŒ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ฌ ๋•Œ ์‚ฌ์šฉ (Read)
POST ์ƒ์„ฑ ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ๋ฅผ ์ƒ์„ฑํ•  ๋•Œ (Create)
PUT ์ „์ฒด ์ˆ˜์ • ๊ธฐ์กด ๋ฐ์ดํ„ฐ๋ฅผ ์™„์ „ํžˆ ๊ต์ฒด (Update)
PATCH ๋ถ€๋ถ„ ์ˆ˜์ • ๋ฐ์ดํ„ฐ์˜ ์ผ๋ถ€๋งŒ ์ˆ˜์ • (Update)
DELETE ์‚ญ์ œ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ญ์ œํ•  ๋•Œ (Delete)

๐Ÿ”ง Flutter์—์„œ Dio ํŒจํ‚ค์ง€ ์‚ฌ์šฉํ•˜๊ธฐ

Dio๋Š” Flutter์—์„œ ๊ฐ€์žฅ ๋งŽ์ด ์‚ฌ์šฉ๋˜๋Š” HTTP ํด๋ผ์ด์–ธํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ž…๋‹ˆ๋‹ค.

// dio ํŒจํ‚ค์ง€ import
import 'package:dio/dio.dart';

void main() async {
  final dio = Dio();

  // GET ์š”์ฒญ - ๋ฐ์ดํ„ฐ ์กฐํšŒ
  final getResp = await dio.get('https://api.example.com/users');

  // POST ์š”์ฒญ - ๋ฐ์ดํ„ฐ ์ƒ์„ฑ
  final postResp = await dio.post(
    'https://api.example.com/users',
    data: {'name': 'John', 'age': 30},
  );

  // PUT ์š”์ฒญ - ๋ฐ์ดํ„ฐ ์ „์ฒด ์ˆ˜์ •
  final putResp = await dio.put(
    'https://api.example.com/users/1',
    data: {'name': 'John Updated', 'age': 31},
  );

  // DELETE ์š”์ฒญ - ๋ฐ์ดํ„ฐ ์‚ญ์ œ
  final deleteResp = await dio.delete('https://api.example.com/users/1');
}

๐Ÿค– Gemini API ์š”์ฒญ ์˜ˆ์‹œ

// Gemini API ํ˜ธ์ถœ ์˜ˆ์‹œ
final String apiKey = 'YOUR_API_KEY';
final String baseUrl = 'https://generativelanguage.googleapis.com/v1beta';

final response = await dio.post(
  '$baseUrl/models/gemini-1.5-flash:generateContent?key=$apiKey',
  data: {
    'contents': [
      {
        'parts': [
          {'text': '์•ˆ๋…•ํ•˜์„ธ์š”! Flutter์— ๋Œ€ํ•ด ์„ค๋ช…ํ•ด์ฃผ์„ธ์š”.'}
        ]
      }
    ]
  },
);
ํ•ญ๋ชฉ ๊ฐ’
์š”์ฒญ URL https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent
Query key=$API_KEY
Method POST
Content-Type application/json

๐Ÿ’พ Isar ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค

Isar๋Š” Flutter์— ์ตœ์ ํ™”๋œ ์ดˆ๊ณ ์† NoSQL ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์ž…๋‹ˆ๋‹ค.

๐Ÿ“Š SQL vs NoSQL ๋น„๊ต

๊ตฌ๋ถ„ SQL NoSQL
๊ตฌ์กฐ ์—„๊ฒฉํ•œ ์Šคํ‚ค๋งˆ (ํ…Œ์ด๋ธ”, ์ปฌ๋Ÿผ) ์œ ์—ฐํ•œ ๊ตฌ์กฐ (์ปฌ๋ ‰์…˜, ๋ฌธ์„œ)
์žฅ์  ๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ, ๊ด€๊ณ„ ๊ด€๋ฆฌ ๋น ๋ฅธ ์†๋„, ์œ ์—ฐ์„ฑ
๋‹จ์  ๋ณต์žกํ•œ ์„ค์ •, ์†๋„ ์ œํ•œ ๋ฌด๊ฒฐ์„ฑ ๊ด€๋ฆฌ ํ•„์š”
์ฃผ์š” ์šฉ๋„ ์„œ๋ฒ„ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ชจ๋ฐ”์ผ ๋กœ์ปฌ DB, ์บ์‹œ

๐Ÿ’ก ํ•ต์‹ฌ: ์„œ๋ฒ„๋Š” ๋ฌด๊ฒฐ์„ฑ์ด ์ค‘์š”ํ•˜์—ฌ SQL์„ ์„ ํ˜ธํ•˜๊ณ , ๋ชจ๋ฐ”์ผ ์•ฑ์€ ์„ฑ๋Šฅ๊ณผ ์‚ฌ์šฉ์„ฑ์ด ์ค‘์š”ํ•˜์—ฌ NoSQL์„ ๋งŽ์ด ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค!

๐Ÿ—๏ธ Isar ์ปฌ๋ ‰์…˜ ์ •์˜ํ•˜๊ธฐ

Isar์—์„œ๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์ปฌ๋ ‰์…˜(Collection) ๋‹จ์œ„๋กœ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

import 'package:isar/isar.dart';

// @collection ์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ ์ปฌ๋ ‰์…˜ ์ •์˜
@collection
class MessageModel {
  // Id ํƒ€์ž…์€ Isar์—์„œ ์ œ๊ณตํ•˜๋Š” ๊ณ ์œ  ์‹๋ณ„์ž
  Id id = Isar.autoIncrement;  // ์ž๋™ ์ฆ๊ฐ€ ID

  // ํ•„๋“œ ์ •์˜
  bool isMine;          // ๋‚ด๊ฐ€ ๋ณด๋‚ธ ๋ฉ”์‹œ์ง€์ธ์ง€ ์—ฌ๋ถ€
  String message;       // ๋ฉ”์‹œ์ง€ ๋‚ด์šฉ
  int? point;           // ํฌ์ธํŠธ (์„ ํƒ์ )
  DateTime date;        // ๋ฉ”์‹œ์ง€ ์ „์†ก ์‹œ๊ฐ„

  // ์ƒ์„ฑ์ž
  MessageModel({
    this.point,
    required this.isMine,
    required this.message,
    required this.date,
  });
}

โš™๏ธ Build Runner ์‹คํ–‰

Build Runner๋ฅผ ์‹คํ–‰ํ•˜์—ฌ Isar๊ฐ€ ์‚ฌ์šฉํ•  ๋ณด์กฐ ํŒŒ์ผ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

# ํŒจํ‚ค์ง€ ์„ค์น˜
flutter pub add isar isar_flutter_libs
flutter pub add dev:isar_generator
flutter pub add dev:build_runner

# Build Runner ์‹คํ–‰
dart run build_runner build

# watch ๋ชจ๋“œ๋กœ ์‹คํ–‰ (ํŒŒ์ผ ๋ณ€๊ฒฝ ์‹œ ์ž๋™ ์žฌ์ƒ์„ฑ)
dart run build_runner watch

์‹คํ–‰ ํ›„ message_model.g.dart ํŒŒ์ผ์ด ์ƒ์„ฑ๋˜๋ฉฐ, ์ด ํŒŒ์ผ์—๋Š” Isar๊ฐ€ ์‚ฌ์šฉํ•  ์Šคํ‚ค๋งˆ์™€ ๋ฉ”์„œ๋“œ๊ฐ€ ์ •์˜๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”ง Isar ์ดˆ๊ธฐํ™”ํ•˜๊ธฐ

import 'package:path_provider/path_provider.dart';
import 'package:isar/isar.dart';

// Isar ์ธ์Šคํ„ด์Šค ์ดˆ๊ธฐํ™”
Future<Isar> initIsar() async {
  // ์•ฑ ๋ฌธ์„œ ๋””๋ ‰ํ† ๋ฆฌ ๊ฐ€์ ธ์˜ค๊ธฐ
  final dir = await getApplicationDocumentsDirectory();

  // Isar ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ด๊ธฐ
  final isar = await Isar.open(
    [MessageModelSchema],  // ์‚ฌ์šฉํ•  ์ปฌ๋ ‰์…˜ ์Šคํ‚ค๋งˆ
    directory: dir.path,   // ์ €์žฅ ๊ฒฝ๋กœ
  );

  return isar;
}

๐Ÿ’ป ์‹ค์Šต ๋ฐ ๊ตฌํ˜„

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

lib/
โ”œโ”€โ”€ main.dart                  # ์•ฑ ์ง„์ž…์ 
โ”œโ”€โ”€ models/
โ”‚   โ””โ”€โ”€ message_model.dart     # ๋ฉ”์‹œ์ง€ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ
โ”œโ”€โ”€ screens/
โ”‚   โ””โ”€โ”€ home_screen.dart       # ์ฑ„ํŒ… ํ™”๋ฉด
โ”œโ”€โ”€ widgets/
โ”‚   โ”œโ”€โ”€ logo.dart              # ๋กœ๊ณ  ์œ„์ ฏ
โ”‚   โ”œโ”€โ”€ message.dart           # ๋ฉ”์‹œ์ง€ ์œ„์ ฏ
โ”‚   โ”œโ”€โ”€ date_divider.dart      # ๋‚ ์งœ ๊ตฌ๋ถ„์„ 
โ”‚   โ”œโ”€โ”€ chat_text_field.dart   # ์ž…๋ ฅ ํ•„๋“œ
โ”‚   โ””โ”€โ”€ point_notification.dart # ํฌ์ธํŠธ ์•Œ๋ฆผ
โ””โ”€โ”€ services/
    โ””โ”€โ”€ gemini_service.dart    # Gemini API ์„œ๋น„์Šค

๐Ÿ“ฆ 1. ์˜์กด์„ฑ ํŒจํ‚ค์ง€ ์„ค์น˜

# pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  dio: ^5.4.0                  # HTTP ํด๋ผ์ด์–ธํŠธ
  isar: ^3.1.0                 # NoSQL ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค
  isar_flutter_libs: ^3.1.0    # Isar Flutter ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
  path_provider: ^2.1.1        # ํŒŒ์ผ ๊ฒฝ๋กœ ์ œ๊ณต
  get_it: ^7.6.4               # ์˜์กด์„ฑ ์ฃผ์ž…

dev_dependencies:
  isar_generator: ^3.1.0       # Isar ์ฝ”๋“œ ์ƒ์„ฑ
  build_runner: ^2.4.6         # ๋นŒ๋“œ ๋„๊ตฌ

๐Ÿ”ง 2. Isar ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ธํŒ…

GetIt์„ ์‚ฌ์šฉํ•œ ์˜์กด์„ฑ ์ฃผ์ž…

import 'package:get_it/get_it.dart';
import 'package:isar/isar.dart';
import 'package:path_provider/path_provider.dart';

// GetIt ์‹ฑ๊ธ€ํ†ค ์ธ์Šคํ„ด์Šค
final getIt = GetIt.instance;

// Isar ์ดˆ๊ธฐํ™” ๋ฐ ๋“ฑ๋ก
Future<void> setupIsar() async {
  // ์•ฑ ๋ฌธ์„œ ๋””๋ ‰ํ† ๋ฆฌ ๊ฐ€์ ธ์˜ค๊ธฐ
  final dir = await getApplicationDocumentsDirectory();

  // Isar ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ
  final isar = await Isar.open(
    [MessageModelSchema],  // ์‚ฌ์šฉํ•  ์Šคํ‚ค๋งˆ
    directory: dir.path,
  );

  // GetIt์— ๋“ฑ๋กํ•˜์—ฌ ์–ด๋””์„œ๋“  ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•จ
  getIt.registerSingleton<Isar>(isar);
}

// main.dart์—์„œ ์ดˆ๊ธฐํ™”
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await setupIsar();  // Isar ์ดˆ๊ธฐํ™”
  runApp(MyApp());
}

๐Ÿ“ฑ 3. StreamBuilder๋กœ ์‹ค์‹œ๊ฐ„ UI ๊ตฌํ˜„

StreamBuilder๋Š” Stream ๋ฐ์ดํ„ฐ๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๊ฐ์ง€ํ•˜์—ฌ UI๋ฅผ ์ž๋™์œผ๋กœ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค.

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

class HomeScreen extends StatefulWidget {
  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  final ScrollController scrollController = ScrollController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Gemini ์ฑ„ํŒ…๋ด‡')),
      body: Column(
        children: [
          // ๋ฉ”์‹œ์ง€ ๋ฆฌ์ŠคํŠธ (StreamBuilder ์‚ฌ์šฉ)
          Expanded(
            child: StreamBuilder<List<MessageModel>>(
              // Isar์˜ watch()๋กœ ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ๊ฐ์ง€
              stream: GetIt.I<Isar>()
                  .messageModels
                  .where()
                  .watch(fireImmediately: true),  // ์ฆ‰์‹œ ์‹คํ–‰
              builder: (context, snapshot) {
                // ๋กœ๋”ฉ ์ค‘์ผ ๋•Œ
                if (!snapshot.hasData) {
                  return Center(child: CircularProgressIndicator());
                }

                // ๋ฉ”์‹œ์ง€ ๋ฐ์ดํ„ฐ
                final messages = snapshot.data ?? [];

                // ๋ฉ”์‹œ์ง€๊ฐ€ ์—†์„ ๋•Œ
                if (messages.isEmpty) {
                  return Center(child: Text('๋ฉ”์‹œ์ง€๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค'));
                }

                // ๋ฉ”์‹œ์ง€ ๋ฆฌ์ŠคํŠธ ๋ Œ๋”๋ง
                return ListView.builder(
                  controller: scrollController,
                  itemCount: messages.length,
                  itemBuilder: (context, index) {
                    final message = messages[index];
                    return MessageWidget(message: message);
                  },
                );
              },
            ),
          ),
          // ์ž…๋ ฅ ํ•„๋“œ
          ChatTextField(
            onSendMessage: (text) async {
              await sendMessage(text);
              scrollToBottom();  // ๋ฉ”์‹œ์ง€ ์ „์†ก ํ›„ ์Šคํฌ๋กค
            },
          ),
        ],
      ),
    );
  }

  // ๋ฉ”์‹œ์ง€ ์ „์†ก ํ•จ์ˆ˜
  Future<void> sendMessage(String text) async {
    final isar = GetIt.I<Isar>();

    // ๋‚ด ๋ฉ”์‹œ์ง€ ์ €์žฅ
    await isar.writeTxn(() async {
      await isar.messageModels.put(MessageModel(
        isMine: true,
        message: text,
        date: DateTime.now(),
      ));
    });

    // Gemini API ํ˜ธ์ถœ ๋ฐ ์‘๋‹ต ์ €์žฅ
    // (์‹ค์ œ ๊ตฌํ˜„์€ GeminiService์—์„œ)
  }

  // ์•„๋ž˜๋กœ ์Šคํฌ๋กค
  void scrollToBottom() {
    if (scrollController.hasClients) {
      scrollController.animateTo(
        scrollController.position.maxScrollExtent,
        duration: Duration(milliseconds: 300),
        curve: Curves.easeOut,
      );
    }
  }
}

โšก 4. ์ž๋™ ์Šคํฌ๋กค ์• ๋‹ˆ๋ฉ”์ด์…˜

WidgetsBinding.addPostFrameCallback์„ ์‚ฌ์šฉํ•˜์—ฌ ์œ„์ ฏ ๋ Œ๋”๋ง ์งํ›„ ์Šคํฌ๋กค์„ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.

class _HomeScreenState extends State<HomeScreen> {
  final ScrollController scrollController = ScrollController();

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<List<MessageModel>>(
      stream: GetIt.I<Isar>().messageModels.where().watch(fireImmediately: true),
      builder: (context, snapshot) {
        final messages = snapshot.data ?? [];

        // ํ”„๋ ˆ์ž„ ๋ Œ๋”๋ง ํ›„ ์Šคํฌ๋กค ์‹คํ–‰ (๋”ฑ ํ•œ ๋ฒˆ๋งŒ!)
        WidgetsBinding.instance.addPostFrameCallback((_) {
          scrollToBottom();
        });

        return buildMessageList(messages);
      },
    );
  }

  // ์Šคํฌ๋กค ์• ๋‹ˆ๋ฉ”์ด์…˜
  Future<void> scrollToBottom() async {
    if (!scrollController.hasClients) return;

    await scrollController.animateTo(
      scrollController.position.maxScrollExtent,
      duration: Duration(milliseconds: 300),
      curve: Curves.easeInOut,
    );
  }
}

๐Ÿ’ก ํ•ต์‹ฌ ํฌ์ธํŠธ: addPostFrameCallback์€ ํ˜„์žฌ ํ”„๋ ˆ์ž„์ด ๋ Œ๋”๋ง๋œ ์งํ›„ ๋”ฑ ํ•œ ๋ฒˆ๋งŒ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. build()๋Š” ์ž์ฃผ ํ˜ธ์ถœ๋˜๋ฏ€๋กœ, ๋งค๋ฒˆ ๋ฌด๊ฑฐ์šด ์ž‘์—…์„ ์‹คํ–‰ํ•˜๋ฉด ์„ฑ๋Šฅ์ด ์ €ํ•˜๋ฉ๋‹ˆ๋‹ค!

๐ŸŽจ 5. ์ฃผ์š” ์œ„์ ฏ ๊ตฌํ˜„ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

  • โœ… Logo ์œ„์ ฏ: ์•ฑ ์ƒ๋‹จ ๋กœ๊ณ  ํ‘œ์‹œ
  • โœ… Message ์œ„์ ฏ: ๋ฉ”์‹œ์ง€ ๋งํ’์„  (๋‚ด ๋ฉ”์‹œ์ง€ / ์ƒ๋Œ€ ๋ฉ”์‹œ์ง€ ๊ตฌ๋ถ„)
  • โœ… DateDivider ์œ„์ ฏ: ๋‚ ์งœ๋ณ„ ๊ตฌ๋ถ„์„ 
  • โœ… ChatTextField ์œ„์ ฏ: ๋ฉ”์‹œ์ง€ ์ž…๋ ฅ ํ•„๋“œ์™€ ์ „์†ก ๋ฒ„ํŠผ
  • โœ… PointNotification ์œ„์ ฏ: ํฌ์ธํŠธ ํš๋“ ์•Œ๋ฆผ

๐Ÿ“ ๋งˆ๋ฌด๋ฆฌ

โœ… ์˜ค๋Š˜ ๋ฐฐ์šด ๊ฒƒ

  • HTTP & REST API: URL ๊ตฌ์กฐ, HTTP ๋ฉ”์„œ๋“œ, Dio๋ฅผ ์‚ฌ์šฉํ•œ API ํ˜ธ์ถœ ๋ฐฉ๋ฒ•
  • Gemini AI ์—ฐ๋™: REST API๋ฅผ ํ†ตํ•œ AI ์ฑ„ํŒ… ๋ฉ”์‹œ์ง€ ์†ก์ˆ˜์‹  ๊ตฌํ˜„
  • Isar NoSQL: ์ปฌ๋ ‰์…˜ ์ •์˜, Build Runner ์‚ฌ์šฉ๋ฒ•, ๋ฐ์ดํ„ฐ CRUD ์ž‘์—…
  • StreamBuilder: ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ๊ฐ์ง€ ๋ฐ ์ž๋™ UI ์—…๋ฐ์ดํŠธ
  • ์ž๋™ ์Šคํฌ๋กค: addPostFrameCallback์„ ํ™œ์šฉํ•œ ํšจ์œจ์ ์ธ ์Šคํฌ๋กค ์ œ์–ด
  • ์˜์กด์„ฑ ์ฃผ์ž…: GetIt์„ ์‚ฌ์šฉํ•œ ์ „์—ญ ์ƒํƒœ ๊ด€๋ฆฌ

๐Ÿš€ ๋‹ค์Œ ๊ณ„ํš

  1. ์ŠคํŠธ๋ฆผ ์‹ฌํ™”: Stream, StreamController ์ง์ ‘ ๋‹ค๋ฃจ๊ธฐ
  2. ์—๋Ÿฌ ์ฒ˜๋ฆฌ: API ํ˜ธ์ถœ ์‹คํŒจ ์‹œ ์žฌ์‹œ๋„ ๋กœ์ง ๊ตฌํ˜„
  3. ์˜คํ”„๋ผ์ธ ๋ชจ๋“œ: ๋„คํŠธ์›Œํฌ ์—†์„ ๋•Œ ๋กœ์ปฌ DB๋งŒ์œผ๋กœ ๋™์ž‘ํ•˜๊ธฐ
  4. ์„ฑ๋Šฅ ์ตœ์ ํ™”: ๋ฉ”์‹œ์ง€ ํŽ˜์ด์ง•, ์ด๋ฏธ์ง€ ์บ์‹ฑ ์ ์šฉ
  5. ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ: ์Œ์„ฑ ์ž…๋ ฅ, ๋งˆํฌ๋‹ค์šด ์ง€์›, ์ฝ”๋“œ ํ•˜์ด๋ผ์ดํŒ…

๋Œ“๊ธ€๋‚จ๊ธฐ๊ธฐ