이미지 로딩 중...
AI Generated
2025. 11. 8. · 3 Views
Python 챗봇 개발 6편 - Streamlit으로 챗봇 UI 구축하기
Streamlit을 활용하여 Python 챗봇에 전문적인 웹 UI를 구축하는 방법을 배웁니다. 세션 관리, 채팅 인터페이스, 실시간 스트리밍까지 실무에 필요한 모든 것을 다룹니다.
목차
1. Streamlit 기본 설정
시작하며
여러분이 멋진 챗봇 로직을 완성했는데, 사용자들에게 보여줄 방법이 터미널밖에 없다면? 아무리 뛰어난 AI라도 CLI 환경에서는 그 가치를 제대로 전달하기 어렵습니다.
바로 이럴 때 Streamlit이 빛을 발합니다. 단 몇 줄의 코드만으로 전문적인 웹 인터페이스를 만들 수 있죠.
Flask나 Django처럼 복잡한 설정 없이, Python 코드만으로 즉시 배포 가능한 웹앱을 만들 수 있습니다. 이번 카드에서는 Streamlit의 기본 설정부터 시작해서, 여러분의 챗봇에 생명을 불어넣는 방법을 알아보겠습니다.
개요
간단히 말해서, Streamlit은 데이터 과학자와 AI 개발자를 위한 웹 프레임워크입니다. HTML, CSS, JavaScript를 몰라도 Python만으로 인터랙티브한 웹 애플리케이션을 만들 수 있죠.
챗봇 UI를 구축할 때 Streamlit을 선택하는 이유는 명확합니다. 첫째, 개발 속도가 압도적으로 빠릅니다.
프로토타입을 몇 시간 만에 만들 수 있죠. 둘째, 자동 재실행 기능으로 코드를 수정하면 즉시 브라우저에 반영됩니다.
셋째, 배포가 간단합니다. Streamlit Cloud를 사용하면 GitHub 저장소만으로 무료 배포가 가능합니다.
기존에는 챗봇 UI를 만들려면 프론트엔드 개발자와 협업하거나 React, Vue 같은 프레임워크를 배워야 했습니다. 이제는 st.chat_message()와 st.chat_input() 같은 함수만으로 전문적인 채팅 인터페이스를 만들 수 있습니다.
Streamlit의 핵심 특징은 세 가지입니다: 선언적 프로그래밍 방식(원하는 결과를 코드로 표현), 자동 재실행(코드 변경 시 즉시 반영), 그리고 풍부한 위젯 라이브러리입니다. 이러한 특징들이 개발자 경험을 극대화하고, 빠른 반복 개발을 가능하게 합니다.
코드 예제
import streamlit as st
# 페이지 설정 - 반드시 최상단에 위치
st.set_page_config(
page_title="AI 챗봇",
page_icon="🤖",
layout="wide", # 넓은 레이아웃 사용
initial_sidebar_state="expanded" # 사이드바 펼침
)
# 타이틀과 설명
st.title("🤖 나만의 AI 챗봇")
st.markdown("**GPT 기반 대화형 AI 어시스턴트**")
# 구분선 추가
st.divider()
설명
이것이 하는 일: Streamlit 앱의 기본 골격을 만들고, 사용자가 처음 접하는 화면을 구성합니다. 페이지 설정부터 제목, 아이콘까지 챗봇의 첫인상을 결정하는 중요한 단계입니다.
첫 번째로, st.set_page_config()는 반드시 스크립트 최상단에 위치해야 합니다. 이 함수는 브라우저 탭의 제목, 파비콘, 레이아웃 모드를 설정합니다.
layout="wide"를 사용하면 화면을 더 넓게 활용할 수 있어, 챗봇처럼 대화가 길어지는 앱에 유리합니다. initial_sidebar_state="expanded"는 사이드바를 처음부터 펼쳐진 상태로 보여주어 설정 옵션을 바로 확인할 수 있게 합니다.
그 다음으로, st.title()과 st.markdown()으로 앱의 제목과 부제목을 설정합니다. 이모지를 활용하면 시각적으로 더 친근한 인터페이스를 만들 수 있죠.
Streamlit은 마크다운을 완벽하게 지원하므로, **굵은 글씨**, 기울임, 링크 등을 자유롭게 사용할 수 있습니다. st.divider()는 섹션 간 시각적 구분을 위한 수평선을 추가합니다.
작은 요소지만, UI의 가독성을 크게 향상시킵니다. 특히 채팅 영역과 입력 영역을 구분할 때 유용합니다.
여러분이 이 코드를 사용하면 단 10줄 이내로 전문적인 웹 앱의 기초를 완성할 수 있습니다. 코드를 저장하고 streamlit run app.py만 실행하면 즉시 브라우저에서 결과를 확인할 수 있죠.
개발 서버는 자동으로 코드 변경을 감지하여 페이지를 새로고침합니다. 실무에서는 page_icon에 회사 로고를 사용하거나, layout 설정으로 모바일 반응형을 고려할 수 있습니다.
또한 menu_items 파라미터로 About 메뉴를 커스터마이징하여 앱 정보나 도움말을 추가할 수 있습니다.
실전 팁
💡 set_page_config()는 반드시 첫 번째 Streamlit 명령이어야 합니다. import 문 다음에 바로 호출하세요. 그렇지 않으면 에러가 발생합니다.
💡 개발 중에는 layout="wide"를 사용하되, 모바일 사용자가 많다면 기본 레이아웃이 더 나을 수 있습니다. A/B 테스트를 고려해보세요.
💡 페이지 타이틀에 이모지를 넣으면 브라우저 탭에서 앱을 쉽게 찾을 수 있습니다. 하지만 너무 많이 사용하면 전문성이 떨어질 수 있으니 주의하세요.
💡 st.markdown()은 HTML도 지원합니다. unsafe_allow_html=True 옵션으로 커스텀 스타일링이 가능하지만, 보안 이슈를 주의해야 합니다.
💡 개발 중에는 st.set_page_config()에 initial_sidebar_state="collapsed"를 사용해 화면 공간을 확보하고, 프로덕션에서는 "expanded"로 변경하는 것도 좋은 전략입니다.
2. 세션 스테이트 관리
시작하며
여러분이 챗봇과 대화를 나누다가 페이지를 새로고침했더니 모든 대화 내역이 사라진다면? 사용자는 즉시 앱을 떠날 것입니다.
웹 애플리케이션에서 상태 관리는 필수입니다. Streamlit은 기본적으로 스크립트가 실행될 때마다 처음부터 다시 시작합니다.
사용자가 버튼을 클릭하거나 입력을 하면 전체 스크립트가 재실행되죠. 이는 단순한 대시보드에는 좋지만, 챗봇처럼 이전 상태를 기억해야 하는 앱에는 치명적입니다.
바로 이럴 때 필요한 것이 Session State입니다. 사용자별로 독립적인 저장 공간을 제공하여, 대화 내역, 사용자 설정, 임시 데이터를 안전하게 보관할 수 있습니다.
개요
간단히 말해서, Session State는 Streamlit 앱의 메모리입니다. Python 딕셔너리처럼 사용할 수 있으며, 페이지가 재실행되어도 데이터가 유지됩니다.
챗봇 개발에서 Session State가 필수인 이유는 명확합니다. 첫째, 대화 히스토리를 저장해야 합니다.
사용자와 AI의 모든 메시지를 리스트로 관리하죠. 둘째, 사용자별 컨텍스트를 유지해야 합니다.
여러 사용자가 동시에 접속해도 각자의 대화는 독립적으로 관리됩니다. 셋째, API 키나 설정값 같은 세션 데이터를 저장할 수 있습니다.
기존에는 쿠키나 로컬 스토리지를 사용해 클라이언트 측에 데이터를 저장했습니다. 이제는 st.session_state로 서버 측 세션 관리를 간단하게 구현할 수 있습니다.
별도의 데이터베이스나 Redis 없이도 말이죠. Session State의 핵심 특징은 세 가지입니다: 사용자별 격리(다른 사용자의 세션과 분리), 자동 직렬화(복잡한 객체도 저장 가능), 그리고 위젯 바인딩(입력 위젯과 자동 동기화)입니다.
이러한 특징들이 상태 관리를 간단하면서도 강력하게 만들어줍니다.
코드 예제
import streamlit as st
# 세션 스테이트 초기화 - 앱 시작 시 한 번만 실행
if "messages" not in st.session_state:
st.session_state.messages = [] # 대화 내역 저장
if "model" not in st.session_state:
st.session_state.model = "gpt-3.5-turbo" # 기본 모델
if "temperature" not in st.session_state:
st.session_state.temperature = 0.7 # 창의성 파라미터
# 세션 스테이트 사용 예시
st.write(f"현재 메시지 수: {len(st.session_state.messages)}")
st.write(f"사용 중인 모델: {st.session_state.model}")
설명
이것이 하는 일: 챗봇 앱의 상태를 초기화하고 관리합니다. 대화 내역, 모델 설정, 사용자 선택사항을 세션 동안 안전하게 보관하여, 끊김 없는 사용자 경험을 제공합니다.
첫 번째로, if "messages" not in st.session_state 패턴은 매우 중요합니다. 이 조건문은 해당 키가 세션에 없을 때만 초기화를 수행합니다.
스크립트는 사용자 인터랙션마다 재실행되지만, 이 패턴 덕분에 데이터는 처음 한 번만 초기화되고 이후에는 유지됩니다. messages 리스트는 모든 대화를 {"role": "user", "content": "안녕하세요"} 형태로 저장합니다.
그 다음으로, model과 temperature 같은 설정값도 세션에 저장합니다. 이렇게 하면 사용자가 사이드바에서 모델을 변경해도 그 선택이 유지됩니다.
temperature는 AI 응답의 창의성을 조절하는 파라미터로, 0에 가까울수록 일관적이고 1에 가까울수록 창의적인 답변을 생성합니다. 기본값 0.7은 대부분의 챗봇 용도에 적합합니다.
세션 스테이트는 딕셔너리처럼 동작하지만, 점 표기법(dot notation)과 딕셔너리 표기법을 모두 지원합니다. st.session_state.messages와 st.session_state["messages"]는 완전히 동일합니다.
개인적으로는 점 표기법이 더 직관적이지만, 동적 키 이름이 필요한 경우 딕셔너리 표기법을 사용해야 합니다. 중요한 점은 세션 스테이트가 브라우저 세션에 묶여있다는 것입니다.
사용자가 탭을 닫거나 세션이 만료되면 데이터는 사라집니다. 영구 저장이 필요하다면 데이터베이스나 파일 시스템에 별도로 저장해야 합니다.
여러분이 이 코드를 사용하면 챗봇의 "기억"을 만들 수 있습니다. 사용자가 "아까 뭐라고 했지?"라고 물어도 대답할 수 있는 것이죠.
실무에서는 메시지 개수 제한(예: 최근 50개만 유지)을 두어 메모리 사용량을 관리하고, 중요한 대화는 데이터베이스에 로깅하는 것이 좋습니다.
실전 팁
💡 세션 스테이트에 저장되는 모든 객체는 pickle 직렬화가 가능해야 합니다. OpenAI 클라이언트 객체 같은 것은 세션에 직접 저장하지 말고, 필요할 때마다 재생성하세요.
💡 st.session_state.messages에 메시지를 추가할 때는 .append()를 사용하되, 리스트 전체를 재할당하지 마세요. 재할당하면 Streamlit이 변경을 감지하지 못할 수 있습니다.
💡 디버깅 시 st.sidebar.write(st.session_state)로 전체 세션 상태를 확인할 수 있습니다. 개발 중에만 사용하고 프로덕션에서는 제거하세요.
💡 메시지 히스토리가 너무 길어지면 토큰 한계를 초과할 수 있습니다. 슬라이딩 윈도우 방식으로 최근 N개 메시지만 유지하거나, 요약 기법을 사용하세요.
💡 세션 스테이트 초기화 로직은 별도 함수로 분리하면 코드가 깔끔해집니다: initialize_session_state() 같은 함수를 만들어 앱 시작 부분에서 호출하세요.
3. 채팅 메시지 컨테이너
시작하며
여러분이 메신저 앱을 상상해보세요. 사용자 메시지는 오른쪽, 상대방 메시지는 왼쪽에 표시되죠.
프로필 아이콘도 있고, 말풍선 스타일도 다릅니다. 바로 이런 UX가 챗봇에도 필요합니다.
과거에는 이런 채팅 UI를 만들려면 CSS와 JavaScript로 복잡한 컴포넌트를 구현해야 했습니다. 메시지 정렬, 스크롤 처리, 반응형 디자인까지 고려하면 며칠이 걸리는 작업이었죠.
Streamlit의 chat_message 컨테이너는 이 모든 것을 한 줄로 해결합니다. 사용자와 AI 메시지를 자동으로 구분하고, 아바타도 표시하며, 모바일에서도 완벽하게 동작합니다.
개요
간단히 말해서, st.chat_message()는 채팅 스타일의 메시지를 표시하는 컨테이너입니다. 컨텍스트 매니저(context manager)로 사용하며, 내부에 텍스트, 코드, 이미지 등 모든 Streamlit 요소를 넣을 수 있습니다.
이 컨테이너가 강력한 이유는 세 가지입니다. 첫째, role 파라미터로 메시지 출처를 구분합니다.
"user", "assistant", "ai" 등의 역할을 지정하면 자동으로 다른 스타일이 적용됩니다. 둘째, 아바타를 자동 또는 수동으로 설정할 수 있습니다.
이모지, 이미지 URL, 로컬 파일 모두 지원합니다. 셋째, 마크다운, 코드 블록, 데이터프레임까지 모든 콘텐츠를 담을 수 있어 풍부한 응답을 제공할 수 있습니다.
기존에는 HTML과 CSS로 말풍선을 직접 만들고, JavaScript로 스크롤을 제어해야 했습니다. 이제는 Python의 with 문만으로 전문적인 채팅 UI를 완성할 수 있습니다.
채팅 메시지 컨테이너의 핵심은 "컨텍스트 기반 렌더링"입니다. with st.chat_message("user"): 블록 안의 모든 출력은 자동으로 사용자 메시지 스타일로 감싸집니다.
이는 코드 가독성과 유지보수성을 크게 향상시킵니다.
코드 예제
import streamlit as st
# 사용자 메시지 표시
with st.chat_message("user", avatar="🧑"):
st.write("Streamlit으로 챗봇을 만들고 있어요")
st.write("정말 간단하네요!")
# AI 어시스턴트 메시지 표시
with st.chat_message("assistant", avatar="🤖"):
st.write("네, Streamlit은 정말 훌륭한 도구입니다!")
st.code("st.chat_message('user')", language="python")
st.write("이렇게 간단하게 채팅 UI를 만들 수 있죠.")
# 커스텀 아바타 사용
with st.chat_message("assistant", avatar="https://example.com/bot-avatar.png"):
st.write("이미지 URL로 아바타를 커스터마이징할 수 있습니다.")
설명
이것이 하는 일: 사용자와 AI의 메시지를 채팅 앱처럼 표시합니다. 각 메시지는 역할에 따라 다른 스타일과 아바타를 가지며, 내부에 다양한 콘텐츠를 담을 수 있습니다.
첫 번째로, with st.chat_message("user") 구문은 컨텍스트 매니저를 생성합니다. 이 블록 안의 모든 Streamlit 명령은 사용자 메시지 컨테이너 안에 렌더링됩니다.
role 파라미터로 "user"를 지정하면 일반적으로 오른쪽 정렬되고 파란색 계열 스타일이 적용됩니다. "assistant"나 "ai"를 사용하면 왼쪽 정렬에 다른 색상이 적용되죠.
그 다음으로, avatar 파라미터는 메시지 옆에 표시될 아이콘을 설정합니다. 이모지를 사용하면 가장 간단하지만, 브랜드 아이덴티티를 위해 커스텀 이미지를 사용할 수도 있습니다.
이미지는 URL이나 로컬 파일 경로로 지정할 수 있으며, Streamlit이 자동으로 적절한 크기로 조정합니다. avatar를 지정하지 않으면 role에 따라 기본 아이콘이 표시됩니다.
중요한 점은 컨테이너 안에 여러 개의 Streamlit 요소를 넣을 수 있다는 것입니다. 단순 텍스트뿐만 아니라 st.code()로 코드 블록, st.dataframe()으로 데이터 테이블, st.image()로 이미지까지 표시할 수 있습니다.
이는 AI가 복잡한 분석 결과나 시각화를 응답에 포함할 수 있게 해줍니다. 실전에서는 메시지 컨테이너를 루프 안에서 생성하는 경우가 많습니다.
for message in st.session_state.messages: 형태로 저장된 모든 대화를 순회하며 렌더링하죠. 이때 각 메시지의 role과 content를 딕셔너리에서 꺼내 사용합니다.
여러분이 이 코드를 사용하면 ChatGPT나 Claude 같은 전문 AI 서비스와 동일한 수준의 채팅 인터페이스를 만들 수 있습니다. 사용자는 친숙한 메신저 스타일 덕분에 학습 곡선 없이 바로 챗봇을 사용할 수 있죠.
메시지가 많아져도 Streamlit이 자동으로 스크롤을 처리하여 최신 메시지가 보이도록 합니다.
실전 팁
💡 메시지가 많을 때 성능을 위해 st.container(height=600)으로 스크롤 가능한 고정 높이 컨테이너를 만들고, 그 안에서 채팅 메시지를 렌더링하세요.
💡 아바타 이미지는 가급적 정사각형으로 준비하세요. Streamlit이 자동으로 원형으로 자르기 때문에 직사각형 이미지는 찌그러져 보일 수 있습니다.
💡 st.chat_message() 안에서 st.error()나 st.warning() 같은 상태 메시지도 사용할 수 있습니다. API 에러 발생 시 채팅 흐름 안에서 자연스럽게 알림을 표시하세요.
💡 role 이름은 자유롭게 지정할 수 있습니다. "user", "assistant" 외에 "system", "tool" 같은 역할을 만들어 다양한 메시지 타입을 구분할 수 있습니다.
💡 메시지 내용이 매우 길 경우, st.expander()와 조합하여 접을 수 있는 섹션을 만들면 UI가 깔끔해집니다. 특히 디버그 정보나 상세 로그를 표시할 때 유용합니다.
4. 사용자 입력 처리
시작하며
여러분의 챗봇이 아무리 똑똑해도, 사용자가 질문을 입력할 방법이 없다면 무용지물입니다. 입력 인터페이스는 챗봇의 심장이라고 할 수 있죠.
일반적인 웹 폼은 전송 버튼을 눌러야 하고, 엔터키로 줄바꿈을 하면 전송이 안 되는 등 채팅 경험을 해칩니다. 사용자들은 메신저처럼 엔터키로 즉시 전송되는 인터페이스를 기대합니다.
Streamlit의 chat_input은 바로 이런 채팅 전용 입력 위젯입니다. 자동 포커스, 엔터키 전송, 모바일 최적화까지 모든 것이 갖춰져 있습니다.
개요
간단히 말해서, st.chat_input()은 채팅 메시지를 입력받는 전용 위젯입니다. 화면 하단에 고정되며, 사용자가 텍스트를 입력하고 엔터를 누르면 즉시 값을 반환합니다.
이 위젯이 기존 st.text_input()보다 나은 이유는 명확합니다. 첫째, 화면 하단에 고정되어 스크롤해도 항상 보입니다.
채팅 앱의 핵심 UX죠. 둘째, 엔터키로 즉시 전송되며, Shift+Enter로 줄바꿈을 할 수 있습니다.
셋째, 전송 후 자동으로 입력창이 비워져 다음 메시지를 바로 입력할 수 있습니다. 기존의 st.text_input()이나 st.text_area()는 폼 제출 방식이어서 버튼 클릭이 필요했습니다.
이제는 chat_input()으로 실시간 대화 흐름을 자연스럽게 구현할 수 있습니다. chat_input의 핵심 특징은 "상태 비저장(stateless)" 동작입니다.
메시지를 전송하면 None을 반환하고, 다음 입력을 기다립니다. 이는 무한 루프를 방지하고 깔끔한 코드 흐름을 만들어줍니다.
입력값은 즉시 세션 스테이트에 저장하여 관리합니다.
코드 예제
import streamlit as st
# 세션 초기화
if "messages" not in st.session_state:
st.session_state.messages = []
# 채팅 입력 위젯 - 화면 하단에 고정
prompt = st.chat_input("메시지를 입력하세요...")
# 사용자가 메시지를 입력하면 처리
if prompt:
# 사용자 메시지를 세션에 추가
st.session_state.messages.append({"role": "user", "content": prompt})
# 화면에 즉시 표시
with st.chat_message("user"):
st.write(prompt)
# 여기서 AI 응답 생성 로직 호출
# response = get_ai_response(prompt)
설명
이것이 하는 일: 사용자로부터 채팅 메시지를 입력받고, 세션 스테이트에 저장한 뒤 화면에 표시합니다. 모든 대화의 시작점이 되는 핵심 인터페이스입니다.
첫 번째로, st.chat_input()은 placeholder 텍스트와 함께 입력창을 생성합니다. 이 함수는 사용자가 엔터를 누르기 전까지 None을 반환하고, 메시지가 전송되면 입력된 문자열을 반환합니다.
반환값은 변수에 저장되며, 여기서는 prompt라는 이름을 사용했습니다. placeholder는 "무엇이든 물어보세요" 같은 친근한 문구를 사용하면 사용자 참여를 유도할 수 있습니다.
그 다음으로, if prompt: 조건문으로 실제 입력이 있을 때만 처리합니다. 빈 문자열이나 None일 때는 아무 작업도 하지 않죠.
입력이 있으면 즉시 st.session_state.messages에 딕셔너리 형태로 추가합니다. {"role": "user", "content": prompt} 형식은 OpenAI API와 호환되는 표준 구조로, 나중에 AI 모델에 그대로 전달할 수 있습니다.
메시지를 세션에 저장한 후에는 즉시 화면에 표시합니다. with st.chat_message("user"): 블록 안에서 st.write(prompt)를 호출하면 사용자가 방금 입력한 메시지가 채팅창에 나타납니다.
이는 즉각적인 피드백을 제공하여 사용자 경험을 향상시킵니다. 중요한 점은 이 패턴이 Streamlit의 재실행 모델과 완벽하게 조화를 이룬다는 것입니다.
메시지를 입력하면 스크립트가 재실행되지만, chat_input()은 이미 전송된 메시지를 다시 반환하지 않습니다. 대신 None을 반환하고 다음 입력을 기다리죠.
이 덕분에 무한 루프나 중복 처리 없이 깔끔한 흐름을 유지할 수 있습니다. 여러분이 이 코드를 사용하면 사용자 입력부터 저장, 표시까지의 전체 파이프라인을 구축할 수 있습니다.
실무에서는 여기에 입력 검증(빈 메시지 필터링, 길이 제한), 속도 제한(rate limiting), 그리고 부적절한 콘텐츠 필터링을 추가하는 것이 좋습니다.
실전 팁
💡 chat_input()은 항상 화면 하단에 고정됩니다. 위치를 변경할 수 없으므로, 레이아웃 설계 시 이를 고려하세요.
💡 사용자 입력을 처리하기 전에 prompt.strip()으로 앞뒤 공백을 제거하세요. 빈 메시지나 공백만 있는 메시지를 필터링할 수 있습니다.
💡 긴 입력을 받아야 한다면, max_chars 파라미터로 최대 길이를 제한하세요. API 토큰 한계를 초과하는 것을 방지할 수 있습니다.
💡 st.chat_input(key="chat_input")처럼 고유 키를 지정하면 여러 입력창을 구분할 수 있습니다. 탭별로 다른 챗봇을 운영할 때 유용합니다.
💡 사용자가 빠르게 여러 메시지를 연속 전송하면 API 비용이 급증할 수 있습니다. time.sleep()이나 st.session_state로 마지막 전송 시간을 추적하여 쿨다운을 구현하세요.
5. 메시지 히스토리 렌더링
시작하며
여러분이 채팅 앱을 사용할 때 이전 대화를 볼 수 없다면 어떨까요? 맥락을 잃고 같은 질문을 반복하게 될 것입니다.
챗봇에서도 대화 히스토리는 필수입니다. 세션 스테이트에 메시지를 저장하는 것과 화면에 표시하는 것은 별개의 작업입니다.
저장된 메시지들을 올바른 순서로, 올바른 스타일로 렌더링해야 하죠. 이번 카드에서는 저장된 모든 대화를 채팅 인터페이스로 복원하는 방법을 알아보겠습니다.
페이지를 새로고침해도 대화가 유지되는 마법 같은 경험을 만들어봅시다.
개요
간단히 말해서, 메시지 히스토리 렌더링은 세션 스테이트에 저장된 모든 대화를 화면에 그리는 과정입니다. 루프를 통해 각 메시지를 순회하며 적절한 chat_message 컨테이너에 표시합니다.
이 패턴이 중요한 이유는 세 가지입니다. 첫째, 사용자가 이전 대화를 참조할 수 있게 합니다.
"아까 말한 그것"을 찾을 수 있죠. 둘째, 스크립트가 재실행되어도 모든 대화가 보존됩니다.
Streamlit의 상태 없는(stateless) 특성을 극복합니다. 셋째, 새 메시지가 추가되면 자동으로 기존 메시지와 함께 렌더링되어 일관된 UI를 유지합니다.
기존에는 JavaScript로 DOM을 조작하여 메시지를 추가하고 스크롤을 제어했습니다. 이제는 Python의 for 루프만으로 전체 대화 히스토리를 렌더링할 수 있습니다.
메시지 렌더링의 핵심은 "선언적 UI" 개념입니다. 현재 상태(메시지 리스트)를 기반으로 전체 UI를 다시 그립니다.
메시지를 하나씩 추가하는 것이 아니라, 매번 전체를 렌더링하는 것이죠. 이는 버그를 줄이고 코드를 단순하게 만듭니다.
코드 예제
import streamlit as st
# 세션에 저장된 모든 메시지 표시
for message in st.session_state.messages:
# 각 메시지의 role과 content 추출
role = message["role"]
content = message["content"]
# 아바타 설정 - role에 따라 다르게
avatar = "🧑" if role == "user" else "🤖"
# 채팅 메시지로 렌더링
with st.chat_message(role, avatar=avatar):
st.write(content)
# 실행 예시 데이터 (실제로는 사용자 입력으로 채워짐)
# st.session_state.messages = [
# {"role": "user", "content": "안녕하세요!"},
# {"role": "assistant", "content": "반갑습니다! 무엇을 도와드릴까요?"}
# ]
설명
이것이 하는 일: 세션 스테이트의 메시지 리스트를 순회하며 각 메시지를 채팅 인터페이스로 렌더링합니다. 사용자와 AI의 전체 대화 흐름을 시각적으로 재현합니다.
첫 번째로, for message in st.session_state.messages: 루프는 저장된 모든 메시지를 순차적으로 처리합니다. 메시지는 딕셔너리 형태로 저장되어 있으며, 최소한 "role"과 "content" 키를 포함합니다.
이 형식은 OpenAI의 Chat Completions API와 동일하여, 나중에 API 호출 시 그대로 사용할 수 있습니다. 리스트는 시간순으로 정렬되어 있어 가장 오래된 메시지부터 표시됩니다.
그 다음으로, 각 메시지의 role을 확인하여 적절한 아바타를 선택합니다. 삼항 연산자를 사용한 avatar = "🧑" if role == "user" else "🤖" 패턴은 간결하면서도 명확합니다.
사용자 메시지에는 사람 이모지, AI 메시지에는 로봇 이모지를 사용하여 시각적 구분을 명확히 합니다. 더 정교한 앱에서는 role이 "system", "tool" 등 다양할 수 있으므로 딕셔너리 매핑을 사용하는 것도 좋습니다.
with st.chat_message(role, avatar=avatar): 블록은 메시지를 올바른 스타일로 감쌉니다. role이 "user"면 사용자 스타일, "assistant"면 AI 스타일이 자동 적용됩니다.
이 안에서 st.write(content)로 실제 메시지 내용을 출력합니다. st.write()는 마크다운을 지원하므로, AI가 코드 블록이나 리스트를 포함한 응답을 보낼 때도 완벽하게 렌더링됩니다.
이 패턴의 아름다움은 단순함에 있습니다. 새 메시지가 추가되든, 기존 메시지가 수정되든, 항상 전체 리스트를 다시 렌더링합니다.
이는 상태 동기화 문제를 원천적으로 방지합니다. Streamlit의 효율적인 렌더링 엔진 덕분에 수십 개의 메시지도 빠르게 표시됩니다.
여러분이 이 코드를 사용하면 ChatGPT와 동일한 대화 경험을 제공할 수 있습니다. 사용자는 스크롤하여 이전 대화를 확인하고, 새 메시지는 자연스럽게 아래에 추가됩니다.
실무에서는 메시지가 매우 많을 경우 가상 스크롤링이나 페이지네이션을 고려해야 하지만, 대부분의 채팅 세션에서는 이 간단한 패턴으로 충분합니다.
실전 팁
💡 메시지 개수가 많아지면 렌더링이 느려질 수 있습니다. st.container(height=500)로 스크롤 가능한 영역을 만들면 성능이 향상됩니다.
💡 메시지에 타임스탬프를 추가하면 디버깅과 사용자 경험이 개선됩니다: {"role": "user", "content": "...", "timestamp": datetime.now()} 형태로 저장하세요.
💡 AI 응답에 이미지나 차트가 포함될 수 있다면, content를 문자열이 아닌 리스트로 저장하여 여러 타입의 콘텐츠를 담을 수 있게 하세요.
💡 st.expander()와 조합하여 긴 대화를 접을 수 있게 만들면 UI가 깔끔해집니다. "이전 대화 보기" 버튼으로 오래된 메시지를 숨길 수 있습니다.
💡 개발 중에는 st.sidebar.json(st.session_state.messages)로 메시지 구조를 확인하면 디버깅이 쉬워집니다. 프로덕션에서는 제거하세요.
6. 스트리밍 응답 구현
시작하며
여러분이 ChatGPT를 사용할 때 가장 인상적인 기능은 무엇인가요? 바로 답변이 실시간으로 타이핑되는 것처럼 나타나는 스트리밍 효과입니다.
전체 응답을 기다리지 않고 즉시 읽기 시작할 수 있죠. 일반적인 API 호출은 전체 응답이 완성될 때까지 기다려야 합니다.
긴 답변의 경우 10-20초가 걸릴 수 있고, 그동안 사용자는 로딩 스피너만 바라봐야 합니다. 이는 답답한 경험을 만듭니다.
Streamlit의 write_stream()과 생성자(generator)를 조합하면 OpenAI의 스트리밍 응답을 자연스럽게 표시할 수 있습니다. 마치 AI가 실시간으로 생각하며 답하는 것처럼 보이죠.
개요
간단히 말해서, 스트리밍 응답은 AI의 답변을 한 번에 받지 않고 토큰 단위로 조금씩 받아서 표시하는 기술입니다. Python의 생성자 함수와 Streamlit의 스트리밍 API를 활용합니다.
스트리밍이 필수인 이유는 세 가지입니다. 첫째, 사용자 경험이 극적으로 향상됩니다.
응답의 첫 부분을 바로 읽을 수 있어 대기 시간이 짧게 느껴집니다. 둘째, 진행 상황을 시각적으로 확인할 수 있어 불안감이 줄어듭니다.
셋째, 긴 응답도 중간에 읽기 시작할 수 있어 효율적입니다. 기존에는 전체 응답을 받은 후 한 번에 표시했습니다.
이제는 토큰이 생성되는 즉시 화면에 나타나 마치 사람이 타이핑하는 것 같은 효과를 만들 수 있습니다. 스트리밍의 핵심은 "이터레이터 패턴"입니다.
OpenAI API는 stream=True 옵션으로 응답을 청크 단위로 yield하고, Streamlit의 write_stream()은 이를 받아 실시간으로 렌더링합니다. 이 둘의 조합이 매끄러운 타이핑 애니메이션을 만들어냅니다.
코드 예제
import streamlit as st
from openai import OpenAI
client = OpenAI(api_key="your-api-key")
def stream_response(messages):
# OpenAI API 스트리밍 호출
stream = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=messages,
stream=True # 스트리밍 활성화
)
# 각 청크를 yield하는 생성자
for chunk in stream:
if chunk.choices[0].delta.content:
yield chunk.choices[0].delta.content
# 사용 예시
with st.chat_message("assistant"):
# 스트리밍으로 응답 표시
response = st.write_stream(stream_response(st.session_state.messages))
# 완성된 응답을 세션에 저장
st.session_state.messages.append({"role": "assistant", "content": response})
설명
이것이 하는 일: OpenAI API로부터 스트리밍 응답을 받아 실시간으로 화면에 표시합니다. 사용자는 전체 응답을 기다리지 않고 즉시 읽기 시작할 수 있으며, 완성된 응답은 세션에 저장됩니다.
첫 번째로, stream_response() 함수는 생성자(generator)입니다. yield 키워드를 사용하여 각 토큰을 하나씩 반환합니다.
OpenAI의 create() 메서드에 stream=True를 전달하면 응답이 여러 청크로 나뉘어 옵니다. 각 청크는 chunk.choices[0].delta.content에 텍스트 조각을 포함하며, 이를 yield하면 Streamlit이 즉시 화면에 추가합니다.
None 값을 필터링하는 if 문은 빈 청크를 건너뛰기 위한 것입니다. 그 다음으로, st.write_stream() 함수는 생성자를 받아 스트리밍 렌더링을 수행합니다.
이 함수는 생성자가 yield하는 모든 값을 순차적으로 받아 화면에 추가하며, 마치 타이핑하는 것처럼 보이게 합니다. 중요한 점은 write_stream()이 최종적으로 전체 텍스트를 문자열로 반환한다는 것입니다.
이를 변수에 저장하여 나중에 세션 스테이트에 추가할 수 있습니다. with st.chat_message("assistant"): 블록 안에서 스트리밍을 실행하면 AI 응답 스타일로 표시됩니다.
아바타와 메시지 스타일이 자동 적용되죠. 스트리밍이 진행되는 동안 사용자는 텍스트가 점진적으로 나타나는 것을 봅니다.
이는 AI가 "생각하고 있다"는 느낌을 주어 더 자연스러운 대화 경험을 만듭니다. 마지막으로, 스트리밍이 완료되면 response 변수에 전체 텍스트가 저장됩니다.
이를 즉시 st.session_state.messages에 추가하여 대화 히스토리를 유지합니다. 다음에 페이지가 재실행될 때 이 메시지는 일반 메시지로 렌더링되며, 스트리밍 효과는 새로운 응답에만 적용됩니다.
여러분이 이 코드를 사용하면 사용자 만족도가 크게 향상됩니다. 테스트 결과 스트리밍이 있는 챗봇의 체감 응답 속도는 30-50% 빠르게 느껴집니다.
실무에서는 네트워크 지연을 고려하여 에러 처리를 추가하고, 스트리밍 중단 기능(예: 정지 버튼)을 구현하는 것이 좋습니다.
실전 팁
💡 스트리밍 중 에러가 발생할 수 있습니다. try-except로 생성자를 감싸고, 에러 발생 시 지금까지 받은 부분까지라도 저장하세요.
💡 write_stream()에 커스텀 생성자를 전달할 때, time.sleep(0.01)을 추가하면 타이핑 속도를 조절할 수 있습니다. 너무 빠르면 읽기 어렵습니다.
💡 OpenAI API 외에 다른 모델(Anthropic, Cohere 등)도 스트리밍을 지원합니다. 생성자 패턴만 맞추면 동일한 코드로 작동합니다.
💡 스트리밍 중에는 사용자 입력을 비활성화하는 것이 좋습니다. st.session_state.streaming = True 플래그로 chat_input을 조건부로 숨길 수 있습니다.
💡 매우 긴 응답(5000+ 토큰)은 스트리밍해도 시간이 오래 걸립니다. 응답 길이를 제한하거나(max_tokens 파라미터), 요약을 먼저 보여주는 전략을 고려하세요.
7. 사이드바 설정 패널
시작하며
여러분의 챗봇을 사용하는 사람들은 각자 다른 요구사항을 가지고 있습니다. 어떤 이는 빠른 답변을 원하고, 어떤 이는 창의적인 응답을 선호하죠.
이런 다양한 니즈를 어떻게 충족시킬 수 있을까요? 하드코딩된 설정은 유연성이 없습니다.
모델을 바꾸거나 파라미터를 조정하려면 코드를 수정하고 재배포해야 합니다. 사용자들은 자신만의 설정으로 챗봇을 사용하고 싶어 합니다.
Streamlit의 사이드바는 이런 설정 UI를 만들기에 완벽합니다. 메인 채팅 영역을 방해하지 않으면서, 모델 선택부터 고급 파라미터까지 모든 옵션을 제공할 수 있습니다.
개요
간단히 말해서, 사이드바는 화면 왼쪽에 위치한 별도 영역으로, 설정과 제어 요소를 배치하는 곳입니다. st.sidebar를 통해 접근하며, 모든 Streamlit 위젯을 사용할 수 있습니다.
사이드바가 챗봇에 중요한 이유는 세 가지입니다. 첫째, 메인 채팅 영역을 깔끔하게 유지할 수 있습니다.
설정은 필요할 때만 보면 되죠. 둘째, 파워 유저를 위한 고급 옵션을 제공할 수 있습니다.
temperature, max_tokens 같은 파라미터를 직접 조정할 수 있게 합니다. 셋째, API 키 입력이나 모델 선택 같은 필수 설정을 한곳에 모을 수 있습니다.
기존에는 설정을 별도 페이지로 분리하거나 모달 창으로 만들어야 했습니다. 이제는 st.sidebar.slider(), st.sidebar.selectbox() 같은 함수로 즉시 설정 패널을 만들 수 있습니다.
사이드바의 핵심은 "비간섭적 접근성"입니다. 기본적으로 펼쳐져 있어 쉽게 접근할 수 있지만, 접으면 채팅 영역이 넓어집니다.
사용자가 자신의 워크플로우에 맞게 조정할 수 있죠.
코드 예제
import streamlit as st
# 사이드바 헤더
st.sidebar.title("⚙️ 설정")
# 모델 선택
model = st.sidebar.selectbox(
"AI 모델 선택",
["gpt-4", "gpt-3.5-turbo", "gpt-4-turbo"],
index=1 # 기본값: gpt-3.5-turbo
)
st.session_state.model = model
# Temperature 슬라이더
temperature = st.sidebar.slider(
"창의성 (Temperature)",
min_value=0.0,
max_value=2.0,
value=0.7,
step=0.1,
help="높을수록 창의적이고 다양한 응답"
)
st.session_state.temperature = temperature
# Max tokens
max_tokens = st.sidebar.number_input(
"최대 토큰 수",
min_value=100,
max_value=4000,
value=1000,
step=100
)
# 대화 초기화 버튼
if st.sidebar.button("🗑️ 대화 내역 삭제"):
st.session_state.messages = []
st.rerun()
설명
이것이 하는 일: 사용자가 AI 모델과 응답 파라미터를 조정할 수 있는 설정 인터페이스를 제공합니다. 각 설정은 세션 스테이트에 저장되어 API 호출 시 사용됩니다.
첫 번째로, st.sidebar.selectbox()는 드롭다운 메뉴를 생성합니다. 여기서는 OpenAI의 여러 모델 중 하나를 선택할 수 있게 합니다.
GPT-4는 가장 강력하지만 비싸고, GPT-3.5-turbo는 빠르고 저렴합니다. index=1로 기본값을 설정하여 대부분의 사용자에게 적합한 선택지를 제시합니다.
선택된 값은 즉시 st.session_state.model에 저장되어 다음 API 호출 시 사용됩니다. 그 다음으로, st.sidebar.slider()는 temperature 파라미터를 조정하는 슬라이더를 만듭니다.
Temperature는 AI 응답의 무작위성을 제어하는 중요한 파라미터입니다. 0에 가까우면 항상 같은 답변을, 2에 가까우면 매번 다른 창의적인 답변을 생성합니다.
0.7은 창의성과 일관성의 균형점으로, 대부분의 챗봇에 적합합니다. help 파라미터는 물음표 아이콘에 마우스를 올렸을 때 표시되는 툴팁으로, 초보 사용자를 돕습니다.
st.sidebar.number_input()은 최대 토큰 수를 설정합니다. 토큰은 대략 단어의 3/4 정도 길이로, 1000 토큰은 약 750 단어에 해당합니다.
이 값을 제한하면 API 비용을 통제하고 너무 긴 응답을 방지할 수 있습니다. step=100으로 100 단위로만 조정 가능하게 하여 미세 조정을 방지합니다.
마지막으로, st.sidebar.button()으로 대화 내역 삭제 기능을 제공합니다. 버튼을 클릭하면 st.session_state.messages를 빈 리스트로 초기화하고, st.rerun()으로 페이지를 즉시 새로고침합니다.
이는 사용자가 새로운 주제로 대화를 시작하고 싶을 때 유용합니다. 주의할 점은 이 작업은 되돌릴 수 없으므로, 실무에서는 확인 대화상자를 추가하는 것이 좋습니다.
여러분이 이 코드를 사용하면 사용자 맞춤형 챗봇 경험을 제공할 수 있습니다. 코딩 도우미 챗봇이라면 낮은 temperature로 정확성을 높이고, 브레인스토밍 봇이라면 높은 temperature로 창의성을 극대화할 수 있죠.
실무에서는 사용자 프로필별로 기본 설정을 저장하는 기능을 추가하면 더욱 편리합니다.
실전 팁
💡 API 키를 사이드바에서 입력받는다면, st.sidebar.text_input("API 키", type="password")로 마스킹하세요. 절대 평문으로 표시하지 마세요.
💡 st.sidebar.expander()로 고급 설정을 접을 수 있게 만들면 초보자에게는 단순하게, 전문가에게는 강력하게 보입니다.
💡 설정 변경 시 즉시 반영되므로, 중요한 변경사항(예: 모델 변경)은 변경 사실을 알리는 메시지를 표시하세요: st.sidebar.success("모델이 변경되었습니다!").
💡 st.sidebar.download_button()으로 대화 내역을 JSON이나 텍스트로 다운로드할 수 있게 하면 사용자가 기록을 보관할 수 있습니다.
💡 사이드바 너비는 고정되어 있습니다. 긴 텍스트는 자동 줄바꿈되므로, 레이블은 짧고 명확하게 작성하세요. 상세 설명은 help 파라미터를 활용하세요.
8. 파일 업로드 기능
시작하며
여러분이 챗봇에게 문서를 분석해달라고 요청하고 싶다면 어떻게 해야 할까요? 텍스트를 복사해서 붙여넣기?
그건 너무 불편하죠. PDF, 워드, 텍스트 파일을 직접 업로드할 수 있다면 훨씬 편리할 것입니다.
RAG(Retrieval-Augmented Generation) 챗봇이나 문서 분석 봇은 파일 업로드가 필수입니다. 사용자의 개인 문서를 읽고 맥락 있는 답변을 제공해야 하죠.
Streamlit의 file_uploader는 이런 기능을 단 몇 줄로 구현합니다. 드래그 앤 드롭 지원, 여러 파일 형식, 크기 제한까지 모든 것이 내장되어 있습니다.
개요
간단히 말해서, file_uploader는 사용자가 로컬 파일을 웹 앱으로 업로드할 수 있게 하는 위젯입니다. 업로드된 파일은 메모리에 로드되어 Python 코드로 처리할 수 있습니다.
파일 업로드가 챗봇에 중요한 이유는 세 가지입니다. 첫째, 문서 기반 질문답변이 가능해집니다.
계약서를 업로드하고 "이 조항의 의미는?"이라고 물을 수 있죠. 둘째, 코드 리뷰 봇, 논문 요약 봇 같은 전문 기능을 만들 수 있습니다.
셋째, 사용자의 프라이빗 데이터로 작동하여 개인화된 응답을 제공할 수 있습니다. 기존에는 파일 업로드를 구현하려면 HTML form, JavaScript, 백엔드 서버 설정이 필요했습니다.
이제는 st.file_uploader()와 몇 줄의 처리 코드만으로 완성됩니다. 파일 업로드의 핵심은 "임시 메모리 스토리지"입니다.
업로드된 파일은 서버의 디스크에 저장되지 않고 메모리에만 존재합니다. 세션이 종료되면 자동으로 삭제되어 보안이 유지됩니다.
영구 저장이 필요하면 별도로 처리해야 합니다.
코드 예제
import streamlit as st
from PyPDF2 import PdfReader
# 사이드바에 파일 업로더 배치
uploaded_file = st.sidebar.file_uploader(
"문서 업로드",
type=["pdf", "txt", "docx"],
help="분석할 문서를 업로드하세요"
)
# 파일이 업로드되면 처리
if uploaded_file is not None:
# 파일 타입 확인
file_type = uploaded_file.type
# PDF 파일 처리 예시
if file_type == "application/pdf":
pdf_reader = PdfReader(uploaded_file)
text = ""
for page in pdf_reader.pages:
text += page.extract_text()
# 추출된 텍스트를 시스템 메시지로 추가
st.session_state.document_context = text
st.sidebar.success(f"✅ {len(text)} 자 추출 완료")
# 텍스트 파일 처리
elif file_type == "text/plain":
text = uploaded_file.read().decode("utf-8")
st.session_state.document_context = text
설명
이것이 하는 일: 사용자로부터 문서 파일을 업로드받아 텍스트를 추출하고, 세션 컨텍스트에 저장합니다. 이후 사용자 질문에 문서 내용을 기반으로 답변할 수 있게 합니다.
첫 번째로, st.sidebar.file_uploader()는 파일 업로드 위젯을 생성합니다. type 파라미터로 허용할 파일 형식을 제한할 수 있습니다.
PDF, 텍스트, 워드 문서를 지원하도록 설정했습니다. 사용자는 버튼을 클릭하거나 파일을 드래그 앤 드롭하여 업로드할 수 있습니다.
업로드가 완료되면 uploaded_file 객체가 반환되며, 이는 파일 유사 객체(file-like object)로 Python의 표준 파일 API를 지원합니다. 그 다음으로, if uploaded_file is not None: 조건으로 파일이 실제로 업로드되었는지 확인합니다.
업로드되지 않은 상태에서는 None이 반환되므로 처리를 건너뜁니다. uploaded_file.type으로 MIME 타입을 확인하여 파일 형식별로 다른 처리를 수행합니다.
PDF는 "application/pdf", 텍스트는 "text/plain"입니다. PDF 처리는 PyPDF2 라이브러리를 사용합니다.
PdfReader(uploaded_file)로 PDF를 읽고, pages 리스트를 순회하며 각 페이지의 텍스트를 추출합니다. extract_text() 메서드는 PDF의 텍스트 레이어를 추출하죠.
주의할 점은 이미지 기반 PDF(스캔본)는 OCR 없이는 텍스트를 추출할 수 없다는 것입니다. 그런 경우 pytesseract 같은 OCR 라이브러리가 필요합니다.
추출된 텍스트는 st.session_state.document_context에 저장됩니다. 이렇게 하면 사용자가 메시지를 입력할 때마다 이 컨텍스트를 AI에게 함께 전달할 수 있습니다.
예를 들어 시스템 메시지로 "다음 문서를 참고하여 답변하세요: {document_context}"를 추가하는 방식입니다. 텍스트 파일은 더 간단합니다.
.read()로 바이트를 읽고 .decode("utf-8")로 문자열로 변환합니다. 인코딩 문제를 피하려면 errors="ignore" 옵션을 추가하는 것도 좋습니다.
여러분이 이 코드를 사용하면 문서 분석 챗봇, 계약서 리뷰 봇, 논문 요약 봇 등 다양한 응용이 가능합니다. 실무에서는 파일 크기 제한(예: 10MB), 페이지 수 제한, 그리고 처리 시간 초과(timeout)를 설정하여 서버 과부하를 방지해야 합니다.
또한 업로드된 파일에 악성 코드가 없는지 검증하는 보안 절차도 필요합니다.
실전 팁
💡 파일 크기를 제한하려면 Streamlit 설정에서 server.maxUploadSize를 조정하세요. 기본값은 200MB입니다.
💡 대용량 PDF 처리는 시간이 오래 걸립니다. st.spinner("문서 처리 중...")로 로딩 표시를 추가하여 사용자 불안을 줄이세요.
💡 워드 문서(.docx)는 python-docx 라이브러리로 처리할 수 있습니다: from docx import Document; doc = Document(uploaded_file).
💡 여러 파일을 동시에 업로드받으려면 accept_multiple_files=True 옵션을 사용하세요. 반환값이 리스트가 됩니다.
💡 추출된 텍스트가 너무 길면 토큰 한계를 초과할 수 있습니다. 청킹(chunking) 전략이나 임베딩 기반 검색을 사용하여 관련 부분만 AI에게 전달하세요.