tracks
FastHTML과 MongoDB는 최신 웹 개발을 위한 고속의 Python 네이티브 접근 방식을 제공합니다. 이 튜토리얼에서는 반응형 실시간 작업 관리자 애플리케이션을 구축하며, 하나의 유지보수 가능한 Python 파일 안에서 CRUD(생성, 조회, 수정, 삭제) 라이프사이클 전체를 구현합니다. Python에서 MongoDB를 직접 사용해 보고 싶다면 Introduction to MongoDB in Python 과정을 추천합니다.
FastHTML이란?
FastHTML은 FastAPI를 기반으로 한 미니멀하고 고성능의 프레임워크입니다. 전통적인 템플릿 대신 재사용 가능한 Python 함수를 사용해 전체 프런트엔드를 구축할 수 있도록 하는 파이써닉한 HTML 패러다임을 도입합니다.
핵심 강점은 HTMX와의 네이티브 통합에 있습니다. 간단한 HTML 속성만으로 서버 사이드 업데이트를 구동함으로써, 복잡한 자바스크립트 빌드 스택 없이도 동적인 단일 페이지 애플리케이션(SPA) 경험을 만들 수 있습니다.
MongoDB란?
MongoDB는 대표적인 범용 문서 지향 NoSQL 데이터베이스입니다. 유연한 스키마와 JSON 유사 BSON 문서 사용으로 현대적이고 반복적인 개발에 이상적입니다.
Python에서는 공식 비동기 드라이버인 Motor를 사용하며, 이는 FastAPI와 FastHTML의 성능 지향 아키텍처에 완벽히 맞는 논블로킹 인터페이스를 제공합니다.
이 스택이 뛰어난 이유
이 기술들을 결합하면 매우 생산적인 개발 환경이 만들어집니다:
- Pydantic과 MongoDB의 타입 안정성: FastHTML은 데이터 모델링과 유효성 검증을 위해 Pydantic을 활용합니다. 이러한 모델은 MongoDB의 문서 구조와 자연스럽게 매핑되어, 무거운 ORM 보일러플레이트 없이 "코드 우선" 경험을 제공합니다.
- 엔드 투 엔드 비동기 성능: Motor와 FastHTML의 비동기 코어를 결합하면 데이터베이스 작업이 이벤트 루프를 차단하지 않습니다. 이는 실시간 반응형 애플리케이션에 중요한 높은 동시성과 낮은 지연 시간을 보장합니다.
- 컨텍스트 전환 감소: 개발자는 하나의 통합된 Python 생태계 안에서 데이터베이스 스키마, 백엔드 로직, 프런트엔드 구성요소를 모두 관리할 수 있어, 전달 속도가 크게 향상됩니다.
설치 및 연결
이 튜토리얼을 따라 하려면 몇 가지 사전 준비가 필요합니다.
- Python 3.8+
- MongoDB 인스턴스: 로컬 설치 또는 MongoDB Atlas 클러스터(실시간 기능에는 권장)
- 기본 지식: Python 데코레이터와 기본 HTML 구조에 대한 이해.
프로젝트 초기화
이 스택의 효율성을 보여주기 위해 데이터베이스 구성, 데이터 모델, 반응형 라우트를 포함한 전체 애플리케이션을 하나의 app.py 파일 안에 구현하겠습니다.
설치
FastHTML 프레임워크, Motor(공식 비동기 MongoDB 드라이버), ASGI 서버용 Uvicorn이 필요합니다.
MongoDB 연결 설정
비동기 연결을 위해 AsyncIOMotorClient 를 사용합니다. 이렇게 하면 애플리케이션이 데이터베이스 I/O를 기다리는 동안에도 다른 동시 요청을 계속 처리할 수 있습니다.
import os
from motor.motor_asyncio import AsyncIOMotorClient
from fasthtml.common import *
from bson import ObjectId
from pydantic import Field, BaseModel
from pymongo.errors import ServerSelectionTimeoutError, ConnectionFailure
# configure MongoDB
MONGO_URI = os.environ.get("MONGO_URI", "mongodb://localhost:27017/") # your Mongo URI
DB_NAME = "fasthtml_tasks_db"
COLLECTION_NAME = "tasks"
# Initialize the async client and reference our collections
client = AsyncIOMotorClient(MONGO_URI)
db = client[DB_NAME]
collection = db[COLLECTION_NAME]
# FastHTML Application Initialization
app = FastHTML()
데이터 모델 정의
문서 지향 워크플로에서는 스키마가 애플리케이션 코드에 존재합니다. 우리는 Pydantic v2를 사용해 MongoDB의 BSON ObjectId와 표준 Python 문자열 사이의 간극을 메웁니다. 이를 통해 데이터베이스에 들어오고 나가는 모든 문서가 우리의 요건에 맞게 검증됩니다.
사용자 정의 PyObjectId 클래스를 정의합니다. 이는 Pydantic이 MongoDB의 ObjectId 타입을 기본적으로 처리하지 못하기 때문입니다. __get_pydantic_core_schema__를 사용해 Pydantic에 이 타입을 JSON에서는 문자열로 다루되, 데이터베이스에서는 BSON 객체로 검증하도록 지시합니다.
python
from pydantic import BaseModel, Field
from pydantic_core import core_schema
from pydantic.json_schema import JsonSchemaValue
class PyObjectId(ObjectId):
"""Custom type to bridge MongoDB ObjectId and Pydantic v2 validation."""
@classmethod
def __get_pydantic_core_schema__(cls, source_type, handler):
# Defines how Pydantic should validate this type
return core_schema.no_info_plain_validator_function(cls.validate)
@classmethod
def validate(cls, v):
if isinstance(v, ObjectId): return v
if isinstance(v, str) and ObjectId.is_valid(v): return ObjectId(v)
raise ValueError("Invalid ObjectId")
@classmethod
def __get_pydantic_json_schema__(cls, _core_schema, handler) -> JsonSchemaValue:
# Ensures the OpenAPI/JSON schema reflects this as a string
return {"type": "string"}
class Task(BaseModel):
"""Pydantic model representing the Task document schema."""
# Maps MongoDB's internal '_id' to a developer-friendly 'id' field
id: PyObjectId | None = Field(None, alias='_id')
title: str
description: str | None = None
completed: bool = False
model_config = ConfigDict(
arbitrary_types_allowed=True
)
레이아웃과 초기 라우트
레이아웃 컴포넌트는 뷰를 감싸는 재사용 가능한 고차 함수입니다. 이를 통해 스타일링을 위한 Tailwind CSS, 상호작용을 위한 HTMX 같은 필수 의존성을 애플리케이션 전반에서 일관되게 사용할 수 있습니다.
FastHTML의 대문자 컴포넌트(예: Main, Div)를 사용하면 HTML 트리를 반영하는 깔끔하고 파이써닉한 구조를 유지할 수 있습니다.
# A responsive layout using Tailwind CSS and the HTMX CDN
def layout(*comps):
"""Wraps the application content in a consistent container."""
return Main(
Div(
H1('📝 Real-Time FastHTML Task Manager',
cls='text-3xl font-bold mb-8 text-center text-gray-800'),
*comps,
cls='container mx-auto p-6 max-w-2xl'
)
)
@app.get('/')
async def home():
"""Initial route rendering the core application layout."""
return layout(
await TaskList(), # Fetches and displays existing tasks (Read)
TaskForm() # Renders the entry form (Create)
)
이제 앱을 실행하면, UI는 다음과 같이 보입니다.

이미지: 초기 설정 후 스크린샷
아직 작업이 없습니다. 다음 섹션에서 작업을 추가하고 업데이트해 보겠습니다.
전체 CRUD 구현
조회: 작업 표시(GET /)
작업을 표시하기 위해 모든 문서를 가져와 컴포넌트로 렌더링합니다. 하지만 실제 환경에서는 데이터베이스가 오프라인일 수도 있습니다. 업데이트된 TaskList는 UI에서 직접 문제 해결 단계를 제공하여 이를 우아하게 처리합니다.
탄력적인 데이터 가져오기
문서를 비동기적으로 가져오기 위해 collection.find().to_list(length=None) 를 사용합니다. 이를 try/except 블록으로 감싸면 MongoDB 연결이 끊긴 상태를 감지해 사용자에게 즉시 피드백을 제공할 수 있습니다.
async def TaskList():
"""Fetches documents from MongoDB and hydrates the Task list view with error handling."""
try:
tasks_data = await collection.find().to_list(length=None)
tasks = [Task(**doc) for doc in tasks_data]
except (ServerSelectionTimeoutError, ConnectionFailure, Exception) as e:
# Catch MongoDB connection errors and provide troubleshooting tips
return Div(
H2('Current Tasks', cls='text-xl font-semibold mb-4'),
Div(
P('⚠️ MongoDB is not running.', cls='text-red-600 font-semibold mb-2'),
P('To start MongoDB:', cls='text-gray-600 mb-1'),
P('1. brew install mongodb-community', cls='text-gray-500 text-sm ml-4'),
P('2. brew services start mongodb-community', cls='text-gray-500 text-sm ml-4'),
cls='p-4 bg-yellow-50 border border-yellow-200 rounded'
),
id='task-list'
)
return Div(
H2('Current Tasks', cls='text-xl font-semibold mb-4'),
*[TaskItem(task) for task in tasks] if tasks else P('No tasks yet. Add one below!', cls='text-gray-500 italic p-4'),
id='task-list',
cls='bg-white rounded-lg shadow-sm border border-gray-200'
)
생성: 새 작업 추가
생성 흐름은 HTMX 속성으로 확장된 HTML 폼을 사용합니다. 업데이트된 버전에서는 Request 객체에서 폼 데이터를 명시적으로 추출하고, MongoDB 삽입을 위해 model_dump 를 사용해 문서를 준비합니다.
@app.post('/add-task')
async def add_task(req: Request):
"""Create: Inserts a task and returns the refreshed list."""
try:
form_data = await req.form()
task = Task(
title=form_data.get('title', ''),
description=form_data.get('description') or None
)
# model_dump(by_alias=True) ensures 'id' is converted back to '_id' for Mongo
task_dict = task.model_dump(exclude_none=True, by_alias=True)
# Remove _id if it's None so MongoDB can generate its own unique ID
if '_id' in task_dict and task_dict['_id'] is None:
del task_dict['_id']
await collection.insert_one(task_dict)
return await TaskList()
except Exception as e:
return Div(P(f'Error: {str(e)}', cls='text-red-500 p-4'), id='task-list')
터미널에서 아래 코드를 실행하면:
curl -X POST http://localhost:8000/add-task -d "title=Complete FastHTML MongoDB integration" -d "description=Verify that tasks can be created, updated, and deleted successfully" -H "Content-Type: application/x-www-form-urlencoded"
이 새 작업이 추가된 것을 확인할 수 있습니다:
이미지: 작업 추가 후 스크린샷
수정: 완료 상태 전환
업데이트를 처리하기 위해 PATCH 요청을 사용합니다. 이는 특정 ID를 사용해 데이터베이스에서 단일 작업 행만 업데이트하고 UI에서 다시 렌더링하는 "마이크로 리프레시"를 보여줍니다.
@app.patch('/toggle-task/{task_id}')
async def toggle_task(task_id: str):
"""Update: Toggles completion status and returns the single row fragment."""
task_doc = await collection.find_one({"_id": ObjectId(task_id)})
if not task_doc: raise HTTPException(404, "Task not found")
await collection.update_one(
{"_id": ObjectId(task_id)},
{"$set": {"completed": not task_doc['completed']}}
)
# Return only the updated TaskItem for a surgical DOM update
updated_doc = await collection.find_one({"_id": ObjectId(task_id)})
return TaskItem(Task(**updated_doc))
삭제: 작업 제거
삭제는 HTMX에 의해 시작됩니다. MongoDB가 삭제를 확인하면 서버는 Empty() 응답을 반환합니다. HTMX는 이 빈 응답을 대상 요소(가장 가까운 div)를 DOM에서 제거하라는 신호로 해석합니다.
@app.delete('/delete-task/{task_id}')
async def delete_task(task_id: str):
"""Delete: Removes a task from MongoDB."""
await collection.delete_one({"_id": ObjectId(task_id)})
# Signal HTMX to remove the element
return Empty()
다음을 실행하면…
curl -X DELETE http://localhost:8000/delete-task/695968244236010c04f313fa
…해당 작업이 삭제됩니다.

이미지: 삭제 후 스크린샷
전체 스크립트는 GitHub에서 확인할 수 있습니다.
마무리
현대적인 Python 중심 스택을 활용해 클라이언트 사이드 자바스크립트 프레임워크의 전통적인 복잡성을 피하는 반응형 애플리케이션을 구축했습니다. 이 아키텍처는 현대 웹 개발에 몇 가지 핵심적인 이점을 제공합니다. FastHTML의 컴포넌트 기반 UI와 MongoDB의 유연한 문서 모델 간의 시너지를 통해 비즈니스 로직, 데이터 무결성, 프레젠테이션을 하나의 응집력 있는 생태계에서 유지할 수 있습니다. 이러한 "올인원 Python" 접근 방식은 개발 부하와 배포 복잡성을 크게 줄여 줍니다.
다음 단계
- 사용자 인증: FastAPI 의존성을 사용해 작업 목록을 특정 사용자로 제한하고, 작업 문서에 user_id 를 저장하세요.
- 고급 쿼리: MongoDB의 집계 프레임워크 또는 간단한 필터를 사용해 UI에 "활성" 및 "완료" 보기를 추가하세요.
- 배포: 프로덕션 수준의 성능을 위해 NGINX 리버스 프록시 뒤에서 Uvicorn으로 app.py 를 배포하세요.
FAQs
FastHTML에서 MongoDB 체인지 스트림을 사용해 "푸시" 업데이트를 구현할 수 있나요?
물론 가능합니다! Motor와 FastHTML 모두 비동기이므로 Python의 async for 루프로 MongoDB 체인지 스트림을 수신할 수 있습니다. 그런 다음 이를 FastHTML의 EventStream(서버 전송 이벤트)와 결합해 데이터베이스의 문서가 변경될 때마다 연결된 모든 사용자에게 실시간으로 업데이트를 푸시할 수 있습니다.
왜 MongoDB에서 원시 Python 딕셔너리 대신 Pydantic 모델을 사용하나요?
MongoDB가 원시 딕셔너리를 허용하긴 하지만, Pydantic은 "애플리케이션 스키마" 역할을 합니다. 이는 데이터 유효성 검증, 타입 힌팅, 기본값 제공(예: completed를 자동으로 False로 설정)을 제공합니다. 이는 "더러운 데이터"가 컬렉션에 들어오는 것을 방지하고, 코드가 커질수록 디버깅을 훨씬 쉽게 해 줍니다.
이 스택에서 데이터베이스 마이그레이션은 어떻게 처리하나요?
MongoDB의 가장 큰 강점 중 하나는 유연한 스키마입니다. 전통적인 SQL 방식의 "마이그레이션"이 필요하지 않습니다. Task 모델에 새 필드를 추가했다면, Pydantic에서 기본값을 제공하면 됩니다. MongoDB의 기존 문서에 해당 필드가 없더라도 애플리케이션에서 로드될 때 기본값으로 "보강(hydrate)"됩니다.
이 작업 관리자에 복잡한 검색 기능을 추가할 수 있나요?
물론입니다. MongoDB에는 강력한 $text 인덱스와 Lucene 기반의 더욱 진보된 Atlas Search가 있습니다. FastHTML에서 hx-get을 사용해 검색창을 쉽게 만들 수 있으며, 사용자가 입력할 때 키워드로 작업을 필터링하는 MongoDB 집계 파이프라인을 트리거할 수 있습니다.
이 스택은 Django나 Flask와 비교해 높은 동시성을 어떻게 처리하나요?
FastHTML은 FastAPI에서 영감을 받은 별도의 프레임워크입니다. ASGI 표준을 사용하며 단일 프로세스에서 수천 개의 동시 연결을 처리할 수 있습니다. Motor의 논블로킹 커넥션 풀링과 결합하면, 데이터베이스 응답을 기다리느라 앱이 "멈추지" 않아 고트래픽 실시간 앱에 훨씬 효율적입니다.