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
폼

Upload

rx.uploadrx.upload.root

공식 문서

공방 주문서

환불 신청 화면에 영수증 사진 첨부 칸을 만들어 주세요. 이미지 3장까지, 끌어다 놓으면 바로 붙고 이름과 크기가 보여야 합니다.

rx.upload는 파일을 끌어다 놓거나 눌러서 고르는 영역(드롭존)입니다. 고른 파일은 먼저 브라우저에 머물고, 이벤트 핸들러에 rx.upload_files()를 넘기면 그때 서버로 갑니다. 서버의 핸들러는 파일 목록을 받아 읽거나 저장합니다.

이 강의의 예제는 공용 서버에서 돌아가므로 파일을 저장하지 않고 이름과 크기만 보여 줍니다. 저장하는 코드는 따로 코드로만 보입니다.

rx.upload(
    rx.text("Drop files here or click to select"),
    id="upload_basic",
    border="2px dashed var(--gray-7)",
    padding="2em",
)

속성 바꿔 보기

드롭존의 동작을 정하는 속성들입니다. 파일을 몇 개 고른 뒤 multiple을 끄거나 no_click을 켜 보세요. 고른 파일 이름은 아래에 배지로 보입니다.

속성 바꿔 보기

label

multiple

disabled

no_click

no_drag

no_keyboard

rx.vstack(rx.upload(rx.text("Drop files here or click to select"), id="upload_lab", multiple=True, disabled=False, no_click=False, no_drag=False, no_keyboard=False, border="2px dashed var(--gray-7)", padding="2em"), rx.foreach(rx.selected_files("upload_lab"), lambda f: rx.badge(f, variant="soft")))
no_click은 눌러서 고르기를, no_drag는 끌어다 놓기를, no_keyboard는 Space·Enter로 여는 것을 막습니다. 0.10에서 multiple의 기본값은 True입니다.
속성값기본값쓰임
id문자열(고정)"default"이 드롭존의 이름. 아래 특수 Var·이벤트가 같은 id를 쓴다
accept{MIME: [확장자...]}모든 파일받을 파일 종류
multipleTrue / FalseTrue여러 개 고르기
max_files정수제한 없음최대 파일 수
max_size, min_size바이트제한 없음파일 하나의 최대·최소 크기
disabledTrue / FalseFalse드롭존 끄기
no_click, no_drag, no_keyboardTrue / FalseFalse누르기 / 끌어다 놓기 / 키보드로 열기 막기
drag_active_style스타일 dict없음파일을 끌고 들어왔을 때의 스타일(0.10)
on_drop이벤트고른 파일을 보관파일을 놓거나 고른 즉시 실행
on_drop_rejected이벤트오류 토스트조건에 맞지 않는 파일이 들어왔을 때

고른 파일 보기: selected_files

파일을 고르면 브라우저의 목록에 들어갑니다. 특수 Var rx.selected_files(id)로 그 이름 목록을 화면에 보이고, 특수 이벤트 rx.clear_selected_files(id)로 비웁니다. 아직 서버로는 아무것도 가지 않았습니다.

id는 드롭존과 특수 Var·이벤트를 잇는 열쇠라 모두 같은 값이어야 합니다. 문자열이나 모듈 상수로 쓰고, State 변수는 쓸 수 없습니다. 드롭존 안의 "Select File" 버튼은 모양일 뿐입니다. 드롭존 안을 누르면 무엇이든 파일 고르기 창이 열리기 때문입니다.

rx.vstack(
    rx.upload(
        rx.vstack(
            rx.button("Select File", variant="outline"),
            rx.text("Drag and drop files here or click to select files", size="2"),
            align="center",
        ),
        id="upload_selected",
        border="1px dotted var(--indigo-8)",
        padding="2em",
    ),
    rx.hstack(rx.foreach(rx.selected_files("upload_selected"), lambda f: rx.badge(f)), wrap="wrap"),
    rx.button("Clear", on_click=rx.clear_selected_files("upload_selected"), variant="soft", color_scheme="gray"),
)

서버로 보내기: rx.upload_files

핸들러에 rx.upload_files(upload_id=id)를 넘기면 고른 파일이 서버로 갑니다. 핸들러는 files: list[rx.UploadFile] 하나를 받는 보통의 async 이벤트입니다. 파일마다 file.name(원래 이름), file.size(바이트), await file.read()(내용)를 쓸 수 있습니다.

업로드 핸들러에는 @rx.event(background=True)를 붙이지 않고 async with self도 쓰지 않습니다. 그것은 아래의 청크 업로드에만 씁니다. 그리고 업로드 버튼은 드롭존 밖에 둡니다. 안에 두면 누를 때마다 파일 창이 열리면서 업로드도 시작됩니다.

def upload_size_label(size: int) -> str:
    return f"{size / 1024:.1f} KB" if size < 1024 * 1024 else f"{size / 1024 / 1024:.2f} MB"


class UploadInfoState(rx.State):
    files: list[dict[str, str]] = []

    @rx.event
    async def handle_upload(self, files: list[rx.UploadFile]):
        # Read only the name and size. Nothing is saved to disk.
        self.files = [{"name": str(f.name), "size": upload_size_label(f.size or 0)} for f in files]


rx.vstack(
    rx.upload(
        rx.text("Drop up to 5 files (2 MB each)"),
        id="upload_info",
        max_files=5,
        max_size=2 * 1024 * 1024,
        border="2px dashed var(--gray-7)",
        padding="2em",
    ),
    rx.hstack(rx.foreach(rx.selected_files("upload_info"), lambda f: rx.badge(f, variant="soft")), wrap="wrap"),
    rx.hstack(
        rx.button("Upload", on_click=UploadInfoState.handle_upload(rx.upload_files(upload_id="upload_info"))),
        rx.button("Clear", on_click=rx.clear_selected_files("upload_info"), variant="soft", color_scheme="gray"),
    ),
    rx.foreach(
        UploadInfoState.files,
        lambda f: rx.hstack(rx.icon("file", size=14), rx.text(f["name"], size="2"), rx.code(f["size"]), align="center"),
    ),
)

저장하고 보여 주기 (코드로만)

실제 앱에서는 rx.get_upload_dir()가 돌려주는 폴더(Path, 기본 ./uploaded_files, 환경 변수 REFLEX_UPLOADED_FILES_DIR로 변경)에 저장하고, 화면에서는 rx.get_upload_url(파일이름)으로 주소를 만들어 rx.image 등에 넣습니다. 아래는 공식 문서의 기본 예제로, 이 페이지에서는 실행하지 않습니다.

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

    @rx.event
    async def handle_upload(self, files: list[rx.UploadFile]):
        for file in files:
            data = await file.read()
            path = rx.get_upload_dir() / file.name
            with path.open("wb") as f:
                f.write(data)
            self.uploaded_files.append(file.name)


def upload_component():
    return rx.vstack(
        rx.upload(id="upload"),
        rx.button("Upload", on_click=State.handle_upload(rx.upload_files("upload"))),
        rx.foreach(
            State.uploaded_files,
            lambda f: rx.image(src=rx.get_upload_url(f), alt="Uploaded file preview"),
        ),
    )
  • rx.get_upload_dir(): 서버(백엔드)에서 저장할 경로. 이벤트 핸들러에서 씁니다.
  • rx.get_upload_url(name): 화면(프런트엔드)에서 쓸 주소. 파이썬 문자열이 아니라 프런트엔드 Var이므로 컴포넌트 코드에서만 씁니다. 주소는 /_upload/파일이름 꼴이지만 직접 쓰지 말고 이 함수를 거칩니다.
  • 업로드 폴더는 /_upload로 누구에게나 공개됩니다. 같은 이름이 겹치지 않게 무작위 접두어를 붙여 저장하는 것이 좋습니다.
  • Reflex Cloud 같은 호스팅에서는 배포할 때마다 업로드 폴더가 비워집니다. 오래 둘 파일은 S3 같은 외부 저장소에 둡니다.

한 번에 올리기: on_drop과 거르기

on_drop에 핸들러를 연결하면 버튼 없이 고르는 즉시 올라갑니다. accept로 파일 종류를, max_files와 max_size로 개수와 크기를 거릅니다. 조건에 맞지 않는 파일은 on_drop_rejected로 오고, 이 이벤트를 주지 않으면 0.10은 오류 토스트를 띄웁니다. 아래에 이미지나 PDF가 아닌 파일, 2MB가 넘는 파일을 놓아 보세요.

class UploadDropState(rx.State):
    names: list[str] = []
    rejected: int = 0

    @rx.event
    async def handle_drop(self, files: list[rx.UploadFile]):
        self.names = [f"{f.name} ({f.size or 0} bytes)" for f in files]

    @rx.event
    def handle_rejected(self, rejections: list):
        self.rejected = len(rejections)


rx.vstack(
    rx.upload(
        rx.text("Images or PDF only, up to 3 files", size="2"),
        id="upload_drop",
        multiple=True,
        accept={
            "application/pdf": [".pdf"],
            "image/png": [".png"],
            "image/jpeg": [".jpg", ".jpeg"],
            "image/webp": [".webp"],
        },
        max_files=3,
        max_size=2 * 1024 * 1024,
        no_keyboard=True,
        on_drop=UploadDropState.handle_drop(rx.upload_files(upload_id="upload_drop")),
        on_drop_rejected=UploadDropState.handle_rejected,
        border="2px dashed var(--gray-7)",
        padding="2em",
    ),
    rx.foreach(UploadDropState.names, lambda n: rx.text(n, size="2")),
    rx.cond(UploadDropState.rejected > 0, rx.text("Rejected: ", UploadDropState.rejected, size="2", color_scheme="red")),
)

꾸미지 않은 드롭존: rx.upload.root

rx.upload는 기본 테두리와 여백이 있는 드롭존입니다. 모양을 처음부터 직접 만들려면 rx.upload.root를 씁니다. 속성과 이벤트는 같습니다. drag_active_style로 파일을 끌고 들어온 동안의 배경을 바꿀 수 있습니다.

rx.vstack(
    rx.upload.root(
        rx.box(
            rx.icon("cloud-upload", size=40, color=rx.color("indigo", 9)),
            rx.hstack(rx.text("Click to upload", weight="bold", color=rx.color("indigo", 11)), rx.text("or drag and drop"), size="2"),
            rx.text("SVG, PNG, JPG or GIF (MAX. 5MB)", size="1", color=rx.color("gray", 10)),
            display="flex",
            flex_direction="column",
            align_items="center",
            gap="6px",
            padding="1.5rem",
            text_align="center",
        ),
        id="upload_root_demo",
        max_width="24rem",
        border=f"2px dashed {rx.color('indigo', 7)}",
        border_radius="12px",
        cursor="pointer",
        drag_active_style={"background_color": rx.color("indigo", 3)},
    ),
    rx.hstack(rx.foreach(rx.selected_files("upload_root_demo"), lambda f: rx.badge(f)), wrap="wrap"),
)

진행률과 취소

rx.upload_files(와 rx.upload_files_chunk)는 on_upload_progress를 받습니다. 올리는 동안 {"loaded": 보낸 바이트, "total": 전체 바이트, "progress": 0~1} dict가 이 핸들러로 옵니다. rx.cancel_upload(id)는 올리는 중인 업로드를 멈춥니다. 이 페이지는 2MB 이하만 받으므로 진행률이 금방 100이 됩니다.

Total bytes received: 0

class UploadProgressState(rx.State):
    uploading: bool = False
    progress: int = 0
    total_bytes: int = 0

    @rx.event
    async def handle_upload(self, files: list[rx.UploadFile]):
        self.total_bytes = sum(f.size or 0 for f in files)

    @rx.event
    def handle_upload_progress(self, progress: dict):
        self.uploading = True
        self.progress = round(progress["progress"] * 100)
        if self.progress >= 100:
            self.uploading = False

    @rx.event
    def cancel_upload(self):
        self.uploading = False
        return rx.cancel_upload("upload_progress")


rx.vstack(
    rx.upload(
        rx.text("Drag and drop files here or click to select files", size="2"),
        id="upload_progress",
        max_size=2 * 1024 * 1024,
        border="1px dotted var(--indigo-8)",
        padding="2em",
    ),
    rx.vstack(rx.foreach(rx.selected_files("upload_progress"), lambda f: rx.text(f, size="2"))),
    rx.progress(value=UploadProgressState.progress, max=100),
    rx.cond(
        ~UploadProgressState.uploading,
        rx.button(
            "Upload",
            on_click=UploadProgressState.handle_upload(
                rx.upload_files(
                    upload_id="upload_progress",
                    on_upload_progress=UploadProgressState.handle_upload_progress,
                ),
            ),
        ),
        rx.button("Cancel", on_click=UploadProgressState.cancel_upload, color_scheme="red"),
    ),
    rx.text("Total bytes received: ", UploadProgressState.total_bytes, size="2"),
    width="100%",
)

큰 파일: 청크 업로드 (코드로만)

await file.read()는 파일 전체를 메모리에 올립니다. 큰 파일은 rx.upload_files_chunk(upload_id=...)로 조각씩 받아 바로 디스크에 씁니다. 청크 핸들러는 @rx.event(background=True)여야 하고, chunk_iter: rx.UploadChunkIterator를 받아 끝까지 읽어야 합니다(중간에 돌아가면 업로드가 실패합니다). 조각마다 chunk.filename, chunk.offset, chunk.content_type, chunk.data가 있습니다. 디스크에 쓰는 코드라 여기서는 실행하지 않습니다.

class ChunkUploadState(rx.State):
    uploaded_files: list[str] = []

    @rx.event(background=True)
    async def handle_large_upload(self, chunk_iter: rx.UploadChunkIterator):
        file_handles = {}
        try:
            async for chunk in chunk_iter:  # must consume every chunk
                fh = file_handles.get(chunk.filename)
                if fh is None:
                    path = rx.get_upload_dir() / "stream" / chunk.filename
                    path.parent.mkdir(parents=True, exist_ok=True)
                    fh = file_handles[chunk.filename] = path.open("wb")
                fh.seek(chunk.offset)
                fh.write(chunk.data)
        finally:
            for fh in file_handles.values():
                fh.close()
        async with self:
            self.uploaded_files = sorted(file_handles)


# rx.button("Upload Large Files",
#           on_click=ChunkUploadState.handle_large_upload(rx.upload_files_chunk(upload_id="large_upload")))

폼 안의 업로드

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

주문서 완성

이미지 종류와 개수·크기를 드롭존이 먼저 거르고, on_drop으로 놓는 즉시 서버가 이름과 크기를 받습니다. 목록은 최근 세 개만 둡니다. 저장은 하지 않습니다.

Receipt photos (max 3, 2 MB each)

class UploadReceiptState(rx.State):
    files: list[dict[str, str]] = []

    @rx.event
    async def attach(self, files: list[rx.UploadFile]):
        added = [{"name": str(f.name), "size": f"{(f.size or 0) / 1024:.0f} KB"} for f in files]
        self.files = (self.files + added)[-3:]

    @rx.event
    def clear(self):
        self.files = []


rx.card(
    rx.vstack(
        rx.text("Receipt photos (max 3, 2 MB each)", weight="bold", size="2"),
        rx.upload(
            rx.hstack(rx.icon("image-plus", size=18), rx.text("Drop or click to attach", size="2"), align="center"),
            id="upload_receipt",
            accept={"image/png": [".png"], "image/jpeg": [".jpg", ".jpeg"]},
            max_files=3,
            max_size=2 * 1024 * 1024,
            on_drop=UploadReceiptState.attach(rx.upload_files(upload_id="upload_receipt")),
            border="1px dashed var(--gray-8)",
            border_radius="8px",
            padding="1em",
            width="100%",
            drag_active_style={"background_color": "var(--accent-3)"},
        ),
        rx.foreach(
            UploadReceiptState.files,
            lambda f: rx.hstack(rx.icon("image", size=14), rx.text(f["name"], size="2"), rx.spacer(), rx.code(f["size"]), width="100%", align="center"),
        ),
        rx.button("Remove all", on_click=UploadReceiptState.clear, size="1", variant="soft", color_scheme="gray"),
        width="100%",
    ),
    width="100%",
    max_width="380px",
)

공방 노트

  • rx.upload(id="고정 id")로 드롭존. 고른 파일은 rx.selected_files(id), 비우기는 rx.clear_selected_files(id).

  • 보내기는 핸들러(rx.upload_files(upload_id=id)). 핸들러는 async, files: list[rx.UploadFile].

  • on_drop이면 고르는 즉시 올라간다. accept · max_files · max_size로 거르고, 탈락은 on_drop_rejected.

  • 저장은 rx.get_upload_dir(), 보여 주기는 rx.get_upload_url(). 업로드 폴더는 공개된다.

  • 업로드 버튼은 드롭존 밖에. 큰 파일은 rx.upload_files_chunk + background 핸들러.

← 이전

폼 · Form

다음 →

데이터 표시 · Badge

새 교시·새 강의 소식 받기

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

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

질문과 답변

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

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

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