flutter+fastAPI+MongoDB 사용해보기

jDiOt·2025년 1월 9일
post-thumbnail

오늘은 간단히 FE : flutter, BE : fastAPI, DB : MongoDB로 구성된 글을 적고 저장하는 앱을 하나 만들어보고 이번주 중으로 이 앱에 랭체인을 곁들여 좀 더 재밌는 뭔갈 만들어볼 생각이다.

flutter는 조금 다뤄본 적 있고 fastAPI는 정말 조금 다뤄본 적 있고, mongoDB는 처음이라 사실 되게 낯설고 뭐가 뭔지 아무것도 모르겠지만 일단 내겐 chatGPT와 gemini라는 뛰어난 선생님들이 계시기 때문에 이 분들과 함께 학습하며 진행해볼 것이다.

구체적으로 llm에 원하는 바를 말해서 코드를 받고 이 코드를 이해하는 방식으로 학습할 것이다.

목차

  1. 프론트엔드
  2. 백엔드

프론트엔드

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';
  • material.dart
    • flutter 앱에서 사용되는 기본적인 material design 위젯과 테마를 제공
    • 위젯, 레이아웃, 테마, 네비게이션 등
  • http.dart
    • HTTP 요청 및 응답 기능 제공.
    • 서버와 통신하여 데이터를 주고받을 수 있도록 함.
    • GET, POST, PUT, DELETE 요청을 할 수 있고 헤더 설정, 응답처리 등을 할 수 있다.
  • convert
    • Dart 객체와 JSON 문자열 간의 변환을 지원
    • JSON 인코딩, JSON 디코딩 가

package:dart: 의 차이?

패키지가 정의된 위치를 나타내는 구분자.

package:는 pub.dev와 같은 패키지 저장소에서 가져온 외부 패키지들이 pubspec.yaml파일에 명시되어 있고 Dart VM이 이 파일을 참조하여 패키지를 찾는다는 것을 의미.
 
dart:는 Dart 언어 자체에서 제공하는 Dart 표준 라이브러리임을 의미하여 이들은 Dart SDK에 내장되어 있어 별도의 설치 없이 바로 사용할 수 있다는 것을 의미.

TextToMongoDBApp

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으로 그 상태 관리할 수 있다.

HomePage의 _saveText 메서드

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: 클라이언트가 받아들일 수 있는 컨텐츠 형식
  1. 바디 (Body)

    - 요청에 필요한 데이터를 담고 있음.
    - 헤더의 content-type에 따라 다양한 형식 데이터 가능
    - json
    - xml
    등등… (오디오, html 등 다양하게 가능한 듯)

HomePage의 build 메서드

  
  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 : 웹 API 프레임워크
  • HTTPException : 에러 처리를 위한 예외 클래스
  • pydantic : 데이터 모델 정의를 위한 라이브러리
  • pymongo : 몽고디비 연결 및 데이터 처리를 위한 라이브러리
  • bson : 몽고디비 데이터 형식 처리를 위한 라이브러리

FastAPI 앱 생성 및 MongoDB 연결, 데이터 모델 정의

# FastAPI 앱 생성
app = FastAPI()

# MongoDB 연결 설정
client = MongoClient("<Your MongoDB Connection String>")
db = client["mydatabase"]
collection = db["texts"]

# 데이터 모델 정의
class Text(BaseModel):
    text: str

읽으면 이해되니까~

물론 db 이론은 뭔가 심오한 거 같긴 함. 그치만 다음에 알아보기로..

텍스트 저장 API

@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 형식의 문서로 저장할 수 있도록 한다.

만약 그 과정에서 오류가 발생하면 에러 메시지를 출력하도록 한다.

텍스트 조회 API

@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 챗봇을 만들어볼거다.

그리고 이 글을 수정할 것이다.

글 진짜 못 쓰네 나.. 그리고 벨로그 너무 어색하다.

profile
Junior Developer In Ongoing Training

0개의 댓글