Flet으로 Python 하나로 웹/모바일/데스크톱 앱 개발하기 - 완전 가이드
⏱️ 예상 읽기 시간: 15분
서론
Python만 알아도 모든 플랫폼 앱을 만들 수 있다면? Flet이 바로 그 답입니다. Flutter를 기반으로 한 Flet은 프론트엔드 경험 없이도 Python 하나로 웹, 모바일, 데스크톱 애플리케이션을 동시에 개발할 수 있게 해주는 혁신적인 프레임워크입니다.
복잡한 JavaScript 프론트엔드, REST API 백엔드, 데이터베이스 등의 아키텍처 대신, Python 모놀리스 앱 하나로 멀티유저 실시간 SPA(Single-Page Application)를 구축할 수 있습니다.
Flet의 핵심 특징
🚀 아이디어에서 앱까지 몇 분 만에
Flet은 다음과 같은 프로젝트에 이상적입니다:
- 내부 도구 또는 팀 대시보드
- 주말 프로젝트 및 프로토타입
- 데이터 입력 폼 및 키오스크 앱
- 고품질 프로토타입 및 MVP
📐 단순한 아키텍처
기존 웹 개발:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ JavaScript │◄──►│ REST API │◄──►│ Database │
│ Frontend │ │ Backend │ │ + Cache │
└─────────────┘ └─────────────┘ └─────────────┘
Flet:
┌─────────────────────────────────────────────────────┐
│ Python 모놀리스 앱 │
│ (멀티유저 실시간 SPA 자동 지원) │
└─────────────────────────────────────────────────────┘
🔋 배터리 포함
- IDE만 있으면 시작: SDK, 복잡한 툴체인 불필요
- 내장 웹 서버: 에셋 호스팅 및 데스크톱 클라이언트 포함
- 의존성 최소화: 수천 개의 패키지 설치 불필요
🌟 Flutter 기반 UI
- 전문적인 외관: Flutter로 구축된 네이티브급 UI
- 모든 플랫폼 지원: Windows, macOS, Linux, iOS, Android, 웹
- 간소화된 모델: 작은 위젯들을 실용적인 컨트롤로 결합
환경 설정 및 설치
기본 환경 요구사항
# Python 3.8 이상 필요
python --version # Python 3.8+
# Flet 설치 (모든 기능 포함)
pip install 'flet[all]'
# 또는 기본 설치만
pip install flet
개발 환경 설정
# 작업 디렉토리 생성
mkdir flet-tutorial
cd flet-tutorial
# 가상환경 생성 (권장)
python -m venv venv
# 가상환경 활성화
# macOS/Linux:
source venv/bin/activate
# Windows:
# venv\Scripts\activate
# Flet 설치
pip install 'flet[all]'
첫 번째 Flet 앱: Hello World
기본 앱 구조
# hello_world.py
import flet as ft
def main(page: ft.Page):
# 페이지 기본 설정
page.title = "첫 번째 Flet 앱"
page.theme_mode = ft.ThemeMode.LIGHT
page.vertical_alignment = ft.MainAxisAlignment.CENTER
# 간단한 텍스트 추가
page.add(
ft.Text(
"안녕하세요, Flet 세계!",
size=30,
color=ft.colors.BLUE_600,
text_align=ft.TextAlign.CENTER
)
)
# 앱 실행
ft.run(main)
앱 실행하기
# 데스크톱 앱으로 실행
python hello_world.py
# 또는 flet 명령어 사용
flet run hello_world.py
웹 앱으로 실행
# 마지막 줄을 다음과 같이 변경
ft.run(main, view=ft.AppView.WEB_BROWSER)
또는 명령어로:
# 웹 브라우저에서 실행
flet run --web hello_world.py
실무 예제: 할 일 관리 앱
완전한 TODO 앱 구현
# todo_app.py
import flet as ft
class TodoApp:
def __init__(self, page: ft.Page):
self.page = page
self.tasks = []
self.setup_page()
self.build_ui()
def setup_page(self):
"""페이지 기본 설정"""
self.page.title = "Flet TODO 앱"
self.page.theme_mode = ft.ThemeMode.LIGHT
self.page.window_width = 400
self.page.window_height = 600
self.page.padding = 20
def build_ui(self):
"""UI 구성요소 생성"""
# 제목
title = ft.Text(
"📝 TODO 리스트",
size=24,
weight=ft.FontWeight.BOLD,
color=ft.colors.BLUE_700
)
# 입력 필드
self.task_input = ft.TextField(
hint_text="새로운 할 일을 입력하세요...",
expand=True,
on_submit=self.add_task
)
# 추가 버튼
add_button = ft.IconButton(
icon=ft.icons.ADD,
bgcolor=ft.colors.BLUE_600,
icon_color=ft.colors.WHITE,
on_click=self.add_task
)
# 입력 영역
input_row = ft.Row([
self.task_input,
add_button
])
# 작업 목록
self.task_list = ft.Column()
# 통계 정보
self.stats = ft.Text(
"총 0개의 할 일",
size=14,
color=ft.colors.GREY_600
)
# 전체 레이아웃 구성
self.page.add(
title,
ft.Divider(),
input_row,
ft.Container(height=20), # 간격
self.stats,
ft.Container(
content=self.task_list,
expand=True,
border_radius=8,
padding=10
)
)
def add_task(self, e):
"""새 작업 추가"""
if self.task_input.value.strip():
task = TodoTask(
text=self.task_input.value.strip(),
on_delete=self.delete_task,
on_toggle=self.toggle_task
)
self.tasks.append(task)
self.task_list.controls.append(task.build())
self.task_input.value = ""
self.update_stats()
self.page.update()
def delete_task(self, task):
"""작업 삭제"""
if task in self.tasks:
self.tasks.remove(task)
self.task_list.controls.remove(task.build())
self.update_stats()
self.page.update()
def toggle_task(self, task):
"""작업 완료 상태 토글"""
self.update_stats()
self.page.update()
def update_stats(self):
"""통계 정보 업데이트"""
total = len(self.tasks)
completed = sum(1 for task in self.tasks if task.completed)
self.stats.value = f"총 {total}개의 할 일 (완료: {completed}개)"
class TodoTask:
def __init__(self, text, on_delete, on_toggle):
self.text = text
self.completed = False
self.on_delete = on_delete
self.on_toggle = on_toggle
self._control = None
def build(self):
"""작업 아이템 UI 구성"""
self.checkbox = ft.Checkbox(
value=self.completed,
on_change=self.toggle_completed
)
self.text_control = ft.Text(
self.text,
size=16,
expand=True
)
delete_button = ft.IconButton(
icon=ft.icons.DELETE,
icon_color=ft.colors.RED_400,
on_click=self.delete_clicked
)
self._control = ft.Container(
content=ft.Row([
self.checkbox,
self.text_control,
delete_button
]),
bgcolor=ft.colors.GREY_100,
border_radius=8,
padding=ft.padding.symmetric(horizontal=10, vertical=5),
margin=ft.margin.only(bottom=5)
)
return self._control
def toggle_completed(self, e):
"""완료 상태 변경"""
self.completed = self.checkbox.value
# 완료된 항목은 취소선 및 회색으로 표시
if self.completed:
self.text_control.style = ft.TextStyle(
decoration=ft.TextDecoration.LINE_THROUGH,
color=ft.colors.GREY_500
)
self._control.bgcolor = ft.colors.GREEN_50
else:
self.text_control.style = ft.TextStyle(
color=ft.colors.BLACK
)
self._control.bgcolor = ft.colors.GREY_100
self.on_toggle(self)
def delete_clicked(self, e):
"""삭제 버튼 클릭"""
self.on_delete(self)
def main(page: ft.Page):
app = TodoApp(page)
# 앱 실행
if __name__ == "__main__":
ft.run(main)
향상된 기능 추가
# advanced_todo.py
import flet as ft
import json
import os
from datetime import datetime
class AdvancedTodoApp:
def __init__(self, page: ft.Page):
self.page = page
self.tasks = []
self.filter_status = "all" # all, active, completed
self.data_file = "tasks.json"
self.setup_page()
self.load_tasks()
self.build_ui()
def setup_page(self):
"""페이지 설정"""
self.page.title = "Flet TODO Pro"
self.page.theme_mode = ft.ThemeMode.LIGHT
self.page.window_width = 500
self.page.window_height = 700
self.page.padding = 20
# 다크모드 토글 추가
self.page.appbar = ft.AppBar(
title=ft.Text("📝 TODO Pro"),
bgcolor=ft.colors.BLUE_600,
actions=[
ft.IconButton(
icon=ft.icons.DARK_MODE,
on_click=self.toggle_theme
)
]
)
def toggle_theme(self, e):
"""다크/라이트 모드 토글"""
if self.page.theme_mode == ft.ThemeMode.LIGHT:
self.page.theme_mode = ft.ThemeMode.DARK
e.control.icon = ft.icons.LIGHT_MODE
else:
self.page.theme_mode = ft.ThemeMode.LIGHT
e.control.icon = ft.icons.DARK_MODE
self.page.update()
def build_ui(self):
"""고급 UI 구성"""
# 입력 영역
self.task_input = ft.TextField(
hint_text="새로운 할 일을 입력하세요...",
expand=True,
on_submit=self.add_task
)
self.priority_dropdown = ft.Dropdown(
label="우선순위",
options=[
ft.dropdown.Option("high", "높음"),
ft.dropdown.Option("medium", "보통"),
ft.dropdown.Option("low", "낮음")
],
value="medium",
width=120
)
add_button = ft.ElevatedButton(
text="추가",
icon=ft.icons.ADD,
on_click=self.add_task
)
input_row = ft.Row([
self.task_input,
self.priority_dropdown,
add_button
])
# 필터 버튼들
filter_row = ft.Row([
ft.TextButton("전체", on_click=lambda e: self.set_filter("all")),
ft.TextButton("진행중", on_click=lambda e: self.set_filter("active")),
ft.TextButton("완료", on_click=lambda e: self.set_filter("completed")),
ft.VerticalDivider(),
ft.TextButton("전체 삭제", on_click=self.clear_completed)
])
# 작업 목록
self.task_list = ft.Column(
scroll=ft.ScrollMode.AUTO,
expand=True
)
# 통계
self.stats = ft.Text("총 0개의 할 일")
# 레이아웃 구성
self.page.add(
input_row,
ft.Divider(),
filter_row,
ft.Container(
content=self.task_list,
expand=True,
padding=10
),
ft.Divider(),
self.stats
)
self.refresh_list()
def add_task(self, e):
"""작업 추가 (향상된 버전)"""
text = self.task_input.value.strip()
if not text:
return
task_data = {
"id": len(self.tasks) + 1,
"text": text,
"completed": False,
"priority": self.priority_dropdown.value,
"created_at": datetime.now().isoformat(),
"completed_at": None
}
self.tasks.append(task_data)
self.task_input.value = ""
self.save_tasks()
self.refresh_list()
self.page.update()
def set_filter(self, filter_type):
"""필터 설정"""
self.filter_status = filter_type
self.refresh_list()
def refresh_list(self):
"""목록 새로고침"""
self.task_list.controls.clear()
# 필터링된 작업들
filtered_tasks = self.get_filtered_tasks()
# 우선순위별 정렬
priority_order = {"high": 1, "medium": 2, "low": 3}
filtered_tasks.sort(key=lambda x: (
x["completed"],
priority_order.get(x["priority"], 2)
))
for task in filtered_tasks:
task_control = self.build_task_control(task)
self.task_list.controls.append(task_control)
self.update_stats()
self.page.update()
def get_filtered_tasks(self):
"""필터에 따른 작업 목록 반환"""
if self.filter_status == "active":
return [task for task in self.tasks if not task["completed"]]
elif self.filter_status == "completed":
return [task for task in self.tasks if task["completed"]]
else: # all
return self.tasks
def build_task_control(self, task):
"""개별 작업 컨트롤 생성"""
# 우선순위 색상
priority_colors = {
"high": ft.colors.RED_100,
"medium": ft.colors.YELLOW_100,
"low": ft.colors.GREEN_100
}
checkbox = ft.Checkbox(
value=task["completed"],
on_change=lambda e, task_id=task["id"]: self.toggle_task(task_id)
)
text_style = ft.TextStyle()
if task["completed"]:
text_style.decoration = ft.TextDecoration.LINE_THROUGH
text_style.color = ft.colors.GREY_500
text_control = ft.Text(
task["text"],
style=text_style,
expand=True,
size=16
)
priority_badge = ft.Container(
content=ft.Text(
task["priority"].upper(),
size=10,
weight=ft.FontWeight.BOLD
),
bgcolor=priority_colors.get(task["priority"], ft.colors.GREY_100),
padding=ft.padding.symmetric(horizontal=8, vertical=2),
border_radius=10
)
delete_btn = ft.IconButton(
icon=ft.icons.DELETE_OUTLINE,
icon_color=ft.colors.RED_400,
on_click=lambda e, task_id=task["id"]: self.delete_task(task_id)
)
return ft.Container(
content=ft.Row([
checkbox,
text_control,
priority_badge,
delete_btn
]),
bgcolor=ft.colors.WHITE if not task["completed"] else ft.colors.GREY_50,
border=ft.border.all(1, ft.colors.GREY_300),
border_radius=8,
padding=10,
margin=ft.margin.only(bottom=5)
)
def toggle_task(self, task_id):
"""작업 완료 상태 토글"""
for task in self.tasks:
if task["id"] == task_id:
task["completed"] = not task["completed"]
task["completed_at"] = datetime.now().isoformat() if task["completed"] else None
break
self.save_tasks()
self.refresh_list()
def delete_task(self, task_id):
"""작업 삭제"""
self.tasks = [task for task in self.tasks if task["id"] != task_id]
self.save_tasks()
self.refresh_list()
def clear_completed(self, e):
"""완료된 작업 모두 삭제"""
self.tasks = [task for task in self.tasks if not task["completed"]]
self.save_tasks()
self.refresh_list()
def update_stats(self):
"""통계 업데이트"""
total = len(self.tasks)
completed = sum(1 for task in self.tasks if task["completed"])
active = total - completed
self.stats.value = f"총 {total}개 | 진행중 {active}개 | 완료 {completed}개"
def save_tasks(self):
"""작업을 파일에 저장"""
try:
with open(self.data_file, 'w', encoding='utf-8') as f:
json.dump(self.tasks, f, ensure_ascii=False, indent=2)
except Exception as e:
print(f"저장 오류: {e}")
def load_tasks(self):
"""파일에서 작업 로드"""
try:
if os.path.exists(self.data_file):
with open(self.data_file, 'r', encoding='utf-8') as f:
self.tasks = json.load(f)
except Exception as e:
print(f"로드 오류: {e}")
self.tasks = []
def main(page: ft.Page):
app = AdvancedTodoApp(page)
if __name__ == "__main__":
ft.run(main)
모바일 앱으로 패키징
Android APK 빌드
# Android 개발 환경 설정 후
flet build apk --project-name "my-todo-app"
# 또는 상세 옵션과 함께
flet build apk \
--project-name "TodoApp" \
--project-description "My Todo Application" \
--project-version "1.0.0" \
--base-url "/apps/todo"
iOS 앱 빌드
# iOS 개발 환경 및 Xcode 설치 후
flet build ipa --project-name "my-todo-app"
데스크톱 앱 빌드
# Windows 실행 파일
flet build windows --project-name "TodoApp"
# macOS 앱 번들
flet build macos --project-name "TodoApp"
# Linux 앱 이미지
flet build linux --project-name "TodoApp"
웹 앱 배포
정적 웹 앱 생성
# 정적 웹 앱 빌드
flet build web --project-name "TodoApp"
# 빌드된 파일들이 dist/ 폴더에 생성됨
ls dist/
Flet 서버로 배포
# server_app.py
import flet as ft
from advanced_todo import AdvancedTodoApp
def main(page: ft.Page):
AdvancedTodoApp(page)
# 서버 모드로 실행
if __name__ == "__main__":
ft.run(
main,
host="0.0.0.0",
port=8000,
view=None # 브라우저 자동 열기 비활성화
)
# 서버 실행
python server_app.py
# 또는 flet 명령어로
flet run --web --port 8000 server_app.py
고급 기능 활용
1. 상태 관리
import flet as ft
class AppState:
def __init__(self):
self.user_name = ""
self.tasks = []
self.theme = "light"
def update_user(self, name):
self.user_name = name
def add_task(self, task):
self.tasks.append(task)
# 전역 상태 사용
app_state = AppState()
def main(page: ft.Page):
page.session.set("app_state", app_state)
# 상태 접근
state = page.session.get("app_state")
print(f"Current user: {state.user_name}")
2. 라우팅 시스템
import flet as ft
def main(page: ft.Page):
page.title = "Multi-Page App"
def route_change(route):
page.views.clear()
if page.route == "/":
page.views.append(home_view())
elif page.route == "/tasks":
page.views.append(tasks_view())
elif page.route == "/settings":
page.views.append(settings_view())
page.update()
def home_view():
return ft.View(
"/",
[
ft.AppBar(title=ft.Text("홈")),
ft.ElevatedButton(
"할 일 관리",
on_click=lambda e: page.go("/tasks")
),
ft.ElevatedButton(
"설정",
on_click=lambda e: page.go("/settings")
)
]
)
def tasks_view():
return ft.View(
"/tasks",
[
ft.AppBar(title=ft.Text("할 일 목록")),
ft.Text("여기는 할 일 페이지입니다"),
ft.ElevatedButton(
"홈으로",
on_click=lambda e: page.go("/")
)
]
)
def settings_view():
return ft.View(
"/settings",
[
ft.AppBar(title=ft.Text("설정")),
ft.Text("여기는 설정 페이지입니다"),
ft.ElevatedButton(
"홈으로",
on_click=lambda e: page.go("/")
)
]
)
page.on_route_change = route_change
page.go(page.route)
ft.run(main)
3. 데이터베이스 연동
import flet as ft
import sqlite3
from datetime import datetime
class DatabaseTodoApp:
def __init__(self, page: ft.Page):
self.page = page
self.init_database()
self.setup_ui()
def init_database(self):
"""SQLite 데이터베이스 초기화"""
self.conn = sqlite3.connect('todos.db')
self.conn.execute('''
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
text TEXT NOT NULL,
completed BOOLEAN DEFAULT FALSE,
priority TEXT DEFAULT 'medium',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMP NULL
)
''')
self.conn.commit()
def add_task_to_db(self, text, priority='medium'):
"""데이터베이스에 작업 추가"""
cursor = self.conn.execute(
'INSERT INTO tasks (text, priority) VALUES (?, ?)',
(text, priority)
)
self.conn.commit()
return cursor.lastrowid
def get_tasks_from_db(self, filter_completed=None):
"""데이터베이스에서 작업 조회"""
query = 'SELECT * FROM tasks'
params = []
if filter_completed is not None:
query += ' WHERE completed = ?'
params.append(filter_completed)
query += ' ORDER BY created_at DESC'
cursor = self.conn.execute(query, params)
return cursor.fetchall()
def update_task_in_db(self, task_id, completed):
"""작업 완료 상태 업데이트"""
completed_at = datetime.now() if completed else None
self.conn.execute(
'UPDATE tasks SET completed = ?, completed_at = ? WHERE id = ?',
(completed, completed_at, task_id)
)
self.conn.commit()
def delete_task_from_db(self, task_id):
"""데이터베이스에서 작업 삭제"""
self.conn.execute('DELETE FROM tasks WHERE id = ?', (task_id,))
self.conn.commit()
def setup_ui(self):
"""UI 설정 및 데이터 로드"""
self.page.title = "Database TODO App"
# UI 구성 요소들...
self.task_input = ft.TextField(
hint_text="새 할 일 입력...",
on_submit=self.add_task
)
self.task_list = ft.Column()
self.page.add(
ft.Row([
self.task_input,
ft.ElevatedButton("추가", on_click=self.add_task)
]),
self.task_list
)
self.refresh_tasks()
def refresh_tasks(self):
"""작업 목록 새로고침"""
self.task_list.controls.clear()
tasks = self.get_tasks_from_db()
for task in tasks:
task_id, text, completed, priority, created_at, completed_at = task
self.task_list.controls.append(
ft.Row([
ft.Checkbox(
value=bool(completed),
on_change=lambda e, tid=task_id: self.toggle_task(tid)
),
ft.Text(text, expand=True),
ft.IconButton(
icon=ft.icons.DELETE,
on_click=lambda e, tid=task_id: self.delete_task(tid)
)
])
)
self.page.update()
def add_task(self, e):
"""새 작업 추가"""
if self.task_input.value:
self.add_task_to_db(self.task_input.value)
self.task_input.value = ""
self.refresh_tasks()
def toggle_task(self, task_id):
"""작업 완료 상태 토글"""
tasks = self.get_tasks_from_db()
for task in tasks:
if task[0] == task_id: # task[0] is id
new_completed = not bool(task[2]) # task[2] is completed
self.update_task_in_db(task_id, new_completed)
break
self.refresh_tasks()
def delete_task(self, task_id):
"""작업 삭제"""
self.delete_task_from_db(task_id)
self.refresh_tasks()
def main(page: ft.Page):
app = DatabaseTodoApp(page)
ft.run(main)
4. API 연동
import flet as ft
import requests
import asyncio
class WeatherApp:
def __init__(self, page: ft.Page):
self.page = page
self.api_key = "YOUR_API_KEY" # OpenWeatherMap API 키
self.setup_ui()
def setup_ui(self):
"""날씨 앱 UI 설정"""
self.page.title = "날씨 앱"
self.page.theme_mode = ft.ThemeMode.LIGHT
self.city_input = ft.TextField(
hint_text="도시명을 입력하세요",
on_submit=self.get_weather
)
self.weather_info = ft.Column()
search_button = ft.ElevatedButton(
"날씨 검색",
icon=ft.icons.SEARCH,
on_click=self.get_weather
)
self.page.add(
ft.Container(
content=ft.Column([
ft.Text("🌤️ 날씨 정보", size=24, weight=ft.FontWeight.BOLD),
ft.Row([self.city_input, search_button]),
ft.Divider(),
self.weather_info
]),
padding=20
)
)
async def fetch_weather_data(self, city):
"""비동기로 날씨 데이터 가져오기"""
url = f"http://api.openweathermap.org/data/2.5/weather"
params = {
"q": city,
"appid": self.api_key,
"units": "metric",
"lang": "kr"
}
try:
response = requests.get(url, params=params)
response.raise_for_status()
return response.json()
except Exception as e:
return {"error": str(e)}
def get_weather(self, e):
"""날씨 정보 가져오기"""
city = self.city_input.value.strip()
if not city:
return
# 로딩 표시
self.weather_info.controls.clear()
self.weather_info.controls.append(
ft.Row([
ft.ProgressRing(scale=0.5),
ft.Text("날씨 정보를 불러오는 중...")
])
)
self.page.update()
# 비동기 함수 실행
asyncio.create_task(self.update_weather_info(city))
async def update_weather_info(self, city):
"""날씨 정보 업데이트"""
data = await self.fetch_weather_data(city)
self.weather_info.controls.clear()
if "error" in data:
self.weather_info.controls.append(
ft.Text(f"오류: {data['error']}", color=ft.colors.RED)
)
else:
# 날씨 정보 표시
temp = data["main"]["temp"]
feels_like = data["main"]["feels_like"]
description = data["weather"][0]["description"]
humidity = data["main"]["humidity"]
weather_card = ft.Card(
content=ft.Container(
content=ft.Column([
ft.ListTile(
leading=ft.Icon(ft.icons.LOCATION_CITY),
title=ft.Text(f"{data['name']}, {data['sys']['country']}"),
subtitle=ft.Text(description.title())
),
ft.Row([
ft.Text(f"🌡️ {temp}°C", size=20),
ft.Text(f"체감 {feels_like}°C", size=14, color=ft.colors.GREY_600)
]),
ft.Text(f"💧 습도: {humidity}%"),
]),
padding=20
)
)
self.weather_info.controls.append(weather_card)
self.page.update()
def main(page: ft.Page):
app = WeatherApp(page)
ft.run(main)
성능 최적화 팁
1. 효율적인 UI 업데이트
import flet as ft
class OptimizedApp:
def __init__(self, page: ft.Page):
self.page = page
self.setup_ui()
def setup_ui(self):
self.items = []
self.list_view = ft.ListView(expand=True, spacing=10)
# 대량 데이터 처리 시 가상화 사용
self.virtual_list = ft.ListView(
expand=True,
item_extent=60, # 각 아이템의 고정 높이
)
self.page.add(
ft.ElevatedButton("1000개 아이템 추가", on_click=self.add_many_items),
self.virtual_list
)
def add_many_items(self, e):
"""대량 아이템 효율적 추가"""
# 배치 업데이트 사용
new_controls = []
for i in range(1000):
item = ft.ListTile(
leading=ft.Icon(ft.icons.ITEM),
title=ft.Text(f"아이템 {i+1}"),
subtitle=ft.Text(f"설명 {i+1}")
)
new_controls.append(item)
# 한 번에 모든 컨트롤 추가
self.virtual_list.controls.extend(new_controls)
self.page.update() # 한 번만 업데이트
2. 메모리 관리
import flet as ft
import gc
class MemoryOptimizedApp:
def __init__(self, page: ft.Page):
self.page = page
self.large_data = []
def cleanup_resources(self):
"""리소스 정리"""
self.large_data.clear()
gc.collect() # 가비지 컬렉션 강제 실행
def on_window_close(self, e):
"""윈도우 닫기 시 정리"""
self.cleanup_resources()
return True # 닫기 허용
def main(page: ft.Page):
app = MemoryOptimizedApp(page)
page.window.on_event = app.on_window_close
디버깅 및 테스팅
1. 로깅 설정
import flet as ft
import logging
# 로깅 설정
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
class DebuggableApp:
def __init__(self, page: ft.Page):
self.page = page
logger.info("앱 초기화 시작")
self.setup_ui()
logger.info("앱 초기화 완료")
def button_click(self, e):
logger.debug(f"버튼 클릭됨: {e.control.text}")
try:
# 작업 수행
pass
except Exception as ex:
logger.error(f"버튼 클릭 처리 중 오류: {ex}")
2. 에러 핸들링
import flet as ft
class ErrorHandlingApp:
def __init__(self, page: ft.Page):
self.page = page
self.page.on_error = self.handle_page_error
self.setup_ui()
def handle_page_error(self, e):
"""페이지 레벨 에러 핸들링"""
error_dialog = ft.AlertDialog(
modal=True,
title=ft.Text("오류 발생"),
content=ft.Text(f"예상치 못한 오류가 발생했습니다:\n{str(e)}"),
actions=[
ft.TextButton("확인", on_click=self.close_error_dialog)
]
)
self.page.dialog = error_dialog
error_dialog.open = True
self.page.update()
def close_error_dialog(self, e):
"""에러 다이얼로그 닫기"""
self.page.dialog.open = False
self.page.update()
def risky_operation(self, e):
"""에러가 발생할 수 있는 작업"""
try:
# 위험한 작업 수행
result = 10 / 0 # ZeroDivisionError 발생
except ZeroDivisionError:
self.show_user_friendly_error("0으로 나눌 수 없습니다.")
except Exception as ex:
self.show_user_friendly_error(f"알 수 없는 오류: {ex}")
def show_user_friendly_error(self, message):
"""사용자 친화적 에러 메시지 표시"""
snack_bar = ft.SnackBar(
content=ft.Text(message),
bgcolor=ft.colors.RED_400
)
self.page.snack_bar = snack_bar
snack_bar.open = True
self.page.update()
배포 및 운영
1. 환경 변수 관리
import flet as ft
import os
from dotenv import load_dotenv
# .env 파일 로드
load_dotenv()
class ProductionApp:
def __init__(self, page: ft.Page):
self.page = page
# 환경 변수 사용
self.api_key = os.getenv('API_KEY', 'default_key')
self.database_url = os.getenv('DATABASE_URL', 'sqlite:///local.db')
self.debug_mode = os.getenv('DEBUG', 'False').lower() == 'true'
if self.debug_mode:
print(f"Debug mode enabled")
print(f"API Key: {self.api_key[:8]}...")
self.setup_ui()
def main(page: ft.Page):
app = ProductionApp(page)
# 환경에 따른 실행
if __name__ == "__main__":
port = int(os.getenv('PORT', 8000))
host = os.getenv('HOST', 'localhost')
ft.run(
main,
host=host,
port=port,
view=None if os.getenv('WEB_MODE') else ft.AppView.DESKTOP
)
2. Docker 배포
# Dockerfile
FROM python:3.11-slim
WORKDIR /app
# 시스템 의존성 설치
RUN apt-get update && apt-get install -y \
curl \
&& rm -rf /var/lib/apt/lists/*
# Python 의존성 설치
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 앱 코드 복사
COPY . .
# 포트 노출
EXPOSE 8000
# 환경 변수 설정
ENV PYTHONUNBUFFERED=1
ENV WEB_MODE=true
# 앱 실행
CMD ["python", "main.py"]
# docker-compose.yml
version: '3.8'
services:
flet-app:
build: .
ports:
- "8000:8000"
environment:
- DEBUG=false
- DATABASE_URL=postgresql://user:password@db:5432/myapp
depends_on:
- db
volumes:
- ./data:/app/data
db:
image: postgres:13
environment:
- POSTGRES_DB=myapp
- POSTGRES_USER=user
- POSTGRES_PASSWORD=password
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
3. CI/CD 파이프라인
# .github/workflows/deploy.yml
name: Build and Deploy
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install -r requirements.txt
- name: Run tests
run: |
python -m pytest tests/
build-and-deploy:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build and push Docker image
env:
DOCKER_REGISTRY: $
run: |
docker build -t $DOCKER_REGISTRY/flet-app:latest .
docker push $DOCKER_REGISTRY/flet-app:latest
- name: Deploy to production
run: |
# 배포 스크립트 실행
echo "Deploying to production..."
결론
Flet은 Python 개발자에게 멀티플랫폼 앱 개발의 새로운 가능성을 열어줍니다. Flutter의 강력함과 Python의 단순함을 결합하여:
🎯 핵심 장점
- 학습 곡선 최소화: 기존 Python 지식만으로 즉시 시작 가능
- 개발 생산성: 하나의 코드베이스로 모든 플랫폼 지원
- 전문적 UI: Flutter 기반의 네이티브급 사용자 경험
- 유연한 배포: 웹, 데스크톱, 모바일 앱으로 자유로운 배포
🚀 적용 분야
- 기업 내부 도구 및 대시보드
- 데이터 시각화 애플리케이션
- 프로토타입 및 MVP 개발
- 교육용 애플리케이션
- 개인 프로젝트 및 취미 앱
Flet을 통해 Python 개발자도 이제 풀스택 앱 개발자가 될 수 있습니다. 복잡한 프론트엔드 기술 스택을 학습할 필요 없이, 친숙한 Python으로 현대적이고 반응성 있는 애플리케이션을 만들어보세요!
참고 자료:
- Flet 공식 GitHub
- Flet 공식 문서
- Flet 예제 갤러리
-
Star: 13.8k Forks: 549 License: Apache-2.0
관련 키워드: #Flet
#Python
#Flutter
#크로스플랫폼
#멀티플랫폼
#GUI
#앱개발