SQL·위젯·차트 통합
Ch 01에서 앱의 화면 구조를 기획했습니다. 이제 데이터 파이프라인을 구성합니다. 위젯에서 값이 나오고, 그 값이 SQL 필터에 들어가고, 결과 데이터프레임이 차트의 입력이 됩니다. 위젯 하나를 건드리면 이 연쇄가 자동으로 실행됩니다. 버튼이 없습니다. 이것이 PART 02에서 배운 DAG 반응형 실행이 실제 앱에서 어떻게 작동하는지를 보여줍니다.
이 챕터에서 완성하는 부분은 데이터 흐름의 뼈대입니다. 레이아웃은 Ch 03에서 다룹니다.
셀 1: 라이브러리 임포트
import marimo as moimport numpy as npimport pandas as pdimport plotly.express as px
PART 06의 eda_project.py와 동일한 임포트입니다. 두 노트북은 독립적입니다. app.py는 자기 완결적으로 동작합니다.
mo.sql()이 반환하는 데이터프레임 타입은 marimo 설정에 따라 pandas 또는 Polars가 됩니다. PART 04에서 설정한 default_sql_output = "pandas"가 적용되어 있으면 mo.sql()은 pandas DataFrame을 반환합니다. 이 챕터의 코드(셀 5의 filtered_df["amount"].sum(), 셀 9의 .copy()와 pd.Categorical 할당)는 pandas DataFrame을 전제합니다. 환경에 이 설정이 없다면 pyproject.toml에 다음을 추가합니다.
[tool.marimo.runtime]
default_sql_output = "pandas"
셀 2: 합성 데이터 생성
고정 시드로 데이터를 생성합니다. PART 06에서 사용한 것과 동일한 데이터셋입니다. np.random.default_rng(42)가 데이터의 재현성을 보장합니다. 누가 언제 실행해도 같은 500행 데이터를 얻습니다.
rng = np.random.default_rng(42)n_rows = 500categories = ["전자기기", "의류", "식품", "도서", "스포츠"]regions = ["서울", "부산", "대구", "인천", "광주"]weekdays = ["월", "화", "수", "목", "금", "토", "일"]df = pd.DataFrame({ "order_id": range(1, n_rows + 1), "category": rng.choice(categories, size=n_rows), "region": rng.choice(regions, size=n_rows), "weekday": rng.choice(weekdays, size=n_rows), "amount": rng.integers(5000, 300000, size=n_rows).astype(float), "quantity": rng.integers(1, 10, size=n_rows),})missing_idx = rng.choice(n_rows, size=25, replace=False)df.loc[missing_idx, "amount"] = float("nan")
EDA 노트북과 달리 df 자체를 셀 마지막 표현식으로 두지 않습니다. 앱 모드에서 원시 데이터프레임 테이블이 화면에 보일 필요는 없습니다.
셀 3: 입력 위젯
세 가지 필터 위젯을 하나의 셀에 모읍니다. PART 03에서 배운 것처럼 위젯은 전역 변수로 정의해야 다른 셀에서 .value를 참조할 수 있습니다.
category_filter = mo.ui.dropdown( options=["전체"] + categories, value="전체", label="카테고리",)amount_min_filter = mo.ui.slider( start=5000, stop=200000, step=5000, value=5000, label="최소 주문 금액 (원)",)region_filter = mo.ui.multiselect( options=regions, value=regions, label="지역",)
세 위젯을 하나의 셀에 담는 이유가 있습니다. PART 06에서는 위젯마다 별도 셀을 두어 각각의 출력을 편집 화면에서 확인했습니다. 앱에서는 위젯들을 사이드바 패널에 모아서 보여줄 것입니다. Ch 03에서 mo.vstack으로 묶을 때 이 세 변수를 가져다 쓸 수 있으면 충분합니다. 이 셀 자체는 출력을 생성하지 않습니다.
셀 4: 반응형 SQL 필터
위젯 값으로 SQL WHERE 절을 구성합니다. PART 06 Ch 02에서 만든 것과 동일한 패턴입니다.
if category_filter.value == "전체": cat_condition = "1=1"else: cat_condition = f"category = '{category_filter.value}'"if region_filter.value: region_list = ", ".join(f"'{r}'" for r in region_filter.value) region_condition = f"region IN ({region_list})"else: region_condition = "1=0"amount_condition = f"amount >= {amount_min_filter.value}"filtered_df = mo.sql(f""" SELECT order_id, category, region, weekday, amount, quantity FROM df WHERE amount IS NOT NULL AND {cat_condition} AND {region_condition} AND {amount_condition} ORDER BY amount DESC""")
이 셀은 category_filter.value, region_filter.value, amount_min_filter.value 세 값에 의존합니다. DAG는 이 의존 관계를 추적합니다. 위젯 하나를 바꾸면 이 셀이 자동으로 재실행되고, filtered_df가 갱신됩니다. filtered_df에 의존하는 모든 셀이 연쇄적으로 재실행됩니다.
SQL 인젝션 주의사항을 짚고 넘어갑니다. 위젯 값은 사용자가 미리 정의된 옵션 중에서만 선택합니다. 드롭다운은 ["전체"] + categories 목록만 허용하고, 멀티셀렉트는 regions 목록만 허용합니다. 자유 입력 텍스트를 SQL에 직접 삽입하는 상황이 아니므로 이 앱에서는 안전합니다. 사용자 자유 입력을 SQL에 넣어야 한다면 파라미터 바인딩을 사용합니다.
셀 5: 요약 지표 계산
필터 결과로 세 가지 지표를 계산합니다. 이 값들은 Ch 03에서 메인 영역 상단에 표시됩니다.
n_filtered = len(filtered_df)total_amount = float(filtered_df["amount"].sum()) if n_filtered > 0 else 0.0avg_amount = float(filtered_df["amount"].mean()) if n_filtered > 0 else 0.0summary_md = mo.md(f"""| 지표 | 값 ||------|-----|| 총 주문 수 | **{n_filtered:,}건** || 총 매출 | **{total_amount:,.0f}원** || 평균 주문 금액 | **{avg_amount:,.0f}원** |""")
filtered_df에 의존하므로 필터가 바뀌면 이 셀도 재실행됩니다. summary_md는 mo.md() 객체입니다. Ch 03에서 mo.vstack의 아이템으로 직접 사용합니다.
셀 6: 화면 선택 위젯
오른쪽 메인 영역에서 어떤 차트를 볼지 선택하는 위젯입니다.
view_selector = mo.ui.radio( options=["카테고리별 현황", "지역별 현황", "요일별 현황"], value="카테고리별 현황", label="화면",)
mo.ui.radio는 옵션 중 하나를 선택합니다. .value는 선택된 문자열을 반환합니다. Ch 03에서 이 값에 따라 다른 차트를 보여주는 분기를 만듭니다.
여기까지 노트북의 상태
셀 1: import (mo, np, pd, px)셀 2: df 생성 (고정 시드, 500행)셀 3: category_filter, amount_min_filter, region_filter 위젯셀 4: filtered_df (SQL, 위젯 세 개에 의존)셀 5: summary_md (filtered_df에 의존)셀 6: view_selector 위젯
데이터 파이프라인이 완성되었습니다. 셀 3의 위젯이 바뀌면 셀 4가 재실행되고, 셀 5가 자동으로 갱신됩니다. 셀 6는 독립적입니다. 셀 1과 셀 2는 초기화 이후 재실행되지 않습니다.
차트와 레이아웃은 Ch 03에서 추가합니다.
정리
- PART 06의 합성 데이터(
np.random.default_rng(42), n_rows=500)를app.py에 그대로 재사용합니다. 두 노트북은 독립적입니다. - 앱에서는 위젯 여러 개를 하나의 셀에 담아도 됩니다. 각 위젯은 전역 변수로 정의되어 다른 셀에서
.value를 참조할 수 있습니다. - 위젯 값을 SQL f-string에 삽입하면 값이 바뀔 때마다 SQL이 자동으로 재실행됩니다. DAG가 의존 관계를 추적합니다.
- 위젯 옵션을 미리 정의된 목록으로 제한하면 자유 입력 없이 SQL에 삽입해도 안전합니다.
mo.md()객체는 레이아웃 컴포넌트로 사용할 수 있습니다. Ch 03에서mo.vstack에 직접 넣습니다.