tutorial_261019/tutorial_261019.py)import dataclasses
from collections import Counter
import reflex as rx
@dataclasses.dataclass
class User:
name: str
email: str
gender: str
class State(rx.State):
users: list[User] = [
User(name="Danilo Sousa", email="danilo@example.com", gender="Male"),
User(name="Zahra Ambessa", email="zahra@example.com", gender="Female"),
]
users_for_graph: list[dict] = []
add_user_dialog_open: bool = False
@rx.event
def set_add_user_dialog_open(self, open_: bool):
self.add_user_dialog_open = open_
@rx.event
def add_user(self, form_data: dict):
self.users.append(User(**form_data))
self.transform_data()
self.add_user_dialog_open = False
@rx.event
def transform_data(self):
gender_counts = Counter(user.gender for user in self.users)
self.users_for_graph = [
{"name": gender_group, "value": count} for gender_group, count in gender_counts.items()
]
def show_user(user: User):
return rx.table.row(
rx.table.cell(user.name),
rx.table.cell(user.email),
rx.table.cell(user.gender),
style={"_hover": {"bg": rx.color("gray", 3)}},
align="center",
)
def add_customer_button() -> rx.Component:
return rx.dialog.root(
rx.dialog.trigger(
rx.button(
rx.icon("plus", size=18),
rx.text("Add User", size="2"),
),
),
rx.dialog.content(
rx.dialog.title(
"Add New User",
),
rx.dialog.description(
"Fill the form with the user's info",
),
rx.form(
rx.flex(
rx.input(placeholder="User Name", name="name", required=True),
rx.input(
placeholder="user@reflex.dev",
name="email",
),
rx.select(
["Male", "Female"],
placeholder="Select gender",
default_value="Male",
name="gender",
custom_attrs={"aria-label": "Gender"},
),
rx.flex(
rx.dialog.close(
rx.button(
"Cancel",
type="button",
variant="soft",
color_scheme="gray",
),
),
rx.button("Submit", type="submit"),
spacing="3",
justify="end",
),
direction="column",
spacing="4",
),
on_submit=State.add_user,
reset_on_submit=False,
),
max_width="450px",
),
open=State.add_user_dialog_open,
on_open_change=State.set_add_user_dialog_open,
)
def graph():
return rx.recharts.bar_chart(
rx.recharts.cartesian_grid(
horizontal=False, stroke_dasharray="3 3", stroke=rx.color("slate", 4)
),
rx.recharts.x_axis(
type_="number",
allow_decimals=False,
axis_line=False,
tick_line=False,
domain=[0, "dataMax + 1"],
),
rx.recharts.y_axis(
type_="category",
data_key="name",
axis_line=False,
tick_line=False,
width=64,
),
rx.recharts.bar(
data_key="value",
fill=rx.color("accent", 9),
radius=[0, 6, 6, 0],
bar_size=28,
),
data=State.users_for_graph,
layout="vertical",
width="100%",
height=160,
margin={"top": 0, "right": 16, "bottom": 0, "left": 0},
)
def index() -> rx.Component:
return rx.box(
rx.box(
rx.box(
rx.box(
rx.heading("Customers", size="6", as_="h3", margin="0"),
rx.badge(
State.users.length(),
variant="soft",
color_scheme="gray",
radius="full",
),
display="flex",
align_items="center",
gap="12px",
),
rx.text(
"Your customer directory and audience breakdown.",
size="2",
color=rx.color("slate", 11),
margin_top="6px",
),
),
add_customer_button(),
display="flex",
align_items="center",
justify_content="space-between",
flex_wrap="wrap",
gap="16px",
padding="24px",
),
rx.box(
rx.table.root(
rx.table.header(
rx.table.row(
rx.table.column_header_cell("Name"),
rx.table.column_header_cell("Email"),
rx.table.column_header_cell("Gender"),
)
),
rx.table.body(rx.foreach(State.users, show_user)),
variant="ghost",
size="3",
width="100%",
),
overflow_x="auto",
width="100%",
border_top=f"1px solid {rx.color('slate', 4)}",
border_bottom=f"1px solid {rx.color('slate', 4)}",
padding_x="12px",
),
rx.box(
rx.text("Audience breakdown", size="3", weight="bold"),
rx.text(
"Customers by gender",
size="2",
color=rx.color("slate", 11),
margin_top="4px",
margin_bottom="20px",
),
graph(),
padding="24px",
),
width="100%",
min_width="0",
max_width="64em",
margin_x="auto",
margin_y="24px",
border=f"1px solid {rx.color('slate', 5)}",
border_radius="16px",
background=rx.color("slate", 1),
overflow="hidden",
box_shadow="0 4px 24px rgba(0, 0, 0, 0.03)",
style={
"& *": {"box_sizing": "border-box"},
"& p, & h3": {"text_align": "left"},
},
)
app = rx.App()
app.add_page(
index,
title="Customer Data App",
description="A simple app to manage customer data.",
on_load=State.transform_data,
)| 용어 | 뜻 |
|---|---|
| 풀스택 | 화면(프론트엔드)과 서버(백엔드)를 모두 다루는 개발 |
| 컴포넌트 | 화면을 이루는 조각. Reflex에서는 rx.xxx(...) 함수 호출 하나 |
| props | 컴포넌트에 주는 설정값. 키워드 인자로 넘김 |
| State | 앱의 데이터와 이벤트 핸들러를 담은 클래스. 서버에 세션별로 존재 |
| Var | 화면 코드 안에서 State 변수를 가리키는 표현식. 브라우저에서 값이 채워짐 |
| computed var | @rx.var로 선언한, 다른 상태에서 계산되는 변수 |
| 이벤트 핸들러 | @rx.event를 붙인 State 메서드. 사용자 행동에 반응해 서버에서 실행 |
| 제어 컴포넌트 | 화면 요소의 값·상태를 State가 결정하는 방식 (open=, value=) |
| 컴파일 타임 | 앱 시작 때 화면 함수가 React 코드로 바뀌는 시점 |
| 런타임 | 앱이 돌면서 사용자가 조작하는 시점. 이벤트 핸들러가 실행됨 |
| WebSocket | 브라우저와 서버가 연결을 열어두고 양방향으로 메시지를 주고받는 통신 방식 |
| 핫 리로드 | 코드를 저장하면 서버를 끄지 않고 자동으로 다시 컴파일·반영하는 기능 |
교시 하나를 글 하나로 옮기면 6편 시리즈가 됩니다.
| 편 | 제목 예시 | 다루는 교시 |
|---|---|---|
| 1 | 파이썬만으로 웹앱 만들기, Reflex 시작하기 (+ Windows 에러 해결) | 1교시 |
| 2 | HTML 없이 화면 그리기: Reflex 컴포넌트와 스타일 | 2교시 |
| 3 | Reflex에서 len()이 안 되는 이유: State와 Var | 3교시 |
| 4 | 폼과 대화상자로 데이터 추가하기 | 4교시 |
| 5 | 파이썬으로 집계해서 차트 그리기 | 5교시 |
| 6 | Reflex 입문자가 꼭 만나는 에러 8가지 | 6교시 6-2 |
강사 노트 인용 블록은 본문 설명으로 풀어 쓰거나 "팁" 박스로 바꿉니다.len() 에러, 4교시 Cancel 버튼 type 누락, 닫히지 않는 대화상자는 일부러 재현할 가치가 크다고 봅니다.더 배우고 싶다면
제휴 링크이 강의 다음에 이어서 보기 좋은 강의입니다. 제휴 링크라서 이 링크로 수강하면 ReflexKR에 수수료가 돌아가고, 수강료는 똑같습니다. 고르는 기준
새 교시·새 강의 소식 받기
이 강의에 새 교시가 더해지거나 이어지는 강의가 열리면 이메일로 알려 드려요.
R
ReflexKR
눌러 보며 배우는 파이썬과 Reflex. 모든 예제는 Reflex 0.10.0에서 직접 실행해 확인했습니다.
ReflexKR은 배너 광고 없이 무료로 운영돼요. 도움이 되셨다면 커피 한 잔으로 응원해 주세요.
© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
질문과 답변
이 교시에서 궁금한 점을 남겨 주세요. 모든 언어 페이지의 질문이 함께 모입니다.