Streamlit 세션 지속성 해결 방법 (3번의 실패 후)
Source: Dev.to
문제
페이지 새로고침 시에도 사용자 세션을 기억해야 하는 Streamlit 앱을 만들고 있었습니다. 간단하죠? 일반 웹 앱처럼 localStorage나 쿠키를 사용하면 됩니다.
틀렸습니다.
Streamlit Cloud는 앱을 샌드박스된 iframe에서 실행합니다. 이는 다음을 의미합니다:
- 표준
localStorage는 컴포넌트 iframe에 격리됩니다 - 쿠키도 동일한 격리 문제에 직면합니다
- 직접적인 DOM 조작은 보안 제한이 있습니다
결과는? 3일간의 좌절과 3번의 실패한 접근 방식이었습니다.
시도 1: localStorage via streamlit_js_eval
My first thought: “I’ll just use JavaScript to access localStorage.”
첫 번째 생각: “JavaScript만 사용해서 localStorage에 접근하겠다.”
from streamlit_js_eval import streamlit_js_eval
# Try to store session
streamlit_js_eval(js_expressions="""
localStorage.setItem('session_token', 'abc123');
""")
# Try to retrieve
token = streamlit_js_eval(js_expressions="""
localStorage.getItem('session_token');
""")
Why it failed: The JavaScript runs inside Streamlit’s component iframe, which has its own isolated storage. The data gets saved, but it’s completely separate from your app’s main context. On reload, it’s gone.
실패한 이유: JavaScript는 Streamlit 컴포넌트 iframe 내부에서 실행되며, 해당 iframe은 자체적인 격리된 저장소를 가지고 있습니다. 데이터는 저장되지만 앱의 메인 컨텍스트와는 완전히 분리되어 있습니다. 페이지를 새로 고치면 데이터가 사라집니다.
시도 2: extra-streamlit-components를 통한 쿠키
다음은 쿠키입니다. iframe을 넘어서도 작동하겠죠?
from extra_streamlit_components import CookieManager
from datetime import datetime, timedelta
cookie_manager = CookieManager()
# Set cookie
cookie_manager.set('session_token', 'abc123', expires_at=datetime.now() + timedelta(days=1))
# Retrieve
token = cookie_manager.get('session_token')
Why it failed: 동일한 iframe 격리 문제입니다. 쿠키가 컴포넌트의 컨텍스트에 설정되며, 부모 프레임에서는 접근할 수 없습니다. Streamlit Cloud의 샌드박싱으로 인해 쿠키를 필요한 곳에서 사용할 수 없습니다.
시도 3: window.parent.localStorage
절박해져서 부모 창의 localStorage에 직접 접근해 보았습니다:
streamlit_js_eval(js_expressions="""
window.parent.localStorage.setItem('session_token', 'abc123');
""")
왜 실패했는가: 브라우저 보안 모델 때문입니다. 교차 출처 iframe 접근은 설계상 차단됩니다. Streamlit Cloud의 iframe 샌드박싱이 이러한 보호를 트리거하며, 이는 정당한 이유가 있습니다—이를 허용하면 보안 재앙이 될 수 있기 때문입니다.
해결책: st.query_params
iframe 벽에 머리를 부딪힌 뒤, 내내 있던 것을 발견했습니다: st.query_params.
Streamlit 1.30에 도입된 이 내장 기능을 사용하면 데이터를 URL의 쿼리 파라미터에 직접 저장할 수 있습니다. 특징은 다음과 같습니다:
- 첫 렌더링 시 동기식으로 사용 가능
- 페이지 새로 고침 시에도 지속
- Streamlit Cloud의 iframe 환경에서 완벽히 작동
- 외부 의존성 전혀 없음
작동 방식은 다음과 같습니다:
import streamlit as st
import base64
import json
def save_session(session_data):
# Encode session data as base64 to handle special characters
json_str = json.dumps(session_data)
encoded = base64.b64encode(json_str.encode()).decode()
# Store in URL
st.query_params['s'] = encoded
def load_session():
# Retrieve from URL
if 's' in st.query_params:
try:
encoded = st.query_params['s']
json_str = base64.b64decode(encoded).decode()
return json.loads(json_str)
except Exception:
return {}
return {}
# Usage
if 'session' not in st.session_state:
st.session_state.session = load_session()
# Save whenever session changes
if st.button('Save'):
save_session(st.session_state.session)
왜 이것이 작동하는가
- URL 기반 저장: 쿼리 매개변수는 URL의 일부이며, 어디서든 접근 가능해 iframe 문제 없이 사용할 수 있습니다.
- Streamlit에 내장: 외부 컴포넌트나 JavaScript 해킹이 필요 없습니다.
- 동기식 접근: 페이지 로드 시 즉시 사용할 수 있으며, 어떤 컴포넌트가 렌더링되기 전에 사용할 수 있습니다.
- Base64 인코딩: 복잡한 데이터 구조를 URL 형식으로 안전하게 처리합니다.
교훈
때로는 가장 좋은 해결책이 가장 간단한 경우가 있습니다. 저는 외부 라이브러리와 JavaScript 우회 방법과 씨름하며 3일을 보냈지만, 실제로 Streamlit에는 처음부터 내장된 해결책이 있었습니다.
서드파티 라이브러리나 영리한 해킹을 시도하기 전에:
- 내장된 해결책이 있는지 확인하세요.
- 최신 변경 로그를 읽어보세요 —
st.query_params같은 기능은 놓치기 쉽습니다. - 배포 환경을 이해하세요 (이번 경우는 iframe 샌드박싱).
더 알고 싶으신가요?
전체 이야기를 읽어보세요. 디버깅 단계와 코드 예제를 모두 포함하고 있는 Chronicle 001: Genesis.