⏱️ 예상 읽기 시간: 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의 단순함을 결합하여:

🎯 핵심 장점

  1. 학습 곡선 최소화: 기존 Python 지식만으로 즉시 시작 가능
  2. 개발 생산성: 하나의 코드베이스로 모든 플랫폼 지원
  3. 전문적 UI: Flutter 기반의 네이티브급 사용자 경험
  4. 유연한 배포: 웹, 데스크톱, 모바일 앱으로 자유로운 배포

🚀 적용 분야

  • 기업 내부 도구 및 대시보드
  • 데이터 시각화 애플리케이션
  • 프로토타입 및 MVP 개발
  • 교육용 애플리케이션
  • 개인 프로젝트 및 취미 앱

Flet을 통해 Python 개발자도 이제 풀스택 앱 개발자가 될 수 있습니다. 복잡한 프론트엔드 기술 스택을 학습할 필요 없이, 친숙한 Python으로 현대적이고 반응성 있는 애플리케이션을 만들어보세요!


참고 자료:

관련 키워드: #Flet #Python #Flutter #크로스플랫폼 #멀티플랫폼 #GUI #앱개발