7장. 웹 UI 연동과 배포
실습 1 — 첫 화면 만들고 다시 실행 세기
구름다리여행 여행 비서의 첫 화면을 앱 파일(out_ch07_hello.py)로 저장하고, AppTest 로 브라우저 없이 열어 봅니다. 이름을 적고 여행지를 고르고 단추를 누르는 손님의 동작을 코드로 흉내 내며, 스크립트가 몇 번 다시 돌았는지와 보통 변수가 어떻게 되는지 셉니다. 모델은 부르지 않으므로 API 키가 없어도 됩니다.
1단계. 앱 파일 쓰기
from pathlib import Path
# 앱 코드. r''' ''' 은 역빗금(\)을 글자 그대로 두는 글자열이라 앱 코드를 그대로 담기 좋다
APP_CODE = r'''
import streamlit as st
# set_page_config: 브라우저 탭에 보일 제목. 화면 코드 가운데 맨 먼저 부른다
st.set_page_config(page_title="구름다리여행 여행 비서")
st.title("구름다리여행 여행 비서")
st.write("여행 조건을 고르고 이름을 적은 뒤 단추를 눌러 보세요.")
# 다시 돌아도 남는 메모장에 '화면을 그린 횟수'를 센다
if "runs" not in st.session_state:
st.session_state.runs = 0
st.session_state.runs += 1
clicks = 0 # 보통 변수: 다시 돌 때마다 0 에서 시작한다
# with st.sidebar: 이 안의 부품은 왼쪽 사이드바에 놓인다
with st.sidebar:
st.header("여행 조건")
city = st.selectbox("여행지", ["제주도", "부산", "강릉"])
nights = st.slider("박수", 1, 5, 2)
# placeholder: 빈칸에 흐리게 보이는 안내 글
name = st.text_input("이름", placeholder="예: 김구름")
if st.button("인사하기"):
clicks += 1
st.success(f"{name} 님, {city} {nights}박 {nights + 1}일 여행을 도와드릴게요.")
st.caption(f"화면을 그린 횟수(session_state): {st.session_state.runs} / 보통 변수 clicks: {clicks}")
'''
# 앱 파일로 저장한다. 이 파일을 streamlit run 으로 열면 브라우저에 화면이 뜬다
Path("out_ch07_hello.py").write_text(APP_CODE.lstrip(), encoding="utf-8")
print("저장: out_ch07_hello.py,", len(APP_CODE.strip().splitlines()), "줄")저장: out_ch07_hello.py, 26 줄코드가 의미하는 것 APP_CODE 글자열이 키오스크 화면 전체이고, write_text 로 out_ch07_hello.py 에 옮겨 적으면 그 파일이 곧 Streamlit 앱이 됩니다. 앱 안에서 st.session_state.runs 는 다시 실행을 건너 쌓이는 횟수, clicks 는 실행마다 0 에서 시작하는 보통 변수로, 둘을 일부러 나란히 두어 차이를 셉니다. with st.sidebar: 안의 고르기 상자와 밀대는 챗봇의 설정 칸이 놓일 자리를 미리 보여 줍니다.
2단계. AppTest 로 첫 화면 열기
from streamlit.testing.v1 import AppTest
# from_file: 앱 파일을 브라우저 없이 연다. run(): 스크립트를 위에서 아래로 한 번 실행한다
at = AppTest.from_file("out_ch07_hello.py").run()
print("제목:", at.title[0].value)
# at.sidebar: 사이드바 안의 부품만 모아 본다
print("사이드바:", [(w.label, w.value) for w in at.sidebar.selectbox], [(w.label, w.value) for w in at.sidebar.slider])
print("본문 입력칸:", [w.label for w in at.text_input], "/ 단추:", [b.label for b in at.button])
print("인사 글:", len(at.success), "개")
# at.main: 사이드바를 뺀 본문 쪽 부품
print(at.main.caption[0].value)제목: 구름다리여행 여행 비서
사이드바: [('여행지', '제주도')] [('박수', 2)]
본문 입력칸: ['이름'] / 단추: ['인사하기']
인사 글: 0 개
화면을 그린 횟수(session_state): 1 / 보통 변수 clicks: 0코드가 의미하는 것 AppTest.from_file(...).run() 은 streamlit run 으로 앱을 띄우고 브라우저를 처음 연 순간과 같습니다. at.sidebar 와 at.main 으로 사이드바와 본문을 나눠 읽으므로, 부품이 어느 쪽에 놓였는지까지 코드로 확인합니다. 첫 실행이라 위젯은 처음 값을 돌려주고 단추는 눌리지 않은 상태입니다.
3단계. 손님처럼 고르고 단추 누르기
# 손님이 하는 일을 흉내 낸다: 이름 적기, 여행지·박수 고르기, 단추 누르기
at.text_input[0].input("김구름") # input: 입력칸에 글을 적는다
at.sidebar.selectbox[0].select("부산") # select: 목록에서 고른다
at.sidebar.slider[0].set_value(3) # set_value: 밀대를 옮긴다
at.button[0].click().run() # 바꾼 것을 모아 단추를 누르고 다시 실행한다
print("인사 글:", at.success[0].value)
print(at.main.caption[0].value)인사 글: 김구름 님, 부산 3박 4일 여행을 도와드릴게요.
화면을 그린 횟수(session_state): 2 / 보통 변수 clicks: 1코드가 의미하는 것 input(), select(), set_value() 는 손님의 손놀림을 흉내 내고, 마지막 click().run() 이 단추를 누른 다시 실행 한 번을 일으킵니다. 이 실행에서만 st.button 이 True 가 되어 인사 글이 그려지고 clicks 가 1 이 됩니다. 챗봇에서 "보내기"처럼 한 번만 일어나야 할 일이 이 자리에 들어갑니다.
4단계. 아무것도 누르지 않고 다시 그리기
# 단추를 누르지 않고 한 번 더 그린다(브라우저에서 다른 칸을 만진 것과 같다)
at.run()
print("인사 글:", len(at.success), "개")
print("이름 칸에 남은 값:", at.text_input[0].value, "/ 여행지:", at.sidebar.selectbox[0].value)
print(at.main.caption[0].value)
print("메모장:", {"runs": at.session_state["runs"]})인사 글: 0 개
이름 칸에 남은 값: 김구름 / 여행지: 부산
화면을 그린 횟수(session_state): 3 / 보통 변수 clicks: 0
메모장: {'runs': 3}코드가 의미하는 것 단추 없이 at.run() 만 부르는 것은 손님이 다른 칸을 만져 화면이 다시 그려진 경우입니다. 이때 위젯의 값과 st.session_state 는 남고, 단추의 True 와 보통 변수의 값은 사라집니다. 대화 기록을 st.session_state 에 두어야 하는 까닭이 이 차이입니다.
이 실습이 뜻하는 것
이 실습은 키오스크를 설치하고, 직원이 손님인 척 단추를 눌러 보며 주문서가 몇 번 새로 쓰였는지 세어 보는 일과 같습니다. 화면을 짜고(1단계), 처음 켜 보고(2단계), 손님처럼 고르고 누르고(3단계), 아무것도 누르지 않은 채 한 번 더 봅니다(4단계). 여기서 키오스크가 out_ch07_hello.py, 손님 역할을 하는 직원이 AppTest, 주문서를 새로 쓴 횟수가 st.session_state.runs 입니다. 비유와 다른 점은, 키오스크는 손님이 누른 단추 하나만 처리하지만 Streamlit 은 단추 하나에도 스크립트 전체를 다시 돈다는 것입니다.
| 단계 | 출력 | 뜻 |
|---|---|---|
| 1. 앱 파일 | 26 줄 | 제목, 사이드바, 입력칸, 단추를 갖춘 화면이 파이썬 파일 한 장입니다 |
| 2. 첫 화면 | 제주도, 2, 인사 글 0개, 횟수 1 / clicks 0 | 아무것도 고르지 않아 위젯은 처음 값을 돌려주었고, 단추를 누르지 않아 인사 글이 없습니다 |
| 3. 고르고 누르기 | "김구름 님, 부산 3박 4일 …", 횟수 2 / clicks 1 | 고른 값이 인사 글에 들어갔고, 스크립트가 한 번 더 돌았습니다 |
| 4. 다시 그리기 | 인사 글 0개, 김구름·부산 그대로, 횟수 3 / clicks 0 | 단추는 다음 실행에서 False 로 돌아가고, 위젯 값과 메모장은 남고, 보통 변수는 0 으로 돌아갔습니다 |
한 줄로 줄이면, Streamlit 앱은 손님이 무엇을 만지든 맨 위부터 다시 돌고, 그때 남는 것은 위젯의 값과 st.session_state 뿐이라는 것을 센 실습입니다. 화면을 세 번 그리는 동안 runs 는 1, 2, 3 으로 늘었지만 보통 변수 clicks 는 단추를 누른 실행에서만 1 이고 다음 실행에서 0 으로 돌아갔습니다. 인사 글도 단추를 누른 실행에서만 나오고 4단계에서는 사라졌습니다. 그래서 채팅 화면의 대화 기록처럼 실행을 건너 남아야 하는 것은 실습 2부터 모두 st.session_state 에 둡니다.
코드 설명
AppTest.from_file("out_ch07_hello.py").run()은 앱을 브라우저 없이 한 번 돌리고, 화면에 놓인 부품을at.title,at.text_input,at.button처럼 종류별 목록으로 담아 둡니다.at.sidebar.selectbox와at.main.caption은 부품을 사이드바 쪽과 본문 쪽으로 나눠 찾습니다. 그냥at.caption은 두 쪽을 모두 모읍니다.input(),select(),set_value()는 손님의 동작을 흉내 내지만 바로 다시 돌지는 않습니다. 마지막의.run()이 바꾼 것을 모아 한 번에 다시 실행합니다. 브라우저에서 손님이 하나씩 만질 때는 만질 때마다 다시 돕니다.if "runs" not in st.session_state:는 처음 열었을 때만 0 을 적습니다. 이 줄 덕분에runs는 다시 돌 때마다 1 씩 늘고, 보통 변수clicks는 늘 0 이나 1 입니다.- 실습이 끝나면 터미널에서
streamlit run out_ch07_hello.py를 실행해(data 폴더에서) 같은 화면을 브라우저로 열어 보세요.