오늘은 간단히 FE : flutter, BE : fastAPI, DB : MongoDB로 구성된 글을 적고 저장하는 앱을 하나 만들어보고 이번주 중으로 이 앱에 랭체인을 곁들여 좀 더 재밌는 뭔갈 만들어볼 생각이다.
flutter는 조금 다뤄본 적 있고 fastAPI는 정말 조금 다뤄본 적 있고, mongoDB는 처음이라 사실 되게 낯설고 뭐가 뭔지 아무것도 모르겠지만 일단 내겐 chatGPT와 gemini라는 뛰어난 선생님들이 계시기 때문에 이 분들과 함께 학습하며 진행해볼 것이다.
구체적으로 llm에 원하는 바를 말해서 코드를 받고 이 코드를 이해하는 방식으로 학습할 것이다.
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
void main() => runApp(TextToMongoDBApp());
class TextToMongoDBApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final TextEditingController _controller = TextEditingController();
String _responseMessage = "";
Future<void> _saveText(String text) async {
final url = Uri.parse('http://localhost:8000/save-text');
try {
final response = await http.post(
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'text': text}),
);
if (response.statusCode == 200) {
setState(() {
_responseMessage = "Text saved successfully!";
});
} else {
setState(() {
_responseMessage = "Failed to save text.";
});
}
} catch (e) {
setState(() {
_responseMessage = "Error: $e";
});
}
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Save Text to MongoDB')),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
controller: _controller,
decoration: InputDecoration(labelText: 'Enter text'),
),
SizedBox(height: 16.0),
ElevatedButton(
onPressed: () {
final text = _controller.text;
if (text.isNotEmpty) {
_saveText(text);
}
},
child: Text('Save Text'),
),
SizedBox(height: 16.0),
Text(_responseMessage),
],
),
),
);
}
}
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
❓
package:와dart:의 차이?패키지가 정의된 위치를 나타내는 구분자.
package:는 pub.dev와 같은 패키지 저장소에서 가져온 외부 패키지들이pubspec.yaml파일에 명시되어 있고 Dart VM이 이 파일을 참조하여 패키지를 찾는다는 것을 의미.
dart:는 Dart 언어 자체에서 제공하는 Dart 표준 라이브러리임을 의미하여 이들은 Dart SDK에 내장되어 있어 별도의 설치 없이 바로 사용할 수 있다는 것을 의미.
void main() => runApp(TextToMongoDBApp());
class TextToMongoDBApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
_HomePageState createState() => _HomePageState();
}
main 함수, 즉 이 flutter 앱이 시작되면 TextToMongoDBApp이라는 앱의 최상위 위젯을 runApp이라는 함수로 렌더링한다.
TextToMongoDBApp 위젯은 stateless widget으로 렌더링되는 동안 상태가 변경되지 않는다.
@override Widget build(BuildContext context) 메서드로 위젯 트리를 생성하고 matrial app을 루트 위젯으로 정의한 뒤 home 속성으로 앱이 처음 시작될 때 HomePage가 표시되도록 지정한다.
❓ 위젯트리란
flutter 앱의 UI를 구성하는 위젯들의 계층 구조를 시각화한 것.
루트위젯, 부모위젯, 자식위젯으로 구성됨.
❓BuildContext란
위젯 트리에서 현재 위젯의 위치를 나타내는 객체. 즉 어떤 위젯이 어떤 위치에 있는지 알려주는 정보를 가짐.
이를 통해 위젯은 자신이 속한 위젯 트리에서 다른 위젯에 접근하거나, 테마, 언어 설정 등 다양한 정보를 얻을 수 있고 화면 전환도 할 수 있음.
위젯 간 데이터 공유, 동적 ui 생성, 설정(테마, 언어) 반영하는 데 이용할 수 있다.
HomePage 위젯은 stateful widget으로 렌더링되는 동안 상태가 변경되며 _HomePageState으로 그 상태 관리할 수 있다.
class _HomePageState extends State<HomePage> {
final TextEditingController _controller = TextEditingController();
String _responseMessage = "";
Future<void> _saveText(String text) async {
final url = Uri.parse('http://localhost:8000/save-text');
try {
final response = await http.post(
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'text': text}),
);
if (response.statusCode == 200) {
setState(() {
_responseMessage = "Text saved successfully!";
});
} else {
setState(() {
_responseMessage = "Failed to save text.";
});
}
} catch (e) {
setState(() {
_responseMessage = "Error: $e";
});
}
}
맨 위에 정의된 두 변수는 각각 TextField 위젯(아래 build 에서 나옴)의 컨트롤러, 응답 받은 메시지를 저장하는 문자열이다.
다음 비동기함수 _saveText는 text를 변수로 받아 주어진 url에 post 요청을 보내고 응답을 기다린다.
요청 헤더를 JSON 컨텐츠 유형으로 설정하고 text에 담긴 문자열 데이터를 json 형식으로 인코딩한다.
이후 상태 코드에 따라 _responseMessage를 저장한다.
❓엔드포인트란?
엔드포인트는 네트워크 상에서 데이터를 주고받을 수 있는 특정한 위치를 가리킨다.
마치 건물의 문처럼 외부에서 시스템에 접근하기 위한 입구이고 이는 웹 서비스에서 일반적으로 url로 표현된다.
특정 기능을 수행하거나 데이터를 제공하는 역할을 한다.
http://localhost:8000/save-text이 url은 8000번 포트에서 실행되는 서버의/save-text라는 경로를 가리키는 엔드포인트이다. 이 엔드포인트로 post 요청을 보내면 서버는 받은 데이터를 저장하는 작업을 수행할 것이다.
❓HTTP 요청 메시지 구조
http 요청은 클라이언트(웹, 앱 등)에서 서버로 데이터를 요청할 때 사용하는 메시지이다.
이 메시지는 크게 header와 body로 구성된다. 헤더는 요청에 대한 메타 정보를, 바디는 실제 정보를 담고 있다.
1. 헤더 (Header)
- 요청 메서드: 어떤 동작 수행할지(GET, POST, PUT, DELETE 등)
- URL: 요청할 자원의 위치
- HTTP 버전
- 헤더 필드: 추가적인 정보
- content-type: 요청 바디의 컨텐츠 형식 (application/json, text/plain)
- authorization: 인증 정보
- user-agent: 클라이언트 정보
- accept: 클라이언트가 받아들일 수 있는 컨텐츠 형식
- 바디 (Body)
- 요청에 필요한 데이터를 담고 있음.
- 헤더의 content-type에 따라 다양한 형식 데이터 가능
- json
- xml
등등… (오디오, html 등 다양하게 가능한 듯)
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Save Text to MongoDB')),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
controller: _controller,
decoration: InputDecoration(labelText: 'Enter text'),
),
SizedBox(height: 16.0),
ElevatedButton(
onPressed: () {
final text = _controller.text;
if (text.isNotEmpty) {
_saveText(text);
}
},
child: Text('Save Text'),
),
SizedBox(height: 16.0),
Text(_responseMessage),
],
),
),
);
}
}
이 메서드는 Scaffold 위젯을 이용하여 앱의 ui를 그림.
텍스트 입력을 받는 TextFeild 위젯, 그 아래 약간의 여백을 두고 _saveText 메서드를 실행하기 위한 ElevatedButton, 그리고 다시 공간을 두고 http 응답 결과를 보이기 위한 Text 위젯으로 구성되어 있음을 알 수 있다.
(물론 위에 앱바도 있다.)
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from pymongo import MongoClient
from bson import ObjectId
# FastAPI 앱 생성
app = FastAPI()
# MongoDB 연결 설정
client = MongoClient("<Your MongoDB Connection String>")
db = client["mydatabase"]
collection = db["texts"]
# 데이터 모델 정의
class Text(BaseModel):
text: str
# 텍스트 저장 API
.post("/save-text")
async def save_text(data: Text):
try:
result = collection.insert_one(data.dict())
return {"message": "Text saved successfully!", "id": str(result.inserted_id)}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error saving text: {str(e)}")
# 텍스트 목록 조회 API (Optional)
.get("/get-texts")
async def get_texts():
texts = list(collection.find({}, {"_id": 1, "text": 1}))
return [{"id": str(item["_id"]), "text": item["text"]} for item in texts]
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from pymongo import MongoClient
from bson import ObjectId
# FastAPI 앱 생성
app = FastAPI()
# MongoDB 연결 설정
client = MongoClient("<Your MongoDB Connection String>")
db = client["mydatabase"]
collection = db["texts"]
# 데이터 모델 정의
class Text(BaseModel):
text: str
읽으면 이해되니까~
물론 db 이론은 뭔가 심오한 거 같긴 함. 그치만 다음에 알아보기로..
@app.post("/save-text")
async def save_text(data: Text):
try:
result = collection.insert_one(data.dict())
return {"message": "Text saved successfully!", "id": str(result.inserted_id)}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error saving text: {str(e)}")
@app.post("/save-text") 데코레이터는 POST 요청을 처리하는 “/save-text” 엔드포인트를 정의한다.
save_text 라는 비동기 함수는 앞서 정의한 Text 데이터 모델 구조의 data를 받아 data.dict() 로 딕셔너리로 변환해 collection에 json 형식의 문서로 저장할 수 있도록 한다.
만약 그 과정에서 오류가 발생하면 에러 메시지를 출력하도록 한다.
@app.get("/get-texts")
async def get_texts():
texts = list(collection.find({}, {"_id": 1, "text": 1}))
return [{"id": str(item["_id"]), "text": item["text"]} for item in texts]
@app.get("/get-texts") 데코레이터는 GET 요청을 처리하는 “/get-texts” 엔드포인트를 정의한다.
get_texts 라는 비동기 함수는 collection에 있는 모든 문서를 조회해 _id와 text 필드의 값을 texts 리스트에 저장한 뒤 이를 반환한다.

플러터 앱 실행 후 텍스트 입력

save text 버튼 누르기!

잘 저장된 모습
이제 저장된 텍스트 내용을 바탕으로 rag를 하는 llm 챗봇을 만들어볼거다.
그리고 이 글을 수정할 것이다.
글 진짜 못 쓰네 나.. 그리고 벨로그 너무 어색하다.