목차
메타 설명
HTML, CSS, JavaScript를 몰라도 Python으로 웹앱을 만들 수 있을까요? Streamlit을 이용해 버튼, 입력창, 데이터프레임, 차트, 파일 업로드, 세션 상태, 캐시, 멀티페이지, 채팅 UI, 머신러닝 모델 연결, Community Cloud 배포까지 실습 중심으로 알아봅니다.
지난 시간에는 MediaPipe와 OpenCV를 이용해 웹캠 속 손, 얼굴, 자세를 실시간으로 추적했습니다.
코드 실행 결과는 멋졌습니다.
손가락 개수:
5개
스쿼트 횟수:
12회
왼쪽 무릎 각도:
96도
눈 깜빡임:
감지그런데 이 프로그램을 다른 사람에게 보여주려면 어떻게 해야 할까요?
Python을 설치하세요.
가상환경을 만드세요.
필요한 패키지를 설치하세요.
모델 파일을 models 폴더에 넣으세요.
터미널에서 명령어를 실행하세요.
카메라 권한도 확인하세요.사용자는 프로그램을 실행하기도 전에 설치 설명서와 먼저 대결해야 합니다.
개발자:
“사용법은 간단합니다.”
사용자:
“벌써 가상환경에서 길을 잃었습니다.”우리가 만든 기능을 브라우저에서 버튼 하나로 사용할 수 있다면 훨씬 편리할 것입니다.
웹페이지 접속
→ 파일 업로드
→ 옵션 선택
→ 분석 버튼 클릭
→ 결과 확인하지만 일반적인 웹 개발에는 여러 기술이 필요합니다.
화면:
HTML
디자인:
CSS
동작:
JavaScript
서버:
Python, Java, Node.js 등
데이터 통신:
HTTP APIPython을 배우러 왔는데 갑자기 다섯 개 기술이 회의실로 들어옵니다.
이때 등장하는 도구가 Streamlit입니다.
Streamlit을 사용하면 일반적인 Python 스크립트에 몇 줄의 명령을 추가하는 것만으로 데이터 분석 결과, 차트, 입력 위젯, 머신러닝 모델을 웹앱으로 만들 수 있습니다.
import streamlit as st
st.title("나의 첫 번째 웹앱")
name = st.text_input(
"이름을 입력하세요"
)
if st.button("인사하기"):
st.success(
f"{name}님, 반갑습니다!"
)이 코드를 실행하면 브라우저에 제목, 입력창, 버튼, 메시지가 나타납니다.
HTML도 없습니다.
JavaScript도 없습니다.
CSS와 씨름하다 책상을 뒤집을 일도 아직 없습니다. 🎈
이제 Python 코드에 웹 인터페이스라는 멋진 옷을 입혀 보겠습니다.
1. Streamlit이란?
Streamlit은 Python 스크립트를 대화형 웹 애플리케이션으로 만들 수 있는 오픈소스 프레임워크입니다.
데이터 과학자, 데이터 분석가, AI·머신러닝 개발자가 분석 결과와 모델을 웹 인터페이스로 빠르게 제공할 수 있도록 설계되었습니다. 공식 소개에서도 Streamlit을 몇 줄의 Python 코드로 대화형 데이터 앱을 만들 수 있는 오픈소스 프레임워크로 설명합니다.
Streamlit은 다음 작업에 특히 잘 어울립니다.
데이터 분석 대시보드
머신러닝 예측 앱
AI 채팅 애플리케이션
CSV·Excel 분석 도구
이미지 분류 앱
텍스트 요약 도구
사내 업무 도구
데이터 입력·검수 화면
모델 데모 페이지
프로토타입2026년 7월 30일 기준 공식 최신 안정 버전은 Streamlit 1.60.0이며, 2026년 7월 21일 공개되었습니다.
버전 확인:
import streamlit as st
print(
st.__version__
)터미널에서도 확인할 수 있습니다.
python -m pip show streamlit2. Streamlit으로 만들 수 있는 것
데이터 대시보드
매출 현황
사용자 통계
서버 모니터링
프로젝트 진행률
재고 현황
실험 결과머신러닝 예측 앱
고객 이탈 예측
주택 가격 예측
꽃 품종 분류
이상 거래 탐지
이미지 분류
감정 분석파일 분석 도구
CSV 파일 업로드
열 선택
조건 필터링
통계 계산
차트 생성
결과 다운로드생성형 AI 앱
AI 챗봇
문서 요약
질의응답
RAG 검색
이미지 설명
코드 분석컴퓨터 비전 앱
카메라 사진 입력
이미지 업로드
객체 검출
얼굴 랜드마크 표시
운동 자세 분석
OCR 문서 인식Streamlit은 완성된 대규모 쇼핑몰이나 복잡한 기업용 서비스의 모든 요구를 자동으로 해결하는 도구는 아닙니다.
하지만 아이디어를 빠르게 화면으로 보여주고 데이터·AI 기능을 직접 체험하게 만드는 데 강력합니다.
Python 함수
→ Streamlit 위젯 연결
→ 브라우저 앱 완성3. 일반 웹 프레임워크와 차이
일반적인 웹 개발
브라우저 화면 개발
→ 서버 API 개발
→ 화면과 API 연결
→ 상태 관리
→ 배포 설정FastAPI나 Flask는 주로 서버와 API 개발에 사용합니다.
React나 Vue는 브라우저 화면 개발에 사용합니다.
Streamlit
Python 코드 작성
→ streamlit run
→ 웹앱 실행Streamlit은 Python 개발자가 데이터와 AI 기능을 빠르게 웹 인터페이스로 제공하는 데 초점을 둡니다.
비교
| 구분 | Streamlit | FastAPI | React |
|---|---|---|---|
| 주 언어 | Python | Python | JavaScript·TypeScript |
| 주요 목적 | 데이터·AI 웹앱 | API 서버 | 웹 화면 |
| 화면 개발 | 매우 간편 | 별도 화면 필요 | 세밀한 화면 제어 |
| 프로토타입 | 매우 빠름 | API 중심 | 초기 구성이 필요 |
| 복잡한 UI | 제한적 | 프런트엔드 필요 | 강력함 |
| 머신러닝 연결 | 간편함 | API 설계 필요 | API 호출 필요 |
언제 Streamlit을 선택할까?
Python 기능을 빠르게 웹으로 보여주고 싶다.
내부 업무 도구를 만들고 싶다.
머신러닝 모델 데모가 필요하다.
데이터 분석 결과를 공유하고 싶다.
웹 개발 경험이 많지 않다.다른 도구를 검토할 상황
복잡한 사용자 권한 체계
매우 정교한 UI·애니메이션
대규모 전자상거래 서비스
복잡한 실시간 협업
세밀한 REST API 설계
모바일 앱 전용 화면Streamlit은 모든 웹 개발을 대체하는 만능 건축 로봇이 아닙니다.
대신 데이터와 AI 기능을 빠르게 무대에 올리는 이동식 공연장에 가깝습니다. 🎪
4. Streamlit 설치하기
가상환경을 만든 뒤 설치하는 것이 좋습니다.
Windows
python -m venv venv
venv\Scripts\activate
python -m pip install --upgrade pip
python -m pip install streamlitmacOS 또는 Linux
python3 -m venv venv
source venv/bin/activate
python -m pip install --upgrade pip
python -m pip install streamlit공식 설치 문서도 `venv`와 `pip`를 이용해 환경을 구성하고 Streamlit을 설치하는 방법을 안내합니다.
설치 확인:
streamlit hello또는:
python -m streamlit hello예제 앱이 브라우저에서 열리면 설치가 완료된 것입니다.
데이터 분석 패키지 함께 설치하기
python -m pip install \
streamlit \
pandas \
numpy \
matplotlib \
scikit-learnWindows 명령 프롬프트에서는 한 줄로 입력해도 됩니다.
python -m pip install streamlit pandas numpy matplotlib scikit-learn5. 첫 번째 앱 만들기
프로젝트 폴더를 만듭니다.
first_streamlit_app/
└─ app.py`app.py`:
import streamlit as st
st.title(
"🎈 나의 첫 번째 Streamlit 앱"
)
st.write(
"Python 코드가 웹페이지가 되었습니다!"
)
name = st.text_input(
"이름을 입력하세요"
)
if st.button(
"인사하기"
):
if name.strip():
st.success(
f"{name}님, 반갑습니다!"
)
else:
st.warning(
"이름을 먼저 입력해 주세요."
)터미널에서 프로젝트 폴더로 이동합니다.
cd first_streamlit_app앱을 실행합니다.
streamlit run app.py더 안정적으로 현재 Python 환경을 명시하려면 다음 명령을 사용할 수 있습니다.
python -m streamlit run app.pyStreamlit은 로컬 서버를 시작하고 기본 웹 브라우저에서 앱을 엽니다. 공식 문서의 기본 실행 방식도 `streamlit run 스크립트.py`입니다.
기본 주소는 일반적으로 다음과 같습니다.
http://localhost:8501터미널에는 다음과 비슷한 내용이 출력됩니다.
Local URL:
http://localhost:8501
Network URL:
http://192.168.x.x:85016. Streamlit 앱 실행하기
기본 실행
streamlit run app.py모듈 방식
python -m streamlit run app.py포트 변경
streamlit run app.py \
--server.port 9000브라우저 자동 실행 끄기
streamlit run app.py \
--server.headless true네트워크에서 접근 허용
streamlit run app.py \
--server.address 0.0.0.0방화벽과 클라우드 보안 정책도 함께 설정해야 합니다.
Streamlit 서버가 8501 포트에서 실행 중
하지만
방화벽에서 8501 차단
→ 외부 접속 불가서버가 문을 열었어도 건물 정문이 잠겨 있으면 손님은 들어오지 못합니다.
7. Streamlit의 재실행 구조
Streamlit을 이해할 때 가장 중요한 개념입니다.
사용자가 위젯을 조작하면 Streamlit은 일반적으로 Python 스크립트를 위에서 아래까지 다시 실행합니다. 각 브라우저 탭은 하나의 사용자 세션으로 취급되며, 일반 Python 변수는 재실행 사이에 자동으로 유지되지 않습니다.
다음 코드를 보겠습니다.
import streamlit as st
st.write(
"코드 시작"
)
name = st.text_input(
"이름"
)
st.write(
f"현재 이름: {name}"
)
st.write(
"코드 종료"
)사용자가 입력창을 변경하면 다음 과정이 발생합니다.
사용자 입력
→ 앱 스크립트 다시 실행
→ text_input이 현재 값 반환
→ 아래 코드 다시 실행
→ 화면 업데이트일반 Python 변수의 문제
count = 0
if st.button(
"증가"
):
count += 1
st.write(
count
)버튼을 클릭할 때마다 스크립트가 처음부터 실행됩니다.
count = 0으로 다시 생성
→ 버튼 클릭 결과로 1
→ 다음 재실행에서 다시 0값을 재실행 사이에 유지하려면 `st.session_state`를 사용해야 합니다.
if "count" not in st.session_state:
st.session_state.count = 0
if st.button(
"증가"
):
st.session_state.count += 1
st.write(
st.session_state.count
)Streamlit을 처음 만난 개발자는 보통 이렇게 생각합니다.
“방금 저장한 변수가 어디 갔지?”변수가 사라진 것이 아니라 다음 공연을 위해 무대가 다시 설치된 것입니다.
8. 페이지 기본 설정
`st.set_page_config()`로 브라우저 탭과 페이지 레이아웃을 설정합니다.
import streamlit as st
st.set_page_config(
page_title="AI 데이터 분석소",
page_icon="🤖",
layout="wide",
initial_sidebar_state="expanded"
)일반적으로 앱 스크립트의 앞부분에서 호출합니다.
page_title
브라우저 탭 제목입니다.
page_title="AI 데이터 분석소"page_icon
브라우저 탭 아이콘입니다.
page_icon="🤖"layout
layout="centered"콘텐츠를 가운데 좁은 영역에 표시합니다.
layout="wide"대시보드처럼 넓게 사용합니다.
사이드바 초기 상태
initial_sidebar_state="expanded"또는:
initial_sidebar_state="collapsed"9. 텍스트 출력하기
제목
st.title(
"데이터 분석 대시보드"
)헤더
st.header(
"매출 분석"
)서브헤더
st.subheader(
"월별 매출"
)일반 텍스트
st.text(
"서식이 적용되지 않는 텍스트입니다."
)다양한 객체 출력
st.write(
"문자열",
123,
{
"status": "success"
}
)`st.write()`는 문자열, 숫자, 딕셔너리, 데이터프레임 등 다양한 객체를 자동으로 표시합니다.
Markdown
st.markdown(
"""
### 분석 결과
- 총 데이터: **1,200건**
- 평균 매출: **₩82,500**
- 상태: :green[정상]
"""
)코드 출력
st.code(
"""
import pandas as pd
df = pd.read_csv("sales.csv")
""",
language="python"
)수식 출력
st.latex(
r"""
\hat{y} = wx + b
"""
)10. 메시지와 상태 표시하기
성공
st.success(
"분석이 완료되었습니다."
)정보
st.info(
"CSV 파일을 업로드해 주세요."
)경고
st.warning(
"결측값이 발견되었습니다."
)오류
st.error(
"모델 파일을 불러오지 못했습니다."
)예외 출력
try:
result = 10 / 0
except Exception as error:
st.exception(
error
)풍선 효과
st.balloons()눈 효과
st.snow()분석이 성공할 때마다 풍선을 실행하면 업무용 대시보드가 갑자기 생일 파티장이 될 수 있습니다.
기쁨도 상황에 맞게 배치해야 합니다. 🎈
11. 입력 위젯 사용하기
Streamlit은 버튼, 슬라이더, 선택창, 텍스트 입력, 파일 업로드 등 다양한 위젯을 제공합니다.
텍스트 입력
name = st.text_input(
"이름"
)여러 줄 입력
content = st.text_area(
"분석할 문장을 입력하세요",
height=150
)숫자 입력
age = st.number_input(
"나이",
min_value=0,
max_value=120,
value=30,
step=1
)슬라이더
score = st.slider(
"점수",
min_value=0,
max_value=100,
value=70
)선택창
category = st.selectbox(
"상품 분류",
[
"노트북",
"모니터",
"키보드"
]
)여러 항목 선택
regions = st.multiselect(
"분석 지역",
[
"서울",
"경기",
"부산",
"대전"
],
default=[
"서울"
]
)라디오 버튼
model_name = st.radio(
"모델 선택",
[
"선형회귀",
"랜덤 포레스트",
"신경망"
]
)체크박스
show_raw_data = st.checkbox(
"원본 데이터 표시"
)토글
dark_chart = st.toggle(
"어두운 차트"
)날짜 입력
selected_date = st.date_input(
"기준일"
)시간 입력
selected_time = st.time_input(
"실행 시간"
)색상 선택
color = st.color_picker(
"차트 색상",
"#FF4B4B"
)12. 버튼의 동작 이해하기
clicked = st.button(
"분석하기"
)
if clicked:
st.write(
"버튼이 클릭되었습니다."
)버튼은 클릭된 재실행에서 `True`를 반환합니다.
다음 재실행에서는 다시 `False`가 됩니다.
버튼 클릭 직후:
True
다른 위젯 조작:
False따라서 버튼 안에서 생성한 결과를 계속 유지해야 한다면 Session State를 사용합니다.
잘못 이해하기 쉬운 코드
if st.button(
"결과 생성"
):
result = "완료"
st.write(
result
)버튼을 클릭하지 않은 첫 실행에서는 `result`가 존재하지 않습니다.
수정:
if "result" not in st.session_state:
st.session_state.result = None
if st.button(
"결과 생성"
):
st.session_state.result = "완료"
if st.session_state.result:
st.write(
st.session_state.result
)13. 사이드바 활용하기
대시보드의 필터나 설정은 사이드바에 배치하면 편리합니다.
st.sidebar.title(
"분석 설정"
)
year = st.sidebar.selectbox(
"연도",
[
2024,
2025,
2026
],
index=2
)
minimum_sales = st.sidebar.slider(
"최소 매출",
min_value=0,
max_value=1_000_000,
value=100_000,
step=10_000
)
show_details = st.sidebar.checkbox(
"상세 데이터 표시"
)`with` 문도 사용할 수 있습니다.
with st.sidebar:
st.header(
"필터"
)
region = st.selectbox(
"지역",
[
"전체",
"서울",
"부산"
]
)
date_range = st.date_input(
"조회 기간",
value=[]
)사이드바는 조종석입니다.
메인 화면에 수십 개의 슬라이더를 펼쳐 놓으면 대시보드가 조종석이 아니라 전선 창고가 됩니다.
14. 열·탭·컨테이너 구성하기
Streamlit은 기본적으로 코드 작성 순서대로 요소를 위에서 아래로 배치합니다. 열, 탭, 컨테이너 등을 사용하면 화면을 구조화할 수 있습니다.
열
left_column, right_column = (
st.columns(2)
)
with left_column:
st.metric(
"매출",
"₩12,500,000",
"8.2%"
)
with right_column:
st.metric(
"주문",
"1,240건",
"5.1%"
)열 너비를 다르게 설정할 수 있습니다.
main_column, side_column = (
st.columns(
[
3,
1
]
)
)탭
chart_tab, data_tab = st.tabs(
[
"📈 차트",
"📋 데이터"
]
)
with chart_tab:
st.line_chart(
chart_data
)
with data_tab:
st.dataframe(
dataframe
)컨테이너
with st.container(
border=True
):
st.subheader(
"분석 조건"
)
st.write(
"이 영역은 하나의 컨테이너입니다."
)접기 영역
with st.expander(
"상세 설명 보기"
):
st.write(
"모델과 데이터에 대한 상세 설명입니다."
)15. 데이터프레임 출력하기
import pandas as pd
import streamlit as st
dataframe = pd.DataFrame({
"상품": [
"노트북",
"모니터",
"키보드"
],
"수량": [
10,
25,
40
],
"매출": [
15_000_000,
8_500_000,
3_200_000
]
})
st.dataframe(
dataframe,
width="stretch",
hide_index=True
)`st.dataframe()`은 정렬과 탐색이 가능한 대화형 표를 출력합니다. 현재 API는 다양한 데이터 객체를 DataFrame 또는 Arrow 형태로 변환해 표시하고, 열 표시 방법을 설정하는 Column Configuration API를 제공합니다.
정적 표
st.table(
dataframe
)`st.table()`은 작은 표를 정적으로 표시할 때 적합합니다.
대부분의 대화형 데이터 표시에는 `st.dataframe()`이 권장됩니다.
열 형식 설정
st.dataframe(
dataframe,
width="stretch",
hide_index=True,
column_config={
"매출": st.column_config.NumberColumn(
"매출액",
help="상품별 총매출",
format="₩%d"
),
"수량": st.column_config.ProgressColumn(
"판매 수량",
min_value=0,
max_value=50
)
}
)16. 편집 가능한 데이터 만들기
`st.data_editor()`를 사용하면 사용자가 웹 화면에서 데이터 값을 수정할 수 있습니다.
edited_dataframe = st.data_editor(
dataframe,
width="stretch",
hide_index=True,
num_rows="dynamic"
)`num_rows="dynamic"`을 사용하면 행을 추가하거나 삭제할 수 있습니다. `st.data_editor()`의 열 표시와 편집 방법도 Column Configuration API로 설정할 수 있습니다.
선택형 열
edited_dataframe = st.data_editor(
dataframe,
width="stretch",
hide_index=True,
num_rows="dynamic",
column_config={
"상태": (
st.column_config
.SelectboxColumn(
"진행 상태",
options=[
"대기",
"진행",
"완료"
],
required=True
)
)
}
)편집 결과 사용
if st.button(
"편집 내용 확인"
):
st.dataframe(
edited_dataframe
)주의할 점은 편집 결과가 자동으로 데이터베이스에 저장되는 것은 아니라는 것입니다.
화면에서 수정
→ Python 변수에 결과 반환
→ 개발자가 DB 또는 파일 저장 로직 구현편집기가 펜은 제공하지만 서류 보관실까지 운영해 주지는 않습니다.
17. 지표 카드 만들기
`st.metric()`은 핵심 수치를 크게 표시합니다.
st.metric(
label="오늘 매출",
value="₩8,250,000",
delta="12.4%",
border=True
)여러 지표를 나란히 표시합니다.
column1, column2, column3 = (
st.columns(3)
)
column1.metric(
"사용자",
"12,430명",
"5.2%",
border=True
)
column2.metric(
"전환율",
"8.4%",
"-0.7%",
border=True
)
column3.metric(
"평균 주문액",
"₩74,000",
"₩3,500",
border=True
)현재 `st.metric()`은 값과 변화량 외에도 테두리와 간단한 차트 데이터를 표시할 수 있는 옵션을 제공합니다.
18. 차트 출력하기
선 차트
st.line_chart(
dataframe[
[
"매출"
]
]
)막대 차트
st.bar_chart(
dataframe,
x="상품",
y="매출"
)영역 차트
st.area_chart(
dataframe,
x="상품",
y="매출"
)산점도
st.scatter_chart(
dataframe,
x="수량",
y="매출"
)Streamlit 기본 차트는 짧은 코드로 빠르게 시각화할 때 유용합니다.
복잡한 스타일과 상호작용이 필요하면 다음 라이브러리를 연결할 수 있습니다.
Matplotlib
Plotly
Altair
PyDeck
Bokeh19. Matplotlib 차트 연결하기
import matplotlib.pyplot as plt
import pandas as pd
import streamlit as st
dataframe = pd.DataFrame({
"월": [
"1월",
"2월",
"3월",
"4월"
],
"매출": [
120,
180,
160,
230
]
})
figure, axis = plt.subplots()
axis.plot(
dataframe["월"],
dataframe["매출"],
marker="o"
)
axis.set_title(
"월별 매출"
)
axis.set_xlabel(
"월"
)
axis.set_ylabel(
"매출"
)
axis.grid(
alpha=0.3
)
st.pyplot(
figure
)Matplotlib 객체를 여러 사용자와 공유하지 않도록 함수 안에서 새 Figure를 생성하는 편이 안전합니다.
def create_chart(
dataframe: pd.DataFrame
):
figure, axis = plt.subplots()
axis.plot(
dataframe["월"],
dataframe["매출"]
)
return figure20. 이미지·오디오·비디오 출력하기
Streamlit은 이미지, 오디오, 비디오, PDF 등 다양한 미디어 요소를 표시할 수 있습니다.
이미지
st.image(
"sample.jpg",
caption="분석 대상 이미지"
)OpenCV 이미지:
import cv2
image = cv2.imread(
"sample.jpg"
)
rgb_image = cv2.cvtColor(
image,
cv2.COLOR_BGR2RGB
)
st.image(
rgb_image,
caption="OpenCV 이미지"
)오디오
st.audio(
"sample.mp3"
)비디오
st.video(
"sample.mp4"
)카메라 입력
camera_image = st.camera_input(
"사진을 촬영하세요"
)
if camera_image is not None:
st.image(
camera_image
)`st.camera_input()`은 웹브라우저의 카메라를 이용해 사진을 입력받습니다.
실시간 프레임 분석이 아니라 사용자가 촬영한 이미지 한 장을 전달하는 방식입니다.
21. 파일 업로드
uploaded_file = st.file_uploader(
"CSV 파일을 업로드하세요",
type=[
"csv"
]
)파일이 업로드되면 다음과 같이 읽을 수 있습니다.
import pandas as pd
if uploaded_file is not None:
dataframe = pd.read_csv(
uploaded_file
)
st.dataframe(
dataframe,
width="stretch"
)`UploadedFile` 객체는 파일처럼 읽을 수 있는 `BytesIO` 계열 객체입니다. 기본적으로 업로드 파일 한 개의 최대 크기는 200MB이며, 서버 설정이나 위젯 옵션으로 제한을 변경할 수 있습니다.
여러 파일 업로드
uploaded_files = st.file_uploader(
"파일을 선택하세요",
type=[
"csv"
],
accept_multiple_files=True
)
for uploaded_file in uploaded_files:
st.write(
uploaded_file.name
)파일 바이트 읽기
file_bytes = (
uploaded_file.getvalue()
)CSV 인코딩 처리
import io
import pandas as pd
def read_csv_file(
file_bytes: bytes
) -> pd.DataFrame:
encodings = [
"utf-8",
"utf-8-sig",
"cp949"
]
last_error = None
for encoding in encodings:
try:
return pd.read_csv(
io.BytesIO(
file_bytes
),
encoding=encoding
)
except UnicodeDecodeError as error:
last_error = error
raise ValueError(
"지원하는 문자 인코딩으로 "
"CSV 파일을 읽지 못했습니다."
) from last_error22. 파일 다운로드
csv_data = dataframe.to_csv(
index=False
).encode(
"utf-8-sig"
)
st.download_button(
label="분석 결과 다운로드",
data=csv_data,
file_name="analysis_result.csv",
mime="text/csv"
)`st.download_button()`은 사용자가 데이터를 직접 내려받을 수 있는 버튼을 생성합니다. 직접 전달한 데이터는 사용자 연결 중 메모리에 보관되므로 큰 파일은 메모리 사용량을 고려해야 합니다.
JSON 다운로드
import json
json_data = json.dumps(
result,
ensure_ascii=False,
indent=2
)
st.download_button(
"JSON 다운로드",
data=json_data,
file_name="result.json",
mime="application/json"
)23. Form으로 입력 한 번에 처리하기
Streamlit은 일반적으로 위젯 값이 변경될 때마다 앱을 다시 실행합니다.
입력 항목이 많다면 사용자가 값을 모두 입력한 뒤 한 번에 제출하도록 Form을 사용할 수 있습니다. Form 내부의 위젯 값은 제출 버튼을 눌렀을 때 일괄 전달됩니다.
with st.form(
"customer_form"
):
name = st.text_input(
"고객 이름"
)
age = st.number_input(
"나이",
min_value=0,
max_value=120,
value=30
)
contract = st.selectbox(
"계약 유형",
[
"월간",
"1년",
"2년"
]
)
submitted = (
st.form_submit_button(
"분석하기"
)
)
if submitted:
st.success(
f"{name} 고객 분석을 시작합니다."
)Form이 없다면:
이름 입력
→ 재실행
나이 변경
→ 재실행
계약 선택
→ 재실행Form을 사용하면:
이름 입력
나이 변경
계약 선택
분석하기 클릭
→ 한 번에 재실행위젯들이 단체 버스를 타고 출발하는 셈입니다. 🚌
24. Session State 이해하기
Session State는 각 사용자 세션에서 재실행 사이에 값을 유지하는 기능입니다.
또한 멀티페이지 앱의 페이지 사이에서도 상태를 공유할 수 있습니다.
값 초기화
if "count" not in st.session_state:
st.session_state.count = 0값 변경
if st.button(
"증가"
):
st.session_state.count += 1값 출력
st.write(
f"현재 값: "
f"{st.session_state.count}"
)초기화 버튼
if st.button(
"초기화"
):
st.session_state.count = 0딕셔너리 방식
st.session_state[
"username"
] = "Python Explorer"속성 방식
st.session_state.username = (
"Python Explorer"
)전체 상태 확인
st.write(
st.session_state
)사용자별 상태
브라우저 탭마다 별도의 세션이 생성됩니다.
사용자 A의 count:
5
사용자 B의 count:
1Session State는 사용자별 장바구니와 비슷합니다.
단, 웹소켓 연결이 끊기거나 세션이 종료되면 영구 저장되지 않을 수 있습니다.
중요한 정보는 데이터베이스에 저장해야 합니다.
25. Callback 활용하기
버튼 클릭이나 입력 변경 시 함수를 실행할 수 있습니다.
def increase_count() -> None:
st.session_state.count += 1
if "count" not in st.session_state:
st.session_state.count = 0
st.button(
"증가",
on_click=increase_count
)
st.write(
st.session_state.count
)인수 전달:
def change_count(
amount: int
) -> None:
st.session_state.count += amount
st.button(
"+10",
on_click=change_count,
args=[
10
]
)입력 변경 Callback:
def name_changed() -> None:
st.session_state.message = (
"이름이 변경되었습니다."
)
st.text_input(
"이름",
key="name",
on_change=name_changed
)버튼과 파일 업로더의 상태값을 Session State API로 직접 설정하는 데에는 제한이 있습니다.
26. 캐시가 필요한 이유
Streamlit 앱은 위젯 조작 때마다 다시 실행됩니다.
다음 코드가 있다면 어떻게 될까요?
dataframe = pd.read_csv(
"large_data.csv"
)슬라이더를 한 번 움직일 때마다 CSV 파일을 다시 읽을 수 있습니다.
슬라이더 이동
→ 스크립트 재실행
→ 500MB CSV 다시 읽기
→ 사용자는 로딩 화면 감상머신러닝 모델도 마찬가지입니다.
model = load_large_model()버튼을 클릭할 때마다 대형 모델을 다시 메모리에 올리면 앱이 매우 느려집니다.
이 문제를 해결하기 위해 Streamlit은 두 가지 주요 캐시를 제공합니다.
st.cache_data
→ 데이터와 계산 결과
st.cache_resource
→ 모델과 DB 연결 같은 공유 자원공식 문서도 직렬화 가능한 데이터 결과에는 `st.cache_data`, 머신러닝 모델이나 데이터베이스 연결 같은 공유 자원에는 `st.cache_resource`를 권장합니다.
27. st.cache_data
CSV 로딩이나 데이터 변환 결과를 캐시합니다.
import pandas as pd
import streamlit as st
@st.cache_data
def load_data(
file_path: str
) -> pd.DataFrame:
return pd.read_csv(
file_path
)
dataframe = load_data(
"sales.csv"
)같은 인수로 함수를 다시 호출하면 저장된 결과를 재사용합니다.
업로드 파일 캐시
import io
import pandas as pd
import streamlit as st
@st.cache_data
def load_uploaded_csv(
file_bytes: bytes
) -> pd.DataFrame:
return pd.read_csv(
io.BytesIO(
file_bytes
)
)
uploaded_file = st.file_uploader(
"CSV 파일",
type=[
"csv"
]
)
if uploaded_file is not None:
dataframe = load_uploaded_csv(
uploaded_file.getvalue()
)
st.dataframe(
dataframe
)유효기간 설정
@st.cache_data(
ttl=3600
)
def load_api_data():
...3,600초 후 캐시를 무효화합니다.
데이터베이스나 API 결과처럼 시간이 지나면 바뀌는 데이터에는 `ttl`을 검토해야 합니다. 공식 캐시 문서도 새 결과가 필요한 데이터베이스 조회 등에 TTL 사용을 안내합니다.
캐시 초기화
if st.button(
"데이터 캐시 삭제"
):
st.cache_data.clear()`st.cache_data`는 반환값을 직렬화해 저장하므로 신뢰할 수 없는 데이터를 역직렬화하지 않도록 주의해야 합니다.
28. st.cache_resource
머신러닝 모델이나 데이터베이스 연결처럼 한 번 생성한 뒤 재사용할 자원을 캐시합니다.
import joblib
import streamlit as st
@st.cache_resource
def load_model():
return joblib.load(
"model.joblib"
)
model = load_model()PyTorch 모델:
import streamlit as st
import torch
@st.cache_resource
def load_pytorch_model():
model = MyModel()
state_dict = torch.load(
"model.pt",
map_location="cpu",
weights_only=True
)
model.load_state_dict(
state_dict
)
model.eval()
return model데이터베이스 연결:
@st.cache_resource
def create_connection():
return database.connect()cache_data와 비교
| 구분 | `st.cache_data` | `st.cache_resource` |
|---|---|---|
| 용도 | 데이터와 계산 결과 | 모델·DB 연결 |
| 반환값 복사 | 호출별 복사본 | 동일 객체 공유 |
| 직렬화 | 필요 | 필수 아님 |
| 대표 예 | DataFrame | ML 모델 |
| 객체 변경 | 비교적 안전 | 공유 객체 변경 주의 |
`st.cache_resource`는 하나의 객체를 여러 세션과 재실행에서 공유할 수 있으므로 반환 객체는 스레드 안전성을 고려해야 합니다. 객체를 변경하면 공유된 캐시 객체 자체가 변경될 수 있습니다.
cache_data:
복사한 문서를 각자 사용
cache_resource:
회의실의 공용 화이트보드 사용공용 화이트보드에 누군가 낙서하면 모두가 보게 됩니다.
29. 로딩 상태와 진행률
Spinner
import time
with st.spinner(
"데이터를 분석하고 있습니다..."
):
time.sleep(2)
st.success(
"분석 완료"
)상태 컨테이너
import time
with st.status(
"분석 작업 진행 중",
expanded=True
) as status:
st.write(
"데이터를 불러오는 중..."
)
time.sleep(1)
st.write(
"결측값을 처리하는 중..."
)
time.sleep(1)
st.write(
"모델을 실행하는 중..."
)
time.sleep(1)
status.update(
label="분석 완료",
state="complete"
)Progress
import time
progress_bar = st.progress(
0
)
status_text = st.empty()
for progress in range(
101
):
progress_bar.progress(
progress
)
status_text.text(
f"진행률: {progress}%"
)
time.sleep(
0.01
)
status_text.success(
"완료되었습니다."
)Placeholder
placeholder = st.empty()
placeholder.info(
"처리 준비"
)
placeholder.warning(
"처리 중"
)
placeholder.success(
"처리 완료"
)30. 오류 처리
사용자 입력과 파일은 항상 정상이라고 가정하면 안 됩니다.
uploaded_file = st.file_uploader(
"CSV 파일",
type=[
"csv"
]
)
if uploaded_file is not None:
try:
dataframe = pd.read_csv(
uploaded_file
)
except UnicodeDecodeError:
st.error(
"CSV 문자 인코딩을 "
"확인해 주세요."
)
except pd.errors.EmptyDataError:
st.error(
"파일에 데이터가 없습니다."
)
except Exception as error:
st.exception(
error
)실행을 중단할 수 있습니다.
if uploaded_file is None:
st.info(
"파일을 업로드해 주세요."
)
st.stop()`st.stop()` 아래 코드는 현재 실행에서 진행되지 않습니다.
입력 검증
if age < 18:
st.warning(
"18세 이상만 분석할 수 있습니다."
)
st.stop()31. 채팅 인터페이스 만들기
Streamlit은 `st.chat_message()`와 `st.chat_input()`을 이용해 채팅 형태의 앱을 만들 수 있습니다. 채팅 메시지 컨테이너 안에는 텍스트뿐 아니라 표와 차트도 표시할 수 있습니다.
메아리 챗봇
import streamlit as st
st.title(
"💬 메아리 챗봇"
)
if "messages" not in st.session_state:
st.session_state.messages = []
for message in st.session_state.messages:
with st.chat_message(
message["role"]
):
st.markdown(
message["content"]
)
prompt = st.chat_input(
"메시지를 입력하세요"
)
if prompt:
st.session_state.messages.append({
"role": "user",
"content": prompt
})
with st.chat_message(
"user"
):
st.markdown(
prompt
)
response = (
f"입력하신 메시지: {prompt}"
)
st.session_state.messages.append({
"role": "assistant",
"content": response
})
with st.chat_message(
"assistant"
):
st.markdown(
response
)현재 코드는 진짜 AI 모델을 호출하지 않습니다.
사용자 입력을 그대로 돌려주는 메아리 챗봇입니다.
사용자:
“오늘 날씨 어때?”
챗봇:
“입력하신 메시지: 오늘 날씨 어때?”인공지능이라기보다 매우 성실한 복사 담당자입니다.
LLM API를 연결하면 실제 AI 채팅 앱으로 확장할 수 있습니다.
32. 멀티페이지 앱 만들기
앱의 기능이 많아지면 페이지를 나눌 수 있습니다.
현재 공식 문서는 `st.Page`와 `st.navigation`을 멀티페이지 앱 구성의 권장 방식으로 안내합니다. 진입점 파일이 페이지 라우터 역할을 하며, 선택된 페이지에 `.run()`을 호출해 실행합니다.
폴더 구조
streamlit_project/
├─ streamlit_app.py
└─ pages/
├─ dashboard.py
├─ prediction.py
└─ settings.py진입점
`streamlit_app.py`:
import streamlit as st
st.set_page_config(
page_title="데이터·AI 센터",
page_icon="🤖",
layout="wide"
)
dashboard_page = st.Page(
"pages/dashboard.py",
title="대시보드",
icon="📊",
default=True
)
prediction_page = st.Page(
"pages/prediction.py",
title="AI 예측",
icon="🤖"
)
settings_page = st.Page(
"pages/settings.py",
title="설정",
icon="⚙️"
)
navigation = st.navigation({
"분석": [
dashboard_page,
prediction_page
],
"관리": [
settings_page
]
})
navigation.run()대시보드 페이지
`pages/dashboard.py`:
import streamlit as st
st.title(
"📊 데이터 대시보드"
)
st.write(
"서비스 현황을 확인합니다."
)예측 페이지
`pages/prediction.py`:
import streamlit as st
st.title(
"🤖 AI 예측"
)
st.write(
"머신러닝 모델을 실행합니다."
)실행:
streamlit run streamlit_app.py진입점에 배치한 위젯은 페이지가 변경되어도 공통 영역으로 유지할 수 있습니다.
33. 설정 파일과 테마
프로젝트에 `.streamlit/config.toml` 파일을 만들 수 있습니다.
streamlit_project/
├─ .streamlit/
│ └─ config.toml
└─ app.py예:
[theme]
primaryColor = "#FF4B4B"
backgroundColor = "#FFFFFF"
secondaryBackgroundColor = "#F4F6F8"
textColor = "#1F2937"
[server]
maxUploadSize = 500`maxUploadSize` 단위는 MB입니다.
기본 파일 업로드 제한을 변경할 수 있지만 서버 메모리와 네트워크 환경도 고려해야 합니다.
설정 확인
streamlit config show명령행에서 설정
streamlit run app.py \
--server.maxUploadSize 500Community Cloud에 사용자 설정을 포함한다면 저장소의 `.streamlit/config.toml` 위치를 올바르게 구성해야 합니다.
34. API 키와 비밀정보 관리
API 키를 코드에 직접 작성하면 안 됩니다.
위험한 코드
API_KEY = "sk-실제비밀키"GitHub에 올리는 순간 비밀이 아니라 공개 전시물이 됩니다.
secrets.toml
다음 파일을 만듭니다.
.streamlit/
└─ secrets.toml내용:
OPENAI_API_KEY = "여기에_API_키"
[database]
host = "localhost"
port = 5432
username = "app_user"
password = "비밀번호"Python에서 읽기:
import streamlit as st
api_key = st.secrets[
"OPENAI_API_KEY"
]
database_host = (
st.secrets["database"]["host"]
)로컬 개발에서는 `.streamlit/secrets.toml`을 사용하고, Community Cloud에서는 배포 설정의 Secrets 영역에 같은 내용을 등록할 수 있습니다.
.gitignore
.streamlit/secrets.toml
.env
*.pem
*.key공식 보안 가이드도 비밀정보를 코드에 직접 저장하지 말고 `.gitignore`를 관리하도록 안내합니다.
35. 실전 프로젝트 준비
이번에는 Streamlit과 scikit-learn을 연결해 붓꽃 품종 분류 AI 웹앱을 만들어 보겠습니다.
프로젝트 기능
붓꽃 데이터 불러오기
Random Forest 모델 학습
학습·평가 데이터 분리
모델 성능 표시
꽃받침·꽃잎 길이 입력
품종 예측
클래스별 확률 표시
예측 기록 저장
CSV 파일 일괄 예측
결과 다운로드
데이터 탐색 차트프로젝트 구조
iris_streamlit_app/
├─ app.py
├─ requirements.txt
└─ .streamlit/
└─ config.tomlrequirements.txt
streamlit
pandas
numpy
scikit-learn
matplotlib실제 배포 프로젝트에서는 로컬에서 검증한 패키지 버전을 고정하는 것이 좋습니다.
36. AI 붓꽃 분류 웹앱
붓꽃 데이터에는 네 가지 특성이 있습니다.
sepal length
→ 꽃받침 길이
sepal width
→ 꽃받침 너비
petal length
→ 꽃잎 길이
petal width
→ 꽃잎 너비예측할 품종:
setosa
versicolor
virginica모델 학습 함수
from sklearn.datasets import (
load_iris
)
from sklearn.ensemble import (
RandomForestClassifier
)
from sklearn.model_selection import (
train_test_split
)
@st.cache_resource
def train_model():
iris = load_iris(
as_frame=True
)
features = iris.data
target = iris.target
(
X_train,
X_test,
y_train,
y_test
) = train_test_split(
features,
target,
test_size=0.2,
random_state=2026,
stratify=target
)
model = RandomForestClassifier(
n_estimators=300,
random_state=2026
)
model.fit(
X_train,
y_train
)
train_accuracy = model.score(
X_train,
y_train
)
test_accuracy = model.score(
X_test,
y_test
)
return {
"model": model,
"data": iris.frame,
"feature_names": (
list(
iris.feature_names
)
),
"target_names": (
list(
iris.target_names
)
),
"train_accuracy": (
train_accuracy
),
"test_accuracy": (
test_accuracy
)
}모델은 `st.cache_resource`로 캐시합니다.
사용자가 슬라이더를 움직일 때마다 숲을 이루는 결정트리 300개를 다시 심을 필요는 없습니다. 🌳
37. 단일 데이터 예측
with st.form(
"prediction_form"
):
sepal_length = st.slider(
"꽃받침 길이",
min_value=4.0,
max_value=8.0,
value=5.8,
step=0.1
)
sepal_width = st.slider(
"꽃받침 너비",
min_value=2.0,
max_value=4.5,
value=3.0,
step=0.1
)
petal_length = st.slider(
"꽃잎 길이",
min_value=1.0,
max_value=7.0,
value=4.3,
step=0.1
)
petal_width = st.slider(
"꽃잎 너비",
min_value=0.1,
max_value=2.6,
value=1.3,
step=0.1
)
submitted = (
st.form_submit_button(
"품종 예측하기",
width="stretch"
)
)입력 DataFrame:
input_dataframe = pd.DataFrame(
[
[
sepal_length,
sepal_width,
petal_length,
petal_width
]
],
columns=feature_names
)예측:
prediction = model.predict(
input_dataframe
)[0]
probabilities = (
model.predict_proba(
input_dataframe
)[0]
)38. 확률 차트 표시
probability_dataframe = pd.DataFrame({
"품종": target_names,
"확률": probabilities
})
st.bar_chart(
probability_dataframe,
x="품종",
y="확률"
)가장 높은 확률:
confidence = float(
probabilities[prediction]
)
st.success(
"예측 품종: "
f"{target_names[prediction]}"
)
st.metric(
"예측 확률",
f"{confidence:.2%}",
border=True
)예측 확률이 높다고 무조건 정답이라는 뜻은 아닙니다.
모델 확률:
현재 학습된 모델의 상대적인 확신
정답 보증서:
아님AI도 자신 있게 틀릴 수 있습니다.
회의실에서 목소리가 가장 큰 사람이 언제나 정답은 아닌 것과 같습니다.
39. CSV 일괄 예측
CSV 파일에는 다음 열이 필요합니다.
sepal length (cm)
sepal width (cm)
petal length (cm)
petal width (cm)템플릿 다운로드
template_dataframe = pd.DataFrame(
columns=feature_names
)
template_csv = (
template_dataframe
.to_csv(
index=False
)
.encode(
"utf-8-sig"
)
)
st.download_button(
"CSV 템플릿 다운로드",
data=template_csv,
file_name="iris_template.csv",
mime="text/csv"
)파일 업로드
uploaded_file = st.file_uploader(
"예측할 CSV 파일",
type=[
"csv"
]
)열 검증
missing_columns = [
column
for column in feature_names
if column not in dataframe.columns
]
if missing_columns:
st.error(
"필수 열이 없습니다: "
+ ", ".join(
missing_columns
)
)
st.stop()일괄 예측
batch_features = dataframe[
feature_names
]
batch_predictions = model.predict(
batch_features
)
batch_probabilities = (
model.predict_proba(
batch_features
)
)
result_dataframe = dataframe.copy()
result_dataframe[
"predicted_class"
] = [
target_names[
prediction
]
for prediction in (
batch_predictions
)
]
result_dataframe[
"confidence"
] = batch_probabilities.max(
axis=1
)40. 예측 기록 관리
Session State에 현재 사용자의 예측 기록을 저장합니다.
if "prediction_history" not in (
st.session_state
):
st.session_state.prediction_history = []예측 완료 후 추가:
st.session_state.prediction_history.append({
"꽃받침 길이": sepal_length,
"꽃받침 너비": sepal_width,
"꽃잎 길이": petal_length,
"꽃잎 너비": petal_width,
"예측 품종": (
target_names[prediction]
),
"예측 확률": confidence
})출력:
history_dataframe = pd.DataFrame(
st.session_state.prediction_history
)
st.dataframe(
history_dataframe,
width="stretch",
hide_index=True
)초기화:
if st.button(
"예측 기록 초기화"
):
st.session_state.prediction_history = []
st.rerun()`st.rerun()`은 즉시 현재 스크립트를 다시 실행합니다.
남용하면 실행 흐름을 이해하기 어려워질 수 있으므로 Callback이나 자연스러운 재실행으로 해결할 수 있는지 먼저 검토합니다.
41. 전체 프로젝트 코드
`app.py` 전체 코드입니다.
from io import BytesIO
import numpy as np
import pandas as pd
import streamlit as st
from sklearn.datasets import load_iris
from sklearn.ensemble import (
RandomForestClassifier
)
from sklearn.model_selection import (
train_test_split
)
st.set_page_config(
page_title="붓꽃 AI 연구소",
page_icon="🌸",
layout="wide",
initial_sidebar_state="expanded"
)
@st.cache_resource
def train_model() -> dict:
"""붓꽃 데이터를 이용해 분류 모델을 학습합니다."""
iris = load_iris(
as_frame=True
)
features = iris.data.copy()
target = iris.target.copy()
(
X_train,
X_test,
y_train,
y_test
) = train_test_split(
features,
target,
test_size=0.2,
random_state=2026,
stratify=target
)
model = RandomForestClassifier(
n_estimators=300,
max_depth=None,
random_state=2026,
n_jobs=-1
)
model.fit(
X_train,
y_train
)
return {
"model": model,
"data": iris.frame.copy(),
"feature_names": (
list(
iris.feature_names
)
),
"target_names": (
list(
iris.target_names
)
),
"train_accuracy": (
model.score(
X_train,
y_train
)
),
"test_accuracy": (
model.score(
X_test,
y_test
)
)
}
@st.cache_data
def read_uploaded_csv(
file_bytes: bytes
) -> pd.DataFrame:
"""업로드된 CSV 파일을 읽습니다."""
encodings = [
"utf-8",
"utf-8-sig",
"cp949"
]
last_error = None
for encoding in encodings:
try:
return pd.read_csv(
BytesIO(
file_bytes
),
encoding=encoding
)
except UnicodeDecodeError as error:
last_error = error
raise ValueError(
"CSV 문자 인코딩을 "
"확인해 주세요."
) from last_error
def create_template_csv(
feature_names: list[str]
) -> bytes:
"""일괄 예측용 CSV 템플릿을 생성합니다."""
template = pd.DataFrame(
[
[
5.1,
3.5,
1.4,
0.2
],
[
6.4,
3.2,
4.5,
1.5
],
[
6.5,
3.0,
5.8,
2.2
]
],
columns=feature_names
)
return template.to_csv(
index=False
).encode(
"utf-8-sig"
)
def add_prediction_history(
input_values: list[float],
feature_names: list[str],
predicted_name: str,
confidence: float
) -> None:
"""현재 세션의 예측 기록을 추가합니다."""
history_item = {
feature_names[index]: value
for index, value in enumerate(
input_values
)
}
history_item.update({
"예측 품종": predicted_name,
"예측 확률": confidence
})
st.session_state[
"prediction_history"
].append(
history_item
)
def render_probability_chart(
target_names: list[str],
probabilities: np.ndarray
) -> None:
"""품종별 예측 확률을 차트로 표시합니다."""
probability_dataframe = pd.DataFrame({
"품종": target_names,
"확률": probabilities
})
st.bar_chart(
probability_dataframe,
x="품종",
y="확률"
)
def main() -> None:
model_bundle = train_model()
model = model_bundle[
"model"
]
data = model_bundle[
"data"
]
feature_names = model_bundle[
"feature_names"
]
target_names = model_bundle[
"target_names"
]
if "prediction_history" not in (
st.session_state
):
st.session_state[
"prediction_history"
] = []
st.title(
"🌸 붓꽃 AI 연구소"
)
st.caption(
"꽃의 측정값을 입력하면 "
"AI가 붓꽃 품종을 예측합니다."
)
with st.sidebar:
st.header(
"앱 설정"
)
show_raw_data = st.toggle(
"원본 데이터 표시",
value=False
)
show_feature_importance = (
st.toggle(
"특성 중요도 표시",
value=True
)
)
st.divider()
st.info(
"이 앱은 scikit-learn의 "
"붓꽃 데이터와 "
"Random Forest 모델을 사용합니다."
)
metric_column1, metric_column2, (
metric_column3
) = st.columns(3)
metric_column1.metric(
"학습 데이터",
f"{len(data)}건",
border=True
)
metric_column2.metric(
"학습 정확도",
(
f"{model_bundle['train_accuracy']:.2%}"
),
border=True
)
metric_column3.metric(
"평가 정확도",
(
f"{model_bundle['test_accuracy']:.2%}"
),
border=True
)
(
prediction_tab,
batch_tab,
data_tab,
history_tab
) = st.tabs([
"🤖 단일 예측",
"📦 CSV 일괄 예측",
"📊 데이터 탐색",
"🕘 예측 기록"
])
with prediction_tab:
st.subheader(
"꽃의 크기를 입력하세요"
)
input_column, result_column = (
st.columns(
[
1,
1
]
)
)
with input_column:
with st.form(
"prediction_form"
):
sepal_length = st.slider(
"꽃받침 길이 (cm)",
min_value=4.0,
max_value=8.0,
value=5.8,
step=0.1
)
sepal_width = st.slider(
"꽃받침 너비 (cm)",
min_value=2.0,
max_value=4.5,
value=3.0,
step=0.1
)
petal_length = st.slider(
"꽃잎 길이 (cm)",
min_value=1.0,
max_value=7.0,
value=4.3,
step=0.1
)
petal_width = st.slider(
"꽃잎 너비 (cm)",
min_value=0.1,
max_value=2.6,
value=1.3,
step=0.1
)
submitted = (
st.form_submit_button(
"품종 예측하기",
width="stretch"
)
)
with result_column:
if submitted:
input_values = [
sepal_length,
sepal_width,
petal_length,
petal_width
]
input_dataframe = pd.DataFrame(
[
input_values
],
columns=feature_names
)
prediction = int(
model.predict(
input_dataframe
)[0]
)
probabilities = (
model.predict_proba(
input_dataframe
)[0]
)
predicted_name = (
target_names[
prediction
]
)
confidence = float(
probabilities[
prediction
]
)
st.success(
"예측 품종: "
f"**{predicted_name}**"
)
st.metric(
"예측 확률",
f"{confidence:.2%}",
border=True
)
render_probability_chart(
target_names,
probabilities
)
add_prediction_history(
input_values,
feature_names,
predicted_name,
confidence
)
if confidence >= 0.9:
st.balloons()
else:
st.info(
"왼쪽에서 값을 입력하고 "
"예측 버튼을 눌러 주세요."
)
with batch_tab:
st.subheader(
"CSV 파일 일괄 예측"
)
st.write(
"필수 열 이름이 포함된 CSV 파일을 "
"업로드하면 여러 데이터를 "
"한 번에 예측합니다."
)
template_csv = create_template_csv(
feature_names
)
st.download_button(
"CSV 템플릿 다운로드",
data=template_csv,
file_name="iris_template.csv",
mime="text/csv"
)
uploaded_file = st.file_uploader(
"예측할 CSV 파일",
type=[
"csv"
],
key="batch_csv"
)
if uploaded_file is not None:
try:
batch_dataframe = (
read_uploaded_csv(
uploaded_file.getvalue()
)
)
missing_columns = [
column
for column in (
feature_names
)
if column not in (
batch_dataframe.columns
)
]
if missing_columns:
st.error(
"필수 열이 없습니다: "
+ ", ".join(
missing_columns
)
)
else:
st.write(
"업로드 데이터"
)
st.dataframe(
batch_dataframe,
width="stretch",
hide_index=True
)
batch_features = (
batch_dataframe[
feature_names
]
)
predictions = (
model.predict(
batch_features
)
)
probabilities = (
model.predict_proba(
batch_features
)
)
result_dataframe = (
batch_dataframe.copy()
)
result_dataframe[
"predicted_class"
] = [
target_names[
int(prediction)
]
for prediction in (
predictions
)
]
result_dataframe[
"confidence"
] = probabilities.max(
axis=1
)
st.success(
f"{len(result_dataframe)}건의 "
"예측을 완료했습니다."
)
st.dataframe(
result_dataframe,
width="stretch",
hide_index=True,
column_config={
"confidence": (
st.column_config
.ProgressColumn(
"예측 확률",
min_value=0.0,
max_value=1.0,
format="%.2f"
)
)
}
)
result_csv = (
result_dataframe
.to_csv(
index=False
)
.encode(
"utf-8-sig"
)
)
st.download_button(
"예측 결과 다운로드",
data=result_csv,
file_name=(
"iris_predictions.csv"
),
mime="text/csv"
)
except Exception as error:
st.exception(
error
)
with data_tab:
st.subheader(
"붓꽃 데이터 탐색"
)
display_data = data.copy()
display_data[
"species"
] = display_data[
"target"
].map({
index: name
for index, name in enumerate(
target_names
)
})
if show_raw_data:
st.dataframe(
display_data,
width="stretch",
hide_index=True
)
species_count = (
display_data[
"species"
]
.value_counts()
.rename_axis(
"품종"
)
.reset_index(
name="데이터 수"
)
)
chart_column1, chart_column2 = (
st.columns(2)
)
with chart_column1:
st.write(
"품종별 데이터 수"
)
st.bar_chart(
species_count,
x="품종",
y="데이터 수"
)
with chart_column2:
st.write(
"꽃잎 길이와 너비"
)
st.scatter_chart(
display_data,
x="petal length (cm)",
y="petal width (cm)",
color="species"
)
if show_feature_importance:
importance_dataframe = (
pd.DataFrame({
"특성": feature_names,
"중요도": (
model
.feature_importances_
)
})
.sort_values(
"중요도",
ascending=False
)
)
st.write(
"모델 특성 중요도"
)
st.bar_chart(
importance_dataframe,
x="특성",
y="중요도"
)
with history_tab:
st.subheader(
"현재 세션의 예측 기록"
)
history = st.session_state[
"prediction_history"
]
if history:
history_dataframe = (
pd.DataFrame(
history
)
)
st.dataframe(
history_dataframe,
width="stretch",
hide_index=True,
column_config={
"예측 확률": (
st.column_config
.ProgressColumn(
"예측 확률",
min_value=0.0,
max_value=1.0,
format="%.2f"
)
)
}
)
history_csv = (
history_dataframe
.to_csv(
index=False
)
.encode(
"utf-8-sig"
)
)
download_column, (
reset_column
) = st.columns(2)
with download_column:
st.download_button(
"기록 다운로드",
data=history_csv,
file_name=(
"prediction_history.csv"
),
mime="text/csv",
width="stretch"
)
with reset_column:
if st.button(
"기록 초기화",
width="stretch"
):
st.session_state[
"prediction_history"
] = []
st.rerun()
else:
st.info(
"아직 예측 기록이 없습니다."
)
st.divider()
st.caption(
"교육용 예제입니다. "
"실제 서비스에서는 데이터 검증, "
"모델 모니터링, 인증과 보안을 "
"추가해야 합니다."
)
if __name__ == "__main__":
main()42. 프로젝트 실행 방법
1단계: 폴더 생성
mkdir iris_streamlit_app
cd iris_streamlit_app2단계: 가상환경 생성
Windows
python -m venv venv
venv\Scripts\activatemacOS·Linux
python3 -m venv venv
source venv/bin/activate3단계: 패키지 설치
python -m pip install \
streamlit \
pandas \
numpy \
scikit-learn4단계: 코드 저장
전체 코드를 `app.py`로 저장합니다.
5단계: 실행
python -m streamlit run app.py브라우저에서 앱이 열립니다.
6단계: 종료
터미널에서 다음 키를 누릅니다.
Ctrl + C43. Community Cloud 배포
Streamlit Community Cloud를 이용하면 GitHub 저장소의 앱을 배포할 수 있습니다.
공식 배포 절차는 GitHub 저장소, 브랜치, 진입점 Python 파일을 선택해 앱을 생성하는 방식입니다. 배포된 앱에는 `streamlit.app` 하위 주소가 부여됩니다.
1단계: GitHub 저장소 준비
iris-streamlit-app/
├─ app.py
├─ requirements.txt
└─ .streamlit/
└─ config.toml2단계: requirements.txt
streamlit
pandas
numpy
scikit-learn3단계: GitHub에 Push
git init
git add .
git commit -m "Create Iris Streamlit app"
git branch -M main
git remote add origin 저장소주소
git push -u origin main4단계: Community Cloud에서 앱 생성
Community Cloud 접속
→ GitHub 계정 연결
→ Create app
→ 저장소 선택
→ 브랜치 선택
→ app.py 선택
→ Deploy5단계: Secrets 등록
API 키가 있다면 저장소에 올리지 않습니다.
배포 설정의 Advanced settings에서 Secrets 내용을 입력합니다. Community Cloud는 배포 시 Python 버전과 비밀정보를 설정할 수 있습니다.
배포 후 업데이트
GitHub 저장소에 변경사항을 Push하면 배포된 앱에도 변경 내용이 반영됩니다.
코드 수정
→ Git Commit
→ Git Push
→ 앱 재배포44. Docker 배포 개념
사내 서버나 클라우드 VM에 배포한다면 Docker를 사용할 수 있습니다.
Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install \
--no-cache-dir \
-r requirements.txt
COPY . .
EXPOSE 8501
CMD [
"streamlit",
"run",
"app.py",
"--server.address=0.0.0.0",
"--server.port=8501"
]이미지 생성
docker build \
-t iris-streamlit-app .컨테이너 실행
docker run \
--rm \
-p 8501:8501 \
iris-streamlit-app접속:
http://localhost:8501공식 문서도 사내망이나 클라우드 환경에서 Streamlit 앱을 배포하기 위한 Docker 컨테이너 구성 방법을 제공합니다.
실제 운영에서는 다음 요소를 추가로 검토해야 합니다.
Nginx 리버스 프록시
HTTPS 인증서
사용자 인증
방화벽
로그 관리
프로세스 재시작
리소스 제한
컨테이너 이미지 보안45. Streamlit 테스트
Streamlit은 앱의 위젯을 조작하고 출력 결과를 검사하는 테스트 API를 제공합니다.
테스트 대상
`app.py`:
import streamlit as st
st.title(
"점수 판정기"
)
score = st.slider(
"점수",
0,
100,
50
)
if score >= 60:
st.success(
"합격"
)
else:
st.error(
"불합격"
)테스트 코드
`test_app.py`:
from streamlit.testing.v1 import (
AppTest
)
def test_passing_score() -> None:
app = AppTest.from_file(
"app.py"
)
app.run()
app.slider[0].set_value(
80
).run()
assert (
app.success[0].value
== "합격"
)실행:
python -m pip install pytest
python -m pytestStreamlit 테스트 도구는 가상 앱을 실행하고 버튼, 슬라이더, 입력창 등의 사용자 조작을 시뮬레이션해 출력 요소를 검사할 수 있습니다.
46. 성능과 보안 주의사항
무거운 작업을 매번 실행하지 않는다
@st.cache_data
def load_data():
...@st.cache_resource
def load_model():
...Session State를 영구 저장소로 사용하지 않는다
브라우저 연결이 종료되면 상태가 사라질 수 있습니다.
임시 UI 상태:
Session State
영구 회원 정보:
데이터베이스업로드 파일을 검증한다
파일 확장자
MIME 유형
파일 크기
열 구조
데이터 타입
행 개수`type=["csv"]`는 사용자 편의를 위한 필터이지 완전한 보안 검증을 대신하지 않습니다.
API 키를 코드에 작성하지 않는다
api_key = st.secrets[
"API_KEY"
]캐시 객체를 함부로 변경하지 않는다
특히 `st.cache_resource`의 반환 객체는 여러 사용자와 공유될 수 있습니다.
데이터 누수를 막는다
모델 데모 앱에서도 학습 데이터와 평가 데이터 분리는 유지해야 합니다.
사용자별 모델을 전역 캐시로 공유할지 검토한다
사용자 데이터에 따라 내부 상태가 변경되는 모델이라면 전역 공유가 위험할 수 있습니다.
대용량 파일을 메모리에 계속 보관하지 않는다
업로드와 다운로드 데이터는 메모리를 소비할 수 있습니다.
서버 파일 시스템을 영구 저장소로 가정하지 않는다
클라우드 환경에서는 앱 재시작 시 로컬 파일이 사라질 수 있습니다.
인증이 필요한 정보는 공개 앱에 노출하지 않는다
사내 매출
개인정보
고객 계약 정보
보안 로그
의료 기록공개 배포 전에 접근 권한과 데이터 보호 방식을 설계해야 합니다.
47. 자주 발생하는 오류
오류 1. streamlit 명령을 찾을 수 없음
streamlit:
command not found또는:
'streamlit'은 내부 또는 외부 명령이 아닙니다.해결:
python -m streamlit run app.py설치 확인:
python -m pip show streamlit가상환경 활성화 여부도 확인합니다.
오류 2. 앱을 python으로 직접 실행함
잘못된 실행:
python app.py이 방식은 Python 스크립트만 실행하고 Streamlit 서버를 시작하지 않습니다.
올바른 실행:
streamlit run app.py또는:
python -m streamlit run app.py오류 3. 버튼 결과가 바로 사라짐
원인:
위젯 조작
→ 스크립트 재실행
→ 일반 변수 초기화해결:
if "result" not in st.session_state:
st.session_state.result = None오류 4. 파일이 업로드되지 않은 상태에서 읽음
잘못된 코드:
dataframe = pd.read_csv(
uploaded_file
)수정:
if uploaded_file is not None:
dataframe = pd.read_csv(
uploaded_file
)오류 5. CSV 한글이 깨짐
여러 인코딩을 시도합니다.
encodings = [
"utf-8",
"utf-8-sig",
"cp949"
]다운로드 CSV에는 `utf-8-sig`를 사용할 수 있습니다.
csv_data = dataframe.to_csv(
index=False
).encode(
"utf-8-sig"
)오류 6. 모델이 위젯 조작 때마다 다시 로딩됨
model = joblib.load(
"model.joblib"
)수정:
@st.cache_resource
def load_model():
return joblib.load(
"model.joblib"
)오류 7. 데이터가 갱신되지 않음
캐시에 이전 데이터가 남아 있을 수 있습니다.
st.cache_data.clear()또는 TTL을 설정합니다.
@st.cache_data(
ttl=300
)
def load_data():
...오류 8. DuplicateWidgetID
같은 구조의 위젯이 동일한 식별자로 반복 생성되면 오류가 발생할 수 있습니다.
for index in range(3):
st.text_input(
"이름",
key=f"name_{index}"
)각 위젯에 고유한 `key`를 지정합니다.
오류 9. Session State 값을 위젯 생성 후 변경함
위젯이 이미 생성된 뒤 같은 키의 값을 변경하면 예외가 발생할 수 있습니다.
Callback을 사용합니다.
def clear_name() -> None:
st.session_state.name = ""
st.text_input(
"이름",
key="name"
)
st.button(
"지우기",
on_click=clear_name
)오류 10. Community Cloud에서 모듈을 찾을 수 없음
ModuleNotFoundError`requirements.txt`에 패키지를 추가합니다.
streamlit
pandas
numpy
scikit-learn파일 이름이 다음처럼 잘못되지 않았는지 확인합니다.
requirements.txt.txt오류 11. 로컬 파일을 찾을 수 없음
경로 기준이 예상과 다를 수 있습니다.
from pathlib import Path
BASE_DIR = Path(
__file__
).resolve().parent
MODEL_PATH = (
BASE_DIR
/ "models"
/ "model.joblib"
)오류 12. 배포 앱에서 비밀정보가 없음
로컬 `secrets.toml`은 GitHub에 올리지 않았기 때문에 Community Cloud 설정에 별도로 등록해야 합니다.
api_key = st.secrets[
"API_KEY"
]키 이름도 정확히 일치해야 합니다.
오류 13. 앱이 계속 느려짐
확인할 내용:
대용량 CSV 반복 로딩
모델 반복 생성
매번 외부 API 호출
과도한 차트 생성
큰 이미지 반복 변환
캐시 미사용실행 시간을 측정합니다.
import time
start_time = time.perf_counter()
result = expensive_function()
elapsed = (
time.perf_counter()
- start_time
)
st.write(
f"실행 시간: {elapsed:.3f}초"
)오류 14. 포트가 이미 사용 중임
Port 8501 is already in use다른 포트로 실행합니다.
streamlit run app.py \
--server.port 8502또는 기존 Streamlit 프로세스를 종료합니다.
오류 15. 외부에서 접속할 수 없음
확인:
server.address
운영체제 방화벽
클라우드 보안 그룹
Docker 포트 매핑
Nginx 설정실행:
streamlit run app.py \
--server.address 0.0.0.0 \
--server.port 850148. 연습 문제
문제 1
제목, 설명, 이름 입력창, 인사 버튼이 있는 Streamlit 앱을 만드세요.
문제 2
슬라이더를 이용해 섭씨 온도를 입력받고 화씨 온도를 출력하세요.
화씨
=
섭씨 × 9 / 5 + 32문제 3
세 개의 `st.metric()`을 열 형태로 배치하세요.
사용자 수
매출
전환율문제 4
CSV 파일을 업로드하고 다음 정보를 출력하세요.
행 개수
열 개수
열 이름
결측값 개수문제 5
업로드한 데이터에서 사용자가 선택한 숫자 열의 히스토그램을 표시하세요.
문제 6
`st.data_editor()`로 수정한 데이터를 CSV로 다운로드할 수 있게 만드세요.
문제 7
버튼을 클릭할 때마다 1씩 증가하는 카운터를 Session State로 구현하세요.
문제 8
사용자의 이름, 나이, 관심 분야를 Form으로 입력받아 한 번에 출력하세요.
문제 9
CSV 로딩 함수를 `st.cache_data`로 캐시하세요.
문제 10
머신러닝 모델 로딩 함수를 `st.cache_resource`로 캐시하세요.
문제 11
사용자 메시지와 챗봇 답변을 Session State에 저장하는 채팅 화면을 만드세요.
문제 12
다음 세 페이지를 가진 멀티페이지 앱을 만드세요.
홈
데이터 분석
AI 예측문제 13
`.streamlit/config.toml`에서 기본 테마 색상을 변경하세요.
문제 14
API 키를 `secrets.toml`에 저장하고 `st.secrets`로 읽으세요.
문제 15
붓꽃 웹앱에 다음 기능을 추가하세요.
예측 결과별 이미지 표시
확률이 70% 미만이면 경고
예측 기록 검색
모델 종류 선택
결과 JSON 다운로드49. 핵심 요약
설치
python -m pip install streamlit실행
python -m streamlit run app.py페이지 설정
st.set_page_config(
page_title="나의 앱",
page_icon="🎈",
layout="wide"
)제목과 텍스트
st.title(
"제목"
)
st.write(
"내용"
)입력
name = st.text_input(
"이름"
)
age = st.number_input(
"나이"
)
category = st.selectbox(
"분류",
options
)버튼
if st.button(
"실행"
):
st.write(
"실행되었습니다."
)파일 업로드
uploaded_file = st.file_uploader(
"CSV 파일",
type=[
"csv"
]
)데이터프레임
st.dataframe(
dataframe,
width="stretch"
)차트
st.line_chart(
dataframe
)
st.bar_chart(
dataframe
)Form
with st.form(
"form"
):
value = st.text_input(
"값"
)
submitted = (
st.form_submit_button(
"제출"
)
)Session State
if "count" not in st.session_state:
st.session_state.count = 0데이터 캐시
@st.cache_data
def load_data():
...자원 캐시
@st.cache_resource
def load_model():
...파일 다운로드
st.download_button(
"다운로드",
data=file_data,
file_name="result.csv"
)멀티페이지
page = st.navigation([
st.Page(
"home.py",
title="홈"
),
st.Page(
"analysis.py",
title="분석"
)
])
page.run()50. 마무리
이번 시간에는 Streamlit을 이용해 Python 코드를 웹 애플리케이션으로 만드는 방법을 알아보았습니다.
Streamlit의 기본 흐름은 매우 단순합니다.
Python 코드 작성
→ Streamlit 명령 추가
→ streamlit run
→ 브라우저에서 실행우리는 다음 기능을 사용했습니다.
텍스트와 메시지 출력
입력창과 버튼
슬라이더와 선택창
사이드바
열과 탭
데이터프레임
차트
파일 업로드
파일 다운로드
Session State
캐시
채팅 UI
멀티페이지
머신러닝 모델 연결
클라우드 배포Streamlit에서 가장 중요한 개념은 재실행입니다.
사용자가 위젯 조작
→ Python 스크립트 위에서 아래까지 재실행
→ 위젯의 현재값으로 화면 다시 구성이 구조를 이해하면 다음 기능도 자연스럽게 이해됩니다.
값을 유지하려면
→ Session State
느린 계산을 반복하지 않으려면
→ cache_data
모델을 반복 로딩하지 않으려면
→ cache_resource
여러 입력을 한 번에 제출하려면
→ FormStreamlit은 HTML과 JavaScript를 완전히 몰라도 Python 기능을 브라우저에 올릴 수 있게 해 줍니다.
하지만 웹앱이 되었다고 모든 운영 문제가 자동으로 해결되는 것은 아닙니다.
사용자 인증
데이터베이스 저장
동시 사용자
보안
서버 비용
로그
모니터링
개인정보 보호이런 요소는 서비스 목적에 맞게 추가로 설계해야 합니다.
Streamlit의 진짜 가치는 웹 개발을 없애는 데 있지 않습니다.
Python 개발자가 자신의 분석과 AI 모델을 빠르게 사용자 앞에 보여줄 수 있게 한다는 데 있습니다.
Jupyter Notebook 속 모델
→ 개발자만 사용
Streamlit 웹앱 속 모델
→ 브라우저에서 누구나 체험모델이 연구실 책상에서 나와 접수 창구에 앉는 순간입니다.
사용자:
“이 CSV 분석해 주세요.”
Streamlit:
“파일을 업로드하세요.”
Pandas:
“데이터를 읽겠습니다.”
scikit-learn:
“예측하겠습니다.”
Streamlit:
“결과 차트와 다운로드 버튼을 준비했습니다.”이제 Python으로 만든 기능이 터미널 안에만 머물 필요가 없습니다.
버튼을 달고, 입력창을 만들고, 차트를 배치하면 하나의 데이터·AI 서비스가 됩니다. 🎈🤖
다음 편 예고
[Python 완전정복 시리즈 #34] Gradio 완벽 이해하기 | 머신러닝·생성형 AI 모델을 웹 데모로 만드는 가장 빠른 방법
다음 시간에는 함수의 입력과 출력을 연결하는 것만으로 머신러닝과 생성형 AI 모델의 웹 데모를 만드는 Gradio를 알아봅니다.
텍스트 입력, 이미지 업로드, 오디오 처리, 실시간 스트리밍, 채팅 인터페이스, PyTorch 모델 연결, Hugging Face Spaces 배포까지 실습해 보겠습니다.
#Python #파이썬 #Python강좌 #파이썬기초 #Streamlit #스트림릿 #웹앱 #데이터앱 #데이터분석 #대시보드 #머신러닝 #인공지능 #AI웹앱 #scikitlearn #Pandas #데이터시각화 #파일업로드 #SessionState #캐시 #멀티페이지 #AI챗봇 #CommunityCloud #Python웹개발 #코딩공부 #프로그래밍
