iBetter Books
수정

레이아웃과 탭 구성 (mo.vstack·hstack)

Ch 02에서 데이터 파이프라인을 완성했습니다. filtered_df가 위젯에 반응하고, summary_md가 그 결과를 요약합니다. 이제 이 요소들을 화면에 배치합니다.

marimo의 레이아웃 도구는 두 가지입니다. mo.vstack은 요소를 위에서 아래로 쌓습니다. mo.hstack은 요소를 나란히 배치하고, widths 파라미터로 각 칸의 너비 비율을 지정합니다. 이 둘을 중첩하면 어떤 레이아웃도 만들 수 있습니다.

mo.hstack의 widths 파라미터

mo.hstackwidths 파라미터는 두 가지 값을 받습니다.

# "equal": 모든 칸을 동일한 너비로mo.hstack([col1, col2, col3], widths="equal")# 숫자 리스트: 상대 비율로 너비 지정mo.hstack([sidebar, main], widths=[1, 3])

widths=[1, 3]은 왼쪽이 1, 오른쪽이 3입니다. 전체를 4로 나눌 때 왼쪽이 25%, 오른쪽이 75%를 차지합니다. 사이드바와 메인 영역을 나누기에 적합한 비율입니다.

셀 7: 카테고리별 차트

filtered_df를 카테고리별로 집계해 막대 차트를 만듭니다.

cat_agg = mo.sql(f"""    SELECT        category,        COUNT(*) AS 주문수,        ROUND(SUM(amount), 0) AS 총_금액    FROM filtered_df    GROUP BY category    ORDER BY 총_금액 DESC""")fig_category = px.bar(    cat_agg,    x="category",    y="총_금액",    color="category",    title="카테고리별 총 주문 금액",    labels={"category": "카테고리", "총_금액": "총 금액 (원)"},)

cat_aggfiltered_df를 다시 SQL로 집계합니다. filtered_df가 바뀌면 이 셀도 재실행됩니다. px는 셀 1에서 정의된 변수입니다. 같은 변수를 여기서 다시 정의하면 MB002 오류가 발생합니다.

셀 8: 지역별 차트

지역별 평균 주문 금액을 가로 막대 차트로 보여줍니다.

region_agg = mo.sql(f"""    SELECT        region,        COUNT(*) AS 주문수,        ROUND(AVG(amount), 0) AS 평균_금액    FROM filtered_df    GROUP BY region    ORDER BY 평균_금액 DESC""")fig_region = px.bar(    region_agg,    x="평균_금액",    y="region",    orientation="h",    title="지역별 평균 주문 금액",    labels={"region": "지역", "평균_금액": "평균 금액 (원)"},    color="평균_금액",    color_continuous_scale="Blues",)

셀 9: 요일별 차트

요일 순서를 월요일부터 일요일 순으로 정렬해 꺾은선 차트로 보여줍니다.

weekday_order = ["월", "화", "수", "목", "금", "토", "일"]weekday_agg = mo.sql(f"""    SELECT        weekday,        COUNT(*) AS 주문수    FROM filtered_df    GROUP BY weekday""")weekday_pd = weekday_agg.copy()weekday_pd["weekday"] = pd.Categorical(    weekday_pd["weekday"],    categories=weekday_order,    ordered=True,)weekday_pd = weekday_pd.sort_values("weekday")fig_weekday = px.line(    weekday_pd,    x="weekday",    y="주문수",    markers=True,    title="요일별 주문 건수",    labels={"weekday": "요일", "주문수": "주문 건수"},)

셀 10: 화면 선택에 따른 차트 분기

view_selector.value에 따라 세 차트 중 하나를 선택합니다. 이 분기를 하나의 셀 안에 작성합니다.

if view_selector.value == "카테고리별 현황":    active_chart = fig_categoryelif view_selector.value == "지역별 현황":    active_chart = fig_regionelse:    active_chart = fig_weekday

active_chart를 하나의 셀 안에서 조건문으로 할당하는 것이 중요합니다. 조건 분기를 여러 셀로 나누면 active_chart를 두 셀 이상에서 정의하게 되어 MB002 오류가 발생합니다. 하나의 셀 안에서 if/elif/else로 분기하고, 마지막 표현식은 두지 않습니다. 이 셀은 변수를 정의할 뿐이며, 셀 12의 main_panelactive_chart를 소비해 화면에 렌더링합니다.

view_selector.value가 바뀌면 이 셀이 재실행됩니다. 갱신된 active_chart를 참조하는 셀 12와 셀 13이 연쇄적으로 재실행됩니다.

라디오 버튼으로 화면을 전환하는 이유

다중 화면 전환이 필요할 때 라디오 버튼이나 드롭다운 위젯으로 active_chart를 분기하는 방식은 marimo에서 안정적으로 검증된 패턴입니다. 위젯 .value는 문자열이고, if/elif 분기는 표준 Python이며, active_chart를 하나의 셀에서 정의하므로 MB002 오류도 없습니다. 구현이 단순하고 동작이 예측 가능합니다.

셀 11: 사이드바 패널 구성

왼쪽 필터 영역을 mo.vstack으로 조립합니다.

sidebar = mo.vstack([    mo.md("### 필터"),    category_filter,    amount_min_filter,    region_filter,])

mo.vstack은 리스트의 요소를 위에서 아래로 쌓습니다. 마크다운 제목, 위젯 세 개가 순서대로 배치됩니다. mo.md("### 필터")는 섹션 제목 역할을 합니다.

셀 12: 메인 패널 구성

오른쪽 결과 영역을 mo.vstack으로 조립합니다.

main_panel = mo.vstack([    mo.md("### 현황 지표"),    summary_md,    mo.md("---"),    view_selector,    active_chart,])

요약 지표 테이블, 구분선, 화면 선택 위젯, 선택된 차트 순서로 배치됩니다. summary_mdactive_chart는 앞 셀에서 정의된 변수입니다. mo.vstack은 이들을 순서대로 세로로 쌓습니다.

셀 13: 전체 레이아웃

사이드바와 메인 패널을 mo.hstack으로 나란히 배치합니다.

app_layout = mo.hstack([sidebar, main_panel], widths=[1, 3])app_layout

widths=[1, 3]이 화면을 1:3 비율로 나눕니다. 왼쪽 사이드바가 전체의 25%, 오른쪽 메인 영역이 75%를 차지합니다. app_layout을 셀 마지막 표현식으로 두면 이 레이아웃이 셀 출력이 됩니다.

앱 모드에서는 셀 코드가 숨겨지고 이 출력만 화면에 표시됩니다.

레이아웃 중첩 패턴

mo.vstackmo.hstack은 서로 중첩할 수 있습니다. 이 챕터에서 만든 레이아웃 구조를 정리합니다.

%% 앱 레이아웃 구성 (hstack / vstack 중첩) graph TD ROOT["app_layout (hstack, widths=[1, 3])"] SIDE["sidebar (vstack)"] MAIN["main_panel (vstack)"] S1["mo.md('### 필터')"] S2["category_filter"] S3["amount_min_filter"] S4["region_filter"] M1["mo.md('### 현황 지표')"] M2["summary_md"] M3["mo.md('---')"] M4["view_selector"] M5["active_chart"] ROOT --> SIDE ROOT --> MAIN SIDE --> S1 SIDE --> S2 SIDE --> S3 SIDE --> S4 MAIN --> M1 MAIN --> M2 MAIN --> M3 MAIN --> M4 MAIN --> M5

레이아웃 코드가 단순합니다. mo.hstackmo.vstack 각각에 파이썬 리스트를 넘기는 것이 전부입니다. CSS나 그리드 설정 없이 대시보드 레이아웃을 만들 수 있습니다.

여기까지 노트북의 상태

셀 1:  import (mo, np, pd, px)셀 2:  df 생성 (고정 시드, 500행)셀 3:  category_filter, amount_min_filter, region_filter 위젯셀 4:  filtered_df (SQL)셀 5:  summary_md셀 6:  view_selector 위젯셀 7:  cat_agg SQL + fig_category셀 8:  region_agg SQL + fig_region셀 9:  weekday_agg SQL + fig_weekday셀 10: active_chart (view_selector.value로 분기)셀 11: sidebar셀 12: main_panel셀 13: app_layout

셀 3의 위젯을 바꾸면 셀 4(filtered_df), 셀 5(summary_md), 셀 7, 8, 9(차트 집계), 셀 10(active_chart), 셀 12(main_panel), 셀 13(app_layout)이 순서대로 재실행됩니다. 셀 1, 셀 2, 셀 6, 셀 11은 영향을 받지 않습니다.

view_selector를 바꾸면 셀 10, 12, 13만 재실행됩니다. 데이터 집계 셀은 다시 실행되지 않습니다. DAG가 최소 범위만 재실행합니다.

정리

  • mo.vstack은 요소를 세로로 쌓고, mo.hstack(widths=[1, 3])은 요소를 가로로 나눠 1:3 비율의 사이드바-메인 레이아웃을 만듭니다.
  • 다중 화면 전환은 mo.ui.radio 위젯과 if/elif 분기로 구현합니다. 분기 결과를 하나의 변수에 담아 하나의 셀에서 정의하되, 마지막 표현식으로 두지 않습니다. 하위 레이아웃 셀(main_panel)이 이 변수를 소비합니다. MB002 오류를 피하고 앱 화면에 중복 출력이 생기지 않습니다.
  • 위젯, mo.md(), plotly Figure, mo.sql() 결과는 모두 mo.vstackmo.hstack의 아이템으로 사용할 수 있습니다.
  • 앱 모드 화면에 표시되는 셀은 마지막 표현식이 있는 셀뿐입니다. 이 파트에서는 셀 13(app_layout)만 출력을 가집니다. 나머지 셀은 변수를 정의할 뿐이며 앱 화면에 나타나지 않습니다.