
Streamlit은 Python으로 데이터 애플리케이션을 만드는 방식을 혁신했다. 그 천재성은 단순함에 있다. HTML도 없고, JavaScript도 없고, 그냥 순수한 Python뿐이다. 몇 분 안에 Jupyter 노트북을 완전히 작동하는 웹 앱으로 변환하고, 땀 한 방울 흘리지 않고 Streamlit Cloud, Heroku, Railway에 배포할 수 있다.
하지만 문제가 있다. Streamlit은 기능을 미관보다 우선시한다. 기본 위젯은 기능적이지만 기본적이다. 솔직히 말하면 대부분의 Streamlit 앱은 "아, Streamlit으로 만들었구나"가 한눈에 보인다.
하지만 꼭 그럴 필요는 없다. 이 가이드의 팁들을 적용하면 이런 말을 들을 수 있다.
"잠깐, 이걸 Streamlit으로 만든 거야?"
시작 전: Streamlit 업데이트하기
💡 초보자 설명: 라이브러리도 스마트폰 앱처럼 주기적으로 업데이트된다. 최신 기능을 쓰려면 최신 버전이 필요하다.
터미널(맥의 경우 Terminal, 윈도우의 경우 명령 프롬프트)을 열고 아래 명령어를 입력한다.
# 업그레이드
pip install --upgrade streamlit
# 버전 확인
streamlit --version
이 글의 기능들은 Streamlit 1.48.0 이상에서 작동한다. 1.48.0보다 낮은 버전이라면 지금 바로 업데이트한다.
1. help 파라미터로 호버 툴팁 추가하기
왜 필요한가
버튼이나 슬라이더 옆에 긴 설명을 붙이면 화면이 지저분해진다. 툴팁은 사용자가 마우스를 올렸을 때만 설명이 나타나는 기능이다. 깔끔하면서도 친절하다.
💡 초보자 설명: 마치 스마트폰 앱에서 버튼에 손가락을 오래 누르면 설명이 뜨는 것과 같다. help="설명 텍스트" 한 줄만 추가하면 된다.
구현
import streamlit as st
st.title("사용자 등록 양식")
# help 파라미터에 설명을 넣으면 끝!
username = st.text_input(
"사용자명",
help="고유한 사용자명을 선택하세요. 영문자, 숫자, 언더스코어만 가능합니다."
# 👆 이 한 줄이 툴팁을 만든다
)
age = st.slider(
"나이",
min_value=18, # 최솟값
max_value=100, # 최댓값
value=25, # 기본값 (처음에 슬라이더가 위치할 곳)
help="등록하려면 최소 18세 이상이어야 합니다."
)
department = st.selectbox(
"부서",
options=["엔지니어링", "영업", "마케팅", "HR"],
# options는 드롭다운에 표시될 선택지 목록이다
help="부서에 따라 접근할 수 있는 대시보드가 결정됩니다."
)
terms = st.checkbox(
"이용약관에 동의합니다",
help="전체 이용약관 및 개인정보 처리방침을 읽으려면 여기를 클릭하세요."
)

각 위젯 오른쪽에 작은 ℹ️ 아이콘이 생긴다. 마우스를 올리면 help에 넣은 텍스트가 툴팁으로 나타난다.
💡 적용 팁: help 파라미터는 st.button, st.checkbox, st.multiselect, st.radio, st.number_input, st.date_input 등 거의 모든 위젯에 쓸 수 있다. 딱 한 줄만 추가하면 되니 지금 당장 모든 위젯에 넣어보자.
2. 인터랙티브 Plotly 차트 통합하기
왜 필요한가
Matplotlib은 대시보드용이 아니다. 정적인 이미지만 만들 수 있어서 사용자가 차트를 확대하거나 특정 데이터를 클릭해서 탐색할 수 없다.
💡 초보자 설명: Matplotlib으로 만든 차트는 "사진"이고, Plotly로 만든 차트는 "인터랙티브 앱"이다. 사용자가 마우스를 올리면 정보가 뜨고, 드래그하면 확대되고, 범례를 클릭하면 데이터를 숨길 수 있다. 설치는 pip install plotly로 간단하게 할 수 있다.
기본 선버스트 차트
import streamlit as st
import plotly.express as px # Plotly의 간단한 버전
import pandas as pd
# 판매 데이터 만들기
# pd.DataFrame은 엑셀 표처럼 데이터를 저장하는 구조다
df = pd.DataFrame({
'category': ['전자제품', '전자제품', '전자제품', '의류', '의류', '식품', '식품'],
'subcategory': ['노트북', '폰', '태블릿', '남성', '여성', '음료', '스낵'],
'sales': [45000, 38000, 22000, 31000, 42000, 15000, 18000],
})
# 선버스트 차트: 원형 계층 구조 차트 (파이차트의 업그레이드 버전)
fig = px.sunburst(
df,
path=['category', 'subcategory'], # 계층 구조: 대분류 → 소분류
values='sales', # 크기를 결정할 값
title='카테고리별 판매 분포',
color='sales', # 색상도 값에 따라 변함
color_continuous_scale='RdYlGn', # 빨강→노랑→초록 색상 스케일
)
fig.update_layout(
height=600, # 차트 높이 (픽셀)
margin=dict(t=50, l=0, r=0, b=0) # 여백 (top, left, right, bottom)
)
# use_container_width=True: 화면 너비에 맞게 자동 조절
st.plotly_chart(fig, use_container_width=True)

고급: 커스텀 호버 템플릿이 있는 꺾은선 그래프
import plotly.graph_objects as go # Plotly의 고급 버전
# 월별 매출 데이터 생성
df_time = pd.DataFrame({
'date': pd.date_range('2024-01-01', periods=12, freq='M'),
# pd.date_range: 날짜 범위를 자동으로 만들어준다
# periods=12: 12개의 날짜, freq='M': 월 단위
'revenue': [125000, 132000, 128000, 145000, 158000, 162000,
175000, 188000, 192000, 205000, 218000, 235000],
'target': [130000, 135000, 140000, 145000, 150000, 160000,
170000, 180000, 190000, 200000, 210000, 220000]
})
fig = go.Figure() # 빈 차트 캔버스 생성
# 실제 매출 선 추가
fig.add_trace(go.Scatter(
x=df_time['date'],
y=df_time['revenue'],
mode='lines+markers', # 선과 점을 함께 표시
name='실제 매출',
line=dict(color='#667eea', width=3), # 선 색상과 두께
marker=dict(size=8, color='#764ba2'), # 점 크기와 색상
hovertemplate='<b>%{x|%B %Y}</b><br>' +
'매출: $%{y:,.0f}<br>' +
'<extra></extra>'
# hovertemplate: 마우스 올렸을 때 표시될 텍스트 형식
# %{x}: x축 값, %{y:,.0f}: y축 값을 천 단위 콤마로 표시
))
# 목표 선 추가 (점선)
fig.add_trace(go.Scatter(
x=df_time['date'],
y=df_time['target'],
mode='lines',
name='목표',
line=dict(color='#FF6B6B', width=2, dash='dash'), # dash='dash': 점선
))
fig.update_layout(
template='plotly_white', # 흰색 배경 테마
hovermode='x unified', # 같은 x 위치의 모든 데이터를 한꺼번에 표시
title='매출 vs 목표 성과',
)
st.plotly_chart(fig, use_container_width=True)
💡 초보자 팁: plotly.express(px)는 간단하고 빠르게 차트를 만들 때, plotly.graph_objects(go)는 세세하게 커스터마이징할 때 사용한다. 처음에는 px로 시작하는 것을 추천한다.

3. 전략적 사이드바 활용으로 앱 구성하기
왜 필요한가
페이지가 많아지면 사용자가 길을 잃는다. 사이드바로 메뉴를 만들면 구글이나 Notion처럼 깔끔한 내비게이션이 생긴다.
💡 초보자 설명: Streamlit에서 여러 페이지를 만들려면 각 페이지를 별도의 .py 파일로 만들어야 한다. 예를 들어 home.py, sales.py, customers.py 같이 나눈다.
파일 구조 먼저 이해하기
내 프로젝트 폴더/
├── app.py ← 메인 파일 (내비게이션 담당)
├── home.py ← 홈 페이지 내용
├── sales.py ← 영업 페이지 내용
├── customers.py ← 고객 페이지 내용
└── products.py ← 제품 페이지 내용
패턴 1: 상단 탭 내비게이션 (페이지가 2~5개일 때)
# app.py
import streamlit as st
import pandas as pd
st.set_page_config(
layout="wide", # 화면을 넓게 사용
page_title="영업 분석" # 브라우저 탭에 표시될 제목
)
# 각 페이지 파일을 등록한다
# icon은 탭에 표시될 이모지
page_home = st.Page("home.py", title="개요", icon="🏠")
page_sales = st.Page("sales.py", title="영업", icon="💰")
page_customers = st.Page("customers.py", title="고객", icon="👥")
page_products = st.Page("products.py", title="제품", icon="📦")
# position="top": 상단에 탭으로 표시
pg = st.navigation(
[page_home, page_sales, page_customers, page_products],
position="top"
)
# 사이드바에 필터 넣기
with st.sidebar:
# with st.sidebar: 블록 안의 모든 코드는 사이드바에 표시된다
st.header("⚙️ 필터")
date_range = st.date_input(
"날짜 범위",
value=(pd.Timestamp('2024-01-01'), pd.Timestamp('2024-12-31')),
# value에 튜플로 시작일과 종료일을 넣으면 범위 선택이 된다
help="분석할 기간을 선택하세요"
)
st.divider() # 구분선 추가 (HR 태그와 같다)
regions = st.multiselect(
"지역",
options=["북미", "유럽", "아시아", "남미"],
default=["북미", "유럽"], # 처음에 선택되어 있을 항목
help="지리적 지역으로 데이터 필터링"
)
min_revenue = st.slider(
"최소 매출 ($)",
min_value=0,
max_value=100000,
value=10000,
step=5000, # 슬라이더가 5000 단위로 이동
help="이 매출 임계값 이상의 항목만 표시"
)
pg.run() # 선택된 페이지 실행
💡 초보자 팁: st.divider()는 내용 사이에 얇은 구분선을 그어준다. 필터들이 많을 때 그룹별로 나누면 훨씬 읽기 쉬워진다.

패턴 2: Sidebar Navigation + Top Filters
import streamlit as st
import pandas as pd
st.set_page_config(layout="wide", page_title="Enterprise Dashboard")
# Sidebar navigation
with st.sidebar:
st.image("logo.png", width=200) # Your company logo
st.title("Navigation")
# Define pages with Material icons
page_overview = st.Page("overview.py", title="Overview", icon=":material/dashboard:")
page_sales = st.Page("sales.py", title="Sales", icon=":material/sell:")
page_marketing = st.Page("marketing.py", title="Marketing", icon=":material/campaign:")
page_finance = st.Page("finance.py", title="Finance", icon=":material/payments:")
page_hr = st.Page("hr.py", title="HR Analytics", icon=":material/groups:")
page_settings = st.Page("settings.py", title="Settings", icon=":material/settings:")
# Navigation
pg = st.navigation([
page_overview,
page_sales,
page_marketing,
page_finance,
page_hr,
page_settings
])
# Top filters bar
col1, col2, col3, col4 = st.columns(4)
with col1:
time_period = st.selectbox(
"Time Period",
options=["Last 7 Days", "Last 30 Days", "Last Quarter", "Last Year"],
index=1
)
with col2:
business_unit = st.selectbox(
"Business Unit",
options=["All Units", "Consumer", "Enterprise", "SMB"]
)
with col3:
metric_type = st.selectbox(
"Metric",
options=["Revenue", "Units Sold", "Profit Margin", "Customer Count"]
)
with col4:
comparison = st.selectbox(
"Compare To",
options=["Previous Period", "Same Period Last Year", "Budget"]
)
st.divider()
# Run page
pg.run()

4. 사이드바 로고로 앱 브랜딩하기
왜 필요한가
로고 하나로 "개인 프로젝트"가 "전문 제품"처럼 보인다. 로고는 Canva(무료)나 AI 이미지 생성 툴로 쉽게 만들 수 있다.
💡 초보자 설명: 이미지 파일(logo.png)을 Python 파일과 같은 폴더에 넣고, st.image()로 불러오면 된다. PNG 형식에 투명 배경이 가장 깔끔하게 보인다.
import streamlit as st
with st.sidebar:
# 로고 이미지 표시
# logo.png 파일이 같은 폴더에 있어야 한다
st.image(
"logo.png",
width=200, # 픽셀 단위 너비
)
# HTML로 앱 이름과 설명 추가
# unsafe_allow_html=True: HTML 태그를 그대로 렌더링
st.markdown("""
<h2 style='text-align: center; color: #667eea; margin: 0;'>
데이터 분석
</h2>
<p style='text-align: center; color: #888; font-size: 14px;'>
비즈니스 인사이트 플랫폼
</p>
""", unsafe_allow_html=True)
st.divider()
# 빠른 통계 표시
st.subheader("📊 오늘의 현황")
# st.metric: 숫자 지표를 보기 좋게 표시
# delta: 변화량 (양수면 초록색, 음수면 빨간색)
st.metric("활성 사용자", "1,234", "+12%")
st.metric("오늘 매출", "$45.6K", "+8%")
💡 로고 만드는 팁: Canva(canva.com)에서 무료로 로고를 만들 수 있다. "투명 배경"으로 PNG 다운로드하면 사이드바에서 가장 깔끔하게 보인다.

5. config.toml로 앱 커스터마이징하기
왜 필요한가
매번 위젯마다 색상을 지정하는 대신, 한 파일에서 전체 색상 테마를 한 번에 설정할 수 있다. 회사 브랜드 색상을 전체 앱에 일괄 적용하는 방법이다.
💡 초보자 설명: config.toml은 앱의 "환경 설정 파일"이다. .streamlit 폴더 안에 만들면 Streamlit이 자동으로 읽는다. TOML은 설정 파일에 자주 쓰이는 단순한 형식으로, 키 = 값 형태로 작성한다.
파일 구조
내 프로젝트 폴더/
├── .streamlit/ ← 이 폴더를 만든다 (폴더명 앞에 점이 있다)
│ └── config.toml ← 여기에 설정을 작성한다
├── app.py
└── ...
config.toml 작성
# .streamlit/config.toml
[theme]
# 주요 브랜드 색상: 버튼, 링크, 강조 색상
# 색상 코드는 CSS의 HEX 코드를 사용한다
primaryColor = "#667eea"
# 메인 배경색
backgroundColor = "#ffffff"
# 사이드바, 컨테이너 배경색
secondaryBackgroundColor = "#f0f2f6"
# 텍스트 색상
textColor = "#262730"
# 폰트: "sans serif", "serif", "monospace" 중 선택
font = "sans serif"
# 기본 테마: "light" 또는 "dark"
base = "light"
[server]
# 클라우드 배포 시 필요한 설정
headless = true
port = 8501
[browser]
# 사용 통계 수집 비활성화
gatherUsageStats = false
💡 색상 코드 찾는 법: coolors.co나 색상 픽커 사이트에서 원하는 색상의 HEX 코드(#으로 시작하는 6자리)를 찾을 수 있다. 회사 로고의 색상 코드를 그대로 쓰면 브랜딩이 일관성 있게 유지된다.
Streamlit 1.46.0부터: 커스텀 폰트
[theme]
# Poppins, Roboto, Open Sans, Lato, Montserrat 등 구글 폰트 사용 가능
font = "Poppins"
6. 커스텀 CSS로 개별 위젯 스타일링하기
왜 필요한가
전체 테마 설정만으로는 한계가 있다. 특정 버튼만 강조 색상으로 만들거나, 특정 입력창만 다르게 꾸미고 싶을 때 CSS를 사용한다.
💡 초보자 설명: CSS는 웹페이지의 "옷"이다. 색상, 크기, 모양, 그림자 등 외형을 담당한다. Streamlit 1.39+ 버전부터 위젯의 key 파라미터를 CSS 클래스처럼 활용할 수 있다. CSS를 몰라도 아래 코드를 복사해서 색상 코드만 바꾸면 적용할 수 있다.
Step 1: CSS 파일 만들기
내 프로젝트 폴더/
├── assets/
│ └── styles.css ← 이 파일을 만든다
└── app.py
/* assets/styles.css */
/* key="primary_cta" 인 버튼에만 적용 */
.st-key-primary_cta button {
/* 그라디언트 배경 (두 색상이 부드럽게 전환) */
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
color: white !important; /* 글자색 흰색 */
border: none !important; /* 테두리 없음 */
border-radius: 12px !important; /* 모서리 둥글게 */
padding: 12px 32px !important; /* 내부 여백 (상하 12px, 좌우 32px) */
font-weight: 600 !important; /* 글자 굵기 */
/* 그림자 효과 */
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4) !important;
transition: all 0.3s ease !important; /* 변화가 0.3초에 걸쳐 부드럽게 */
}
/* 마우스를 올렸을 때 */
.st-key-primary_cta button:hover {
transform: translateY(-2px) !important; /* 2픽셀 위로 이동 (떠오르는 효과) */
box-shadow: 0 6px 16px rgba(102, 126, 234, 0.6) !important;
}
/* 성공 버튼 (초록색 계열) */
.st-key-success_btn button {
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%) !important;
color: white !important;
border-radius: 8px !important;
border: none !important;
}
/* 위험/삭제 버튼 (빨간색 계열) */
.st-key-danger_btn button {
background: linear-gradient(135deg, #eb3349 0%, #f45c43 100%) !important;
color: white !important;
border-radius: 8px !important;
border: none !important;
}
Step 2: Python에서 CSS 불러오기
import streamlit as st
# CSS 파일을 읽어서 앱에 적용하는 함수
def load_css():
with open("assets/styles.css") as f:
# st.markdown으로 CSS를 HTML style 태그 안에 넣는다
st.markdown(f'<style>{f.read()}</style>', unsafe_allow_html=True)
load_css() # 앱 시작 시 한 번만 호출하면 된다
st.title("커스텀 스타일 버튼 예시")
col1, col2, col3 = st.columns(3) # 3개 컬럼으로 나누기
with col1:
# key="primary_cta": CSS에서 .st-key-primary_cta로 매핑된다
if st.button("주요 액션", key="primary_cta"):
st.success("클릭됨!")
with col2:
if st.button("완료", key="success_btn"):
st.success("완료!")
with col3:
if st.button("삭제", key="danger_btn"):
st.error("삭제됨!")
💡 핵심 원리: key="버튼이름"을 설정하면, CSS에서 .st-key-버튼이름으로 해당 위젯만 골라서 스타일을 적용할 수 있다. !important는 다른 스타일보다 이 스타일을 우선 적용하라는 뜻이다.
7. Material 아이콘으로 내비게이션 향상하기
왜 필요한가
이모지도 좋지만, Google Material 아이콘을 쓰면 기업 SaaS 서비스 수준의 세련된 느낌이 난다. 2,500개 이상의 아이콘을 무료로 사용할 수 있다.
💡 초보자 설명: 아이콘은 :material/아이콘이름: 형식으로 쓴다. 아이콘 이름은 fonts.google.com/icons에서 원하는 아이콘을 찾아서 이름을 복사하면 된다.
import streamlit as st
# 내비게이션에 Material 아이콘 적용
sales_dashboard = st.Page(
"sales.py",
title="영업 대시보드",
icon=":material/dashboard:" # 대시보드 격자 아이콘
)
hr_analytics = st.Page(
"hr.py",
title="HR 분석",
icon=":material/analytics:" # 막대 그래프 아이콘
)
page_settings = st.Page(
"settings.py",
title="설정",
icon=":material/settings:" # 톱니바퀴 아이콘
)
with st.sidebar:
st.image("logo.png", width=180)
st.divider()
pg = st.navigation([sales_dashboard, hr_analytics, page_settings])
pg.run()
내비게이션 외에도 어디서나 사용 가능
# 버튼에 아이콘 추가
if st.button(":material/download: 리포트 다운로드"):
st.success("다운로드 시작!")
# 섹션 제목에 아이콘 추가
st.subheader(":material/trending_up: 매출 성장 추이")
# 메트릭에 아이콘 추가
col1, col2, col3 = st.columns(3)
with col1:
st.metric(":material/people: 활성 사용자", "1,234", "+12%")
with col2:
st.metric(":material/shopping_cart: 주문 수", "567", "+8%")
with col3:
st.metric(":material/attach_money: 매출", "$45.6K", "+15%")
# 아이콘만 있는 미니 버튼
col_a, col_b, col_c = st.columns(3)
with col_a:
st.button(":material/edit:") # 연필 아이콘
with col_b:
st.button(":material/delete:") # 쓰레기통 아이콘
with col_c:
st.button(":material/share:") # 공유 아이콘
💡 아이콘 찾는 법: fonts.google.com/icons에 접속 → 원하는 아이콘 검색 → 아이콘 클릭 → 이름 확인. 예: home이라는 아이콘은 :material/home:으로 쓴다.
8. st.multiselect 대신 st.pills 사용하기
왜 필요한가
드롭다운은 클릭해야 옵션이 보인다. 필(Pill)은 모든 옵션이 처음부터 화면에 보인다. 선택지가 8개 이하라면 필이 훨씬 직관적이다.
💡 초보자 설명: 필은 클릭 가능한 태그처럼 생겼다. 선택하면 색이 채워지고, 다시 클릭하면 해제된다. 요즘 앱들의 해시태그 필터나 카테고리 버튼과 똑같은 느낌이다.
import streamlit as st
st.title("Multiselect vs Pills 비교")
# 전통적인 방식: 드롭다운 (옵션이 숨겨져 있다)
st.subheader("기존 방식: Multiselect")
selected_old = st.multiselect(
"부서 선택",
options=["영업", "마케팅", "재무", "HR", "제품", "디자인"],
help="클릭해야 옵션이 보입니다"
)
st.write(f"선택된 부서: {selected_old}")
st.divider()
# 새로운 방식: 필 (옵션이 모두 보인다)
st.subheader("새로운 방식: Pills")
selected_new = st.pills(
"부서 선택",
options=["영업", "마케팅", "재무", "HR", "제품", "디자인"],
selection_mode="multi", # 여러 개 선택 가능 ("single"로 하면 하나만 선택)
default=["영업", "마케팅"], # 처음에 선택되어 있을 항목
help="모든 옵션이 보입니다"
)
st.write(f"선택된 부서: {selected_new}")
st.divider()
# 실용적인 예시: 대시보드 필터로 활용
st.subheader("실용 예시: 대시보드 필터")
col1, col2 = st.columns(2)
with col1:
# selection_mode="single": 하나만 선택 가능
time_period = st.pills(
"기간",
options=["오늘", "이번 주", "이번 달", "올해"],
selection_mode="single",
default="이번 달"
)
with col2:
metric_type = st.pills(
"보고 싶은 지표",
options=["매출", "사용자 수", "전환율", "이탈율"],
selection_mode="multi",
default=["매출", "사용자 수"]
)
# f-string으로 선택된 값을 문자열에 삽입
st.info(f"📊 {time_period} 기간의 [{', '.join(metric_type if metric_type else ['없음'])}] 데이터를 표시합니다")
💡 언제 어떤 걸 쓸까?
- 선택지 8개 이하 → st.pills (더 직관적)
- 선택지 9개 이상 → st.multiselect (검색 기능 포함)
9. 수평 플렉스 컨테이너로 레이아웃 구성하기
왜 필요한가
기본 st.columns(3)은 항상 화면을 3등분한다. 플렉스 컨테이너를 활용하면 버튼을 왼쪽/가운데/오른쪽에 배치하거나, 요소 사이 간격을 자유롭게 조절할 수 있다.
💡 초보자 설명: st.columns([3, 1])처럼 숫자를 넣으면 비율을 지정할 수 있다. [3, 1]은 3:1 비율로 나누는 것이다. gap 파라미터로 컬럼 사이 간격도 조절할 수 있다.
import streamlit as st
# 기본: 동일한 너비의 3개 컬럼
col1, col2, col3 = st.columns(3)
with col1:
st.button("버튼 1")
with col2:
st.button("버튼 2")
with col3:
st.button("버튼 3")
st.divider()
# 비율 지정: 3:1 비율 (왼쪽이 오른쪽보다 3배 넓다)
header_col, btn_col = st.columns([3, 1])
with header_col:
st.title("📊 분석 대시보드")
with btn_col:
# 오른쪽 영역을 다시 3등분해서 버튼 배치
a, b, c = st.columns(3, gap="small")
with a:
st.button(":material/refresh:", key="refresh")
with b:
st.button(":material/download:", key="dl")
with c:
st.button(":material/settings:", key="cfg")
st.divider()
# gap 파라미터: 컬럼 사이 간격 조절
st.write("**좁은 간격 (관련된 요소끼리)**")
c1, c2, c3, c4 = st.columns(4, gap="small")
with c1: st.metric("지표 1", "100", "+5%")
with c2: st.metric("지표 2", "200", "+3%")
with c3: st.metric("지표 3", "300", "-2%")
with c4: st.metric("지표 4", "400", "+8%")
st.write("**넓은 간격 (독립된 섹션)**")
c1, c2, c3, c4 = st.columns(4, gap="large")
with c1: st.metric("지표 1", "100", "+5%")
with c2: st.metric("지표 2", "200", "+3%")
with c3: st.metric("지표 3", "300", "-2%")
with c4: st.metric("지표 4", "400", "+8%")
st.divider()
# 중첩 컬럼: 복잡한 레이아웃
st.subheader("중첩 레이아웃 예시")
left, right = st.columns([2, 1], gap="medium") # 2:1 비율
with left:
st.markdown("### 📈 메인 차트 영역")
# 왼쪽 안에서 다시 2개로 나누기
inner_l, inner_r = st.columns(2, gap="small")
with inner_l:
st.info("차트 A")
with inner_r:
st.info("차트 B")
with right:
st.markdown("### 🎛️ 컨트롤 패널")
st.success("필터")
st.warning("설정")
💡 gap 사용 기준:
- gap="small": 버튼 그룹, 관련된 지표처럼 서로 가까워야 하는 요소
- gap="medium": 일반적인 콘텐츠 영역 구분
- gap="large": 완전히 다른 섹션을 시각적으로 분리할 때
10. 전문가처럼 KPI 표시하기
왜 필요한가
KPI(핵심 성과 지표)는 대시보드의 핵심이다. 한눈에 보이고, 변화를 즉시 파악할 수 있어야 한다. 세 가지 방법으로 수준을 높일 수 있다.
💡 초보자 설명: st.metric은 숫자와 변화율을 보기 좋게 표시해주는 Streamlit 내장 기능이다. delta에 양수를 넣으면 초록색 ▲, 음수를 넣으면 빨간색 ▼이 표시된다.
방법 1: 기본 st.metric (가장 간단)
import streamlit as st
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric(
label="이번 달 매출", # 제목
value="$125.4K", # 현재 값
delta="+12.5%", # 변화량 (양수 → 초록, 음수 → 빨강)
help="지난달 대비 매출 변화"
)
with col2:
st.metric(
label="활성 사용자",
value="1,234",
delta="+83명",
delta_color="normal" # "normal": 기본 (양수=초록, 음수=빨강)
)
with col3:
st.metric(
label="전환율",
value="3.2%",
delta="+0.5%",
)
with col4:
st.metric(
label="이탈율",
value="2.1%",
delta="-0.3%",
# delta_color="inverse": 반전 (감소가 좋은 지표에 사용)
# 이탈율은 낮을수록 좋으니까 -0.3%는 초록색으로 표시
delta_color="inverse"
)

방법 2: HTML/CSS 커스텀 카드 (브랜딩된 디자인)
def metric_card(title, value, delta, delta_color="green", icon="📊"):
"""
커스텀 메트릭 카드를 만드는 함수
파라미터:
- title: 카드 제목
- value: 표시할 값
- delta: 변화율 텍스트
- delta_color: "green" 또는 "red"
- icon: 카드 아이콘 이모지
"""
# 색상 선택
if delta_color == "green":
delta_bg = "linear-gradient(135deg, #11998e 0%, #38ef7d 100%)"
elif delta_color == "red":
delta_bg = "linear-gradient(135deg, #eb3349 0%, #f45c43 100%)"
else:
delta_bg = "linear-gradient(135deg, #667eea 0%, #764ba2 100%)"
# f-string으로 HTML 생성
# 중괄호 {}안의 변수들이 실제 값으로 치환된다
card_html = f"""
<div style='
background: white;
padding: 24px;
border-radius: 16px;
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
border-left: 4px solid #667eea;
margin: 8px 0;
'>
<div style='display: flex; justify-content: space-between; align-items: center;'>
<span style='font-size: 48px;'>{icon}</span>
<div style='
background: {delta_bg};
color: white;
padding: 6px 12px;
border-radius: 20px;
font-size: 14px;
font-weight: 600;
'>{delta}</div>
</div>
<h3 style='color: #888; font-size: 14px; margin: 16px 0 8px 0;
text-transform: uppercase; letter-spacing: 0.5px;'>
{title}
</h3>
<p style='color: #262730; font-size: 36px; font-weight: 700; margin: 0;'>
{value}
</p>
</div>
"""
# unsafe_allow_html=True: HTML을 그대로 렌더링
st.markdown(card_html, unsafe_allow_html=True)
# 함수 사용 예시
st.subheader("커스텀 카드 KPI")
col1, col2, col3, col4 = st.columns(4)
with col1:
metric_card("매출", "$125.4K", "+12.5%", "green", "💰")
with col2:
metric_card("사용자", "1,234", "+8.3%", "green", "👥")
with col3:
metric_card("주문", "567", "+15.2%", "green", "🛒")
with col4:
metric_card("이탈율", "2.1%", "-0.3%", "red", "📉")

방법 3: Plotly 게이지 차트 (경영진 대시보드)
import plotly.graph_objects as go
def create_gauge(value, title, max_value=100, color="#667eea"):
"""
게이지(속도계 모양) 차트를 만드는 함수
파라미터:
- value: 현재 값
- title: 게이지 제목
- max_value: 최댓값 (기본 100)
- color: 게이지 바 색상
"""
fig = go.Figure(go.Indicator(
mode="gauge+number+delta", # 게이지 + 숫자 + 변화량 모두 표시
value=value,
title={'text': title, 'font': {'size': 20}},
# reference: 비교 기준값 (80% 수준과 비교)
delta={'reference': max_value * 0.8, 'increasing': {'color': "#38ef7d"}},
gauge={
'axis': {'range': [None, max_value]},
'bar': {'color': color},
# 배경색 구간 설정 (빨강→노랑→초록)
'steps': [
{'range': [0, max_value * 0.6], 'color': '#FFE4E1'}, # 빨강 구간
{'range': [max_value * 0.6, max_value * 0.8], 'color': '#FFE4B5'}, # 노랑 구간
{'range': [max_value * 0.8, max_value], 'color': '#90EE90'} # 초록 구간
],
# 경계선: 90% 지점에 빨간 선
'threshold': {
'line': {'color': "red", 'width': 4},
'thickness': 0.75,
'value': max_value * 0.9
}
}
))
fig.update_layout(
height=250,
margin=dict(l=10, r=10, t=50, b=10)
)
return fig
# 사용 예시
st.subheader("게이지 차트 KPI")
col1, col2, col3 = st.columns(3)
with col1:
st.plotly_chart(
create_gauge(87, "고객 만족도", 100, "#667eea"),
use_container_width=True
)
with col2:
st.plotly_chart(
create_gauge(73, "서버 상태", 100, "#11998e"),
use_container_width=True
)
with col3:
st.plotly_chart(
create_gauge(92, "영업 목표 달성률", 100, "#eb3349"),
use_container_width=True
)

💡 어떤 방법을 선택할까?
- st.metric: 빠르게 만들어야 할 때, 내부 툴
- HTML 카드: 고객에게 보여주는 대시보드, 브랜딩이 중요할 때
- Plotly 게이지: 목표 달성률, SLA 모니터링처럼 "얼마나 채웠나"가 중요할 때
🎁 보너스: 프로덕션에서 꼭 필요한 기능들
1. 캐싱: 느린 데이터 로딩 해결하기
import streamlit as st
import pandas as pd
import time
# @st.cache_data: 같은 함수를 다시 호출해도 저장된 결과를 반환
# 페이지를 새로고침해도 데이터를 다시 불러오지 않는다
@st.cache_data(ttl=3600) # ttl=3600: 1시간(3600초) 동안 캐시 유지
def load_data():
"""데이터베이스나 API에서 데이터 불러오기"""
time.sleep(2) # 느린 로딩을 흉내내는 코드
return pd.read_csv("data.csv")
# @st.cache_resource: ML 모델처럼 한 번만 로드해야 하는 것들
@st.cache_resource
def load_model():
"""ML 모델 불러오기 (서버 재시작 전까지 메모리에 유지)"""
import joblib
return joblib.load("model.pkl")
df = load_data() # 처음 한 번만 실제로 불러오고, 이후엔 캐시 사용
model = load_model() # 서버가 재시작되기 전까지 한 번만 로드
💡 캐싱이 왜 중요한가: Streamlit은 사용자가 버튼을 클릭하거나 슬라이더를 움직일 때마다 전체 코드를 처음부터 다시 실행한다. 캐싱 없이는 데이터를 매번 다시 불러와서 앱이 느려진다.
2. 세션 상태: 페이지를 넘나드는 데이터 유지
import streamlit as st
# st.session_state: 새로고침해도 값이 유지되는 저장소
# 딕셔너리처럼 사용한다
# 초기화: 처음 실행될 때만 값을 설정
if 'page_views' not in st.session_state:
st.session_state.page_views = 0
if 'username' not in st.session_state:
st.session_state.username = ""
# 값 업데이트
st.session_state.page_views += 1
st.write(f"이 세션에서 페이지를 {st.session_state.page_views}번 봤습니다")
# 사용자 이름 저장
username = st.text_input("이름을 입력하세요", value=st.session_state.username)
if st.button("저장"):
st.session_state.username = username
st.success(f"안녕하세요, {username}님!")
3. 파일 다운로드: 데이터 내보내기
import streamlit as st
import pandas as pd
import io
from datetime import datetime
# 샘플 데이터
df = pd.DataFrame({'이름': ['김철수', '이영희'], '매출': [100, 200]})
# CSV 다운로드 (가장 간단)
csv = df.to_csv(index=False).encode('utf-8-sig')
# utf-8-sig: 한글이 깨지지 않도록 BOM 인코딩 사용
st.download_button(
label="📥 CSV로 다운로드",
data=csv,
file_name=f'report_{datetime.now().strftime("%Y%m%d")}.csv',
mime='text/csv'
)
# 엑셀 다운로드
buffer = io.BytesIO() # 메모리 안의 파일 (실제 디스크에 저장하지 않음)
with pd.ExcelWriter(buffer, engine='openpyxl') as writer:
df.to_excel(writer, sheet_name='데이터', index=False)
buffer.seek(0) # 파일 포인터를 처음으로 이동
st.download_button(
label="📥 엑셀로 다운로드",
data=buffer,
file_name=f'report_{datetime.now().strftime("%Y%m%d")}.xlsx',
mime='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
)
4. 로딩 표시: 처리 중임을 알리기
import streamlit as st
import time
# 스피너: 처리 중 빙글빙글 도는 아이콘 표시
with st.spinner("🔄 데이터를 처리하는 중입니다..."):
time.sleep(2) # 실제로는 데이터 처리 코드가 들어간다
st.success("✅ 완료!")
# 프로그레스 바: 진행률 표시
progress = st.progress(0, text="시작 중...")
for i in range(100):
time.sleep(0.01)
# progress 함수에 0~100 사이의 값을 넣으면 바가 채워진다
progress.progress(i + 1, text=f"처리 중... {i+1}%")
progress.empty() # 완료 후 프로그레스 바 제거
st.success("처리 완료!")
5. 에러 처리: 앱이 갑자기 죽지 않게 하기
import streamlit as st
user_input = st.text_input("파일 경로를 입력하세요")
if st.button("파일 읽기"):
try:
# try: 오류가 발생할 수 있는 코드
import pandas as pd
df = pd.read_csv(user_input)
st.success("✅ 파일을 성공적으로 읽었습니다!")
st.dataframe(df)
except FileNotFoundError:
# 파일이 없을 때
st.error("❌ 파일을 찾을 수 없습니다.")
st.info("💡 팁: 파일 경로가 올바른지 확인하세요.")
except pd.errors.EmptyDataError:
# 파일이 비어있을 때
st.error("❌ 파일이 비어있습니다.")
except Exception as e:
# 그 외 모든 오류
st.error(f"❌ 예상치 못한 오류가 발생했습니다: {str(e)}")
6. 폼: 여러 입력을 한꺼번에 제출하기
import streamlit as st
# st.form: 안의 모든 입력이 "제출" 버튼을 눌러야 처리된다
# 일반 위젯은 값을 바꿀 때마다 전체 코드가 재실행되지만
# form 안의 위젯은 제출 전까지 재실행되지 않아 성능이 좋다
with st.form("등록_폼"):
st.subheader("👤 사용자 등록")
name = st.text_input("성명")
email = st.text_input("이메일")
age = st.number_input("나이", min_value=18, max_value=100, value=25)
department = st.selectbox("부서", ["영업", "마케팅", "엔지니어링", "HR"])
# form_submit_button: 폼 제출 버튼
submitted = st.form_submit_button("등록하기 →")
if submitted:
# 입력값 검증
if not name or not email:
st.error("이름과 이메일은 필수입니다.")
elif "@" not in email:
st.error("올바른 이메일 형식이 아닙니다.")
else:
st.success(f"🎉 환영합니다, {name}님! 등록이 완료됐습니다.")
st.json({
"이름": name,
"이메일": email,
"나이": age,
"부서": department
})
실행 로드맵 (지금 당장 시작하기)
💡 초보자를 위한 추천 순서: 한꺼번에 다 하려고 하지 않는다. 단계별로 하나씩 적용해보자.
즉시 적용 가능 (오늘 30분)
- ✅ Streamlit 1.48.0으로 업데이트
- ✅ 기존 위젯에 help="설명" 추가
- ✅ .streamlit/config.toml 만들어서 색상 설정
- ✅ Matplotlib 차트를 Plotly로 교체
이번 주 목표 (1~2시간)
- ✅ 사이드바로 필터 구성
- ✅ Material 아이콘 적용
- ✅ st.multiselect를 st.pills로 교체
- ✅ 커스텀 KPI 카드 만들기
이번 달 목표 (2~4시간)
- ✅ assets/styles.css 만들어서 버튼 커스터마이징
- ✅ 로고 만들어서 사이드바에 추가
- ✅ @st.cache_data로 성능 최적화
- ✅ st.session_state로 상태 관리 구현
마무리
최고의 Streamlit 앱은 Streamlit 앱처럼 보이지 않는다.
제품처럼 보이고, 의도적으로 느껴지고, 사려 깊은 디자인으로 실제 문제를 해결한다.
💡 초보자에게 드리는 마지막 조언: 처음부터 완벽하게 만들려고 하지 않아도 된다. 일단 기능이 작동하는 앱을 만들고, 이 팁들을 하나씩 적용해 나가면 어느 순간 "이게 Streamlit이야?"라는 말을 듣게 될 것이다. 배움은 항상 작은 것부터 시작된다.
Essential Resources
Official Documentation
- Streamlit Docs: https://docs.streamlit.io
- API Reference: https://docs.streamlit.io/library/api-reference
- Cheat Sheet: https://docs.streamlit.io/library/cheatsheet
- config.toml Guide: https://docs.streamlit.io/library/advanced-features/configuration
Design Resources
- Material Icons: https://fonts.google.com/icons
- CSS Gradients: https://cssgradient.io
- Color Palettes: https://coolors.co
- Google Fonts: https://fonts.google.com
Visualization Libraries
- Plotly Python: https://plotly.com/python/
- Plotly Express: https://plotly.com/python/plotly-express/
- Altair: https://altair-viz.github.io
- Bokeh: https://docs.bokeh.org
Community & Learning
- Streamlit Forum: https://discuss.streamlit.io
- Streamlit Gallery: https://streamlit.io/gallery
- Streamlit Components: https://streamlit.io/components
- GitHub Examples: https://github.com/streamlit/streamlit/tree/develop/examples
Deployment
- Streamlit Cloud: https://streamlit.io/cloud
- Docker Deployment: https://docs.streamlit.io/knowledge-base/tutorials/deploy/docker
- AWS/GCP/Azure Guides: https://docs.streamlit.io/knowledge-base/tutorials/deploy
'최신 IT' 카테고리의 다른 글
| 5 Data Ingestion Methods (0) | 2026.06.18 |
|---|---|
| OLAP Tools 3가지 비교 : Databricks, Snowflake, BigQuery (0) | 2026.06.04 |
| 로컬 데이터 분석 도구 — Python + Streamlit (1) | 2026.04.10 |
| n8n + Google Sheets로 LinkedIn 포스팅을 자동화한 방법 (0) | 2026.03.10 |
| Node.js 에 대한 고찰 (5) | 2026.01.21 |