컴포넌트 공방: 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
공방 주문서
환불 신청 화면에 영수증 사진 첨부 칸을 만들어 주세요. 이미지 3장까지, 끌어다 놓으면 바로 붙고 이름과 크기가 보여야 합니다.
rx.upload는 파일을 끌어다 놓거나 눌러서 고르는 영역(드롭존)입니다. 고른 파일은 먼저 브라우저에 머물고, 이벤트 핸들러에 rx.upload_files()를 넘기면 그때 서버로 갑니다. 서버의 핸들러는 파일 목록을 받아 읽거나 저장합니다.
이 강의의 예제는 공용 서버에서 돌아가므로 파일을 저장하지 않고 이름과 크기만 보여 줍니다. 저장하는 코드는 따로 코드로만 보입니다.
Drop files here or click to select
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을 켜 보세요. 고른 파일 이름은 아래에 배지로 보입니다.
속성 바꿔 보기
Drop files here or click to select
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")))| 속성 | 값 | 기본값 | 쓰임 |
|---|---|---|---|
id | 문자열(고정) | "default" | 이 드롭존의 이름. 아래 특수 Var·이벤트가 같은 id를 쓴다 |
accept | {MIME: [확장자...]} | 모든 파일 | 받을 파일 종류 |
multiple | True / False | True | 여러 개 고르기 |
max_files | 정수 | 제한 없음 | 최대 파일 수 |
max_size, min_size | 바이트 | 제한 없음 | 파일 하나의 최대·최소 크기 |
disabled | True / False | False | 드롭존 끄기 |
no_click, no_drag, no_keyboard | True / False | False | 누르기 / 끌어다 놓기 / 키보드로 열기 막기 |
drag_active_style | 스타일 dict | 없음 | 파일을 끌고 들어왔을 때의 스타일(0.10) |
on_drop | 이벤트 | 고른 파일을 보관 | 파일을 놓거나 고른 즉시 실행 |
on_drop_rejected | 이벤트 | 오류 토스트 | 조건에 맞지 않는 파일이 들어왔을 때 |
파일을 고르면 브라우저의 목록에 들어갑니다. 특수 Var rx.selected_files(id)로 그 이름 목록을 화면에 보이고, 특수 이벤트 rx.clear_selected_files(id)로 비웁니다. 아직 서버로는 아무것도 가지 않았습니다.
id는 드롭존과 특수 Var·이벤트를 잇는 열쇠라 모두 같은 값이어야 합니다. 문자열이나 모듈 상수로 쓰고, State 변수는 쓸 수 없습니다. 드롭존 안의 "Select File" 버튼은 모양일 뿐입니다. 드롭존 안을 누르면 무엇이든 파일 고르기 창이 열리기 때문입니다.
Drag and drop files here or click to select files
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(upload_id=id)를 넘기면 고른 파일이 서버로 갑니다. 핸들러는 files: list[rx.UploadFile] 하나를 받는 보통의 async 이벤트입니다. 파일마다 file.name(원래 이름), file.size(바이트), await file.read()(내용)를 쓸 수 있습니다.
업로드 핸들러에는 @rx.event(background=True)를 붙이지 않고 async with self도 쓰지 않습니다. 그것은 아래의 청크 업로드에만 씁니다. 그리고 업로드 버튼은 드롭존 밖에 둡니다. 안에 두면 누를 때마다 파일 창이 열리면서 업로드도 시작됩니다.
Drop up to 5 files (2 MB each)
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로 누구에게나 공개됩니다. 같은 이름이 겹치지 않게 무작위 접두어를 붙여 저장하는 것이 좋습니다.on_drop에 핸들러를 연결하면 버튼 없이 고르는 즉시 올라갑니다. accept로 파일 종류를, max_files와 max_size로 개수와 크기를 거릅니다. 조건에 맞지 않는 파일은 on_drop_rejected로 오고, 이 이벤트를 주지 않으면 0.10은 오류 토스트를 띄웁니다. 아래에 이미지나 PDF가 아닌 파일, 2MB가 넘는 파일을 놓아 보세요.
Images or PDF only, up to 3 files
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는 기본 테두리와 여백이 있는 드롭존입니다. 모양을 처음부터 직접 만들려면 rx.upload.root를 씁니다. 속성과 이벤트는 같습니다. drag_active_style로 파일을 끌고 들어온 동안의 배경을 바꿀 수 있습니다.
Click to upload
or drag and drop
SVG, PNG, JPG or GIF (MAX. 5MB)
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이 됩니다.
Drag and drop files here or click to select files
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)
Drop or click to attach
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 핸들러.
새 교시·새 강의 소식 받기
이 강의에 새 교시가 더해지거나 이어지는 강의가 열리면 이메일로 알려 드려요.
R
ReflexKR
눌러 보며 배우는 파이썬과 Reflex. 모든 예제는 Reflex 0.10.0에서 직접 실행해 확인했습니다.
ReflexKR은 배너 광고 없이 무료로 운영돼요. 도움이 되셨다면 커피 한 잔으로 응원해 주세요.
© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
질문과 답변
이 교시에서 궁금한 점을 남겨 주세요. 모든 언어 페이지의 질문이 함께 모입니다.