
Dart/Flutter์์ ์ปดํ์ผ ํ์ ์์ ๊ฐ์ฒด๋ฅผ ๋ง๋ค๊ธฐ ์ํ ํด๋์ค
์์ฑ์ ์์ const๋ฅผ ๋ถ์ด๊ณ , ํด๋น ํด๋์ค๋ก ๋ง๋ ๊ฐ์ฒด๋ const๋ก ์์ฑํด์ผ ํจ
๋ํ์ ์ผ๋ก Text, Icon ๊ฐ์ Flutter ์์ ฏ๋ค์ด ์ด์ ํด๋น
๊ณ ์ ๋ ๊ฐ์ผ๋ก ์์ ฏ์ ์์ฑํ์ง๋ง const๋ฅผ ์ฌ์ฉํ์ง ์์์ ๋ ๋ฐ์
IDE ๊ฒฝ๊ณ ๋ฉ์์ง:
Use 'const' with the constructor to improve performance.
์๋ฏธ: const๋ฅผ ์ฐ๋ฉด ์ฑ๋ฅ์ด ๋ ์ข์์ง ์ ์๋ค
๊ฐ์ฒด ์์ฑ ์ดํ ์์ฑ ๋ณ๊ฒฝ์ด ๋ถ๊ฐ๋ฅํ ๊ฐ์ฒด
๋ชจ๋ ํ๋๊ฐ final
class A {
final String id;
A(this.id);
}
๋ถ๋ณ ๊ฐ์ฒด ์ค์์๋
์์ฑ์์ 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('Hello');
Flutter๋ ์์ ฏ ํธ๋ฆฌ๋ฅผ ์์ฃผ rebuildํจ
ํ์ง๋ง ์์ ๊ฐ์ฒด(const) ๋
build() ํธ์ถ ์์ฒด๊ฐ ์๋ต๋จOpen DevTools Performance PageTrack widget build counts ํ์ฑํ๋ฒํผ 10๋ฒ ํด๋ฆญ ์
const ์ฌ์ฉ โ build ํ์ ๊ฑฐ์ ์์const ๋ฏธ์ฌ์ฉ โ build ๋งค๋ฒ ํธ์ถconst Icon(Icons.home);
const SizedBox(height: 16);
const๋ ๋ถ๋ณ + ์บ์ฑ = ๋ถํ์ํ rebuild ์ ๊ฑฐ โ ์ฑ๋ฅ ํฅ์
Flutter์์๋ "์ธ ์ ์์ผ๋ฉด ๋ฌด์กฐ๊ฑด const"๊ฐ ๊ธฐ๋ณธ

๋น์ฆ๋์ค์ ํต์ฌ ๊ฐ๋
์์ Dart ํด๋์ค
์: User, Movie
๋น์ฆ๋์ค ๊ท์น + ํ๋ฆ
Entity๋ฅผ ์ฌ์ฉํด ๋ฌด์์ ํ ์ง ์ ์
์: ์ํ ๋ชฉ๋ก ์กฐํ, ํ์๊ฐ์
์ธ๋ถ โ ๋ด๋ถ ๋ณํ ๊ณ์ธต
Controller / Presenter / ViewModel
๋ฐ์ดํฐ ํํ ๋ณํ ๋ด๋น
UI, DB, API, Flutter ํ๋ ์์ํฌ
์ธ์ ๋ ๊ต์ฒด ๊ฐ๋ฅํ ์์ญ

UI ๋ฐ ์ํ ๊ด๋ฆฌ, ์ฌ์ฉ์์ ๋ง๋๋ ๊ณณ
Widget (ui) : ์ํ๋ฅผ ๊ตฌ๋
(Watch)ํ์ฌ ํ๋ฉด์ ๊ทธ๋ฆผViewModel : UseCase๋ฅผ ์คํํ๊ณ ๊ทธ ๊ฒฐ๊ณผ๋ก ์ํ(State)๋ฅผ ์
๋ฐ์ดํธ๊ฐ์ฅ ์์ชฝ, ์์ํจ
Entity : ๋น์ฆ๋์ค์ ํต์ฌ ๋ฐ์ดํฐ ๋ชจ๋ธ (์: Movie)UseCase : ์ฑ์ด ์ํํด์ผ ํ ๊ตฌ์ฒด์ ์ธ ๊ธฐ๋ฅ ๋จ์ (์: FetchMoviesUseCase)Repository Interface : "๋ ์ด๋ฐ ๋ฐ์ดํฐ๊ฐ ํ์ํด"๋ผ๊ณ ์ ์๋ง ํด๋ ๋ช
์ธ์๋ฐ๊นฅ์ชฝ, ๊ตฌํ ๋ด๋น
Repository Impl : Interface๋ฅผ ์ค์ ๋ก ๊ตฌํํ์ฌ UseCase์ ๋ฐ์ดํฐ ์ ๋ฌDataSource : ์ค์ ๋ฐ์ดํฐ์ ์ ๊ทผ (๋คํธ์ํฌ ํต์ , ๋ก์ปฌ DB)DTO (Data Transfer Object) : ์๋ฒ์์ ์ค๋ ๋ฐ์ดํฐ ํํ ๊ทธ๋๋ก๋ฅผ ๋ด๋ ๊ฐ์ฒด (Entity์ ๋ถ๋ฆฌํ์ฌ ์๋ฒ ๋ณ๊ฒฝ์ ๋์)์ค์: Domain์ Data๋ฅผ ๋ชจ๋ฅธ๋ค
UseCase๊ฐ Repository ๊ตฌํ์ฒด๋ฅผ ์ง์ ์ฐธ์กฐ
โ ์์กด์ฑ ์ญ์ ์๋ฐ
Interface ๋์
Repository๋ Domain์ ์ธํฐํ์ด์ค๋ก ์ ์
Data Layer์์ ๊ตฌํ
lib/
โโ data/
โ โโ data_source/
โ โโ dto/
โ โโ repository/
โโ domain/
โ โโ entity/
โ โโ repository/
โ โโ usecase/
โโ presentation/
โโ pages/
โ โโ movie_list/
โโ widgets/
์๋ฒ/API ์์ด๋ ๋ก์ง ๊ฒ์ฆ ๊ฐ๋ฅ
๊ณ์ธต๋ณ ๋จ์ ํ
์คํธ ๊ฐ๋ฅ
Notifier์์ ํ๋ก์ ํธ์์ ์ค๋ฒ์์ง๋์ด๋ง์ด ๋ ์ ์์

ํด๋ฆฐ ์ํคํ
์ฒ๊ฐ ์์ ๋ (์คํ๊ฒํฐ ์ฝ๋)
์๋์ด ์ค๋ฉด ์จ์ดํฐ๊ฐ ์ฃผ๋ฌธ์ ๋ฐ๊ณ , ์ง์ ์ฃผ๋ฐฉ์ ๋ค์ด๊ฐ์ ์ฌ๋ฃ๋ฅผ ๋์ฅ๊ณ ์์ ๊บผ๋ด๊ณ , ์๋ฆฌ๊น์ง ํด์ ์๋น
์จ์ดํฐ ํ ๋ช
์ด ๋ชจ๋ ๊ฑธ ๋ค ์์์ผ ํ๊ณ , ๋์ฅ๊ณ ์์น๊ฐ ๋ฐ๋๋ฉด ์จ์ดํฐ๊ฐ ์ผ์ ๋ชป ํ๊ฒ ๋จ -> ์๋ง์ง์ฐฝ

ํด๋ฆฐ ์ํคํ ์ฒ๋ฅผ ์ ์ฉํ์ ๋
์ญํ ์ ํ์คํ ๋๋ ์ ์์
์ด๋ ๊ฒ ๋๋๋ฉด ๋งํธ๊ฐ ๋ฐ๋์ด๋ ์ ฐํ๋ ์๊ด์๊ณ , ๋ฉ๋ดํ ๋์์ธ์ด ๋ฐ๋์ด๋ ์๋ฆฌ ๋ง์ ๋ณํ์ง ์์!

๋๋ ๋๋ ์ง์ผ์ผ ํ ์ฝ์
์ธํฐํ์ด์ค๋ ๊ตฌ์ฒด์ ์ธ ๊ธฐ๋ฅ์ด ์๋๋ผ "์ด๋ค ๊ธฐ๋ฅ์ด ์์ด์ผ ํ๋ค"๋ผ๋ ํ์ค ๊ท๊ฒฉ
์ฝ์ผํธ = ์ธํฐํ์ด์ค
์ ๊ฐ์ ๋ฐ์ดํฐ๋ฅผ ๋ ๋ฒ ์ ์ํ ๊น?

DTO (Data Transfer Object)
๋ฐญ์์ ๋ง ์บ์จ ํ ๋ฌป์ ๋น๊ทผ
์๋ฒ(API)์์ ์ฃผ๋ ๋ ๊ฒ ๊ทธ๋๋ก์ ๋ฐ์ดํฐ
์ฐ๋ฆฌ ์ฑ์๋ ํ์ ์๋ ์ ๋ณด(ํ)๊ฐ ๋ฌป์ด์์ ์๋ ์๊ณ , ํ์์ด ์ง์ ๋ถํ ์๋ ์์
์ํฐํฐ (Entity)
์
ฐํ๊ฐ ์๋ฆฌ์ ์ฐ๊ธฐ ์ข๊ฒ ๊นจ๋์ด ์ป๊ณ ๋ค๋ฌ์ ๋น๊ทผ
์ฐ๋ฆฌ ์ฑ์ ํต์ฌ ๋น์ฆ๋์ค ๋ก์ง์์ ์ฌ์ฉํ๋ ๊น๋ํ ๋ฐ์ดํฐ
๋ฐ์ดํฐ ๊ณ์ธต(์ฐฝ๊ณ )์์๋ ํ ๋ฌป์ ๋น๊ทผ(DTO)์ ๋ฐ์์,
๊นจ๋ํ ๋น๊ทผ(Entity)์ผ๋ก ์ป์ด์ ๋๋ฉ์ธ ๊ณ์ธต(์ฃผ๋ฐฉ)์ผ๋ก ๋๊ฒจ์ค
ํด๋ฆฐ ์ํคํ ์ฒ๋ โ๋ณ๊ฒฝ์ ๊ฐํ ๊ตฌ์กฐโ๋ฅผ ๋ง๋๋ ์ค๊ณ ๋ฐฉ๋ฒ
lib/
โ main.dart
โ home_page/
โ home_page.dart
โ widgets/
โ chat_item.dart
๐ 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,
),
),
),
);
}
}
๐ 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]!));
},
),
),
),
),
],
),
),
),
);
}
}
๋ฐ๊ธ ๊ฒฝ๋ก
๋ฐ๊ธ ์์
Gemini SDK
flutter pub add google_generative_ai
์ํ ๊ด๋ฆฌ (ViewModel์ฉ)
flutter pub add flutter_riverpod
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');
์ฑํ
์ํ ๊ด๋ฆฌ
Gemini AI์ ํต์ ๋ด๋น
class Chat {
final String content;
final bool isReceived;
const Chat({
required this.content,
required this.isReceived,
});
}
content : ๋ฉ์์ง ๋ด์ฉ
isReceived : AI ์๋ต ์ฌ๋ถ
class HomeViewModel extends Notifier<List<Chat>> {
List<Chat> build() => [];
}
์ํ: List<Chat>
์ด๊ธฐ ์ํ: ๋น ๋ฆฌ์คํธ
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)];
}
}
final homeViewModel =
NotifierProvider<HomeViewModel, List<Chat>>(() => HomeViewModel());
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();
},