7장. 웹 UI 연동과 배포
비밀 값: st.secrets 와 .env
식당 금고 열쇠는 메뉴판에 적어 두지 않습니다. 사장이 집에서는 서랍에, 가게에서는 금고 옆 열쇠함에 따로 둡니다. 두는 곳은 달라도 "열쇠는 메뉴판(누구나 보는 것)에 적지 않는다"는 규칙은 같습니다. API 키도 마찬가지입니다. 2장에서 키를 코드에 적지 않고 .env 에 두었습니다. 웹에 앱을 올리면 .env 를 올리지 않으므로, 클라우드에서는 Streamlit 의 비밀 값 칸(Secrets)에 키를 넣고 st.secrets 로 읽습니다.
세 상자가 키를 두는 세 곳입니다. 왼쪽 파랑 상자는 2장부터 써 온 .env, 가운데 보라 상자는 Streamlit 이 내 PC 에서 읽는 .streamlit/secrets.toml, 오른쪽 하늘 상자는 Streamlit Community Cloud 의 앱 설정에 있는 Secrets 칸입니다. 셋 다 GitHub 에 올리지 않는다는 점이 같습니다. 여기서 메뉴판이 GitHub 에 올리는 코드, 서랍이 .env, 가게의 열쇠함이 클라우드의 Secrets 칸입니다. 비유와 다른 점은, 클라우드의 Secrets 칸에 적은 값은 우리가 열쇠함을 열지 않아도 앱이 돌 때 저절로 st.secrets 에 들어간다는 것입니다.
키를 두는 곳
| 곳 | 파일과 위치 | 앱에서 읽는 법 | GitHub 에 |
|---|---|---|---|
| 내 PC(2장부터) | 과목 폴더의 .env | load_dotenv(find_dotenv(usecwd=True)) → os.getenv("OPENAI_API_KEY") | 올리지 않는다(.gitignore) |
| 내 PC(Streamlit 방식) | 앱 폴더의 .streamlit/secrets.toml | st.secrets["OPENAI_API_KEY"] | 올리지 않는다(.gitignore) |
| Streamlit Community Cloud | 앱 설정의 Secrets 칸(TOML 꼴로 붙여 넣는다) | st.secrets["OPENAI_API_KEY"] | 처음부터 저장소 밖에 있다 |
Streamlit 공식 문서에 따르면 secrets.toml 의 맨 위 칸(제목 […] 아래가 아닌 곳)에 적은 값은 환경 변수로도 들어갑니다. 그래도 이 장의 앱은 뜻이 분명하게 st.secrets 를 먼저 보고, 없으면 .env 를 읽게 씁니다. secrets.toml 의 꼴은 아래와 같습니다. 값은 자리표시이고 진짜 키는 여러분이 넣습니다.
# .streamlit/secrets.toml (TOML 꼴. 이 파일은 GitHub 에 올리지 않는다)
OPENAI_API_KEY = "발급받은_인증키"
OPENAI_MODEL = "gpt-4.1-mini"짧은 예시
st.secrets 에서 먼저 찾고 없으면 환경 변수에서 찾는 함수를 앱에 넣고, AppTest 의 secrets 로 Secrets 칸을 흉내 냅니다. 키 자체는 화면에 찍지 않습니다.
from streamlit.testing.v1 import AppTest
APP = """
import os
import streamlit as st
def get_api_key():
try:
# 클라우드의 Secrets 칸이나 .streamlit/secrets.toml 에 적은 값
return st.secrets["OPENAI_API_KEY"], "Secrets"
except (FileNotFoundError, KeyError):
# secrets.toml 이 없으면 FileNotFoundError, 이름이 없으면 KeyError 가 난다
return os.getenv("OPENAI_API_KEY"), "환경 변수"
key, where = get_api_key()
# 키 자체는 화면에 찍지 않는다. 어디서 읽었는지와 찾았는지만 보인다
st.write(f"읽은 곳: {where}, 찾았나: {bool(key)}")
"""
at = AppTest.from_string(APP).run()
print("secrets.toml 이 없을 때 →", at.markdown[0].value)
at = AppTest.from_string(APP)
# AppTest 의 secrets: 시험할 때만 쓰는 Secrets 칸. 진짜 키 대신 시험용 값을 넣는다
at.secrets["OPENAI_API_KEY"] = "시험용-가짜-값"
at.run()
print("Secrets 에 넣었을 때 →", at.markdown[0].value)secrets.toml 이 없을 때 → 읽은 곳: 환경 변수, 찾았나: False
Secrets 에 넣었을 때 → 읽은 곳: Secrets, 찾았나: True첫 시험은 secrets.toml 도 환경 변수도 없어 "찾았나: False" 입니다. 이 예시는 load_dotenv 를 부르지 않으므로 .env 가 있어도 읽지 않습니다. 둘째 시험은 Secrets 칸에 값이 있어 그것을 읽었습니다. st.secrets 는 파일이 없으면 FileNotFoundError 를 내므로 try 로 감싸야 내 PC 와 클라우드에서 같은 코드가 돕니다.
코드가 의미하는 것
이 코드는 금고 열쇠를 가게의 열쇠함에서 먼저 찾고, 없으면 다른 곳에서 찾는 규칙을 함수 하나로 적은 것입니다. 열쇠함이 빈 경우와 찬 경우를 차례로 시험합니다.
| 코드 | 하는 일 | 챗봇에서 뜻하는 것 |
|---|---|---|
st.secrets["OPENAI_API_KEY"](앱 안) | Secrets 칸이나 secrets.toml 에서 키를 꺼낸다 | 클라우드에 올린 챗봇이 키를 읽는 길입니다 |
except (FileNotFoundError, KeyError):(앱 안) | 파일이 없거나 이름이 없을 때 잡는다 | secrets.toml 이 없는 내 PC 에서도 앱이 멈추지 않게 합니다 |
os.getenv("OPENAI_API_KEY")(앱 안) | 환경 변수에서 키를 찾는다 | 2장부터 써 온 길로 넘어가는 자리입니다 |
st.write(f"읽은 곳: {where}, 찾았나: {bool(key)}")(앱 안) | 키 대신 읽은 곳과 찾았는지만 쓴다 | 화면과 로그에 키를 남기지 않는 습관입니다 |
at.secrets["OPENAI_API_KEY"] = "시험용-가짜-값" | 시험용 Secrets 칸에 값을 넣는다 | 진짜 키 없이 "클라우드에서 연 경우"를 흉내 냅니다 |
결과에서 읽을 것은 두 가지입니다.
- Secrets 가 비면 환경 변수로 넘어갑니다. 첫 줄은 "읽은 곳: 환경 변수, 찾았나: False" 입니다.
- Secrets 에 값이 있으면 그것을 씁니다. 둘째 줄은 "읽은 곳: Secrets, 찾았나: True" 입니다. 같은 앱 코드 하나로 두 곳을 모두 다룹니다.
주의할 점
secrets.toml이 없을 때st.secrets.get("OPENAI_API_KEY")를 부르면None이 아니라 오류가 납니다. 이 교안에서 확인한 판(Streamlit 1.66.1)의 동작이라try·except로 감쌌습니다.- 앱 폴더에
.streamlit/secrets.toml을 만들었으면.gitignore에.streamlit/secrets.toml을 꼭 적습니다. 남과 나누는 견본은 값을발급받은_인증키로 바꾼secrets.toml.example로 둡니다. - 2장에서 본 것처럼 키는 화면에 찍지 않습니다. 화면이나 로그에 남기는 것은 "어디서 읽었나", "찾았나" 정도로 그칩니다.
- 웹에 올린 챗봇은 주소를 아는 누구나 쓸 수 있고, 손님의 질문마다 키의 주인에게 요금이 붙습니다. 수업용 키로 앱을 공개해 두지 않습니다.