2025. 1. 7. 14:04ㆍProject Log/학부 졸업프로젝트
월요일에 줌으로 교수님 면담을 했다. 4시간 정도 면담을 하면서 궁금했던 것도 여쭤보고, 모듈 설계와 Flutter, GPT 활용에 대한 유익한 이야기를 들을 수 있었다. 프로젝트 안에서 어떤 기술을 어떻게 쓸지 꾸준히 물음표에 부딪힌다. 면담하면 그 부분들에 대한 솔루션을 얻을 수 있어서 좋다.
면담 내용을 다시 한번 정리하는 것은 시간을 꽤 쓰는 일이다. 해야 하는 다른 일도 많지만, 정리는 꼭 필요하다. 이야기하는 동안 중요한 내용을 메모하는데, 나중에 보면 시간 순서로 띄엄띄엄 내용이 떨어져 있다. 이 메모들을 유기적으로 연결하고 추가로 정보를 찾으며 보충하다 보면 이 면담에 어떤 의미가 있었는지 느낄 수 있다.
음성 대화 모듈 질의응답
지난주부터 음성 대화 모듈 개발을 시작하면서 모호한 부분이 조금 있었다. 그 부분을 말씀드리고 조언을 받았다.
- 대화 설계와 Lex 및 GPT 활용 방향을 어떻게 설정할지, 깊이 파면 끝이 없다고 느껴져서 질문드렸다. 먼저 며칠 전에 읽은 CLARA 논문을 보여드리면서 LLM 기반 대화 설계는 생각할 부분이 매우 많은 것 같다고 말씀드렸다.
→ 논문에 나온 것처럼 '대화 설계'는 하나의 연구 주제로서 흥미로운 부분이라고 하셨다. '음성'이라는 것이 가지는 수많은 경우의 수를 컨트롤하는 일의 어려움도 알려주셨다. 음성이라는 것을 하나의 앱 안에 가둘 수 있도록 하고, 음성으로 소통하는 것뿐만 아니라, Flutter 앱 UI를 동시에 활용하여 정보를 수집하라는 말씀하셨다.
- 그리고 Lex가 사용자 의도를 파악하지 못했을 때 GPT에 요청을 보내 응답하도록 하는 프로젝트 사례를 보여드리면서, 처음에 GPT를 활용하려던 방향과 프로젝트 방향은 다른 것 같기는 한데, 앞으로 둘을 어떻게 연결할지 여쭤봤다.
→ Lex로 간단한 챗봇을 만들어보는 게 우선이라고 하셨다. 기본 모듈은 핵심 기능을 문제없이 동작시키는 게 중요하므로, 맨 처음부터 복잡한 대화 설계에 얽매일 필요는 없다는 것이다. 기본 모듈을 개발한 후에 advanced option을 추가해 나가면 된다고 하셨다.
프로젝트 Task 정리
교수님께서 PPT로 프로젝트에서 지속적으로 논의해야 하는 Task를 설명해 주셨다.
기획 업무에서는 긍정적 경험을 평가하기 위한 질문을 던진다면 '점심에 무엇을 드셨나요?'가 아니라, '점심에 무엇을 드셨나요? 현재 기분은 어떠신가요?'처럼 경험을 평가할 만한 요소를 함께 물어봐야 한다는 게 기억에 남는다. 단일 경험뿐만 아니라 서로 영향을 주는 복합 경험도 고려해야 한다.
개발 업무에서는 전체 시스템을 AWS 컴포넌트 기반으로 전환할 것임을 강조하셨다.
[ 프로젝트 기획 업무 ]
- 노인의 정신 및 신체 건강 상태를 어떻게 관리하는가? 긍정적 영향을 주는 '경험'을 찾아내고, 긍정적 경험을 '습관화' 시킴으로써 관리할 수 있다.
⭐ Task #1: 경험과 습관의 개념을 정의한다. - 경험에는 식사, 수면, 운동, 교제, 일상생활 등이 있다. 습관에는 균형된 식사, 규칙적 식사, 규칙적 운동, 적절한 운동 등이 있다.
⭐ Task #2: 긍정적 경험에서 '긍정'에 대한 평가 방법, 습관에 대한 평가 방법을 수립한다. 정신 상태, 신체 상태에 대해 구체적으로 어떤 기준으로 평가할 수 있는지 생각한다. (건강에 대한 조언까지만 할지, 습관 형성까지 할지는 고민해 볼 부분) - '경험'을 찾아내기 위한 데이터 수집 방법으로 Fitbit과 스마트폰을 사용한다. Fitbit으로 신체 데이터를 파악하고, 스마트폰으로 식사, 정신건강 상태(감정, 기분), 신체 건강 상태, 운동, 대화, 일상생활 데이터를 파악할 수 있다.
⭐ Task #3: 경험과 습관에 관한 모니터링 방법을 수립한다. - 긍정적 경험을 찾아내기 위한 질문을 해야 한다.
⭐ Task #4: 긍정적 경험을 찾아내기 위한 ChatGPT 프롬프트를 만든다. - 습관화를 위한 동기 부여 방법이 필요하다.
⭐ Task #5: Habit Tracker App, Fitbit App을 조사하면서 습관화를 위한 전략을 찾는다.
[ 개발 업무 ]
- Flutter App Demo
- Fitbit Data Downloader (RESTful API 사용)
- ChatGPT Service Agent (ChatGPT RESTful API를 호출)
- Database Agent (MongoDB Atlas, 간단한 데이터 읽고 쓰기)
- 개발 단계
1차: 개인 프로그램
2차: Lambda function으로 전환
Flutter 설명
교수님께서 PPT로 Flutter의 동작 방식과 구성 요소, 위젯의 상태 업데이트 등을 설명해 주셨다.
GUI 프로그래밍은 명확한 Control flow가 없고, Widgets을 기반으로 한다. Widget의 구성 요소로는 [Context, Code, State, Display]가 있다.
GUI Runtime System은 Widgets 실행, Context 관리, Events 핸들링을 한다. Events란 키보드 및 마우스 입력, 네트워크 통신 등이 될 수 있다. GUI 동작 방식은 아래와 같고, Flutter 공식 문서의 앱 구조도 찾아보았다.
주요 위젯은 MaterialApp과 Scaffold이다. MaterialApp은 앱을 빌드하는 기반이며, Scaffold는 하나의 페이지이다. Scaffold는 appBar, body, drawer, snackBar로 구성된다.
Widget에는 Stateless Widgets와 Stateful Widgets이 있다. Stateless Widgets로는 Stateless Widget의 서브 클래스, Icon, IconButton, Text가 있다. Stateful Widgets로는 StatefulWidget의 서브 클래스, CheckBox, Radio, Slider, InkWell, Form, TextField 등이 있다.
위젯끼리는 철저하게 부모-자식 관계로 이루어지며, 하나의 트리가 생성된다고 볼 수 있다. 화면을 다시 그릴 때, 루트 노드부터 리프 노트까지 전체를 업데이트하는 건 비효율적인 일이다. 따라서, 상태가 업데이트되는 노드부터 그 아래 서브 트리를 업데이트하는 방식을 사용한다.
아래 코드에서 빨간 선 위로는 Constructor의 역할을 한다. 빨간 선 아래로 _ParentWidgetState에서는 상태 관리가 필요한 위젯들을 다루고 있다. 화면이 다시 그려질 때, 트리에 속하는 전체 위젯이 모두 새로 그려지는 것이 아니라, Stateful Widgets만 업데이트한다.
route와 navigator를 통한 화면 이동 방법도 있다. 이전에 Kotlin으로 안드로이드 앱을 개발할 때, Navigation 코드를 작성해 봤기 때문에 기본 개념은 익숙했다.
하지만, Kotlin과 Dart 언어와 Flutter 프레임워크는 차이가 크니, 교수님 PPT 코드를 기준으로 공식 문서 코드를 열심히 살펴보았다.
먼저 StatelessWidget으로 FirstScreen과 SecondScreen을 생성한다.
import 'package:flutter/material.dart';
class FirstScreen extends StatelessWidget {
const FirstScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('First Screen'),
),
body: Center(
child: ElevatedButton(
onPressed: () {
// Navigate to the second screen when tapped.
},
child: const Text('Launch screen'),
),
),
);
}
}
class SecondScreen extends StatelessWidget {
const SecondScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Second Screen'),
),
body: Center(
child: ElevatedButton(
onPressed: () {
// Navigate back to first screen when tapped.
},
child: const Text('Go back!'),
),
),
);
}
}
route를 정의한다.
MaterialApp(
title: 'Named Routes Demo',
// Start the app with the "/" named route. In this case, the app starts
// on the FirstScreen widget.
initialRoute: '/',
routes: {
// When navigating to the "/" route, build the FirstScreen widget.
'/': (context) => const FirstScreen(),
// When navigating to the "/second" route, build the SecondScreen widget.
'/second': (context) => const SecondScreen(),
},
)
FirstScreen 내부 버튼 위젯의 onPressed 콜백 함수에 Navigator.pushNamed(context, '/{route}')를 추가한다. 버튼을 클릭하면 SecondScreen으로 이동한다.
// Within the `FirstScreen` widget
onPressed: () {
// Navigate to the second screen using a named route.
Navigator.pushNamed(context, '/second');
}
SecondScreen 내부 버튼 위젯의 onPressed 콜백 함수에 Navigator.pop(context)를 추가한다. 버튼을 클릭하면 현재 화면을 스택에서 제거하고, 이전 화면으로 돌아간다.
// Within the SecondScreen widget
onPressed: () {
// Navigate back to the first screen by popping the current route
// off the stack.
Navigator.pop(context);
}
Provider에 대한 내용은 조금 생소했다. Provider는 여러 개의 위젯 간에 데이터를 공유하고 상호작용을 하도록 도와준다. 이 부분도 교수님 PPT 코드를 기준으로 공식 docs 코드를 찾아보았다.
ChangeNotifier는 app state를 캡슐화하고 공유하기 위해 사용된다. app state를 관리하는 클래스에 notifyListeners 함수를 추가한다. 그 정보를 받으려는 위젯에서는 클래스 객체를 생성하고 addListener 함수와 콜백 함수를 호출한다.
class CartModel extends ChangeNotifier {
/// Internal, private state of the cart.
final List<Item> _items = [];
/// An unmodifiable view of the items in the cart.
UnmodifiableListView<Item> get items => UnmodifiableListView(_items);
/// The current total price of all items (assuming all items cost $42).
int get totalPrice => _items.length * 42;
/// Adds [item] to cart. This and [removeAll] are the only ways to modify the
/// cart from the outside.
void add(Item item) {
_items.add(item);
// This call tells the widgets that are listening to this model to rebuild.
notifyListeners();
}
/// Removes all items from the cart.
void removeAll() {
_items.clear();
// This call tells the widgets that are listening to this model to rebuild.
notifyListeners();
}
}
// within some widget
test('adding item increases total cost', () {
final cart = CartModel();
final startingPrice = cart.totalPrice;
var i = 0;
cart.addListener(() {
expect(cart.totalPrice, greaterThan(startingPrice));
i++;
});
cart.add(Item('Dash'));
expect(i, 1);
});
ChangeNotifierProvider는 자손 위젯에게 ChangeNotifier 객체를 제공한다. create 함수는 ChangeNotifier 클래스의 객체를 생성하며, 생성된 객체는 Consumer 위젯에게 전달된다. child 파라미터는 자손 위젯의 root 위젯이다.
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CartModel(),
child: const MyApp(),
),
);
}
Consumer는 ChangeNotifier 객체의 변경 사항을 listen한다. builder 파라미터는 위젯을 빌드하는 함수로 context, ChangeNotifier 객체, 자식 위젯을 인자로 받는다.
return Consumer<CartModel>(
builder: (context, cart, child) {
return Text('Total price: ${cart.totalPrice}');
},
);
MultiProvider로 여러 개의 클래스를 providers로 제공할 수도 있다.
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => CartModel()),
Provider(create: (context) => SomeOtherClass()),
],
child: const MyApp(),
),
);
}
GPT 세미나 피드백
아래 포스팅을 화면 공유하여, 주요 사항과 직접 GPT 프로젝트 예제 4개를 테스트하면서 느낀 점을 말씀드렸다.
https://yr-dev.tistory.com/entry/문헌-세미나-3장-GPT-4와-챗GPT로-애플리케이션-구축하기-feat-2장-간단한-리뷰-포함
- 학교 모집 요강 PDF 파일을 GPT에 제공하고, PDF 자료를 기반으로 답변하도록 만들었다. PDF에 있는 실제 데이터와 다른 잘못된 숫자나 답변을 주기도 했다. 이런 할루시네이션 현상을 어떻게 받아들여야 할지 모르겠다고 질문드렸다.
→ 먼저, 내가 사용한 모델은 'gpt-3.5-turbo'였다. 교수님께서 이 모델은 오래된 모델이므로, 최신 모델을 사용하여 동일한 일을 시켜보면 정확도의 차이가 날 것이라고 하셨다. 그리고 PDF 기반 응답 결과는 GPT가 가지고 있는 한 가지 모습이니, 텍스트 북 바깥의 더 많은 시도를 해보면서 GPT에 대해 알아가려는 태도를 가져야 한다고 하셨다.
* 참고 사이트
- Flutter architectural overview: https://docs.flutter.dev/resources/architectural-overview
- Add interactivity to your Flutter app: https://docs.flutter.dev/ui/interactivity
- Navigate with named routes: https://docs.flutter.dev/cookbook/navigation/named-routes
- Simple app state management: https://docs.flutter.dev/data-and-backend/state-mgmt/simple