14장. Streamlit 화면과 Docker 컨테이너
실습 4 — 화면과 API 를 실제 서버로 띄우기
지금까지는 AppTest 로 화면을 이 프로그램 안에서 실행했습니다. 이번에는 API(8013 번 포트)와 화면(8501 번 포트)을 실제 서버 두 개로 띄워, 브라우저로 열 수 있는지 확인하고 끕니다. 실습 3의 파일이 있어야 합니다.
1단계. 두 서버 띄우기
import os, subprocess, sys, time, urllib.request
env = dict(os.environ, API_URL="http://127.0.0.1:8013") # 화면 서버에 줄 설정
api = subprocess.Popen([sys.executable, "-m", "uvicorn", "out_delay_api:app", "--port", "8013", "--log-level", "warning"])
# 터미널에서 "streamlit run out_app_api.py --server.port 8501" 을 실행한 것과 같다
ui = subprocess.Popen([sys.executable, "-m", "streamlit", "run", "out_app_api.py", "--server.port", "8501",
"--server.headless", "true", "--browser.gatherUsageStats", "false"],
env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
def wait(url, seconds=60):
"""주소가 답할 때까지 기다리고 걸린 시간을 돌려준다"""
t = time.perf_counter()
while time.perf_counter() - t < seconds:
try:
urllib.request.urlopen(url, timeout=2)
return time.perf_counter() - t
except OSError:
time.sleep(0.5)
raise TimeoutError(url)
print(f"API: {wait('http://127.0.0.1:8013/health'):.1f}초 · 화면: {wait('http://127.0.0.1:8501/_stcore/health'):.1f}초 만에 준비")API: 4.1초 · 화면: 0.0초 만에 준비2단계. 화면 서버가 주는 것 보기
html = urllib.request.urlopen("http://127.0.0.1:8501").read().decode()
print("상태 확인:", urllib.request.urlopen("http://127.0.0.1:8501/_stcore/health").read().decode())
print("첫 페이지:", len(html), "글자 ·", html[html.find("<title>"):html.find("</title>") + 8])
print("브라우저로 열 주소: http://127.0.0.1:8501")상태 확인: ok
첫 페이지: 6467 글자 · <title>Streamlit</title>
브라우저로 열 주소: http://127.0.0.1:85013단계. 두 서버 끄기
for name, proc in [("화면", ui), ("API", api)]:
proc.terminate()
proc.wait(timeout=10)
print(name, "꺼짐, 종료 코드", proc.returncode)화면 꺼짐, 종료 코드 1
API 꺼짐, 종료 코드 1아래 그림은 교안을 만든 PC 에서 같은 두 서버를 띄우고 브라우저로 열어, 실습 3과 같은 주문을 넣고 "예측하기"를 누른 화면을 찍은 것입니다. 화면을 본뜬 그림이 아니라 실제 화면의 캡처입니다.

이 실습이 뜻하는 것
이 실습은 본사와 매장 두 곳의 문을 실제로 여는 날과 같습니다. 두 곳의 문을 열고 간판 불이 켜질 때까지 기다리고(1단계), 매장 입구에 무엇이 놓였는지 들여다본 뒤(2단계), 두 곳의 문을 닫습니다(3단계). 여기서 문 열기가 서버 띄우기, 간판 불이 상태 확인 주소(/health, /_stcore/health), 가게의 번지가 포트 번호(8013, 8501)입니다. 비유와 다른 점은, 매장 입구에 놓인 것이 완성된 진열대가 아니라 빈 틀이라는 것입니다. 위젯은 브라우저가 들어온 뒤에 그려집니다.
| 단계 | 출력 | 뜻 |
|---|---|---|
| 1. 두 서버 | API: 4.1초 · 화면: 0.0초 만에 준비 | API 가 답할 때까지 4.1초 걸렸고, 그사이 화면 서버도 떠서 바로 답했습니다. 이 PC 에서 잰 시간입니다 |
| 2. 상태 확인 | ok | 화면 서버가 살아 있다는 답입니다 |
| 2. 첫 페이지 | 6467 글자, 페이지 제목 Streamlit | 처음 받는 페이지는 위젯이 없는 빈 틀입니다. 위젯은 브라우저가 이 페이지의 자바스크립트를 실행한 뒤 그립니다 |
| 2. 주소 | http://127.0.0.1:8501 | 이 주소를 브라우저로 열면 사람이 화면을 쓸 수 있습니다 |
| 3. 끄기 | 종료 코드 1 | Windows 에서 terminate() 가 프로그램을 강제로 끝냈다는 표시이고, 오류가 아닙니다 |
| 실제 화면 그림 | 지연 확률이 나온 화면 | 교안을 만든 PC 에서 같은 두 서버를 띄우고 브라우저로 같은 주문을 넣은 캡처입니다. 68.9% 와 "지연 예상"이 보입니다 |
한 줄로 줄이면, AppTest 대신 진짜 서버 둘을 띄워 사람이 브라우저로 쓸 수 있는 상태까지 가 본 실습입니다. 서버 둘을 손으로 하나씩 띄우고 끄는 일을 묶어 주는 것이 docker compose 이고, 실습 5와 실습 6에서 그 준비를 합니다. 띄운 곳은 이 PC 하나이고, 다른 PC 에서 접속하는 것이나 여러 사람이 함께 쓸 때의 속도는 아직 재지 않았습니다.
코드 설명
subprocess.Popen으로 API 와 화면을 각각 다른 프로그램으로 띄웠습니다. API 는 4.1초 만에 답했고, 그동안 화면 서버도 떠서 바로(0.0초) 답했습니다. Streamlit 서버의 상태 확인 주소는/_stcore/health입니다.- 화면 서버가 처음에 주는 것은 6,467글자의 빈 웹 페이지(제목 "Streamlit")입니다. 실제 위젯은 브라우저가 이 페이지의 자바스크립트를 실행한 뒤 서버와 웹소켓으로 주고받으며 그립니다. 그래서 화면을 시험할 때는 페이지를 받아 보는 것보다
AppTest가 편합니다. 3단계에서 끈 두 서버의 종료 코드가 1인 것은 Windows 에서terminate()가 프로그램을 강제로 끝내기 때문이며, 오류가 아닙니다. 아래의 실제 화면 그림도 같은 주문에 68.9% 와 "지연 예상"을 보입니다. --server.headless true는 서버를 띄울 때 브라우저를 저절로 열지 않게 하고,--browser.gatherUsageStats false는 Streamlit 이 사용 통계를 보내지 않게 합니다. 폐쇄망에서는 통계 끄기를 꼭 켭니다(11장).- 실제로 쓸 때는 터미널 두 개를 열어 하나에서
uvicorn out_delay_api:app --port 8013, 다른 하나에서streamlit run out_app_api.py를 실행하고, 브라우저로http://127.0.0.1:8501을 엽니다. 끌 때는 각 터미널에서 Ctrl+C 를 누릅니다.