티스토리 뷰
작성일: 2026년 01월 09일
카테고리: Python, Developer Tools, Testing
키워드: Streamlit, Python, API Testing, Developer Experience, Internal Tools
요약
API 테스트 도구를 Postman이나 Insomnia 없이 Python만으로 만들 수 있다. Streamlit은 데이터 앱 구축 프레임워크로, 50줄 이내의 코드로 세션 관리와 API 호출 UI를 갖춘 테스트 도구를 만들 수 있다. 이 글에서는 Streamlit의 핵심 기능을 살펴보고, 실제 API Integration Tester 구현 사례를 통해 실무에서 활용하는 방법을 다룬다.
왜 Streamlit인가?
Streamlit이란
Streamlit은 Python 스크립트를 인터랙티브 웹 애플리케이션으로 변환하는 프레임워크다. 2019년 오픈소스로 공개되었고, 2022년 Snowflake에 인수되었다. 원래 데이터 사이언티스트가 머신러닝 모델 데모나 데이터 대시보드를 빠르게 만들 수 있도록 설계되었다.
핵심 가치는 "Python만으로 웹 UI를 만든다"는 것이다. HTML, CSS, JavaScript 없이 Python 코드만으로 버튼, 입력 필드, 차트, 테이블이 있는 웹앱을 구축할 수 있다. 이 특성이 데이터 앱뿐 아니라 내부 도구 개발에도 적합하다.
내부 도구 개발의 선택지
개발 팀에서 API 테스트 도구가 필요할 때, 선택지는 몇 가지로 압축된다.
| 선택지 | 장점 | 단점 |
|---|---|---|
| Postman/Insomnia | 완성도 높은 UI | 팀 공유 시 라이선스 비용, 커스터마이징 한계 |
| curl + 쉘 스크립트 | 가볍고 빠름 | UI 없음, 비개발자 사용 불가 |
| React/Vue 웹앱 | 완전한 커스터마이징 | 프론트엔드 개발 리소스 필요 |
| Streamlit | Python만으로 UI 구축 | 복잡한 인터랙션에는 부적합 |
Streamlit은 "Python 코드만으로 웹 UI를 만든다"는 단순한 전제에서 출발한다. 프론트엔드 지식 없이 데이터 사이언티스트가 대시보드를 만들 수 있도록 설계되었지만, 이 특성이 내부 도구 개발에 적합하다.
아키텍처 관점에서의 Streamlit
Streamlit의 실행 모델은 전통적인 웹 프레임워크와 다르다.
sequenceDiagram
participant User as 사용자
participant Browser as 브라우저
participant Server as Streamlit Server
participant Script as Python Script
User->>Browser: 버튼 클릭
Browser->>Server: WebSocket 이벤트
Server->>Script: 전체 스크립트 재실행
Script->>Server: UI 위젯 반환
Server->>Browser: 화면 업데이트
Browser->>User: 결과 표시
핵심은 "사용자 인터랙션마다 전체 스크립트가 재실행된다"는 점이다. React의 Virtual DOM이나 Vue의 반응형 시스템과 달리, Streamlit은 단순히 위에서 아래로 스크립트를 다시 실행한다. 이 단순함이 학습 곡선을 낮추지만, 상태 관리에서는 별도의 패턴이 필요하다.
적합한 사용 사례
Streamlit이 효과적인 영역:
- 데이터 대시보드: 차트, 테이블, 필터 조합
- 내부 관리 도구: CRUD 인터페이스, 로그 뷰어
- API 테스트 클라이언트: 요청/응답 시각화
- 프로토타입: 아이디어 검증용 빠른 UI
Streamlit이 부적합한 영역:
- 실시간 협업 도구: 복잡한 상태 동기화 필요
- 고도화된 인터랙션: 드래그앤드롭, 복잡한 폼 검증
- 공개 서비스: 성능과 보안 요구사항이 높은 경우
핵심 기능과 시작하기
설치와 첫 실행
pip install streamlit
첫 번째 앱을 만들어보자.
# app.py
import streamlit as st
st.title("API Tester")
st.write("Streamlit으로 만든 첫 번째 테스트 도구")
url = st.text_input("API URL", "https://api.example.com/health")
method = st.selectbox("Method", ["GET", "POST", "PUT", "DELETE"])
if st.button("Send Request"):
st.success(f"{method} 요청을 {url}로 전송")
실행:
streamlit run app.py
브라우저에서 http://localhost:8501이 자동으로 열린다. 12줄의 코드로 입력 필드, 드롭다운, 버튼이 있는 UI가 완성된다.
레이아웃 구성
실제 도구에서는 레이아웃 구성이 중요하다. Streamlit은 사이드바, 컬럼, 탭을 제공한다.
import streamlit as st
# 사이드바: 설정, 네비게이션
with st.sidebar:
st.header("Settings")
base_url = st.text_input("Base URL", "https://api.example.com")
timeout = st.slider("Timeout (sec)", 1, 30, 10)
# 메인 영역: 탭으로 구분
tab1, tab2, tab3 = st.tabs(["Request", "Response", "History"])
with tab1:
col1, col2 = st.columns([3, 1])
with col1:
endpoint = st.text_input("Endpoint", "/users")
with col2:
method = st.selectbox("Method", ["GET", "POST"])
with tab2:
st.json({"status": "waiting for request"})
with tab3:
st.write("No history yet")
레이아웃 구조:
+------------------+--------------------------------+
| Sidebar | Main Area |
| | +----------------------------+ |
| [Settings] | | Tab1 | Tab2 | Tab3 | |
| - Base URL | +----------------------------+ |
| - Timeout | | | |
| | | [Endpoint] [Method] | |
| | | [Content Area] | |
| | | | |
+------------------+--------------------------------+상태 관리: st.session_state
Streamlit은 스크립트가 재실행될 때마다 변수가 초기화된다. 이 문제를 해결하는 것이 st.session_state다.
import streamlit as st
# 세션 상태 초기화
if "request_history" not in st.session_state:
st.session_state.request_history = []
if "auth_token" not in st.session_state:
st.session_state.auth_token = None
# 상태 읽기/쓰기
def add_to_history(request_data):
st.session_state.request_history.append(request_data)
def set_auth_token(token):
st.session_state.auth_token = token
# 상태 기반 UI 렌더링
if st.session_state.auth_token:
st.success(f"Authenticated: {st.session_state.auth_token[:20]}...")
else:
st.warning("Not authenticated")
st.session_state는 딕셔너리처럼 동작하며, 브라우저 탭이 열려 있는 동안 값을 유지한다.
캐싱으로 성능 최적화
API 호출이나 데이터 로딩은 비용이 크다. @st.cache_data와 @st.cache_resource로 캐싱한다.
import streamlit as st
import requests
# 데이터 캐싱: 동일한 인자로 호출하면 캐시된 결과 반환
@st.cache_data(ttl=300) # 5분간 캐시
def fetch_api_data(url: str, method: str):
response = requests.request(method, url, timeout=10)
return {
"status_code": response.status_code,
"body": response.json() if response.headers.get("content-type", "").startswith("application/json") else response.text,
"headers": dict(response.headers)
}
# 리소스 캐싱: DB 커넥션, 모델 등 한 번만 생성
@st.cache_resource
def get_http_session():
session = requests.Session()
session.headers.update({"User-Agent": "StreamlitTester/1.0"})
return session
| 데코레이터 | 용도 | 예시 |
|---|---|---|
@st.cache_data |
직렬화 가능한 데이터 | API 응답, DataFrame |
@st.cache_resource |
직렬화 불가능한 객체 | DB 커넥션, ML 모델 |
실제 사례: API Integration Tester
실제 프로젝트에서 구현한 API Integration Tester를 살펴보자. 이 도구는 세션 기반 인증이 필요한 외부 API를 테스트하기 위해 만들었다.
요구사항 분석
테스트 대상 API의 특성:
- 세션 기반 인증: 로그인 후 세션 정보(테넌트 ID, Portal ID 등)를 모든 요청에 포함
- 복잡한 요청 구조: 회사번호, 회계연도 등 다중 파라미터
- 응답 검증: JSON 구조 확인, 특정 필드 값 검증
프로젝트 구조
integration-tester/
├── app.py # 메인 앱
├── api_client.py # API 호출 래퍼
├── session_manager.py # 세션 관리
└── requirements.txt세션 관리 구현
# session_manager.py
from dataclasses import dataclass
from datetime import datetime
from typing import Optional
import streamlit as st
@dataclass
class SessionInfo:
tenant_id: str
portal_id: str
company_no: str
company_code: str
access_token: str
created_at: datetime
expires_at: datetime
def is_expired(self) -> bool:
return datetime.now() > self.expires_at
def to_headers(self) -> dict:
return {
"Authorization": f"Bearer {self.access_token}",
"X-Tenant-Id": self.tenant_id,
"X-Portal-Id": self.portal_id,
"X-Company-No": self.company_no
}
def init_session_state():
"""세션 상태 초기화"""
if "session_info" not in st.session_state:
st.session_state.session_info = None
if "api_history" not in st.session_state:
st.session_state.api_history = []
def get_session() -> Optional[SessionInfo]:
"""현재 세션 정보 반환"""
session = st.session_state.session_info
if session and session.is_expired():
st.session_state.session_info = None
return None
return session
def set_session(session: SessionInfo):
"""세션 정보 저장"""
st.session_state.session_info = session
API 클라이언트 구현
# api_client.py
import requests
from typing import Optional, Any
from datetime import datetime
import streamlit as st
class APIClient:
def __init__(self, base_url: str):
self.base_url = base_url.rstrip("/")
self.session = requests.Session()
def request(
self,
method: str,
endpoint: str,
headers: Optional[dict] = None,
params: Optional[dict] = None,
json_body: Optional[dict] = None,
timeout: int = 30
) -> dict:
"""API 요청 실행 및 결과 반환"""
url = f"{self.base_url}/{endpoint.lstrip('/')}"
start_time = datetime.now()
try:
response = self.session.request(
method=method,
url=url,
headers=headers,
params=params,
json=json_body,
timeout=timeout
)
elapsed = (datetime.now() - start_time).total_seconds()
result = {
"success": True,
"status_code": response.status_code,
"elapsed_seconds": round(elapsed, 3),
"headers": dict(response.headers),
"body": self._parse_body(response)
}
except requests.RequestException as e:
elapsed = (datetime.now() - start_time).total_seconds()
result = {
"success": False,
"error": str(e),
"elapsed_seconds": round(elapsed, 3)
}
# 히스토리에 기록
self._add_to_history(method, endpoint, result)
return result
def _parse_body(self, response: requests.Response) -> Any:
content_type = response.headers.get("content-type", "")
if "application/json" in content_type:
try:
return response.json()
except ValueError:
return response.text
return response.text
def _add_to_history(self, method: str, endpoint: str, result: dict):
st.session_state.api_history.insert(0, {
"timestamp": datetime.now().isoformat(),
"method": method,
"endpoint": endpoint,
"status": result.get("status_code", "ERROR"),
"elapsed": result.get("elapsed_seconds", 0)
})
# 최근 50개만 유지
st.session_state.api_history = st.session_state.api_history[:50]
메인 앱 구현
# app.py
import streamlit as st
from datetime import datetime, timedelta
from session_manager import init_session_state, get_session, set_session, SessionInfo
from api_client import APIClient
# 페이지 설정
st.set_page_config(
page_title="API Integration Tester",
page_icon="🔧",
layout="wide"
)
# 세션 상태 초기화
init_session_state()
# API 클라이언트 초기화
@st.cache_resource
def get_client():
return APIClient("https://api.example.com")
client = get_client()
# === 사이드바: 설정 및 세션 정보 ===
with st.sidebar:
st.header("Session Info")
session = get_session()
if session:
st.success("Authenticated")
st.text(f"Tenant: {session.tenant_id}")
st.text(f"Company: {session.company_code}")
st.text(f"Expires: {session.expires_at.strftime('%H:%M:%S')}")
if st.button("Logout"):
st.session_state.session_info = None
st.rerun()
else:
st.warning("Not authenticated")
with st.form("login_form"):
st.subheader("Login")
tenant_id = st.text_input("Tenant ID")
portal_id = st.text_input("Portal ID")
company_no = st.text_input("Company No")
company_code = st.text_input("Company Code")
access_token = st.text_input("Access Token", type="password")
if st.form_submit_button("Login"):
if all([tenant_id, portal_id, company_no, company_code, access_token]):
new_session = SessionInfo(
tenant_id=tenant_id,
portal_id=portal_id,
company_no=company_no,
company_code=company_code,
access_token=access_token,
created_at=datetime.now(),
expires_at=datetime.now() + timedelta(hours=1)
)
set_session(new_session)
st.rerun()
else:
st.error("All fields are required")
st.divider()
st.header("Service Status")
# 서비스 상태 체크
if st.button("Check Health"):
result = client.request("GET", "/health")
if result["success"] and result["status_code"] == 200:
st.success("API Server: Online")
else:
st.error("API Server: Offline")
# === 메인 영역: 탭 구성 ===
st.title("API Integration Tester")
tab_api, tab_clients, tab_financial, tab_history = st.tabs([
"API Test", "Clients", "Financial Statements", "History"
])
# === API 테스트 탭 ===
with tab_api:
st.subheader("Custom API Request")
col1, col2 = st.columns([4, 1])
with col1:
endpoint = st.text_input("Endpoint", "/api/v1/users")
with col2:
method = st.selectbox("Method", ["GET", "POST", "PUT", "DELETE"])
# 요청 파라미터
with st.expander("Request Parameters", expanded=False):
params_text = st.text_area(
"Query Parameters (JSON)",
'{"page": 1, "limit": 10}',
height=100
)
if method in ["POST", "PUT"]:
body_text = st.text_area(
"Request Body (JSON)",
'{}',
height=150
)
if st.button("Send Request", type="primary"):
session = get_session()
if not session:
st.error("Please login first")
else:
import json
try:
params = json.loads(params_text) if params_text.strip() else None
body = json.loads(body_text) if method in ["POST", "PUT"] and body_text.strip() else None
except json.JSONDecodeError as e:
st.error(f"Invalid JSON: {e}")
params, body = None, None
if params is not None or body is not None or method == "GET":
with st.spinner("Sending request..."):
result = client.request(
method=method,
endpoint=endpoint,
headers=session.to_headers(),
params=params,
json_body=body
)
# 결과 표시
if result["success"]:
status = result["status_code"]
if 200 <= status < 300:
st.success(f"Status: {status} ({result['elapsed_seconds']}s)")
elif 400 <= status < 500:
st.warning(f"Status: {status} ({result['elapsed_seconds']}s)")
else:
st.error(f"Status: {status} ({result['elapsed_seconds']}s)")
st.subheader("Response Body")
st.json(result["body"])
with st.expander("Response Headers"):
st.json(result["headers"])
else:
st.error(f"Request failed: {result['error']}")
# === 수임처 조회 탭 ===
with tab_clients:
st.subheader("Client List")
session = get_session()
if not session:
st.info("Please login to view clients")
else:
col1, col2 = st.columns(2)
with col1:
year = st.selectbox("Year", [2026, 2025, 2024, 2023])
with col2:
status = st.selectbox("Status", ["All", "Active", "Inactive"])
if st.button("Load Clients"):
with st.spinner("Loading..."):
result = client.request(
method="GET",
endpoint="/api/v1/clients",
headers=session.to_headers(),
params={"year": year, "status": status.lower() if status != "All" else None}
)
if result["success"] and result["status_code"] == 200:
clients = result["body"].get("data", [])
if clients:
st.dataframe(
clients,
column_config={
"client_name": "Client Name",
"business_no": "Business No",
"status": "Status"
},
use_container_width=True
)
else:
st.info("No clients found")
else:
st.error("Failed to load clients")
# === 재무제표 조회 탭 ===
with tab_financial:
st.subheader("Financial Statements")
session = get_session()
if not session:
st.info("Please login to view financial statements")
else:
col1, col2, col3 = st.columns(3)
with col1:
client_code = st.text_input("Client Code")
with col2:
fiscal_year = st.selectbox("Fiscal Year", [2026, 2025, 2024])
with col3:
statement_type = st.selectbox(
"Statement Type",
["Balance Sheet", "Income Statement", "Cash Flow"]
)
if st.button("Load Statement"):
if not client_code:
st.error("Client code is required")
else:
type_map = {
"Balance Sheet": "BS",
"Income Statement": "IS",
"Cash Flow": "CF"
}
with st.spinner("Loading..."):
result = client.request(
method="GET",
endpoint=f"/api/v1/financial/{client_code}",
headers=session.to_headers(),
params={
"year": fiscal_year,
"type": type_map[statement_type]
}
)
if result["success"] and result["status_code"] == 200:
st.json(result["body"])
else:
st.error(f"Failed to load statement: {result.get('error', 'Unknown error')}")
# === 히스토리 탭 ===
with tab_history:
st.subheader("Request History")
history = st.session_state.api_history
if history:
st.dataframe(
history,
column_config={
"timestamp": st.column_config.DatetimeColumn("Time", format="HH:mm:ss"),
"method": "Method",
"endpoint": "Endpoint",
"status": "Status",
"elapsed": st.column_config.NumberColumn("Time (s)", format="%.3f")
},
use_container_width=True,
hide_index=True
)
if st.button("Clear History"):
st.session_state.api_history = []
st.rerun()
else:
st.info("No requests yet")
실행 결과
위 코드를 실행하면 다음과 같은 화면이 나타난다.
+-----------------------+----------------------------------------+
| Session Info | API Integration Tester |
| | |
| [!] Not authenticated | [API Test] [Clients] [Financial] [Hist]|
| | +------------------------------------+ |
| --- Login --- | | Endpoint: [/api/v1/users ] [GET]| |
| Tenant ID: [ ] | | | |
| Portal ID: [ ] | | > Request Parameters | |
| Company No: [ ] | | {"page": 1, "limit": 10} | |
| Company Code: [ ] | | | |
| Access Token: [*****] | | [Send Request] | |
| | | | |
| [Login] | | Response Body | |
| | | { "status": "ok" } | |
+-----------------------+----------------------------------------+배포와 다음 단계
Streamlit Community Cloud 배포
Streamlit은 자체 클라우드 서비스를 무료로 제공한다. GitHub 리포지토리를 연결하면 자동으로 배포된다.
배포 순서:
- GitHub에 코드 푸시
- share.streamlit.io 접속
- "New app" 클릭 후 리포지토리 선택
app.py경로 지정- "Deploy" 클릭
requirements.txt 파일이 필요하다.
streamlit>=1.40.0
requests>=2.31.0배포 후 https://{app-name}.streamlit.app 형태의 URL이 생성된다. 무료 티어에서는 앱이 일정 시간 사용되지 않으면 슬립 모드로 전환되며, 재접속 시 수 초간 로딩이 발생한다.
사내 배포 옵션
외부 클라우드 대신 사내에 배포하려면 Docker를 사용한다.
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8501
CMD ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"]
Kubernetes 환경이라면 Deployment와 Service로 배포할 수 있다.
확장 방향
이 튜토리얼에서 다루지 않은 고급 기능:
| 기능 | 설명 | 참고 자료 |
|---|---|---|
| Multi-page Apps | 여러 페이지로 앱 분리 | pages/ 디렉토리 사용 |
| Custom Components | React 기반 커스텀 위젯 | streamlit-component-template |
| Authentication | 로그인 기능 추가 | streamlit-authenticator |
| Database 연동 | PostgreSQL, MongoDB 연결 | @st.cache_resource 활용 |
학습 로드맵
Streamlit을 실무에 적용하기 위한 단계별 학습:
- 기초 (1-2일): 위젯, 레이아웃, 세션 상태
- 중급 (3-5일): 캐싱, Multi-page, 데이터베이스 연동
- 고급 (1-2주): Custom Components, 성능 최적화
공식 문서(docs.streamlit.io)가 잘 정리되어 있어 레퍼런스로 충분하다.
참고 자료
공식 문서
관련 블로그
- Python Streamlit GitHub Repository - 43K+ Stars
버전 정보
- 이 글은 Streamlit 1.40+ 기준으로 작성되었다
- 2025년 후반 릴리즈된
st.datetime_input, Custom Components v2는 별도 문서 참고
'실제 경험과 인사이트를 AI와 함께 정리한 글' 카테고리의 다른 글
| 배포와 운영: 프로덕션 가이드 (1) | 2026.01.09 |
|---|---|
| 월간 리포트 자동 생성 파이프라인 (0) | 2026.01.09 |
| 세무 분석 에이전트 구축 (0) | 2026.01.09 |
| GraphRAG: 지식그래프와 LLM의 시너지 (0) | 2026.01.09 |
| 세무 분석 규칙 SHACL로 정의하기 (0) | 2026.01.09 |
- Total
- Today
- Yesterday
- authentication
- GPT-5.1
- CLAUDE.md
- Tax Analysis
- troubleshooting
- knowledge graph
- claude code
- security
- api gateway
- AI agent
- SHACL
- AGENTS.md
- LLM
- Rag
- EnvironmentAgnostic
- react
- Claude Opus 4.5
- Next.js
- frontend
- Developer Tools
- Tailwind CSS
- authorization
- Kubernetes
- Development Tools
- AI
- backend
- Go
- architecture
- imprun.dev
- Ontology
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | ||||
| 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 11 | 12 | 13 | 14 | 15 | 16 | 17 |
| 18 | 19 | 20 | 21 | 22 | 23 | 24 |
| 25 | 26 | 27 | 28 | 29 | 30 | 31 |
