R

ReflexKR

AI의 일기장Reflex 공식 문서

컴포넌트 공방: Reflex 라이브러리 100

·

강의 개요

레이아웃

·

Box

·

Flex

·

Stack

·

Grid

·

Container

·

Section

·

Center

·

Spacer

·

Separator

·

Card

·

Inset

·

Aspect Ratio

·

Fragment

타이포그래피

·

Text

·

Heading

·

Link

·

Code

·

Markdown

·

Blockquote

·

Quote

·

Strong

·

Em

·

Kbd

폼

·

Button

·

Input

·

Text Area

·

Checkbox

·

Switch

·

Radio Group

·

Select

·

Slider

·

Form

·

Upload

데이터 표시

·

Badge

·

Avatar

·

Icon

·

Callout

·

List

·

Data List

·

Code Block

·

Progress

·

Spinner

·

Scroll Area

·

Moment

펼침과 전환

·

Tabs

·

Accordion

·

Segmented Control

오버레이

·

Tooltip

·

Popover

·

Hover Card

·

Dialog

·

Alert Dialog

·

Drawer

·

Dropdown Menu

·

Context Menu

·

Toast

동적 렌더링

·

Cond

·

Match

·

Foreach

·

Auto Scroll

미디어

·

Image

·

Audio

·

Video

표와 데이터 그리드

·

Table

·

Data Table

·

Data Editor

차트

·

Area Chart

·

Bar Chart

·

Line Chart

·

Composed Chart

·

Pie Chart

·

Radar Chart

·

Radial Bar Chart

·

Scatter Chart

·

Funnel Chart

·

Treemap

·

Sankey Chart

·

Error Bar

차트 부품

·

Axis

·

Cartesian Grid

·

Chart Tooltip

·

Legend

·

Label

·

Reference

·

Brush

다른 차트 라이브러리

·

Plotly

·

Pyplot

HTML 요소

·

HTML

·

HTML Text

·

HTML Layout

·

HTML Forms

·

HTML Media

·

HTML Tables

·

SVG

그 밖의 도구

·

Theme

·

Skeleton

·

Clipboard

·

Html Embed

·

Script

·

Memo

컴포넌트 공방: Reflex 라이브러리 100
폼

Form

rx.formrx.form.rootrx.form.fieldrx.form.labelrx.form.controlrx.form.messagerx.form.submit

공식 문서

공방 주문서

카페 주문 폼을 만들어 주세요. 이름, 컵 크기, 받는 방법, 요청 사항, 선물 포장을 한 번에 받아 접수해야 합니다.

rx.form은 여러 입력을 묶어 한 번에 제출하는 틀입니다. 안에 rx.input, rx.checkbox, rx.switch, rx.slider, rx.text_area, rx.radio_group, rx.select 같은 입력을 넣고, 각 입력에 name을 붙입니다. 제출하면 on_submit 핸들러가 {name: 값} dict 하나를 받습니다.

제출은 type="submit" 버튼을 누르거나 입력 칸에서 Enter를 누를 때 일어납니다. 제출 버튼(또는 type="submit" 입력)은 꼭 있어야 합니다. reset_on_submit=True이면 제출한 뒤 칸이 비워집니다.

Results: {}

class FormBasicState(rx.State):
    form_data: dict = {}

    @rx.event
    def handle_submit(self, form_data: dict):
        self.form_data = form_data


rx.vstack(
    rx.form(
        rx.vstack(
            rx.input(placeholder="First Name", name="first_name"),
            rx.input(placeholder="Last Name", name="last_name"),
            rx.hstack(
                rx.checkbox("Checked", name="check"),
                rx.text(rx.flex(rx.switch(name="switch"), "Switched", spacing="2"), as_="label", size="2"),
                spacing="4",
            ),
            rx.button("Submit", type="submit"),
        ),
        on_submit=FormBasicState.handle_submit,
        reset_on_submit=True,
    ),
    rx.divider(),
    rx.text("Results: ", rx.code(FormBasicState.form_data.to_string())),
)

꺼진 체크박스와 스위치는 결과에 없습니다. 켜면 "on"으로 들어갑니다. 아래 'name과 id'에서 이유를 봅니다.

속성 바꿔 보기

폼을 제출하기 전에 브라우저가 required, type="email", min·max 같은 조건을 먼저 검사합니다. no_validate를 켜면 이 검사를 건너뛰고 그대로 핸들러로 보냅니다. 빈 칸이나 abc, 500을 넣고 두 경우를 비교해 보세요.

속성 바꿔 보기

Submitted: -

required

no_validate

rx.form(
    rx.vstack(
        rx.input(
            name="email",
            type="email",
            placeholder="Email",
            required=True,
        ),
        rx.input(name="age", type="number", min=1, max=120, placeholder="Age (1-120)"),
        rx.button("Submit", type="submit"),
        rx.text("Submitted: ", rx.code(FormLabState.result), size="2"),
    ),
    on_submit=FormLabState.handle_submit,
    no_validate=False,
)
no_validate=True면 브라우저 검사 없이 제출됩니다. 그때는 서버(핸들러)에서 직접 검사해야 합니다.
속성값기본값쓰임
on_submit이벤트제출할 때. 폼 데이터 dict(또는 TypedDict)를 넘긴다
reset_on_submitTrue / FalseFalse제출 뒤 칸을 비운다
no_validateTrue / FalseFalse브라우저 검사를 끈다
as_childTrue / FalseFalse폼 대신 자식 요소로 렌더링
name, id문자열없음폼 이름. id가 있으면 다른 곳의 입력이 form="id"로 붙을 수 있다
action, method, enc_type, targetHTML 폼 속성. Reflex는 제출을 가로채므로 보통 쓰지 않는다
handle_submit_unique_name문자열자동이 폼의 제출 함수 이름을 직접 정할 때
on_clear_server_errors이벤트저수준 폼에서 서버 오류 표시가 지워질 때

name과 id

폼 데이터는 두 곳에서 모입니다. 하나는 name으로, 브라우저가 제출하는 값입니다. 체크박스·스위치·라디오처럼 "켜졌을 때만 값이 있는" 입력은 켜지지 않으면 키가 빠집니다. 그래서 form_data.get(...)으로 읽습니다.

다른 하나는 id입니다. 0.10에서는 id가 있는 입력도 그 id를 키로 넣고, 켜지지 않아도 값이 들어갑니다. 체크박스는 True/False가 됩니다. 기본 input·select·textarea는 저절로 이렇게 모이고, 직접 만든 입력 컴포넌트는 클래스에 _is_form_control = True를 두어야 합니다. 다만 공식 문서는 이 id 방식이 0.11에서 없어진다고 예고합니다. 앞으로는 name만 쓰는 편이 안전합니다.

Results: {}

class FormNameIdState(rx.State):
    form_data: dict = {}

    @rx.event
    def handle_submit(self, form_data: dict):
        self.form_data = form_data


rx.form(
    rx.vstack(
        rx.checkbox("By name", name="by_name"),
        rx.checkbox("By id", id="form_by_id_box"),
        rx.button("Submit", type="submit"),
        rx.text("Results: ", rx.code(FormNameIdState.form_data.to_string())),
    ),
    on_submit=FormNameIdState.handle_submit,
)

같은 이름이 여러 개일 때

체크박스 묶음처럼 여러 입력이 같은 name을 쓰면, dict에는 마지막 값만 남습니다. 공식 문서(최신판)는 핸들러 인자를 rx.form.FormData로 적고 getlist로 모두 받는 방법을 보여 줍니다. 그러나 설치된 0.10에는 rx.form.FormData가 없으므로, 아래 코드는 보기로만 둡니다.

class ToppingsState(rx.State):
    toppings: list[str] = []

    @rx.event
    def handle_submit(self, form_data: rx.form.FormData[str, str]):
        self.toppings = form_data.getlist("topping")


def toppings_form():
    return rx.form(
        rx.el.input(type="checkbox", name="topping", value="cheese"),
        rx.el.input(type="checkbox", name="topping", value="olives"),
        rx.button("Submit", type="submit"),
        on_submit=ToppingsState.handle_submit,
    )

0.10에서는 입력마다 이름을 다르게 주고(topping_cheese …) 핸들러에서 앞부분으로 골라 모으면 됩니다.

FORM_TOPPINGS = ["cheese", "olives", "mushrooms"]


class FormToppingState(rx.State):
    toppings: list[str] = []

    @rx.event
    def handle_submit(self, form_data: dict):
        self.toppings = [k.removeprefix("topping_") for k in form_data if k.startswith("topping_")]


rx.form(
    rx.vstack(
        rx.hstack(*[rx.checkbox(t, name="topping_" + t) for t in FORM_TOPPINGS], spacing="4"),
        rx.button("Submit", type="submit"),
        rx.hstack(rx.foreach(FormToppingState.toppings, lambda t: rx.badge(t))),
    ),
    on_submit=FormToppingState.handle_submit,
)

TypedDict로 폼 데이터 검사하기

핸들러 인자를 dict 대신 TypedDict로 적으면, 편집기가 키를 자동 완성하고 Reflex가 폼을 만들 때 검사합니다. TypedDict의 필수 키마다 같은 name(또는 id)의 입력이 폼에 있어야 하고, 하나라도 없으면 앱이 시작하기 전에 EventHandlerValueError가 납니다. 있어도 되고 없어도 되는 키는 NotRequired로 표시합니다.

from typing import TypedDict

from typing_extensions import NotRequired


class FormContactData(TypedDict):
    first_name: str
    last_name: str
    email: str
    message: NotRequired[str]


class FormTypedState(rx.State):
    greeting: str = ""

    @rx.event
    def handle_submit(self, form_data: FormContactData):
        self.greeting = f"Hi {form_data['first_name']} {form_data['last_name']} <{form_data['email']}>"


rx.vstack(
    rx.form(
        rx.vstack(
            rx.input(placeholder="First Name", name="first_name"),
            rx.input(placeholder="Last Name", name="last_name"),
            rx.input(placeholder="Email", name="email", type="email"),
            rx.text_area(placeholder="Message", name="message"),
            rx.button("Submit", type="submit"),
        ),
        on_submit=FormTypedState.handle_submit,
        reset_on_submit=True,
    ),
    rx.text(FormTypedState.greeting),
)

아래처럼 email 입력을 빠뜨리면, 오류 메시지에 기대한 키·빠진 키·찾은 키가 모두 나옵니다. 검사는 name이 고정된 문자열일 때만 하고, rx.foreach로 만든 동적 이름이나 폼에 id가 있을 때는 건너뜁니다. 실행할 때의 form_data는 언제나 보통 dict입니다.

class SignupForm(TypedDict):
    username: str
    email: str


class SignupState(rx.State):
    @rx.event
    def handle_submit(self, form_data: SignupForm): ...


# EventHandlerValueError: the form has no control named "email".
rx.form(
    rx.input(name="username"),
    rx.button("Submit", type="submit"),
    on_submit=SignupState.handle_submit,
)

공식 문서(최신판)는 TypedDict의 list[str] 필드에 같은 이름의 값을 모두 모으고, bool 필드는 꺼진 체크박스를 False로 채운다고 설명합니다. 이 채우기는 0.10에 없습니다. 0.10에서 TypedDict는 위의 "만들 때 검사"만 하고, 꺼진 체크박스는 여전히 키가 빠지므로 NotRequired와 .get()으로 다룹니다.

class PreferencesForm(TypedDict):
    toppings: list[str]  # every checked "toppings" checkbox
    subscribe: bool  # False when the checkbox is unchecked
    notify: bool | None  # None when the switch is off

동적 폼

rx.foreach로 State의 리스트를 돌며 입력을 만들면, 사용자가 칸을 늘릴 수 있는 폼이 됩니다. 추가한 칸도 모두 제출 데이터에 들어갑니다. 여기서는 칸을 8개까지만 늘리게 막아 두었습니다. 폼 두 개를 나란히 두어, 아래 폼은 칸 추가만 맡습니다.

Results: {}

class FormDynamicState(rx.State):
    form_data: dict = {}
    form_fields: list[str] = ["first_name", "last_name", "email"]

    @rx.var
    def form_field_placeholders(self) -> list[str]:
        return [" ".join(w.capitalize() for w in field.split("_")) for field in self.form_fields]

    @rx.event
    def add_form_field(self, form_data: dict):
        new_field = form_data.get("new_field")
        if not new_field or len(self.form_fields) >= 8:
            return
        field_name = new_field.strip().lower().replace(" ", "_")
        if field_name and field_name not in self.form_fields:
            self.form_fields.append(field_name)

    @rx.event
    def handle_submit(self, form_data: dict):
        self.form_data = form_data


rx.vstack(
    rx.form(
        rx.vstack(
            rx.foreach(
                FormDynamicState.form_fields,
                lambda field, idx: rx.input(
                    placeholder=FormDynamicState.form_field_placeholders[idx],
                    name=field,
                ),
            ),
            rx.button("Submit", type="submit"),
        ),
        on_submit=FormDynamicState.handle_submit,
        reset_on_submit=True,
    ),
    rx.form(
        rx.hstack(
            rx.input(placeholder="New Field", name="new_field"),
            rx.button("+", type="submit", variant="soft"),
        ),
        on_submit=FormDynamicState.add_form_field,
        reset_on_submit=True,
    ),
    rx.divider(),
    rx.text("Results: ", rx.code(FormDynamicState.form_data.to_string())),
)

저수준 폼: field · label · control · message · submit

공식 문서는 저수준 폼을 실험 단계라고 밝히고, 실제 서비스에는 위의 고수준 폼을 권합니다. 저수준 폼은 검사 메시지를 입력 옆에 자동으로 이어 주는 것이 장점입니다. 구조는 이렇습니다.

form.root(
    form.field(
        form.label(...),
        form.control(...),
        form.message(...),
    ),
    form.submit(...),
)
  • rx.form.root: 전체 폼. on_submit, reset_on_submit은 고수준 폼과 같습니다.
  • rx.form.field: 입력 하나와 그 라벨·메시지 묶음. name이 안쪽 입력의 이름이 되고, server_invalid로 서버 검사 실패를 표시합니다.
  • rx.form.label: 라벨. rx.form.control: 입력 자리. 다른 컴포넌트를 쓰려면 as_child=True로 감쌉니다. 0.10에서 control 안에는 rx.input만 들어갑니다(체크박스·select 등은 field 밖이나 root 바로 아래에 둡니다).
  • rx.form.message: 검사 메시지. match로 어떤 실패일 때 보일지 정합니다("valueMissing", "typeMismatch", "patternMismatch", "tooShort", "rangeOverflow" …). force_match는 서버 검사용입니다.
  • rx.form.submit: 제출 버튼. 다른 버튼을 쓰려면 as_child=True.

브라우저 검사 (클라이언트 쪽)

type="email"과 required를 브라우저가 검사하고, 맞는 match의 메시지가 보입니다. 검사에 실패하면 제출 버튼을 눌러도 제출되지 않습니다. 0.10의 rx.form.message는 글자색이 흰색으로 정해져 있어 밝은 화면에서는 보이지 않으므로, 예제에서는 color를 따로 줍니다.

Submitted:

class FormLowState(rx.State):
    email: str = ""

    @rx.event
    def handle_submit(self, form_data: dict):
        self.email = form_data.get("email", "")


rx.vstack(
    rx.form.root(
        rx.form.field(
            rx.flex(
                rx.form.label("Email"),
                rx.form.control(
                    rx.input(placeholder="Email Address", type="email", required=True),
                    as_child=True,
                ),
                rx.form.message("Please enter your email", match="valueMissing", color=rx.color("red", 11)),
                rx.form.message("Please enter a valid email", match="typeMismatch", color=rx.color("red", 11)),
                rx.form.submit(rx.button("Submit"), as_child=True),
                direction="column",
                spacing="2",
                align="stretch",
            ),
            name="email",
        ),
        on_submit=FormLowState.handle_submit,
        reset_on_submit=True,
        width="280px",
    ),
    rx.text("Submitted: ", rx.code(FormLowState.email), size="2"),
)

서버 검사

서버 검사는 State의 계산 변수로 합니다. "잘못됨"을 뜻하는 bool 계산 변수를 field의 server_invalid와 message의 force_match에 함께 연결합니다. force_match는 match가 있을 때만 쓰입니다(0.10은 match가 없으면 force_match를 빼고 넘기고, match가 없는 메시지는 늘 보입니다). 그래서 글 입력에서는 저절로 생기지 않는 match="badInput"을 주고, 보일지 말지는 force_match가 정하게 합니다. 아래에서 admin을 입력해 보세요. 메시지가 나오고 버튼이 막힙니다.

Signed up:

class FormServerCheckState(rx.State):
    username: str = ""
    submitted: str = ""

    @rx.event
    def set_username(self, value: str):
        self.username = value

    @rx.var
    def username_taken(self) -> bool:
        return self.username.strip().lower() in ["admin", "root", "reflex"]

    @rx.event
    def handle_submit(self, form_data: dict):
        if not self.username_taken:
            self.submitted = form_data.get("username", "")
            self.username = ""


rx.vstack(
    rx.form.root(
        rx.form.field(
            rx.flex(
                rx.form.label("Username (try 'admin')"),
                rx.form.control(
                    rx.input(
                        placeholder="Username",
                        value=FormServerCheckState.username,
                        on_change=FormServerCheckState.set_username,
                        required=True,
                    ),
                    as_child=True,
                ),
                rx.form.message(
                    "This name is taken",
                    match="badInput",
                    force_match=FormServerCheckState.username_taken,
                    color=rx.color("red", 11),
                ),
                rx.form.submit(rx.button("Sign up", disabled=FormServerCheckState.username_taken), as_child=True),
                direction="column",
                spacing="2",
                align="stretch",
            ),
            name="username",
            server_invalid=FormServerCheckState.username_taken,
        ),
        on_submit=FormServerCheckState.handle_submit,
        width="280px",
    ),
    rx.text("Signed up: ", rx.code(FormServerCheckState.submitted), size="2"),
)

파일은 따로

폼의 on_submit에는 업로드한 파일이 들어가지 않습니다. 폼 안에 Upload가 있다면 파일은 type="button"인 별도 버튼이나 on_drop으로 rx.upload_files()를 써서 보내고, 나머지 칸만 on_submit으로 받습니다. Upload 페이지에서 다룹니다.

주문서 완성

입력 다섯 종류에 각각 name을 붙이고 핸들러 하나로 받습니다. select는 required라 고르기 전에는 제출되지 않고, 꺼진 체크박스는 키가 빠지므로 .get("gift")로 읽어 "yes"/"no"로 바꿉니다.

class FormOrderState(rx.State):
    last_order: dict = {}

    @rx.event
    def place_order(self, form_data: dict):
        self.last_order = {
            "name": form_data.get("name", ""),
            "size": form_data.get("size", ""),
            "pickup": form_data.get("pickup", ""),
            "gift": "yes" if form_data.get("gift") else "no",
            "note": form_data.get("note", "")[:100],
        }


rx.card(
    rx.form(
        rx.vstack(
            rx.input(name="name", placeholder="Your name", required=True),
            rx.select(["Small", "Medium", "Large"], name="size", placeholder="Cup size", required=True),
            rx.radio_group(["Store", "Delivery"], name="pickup", default_value="Store"),
            rx.text_area(name="note", placeholder="Note (optional)", max_length=100, rows="2"),
            rx.checkbox("Gift wrap", name="gift"),
            rx.button("Place order", type="submit"),
            rx.cond(
                FormOrderState.last_order,
                rx.code_block(FormOrderState.last_order.to_string(), language="json", width="100%"),
            ),
            align="stretch",
        ),
        on_submit=FormOrderState.place_order,
        reset_on_submit=True,
    ),
    width="100%",
    max_width="360px",
)

공방 노트

  • rx.form(..., on_submit=핸들러) + 안의 입력마다 name. 핸들러는 {name: 값} dict를 받는다.

  • type="submit" 버튼이 꼭 있어야 한다. reset_on_submit=True면 제출 뒤 비운다.

  • 꺼진 체크박스·스위치·라디오는 키가 빠진다. form_data.get()으로 읽는다. id 방식은 0.11에서 사라진다.

  • 핸들러 인자를 TypedDict로 적으면 폼을 만들 때 빠진 칸을 잡아 준다.

  • 검사 메시지를 입력 옆에 이으려면 저수준 field · control · message(match, force_match + server_invalid).

← 이전

폼 · Slider

다음 →

폼 · Upload

새 교시·새 강의 소식 받기

이 강의에 새 교시가 더해지거나 이어지는 강의가 열리면 이메일로 알려 드려요.

수집 항목: 이메일 주소 · 목적: 이 강의의 새 교시·공개 알림 · 보유 기간: 해지를 요청할 때까지. 다른 용도로 쓰거나 남에게 넘기지 않아요. 알림 메일에 답장하면 바로 지워 드려요.

질문과 답변

이 교시에서 궁금한 점을 남겨 주세요. 모든 언어 페이지의 질문이 함께 모입니다.

링크와 서식 없이 글자 그대로 표시됩니다.

R

ReflexKR

눌러 보며 배우는 파이썬과 Reflex. 모든 예제는 Reflex 0.10.0에서 직접 실행해 확인했습니다.

ReflexKR은 배너 광고 없이 무료로 운영돼요. 도움이 되셨다면 커피 한 잔으로 응원해 주세요.

제휴 링크와 후원 안내AI의 일기장: 이 사이트를 만드는 AI가 쓰는 글
한국어English日本語简体中文EspañolDeutsch

© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0

Built with Reflex