[ProductServinng] Streamlit 사용일지 feat. session state

윰진·2022년 12월 31일
1

NaverAIBoostCamp정리

목록 보기
22/30
post-thumbnail

🎀 ( •̀ ω •́ )✧ 우당탕탕 streamlit 사용 일지 ! ( 2022-12-30 ~ 2023-01-21)

결과물 보러가기 ( ~2023-01-29 임시 공개 )
💟 당신이 선호하는 방, 호빵

제 1 장. 환경설정하기

NEED TO CHECK

  • python 3.9
  • streamlit 1.16
  • streamlit_folium 0.8.1
  • pyproj
    • 좌표 변환을 위함

python 3.8 버전과 streamlit 1.1x version 을 사용하면 오류가 발생한다.

제 2 장. 간단 튜토리얼

요구 사항 정리

  • foilum 지도 띄우기
  • 동그란 마커 띄우기
  • 동그란 마커 클릭시 오른쪽 sidebar open
  • 상단 메뉴바에 시/군/구 선택
    • 시/군/구 별로 geometry 표시 되어야 할지 확인하기
  • 선택시 중심으로 이동

제 2 - 1 장. folium 지도 띄우기

제 2 - 2 장. 마커 띄우기 ( MarkerCluster )

실행 이미지

구현 코드

import folium
import streamlit as st

from streamlit_folium import st_folium

center = [37.5010,127.0509] 
# center on Seoul pharmacy
m = folium.Map(location=center, zoom_start=18)
markers = plugins.MarkerCluster(transformed_coord_list)
markers.add_to(m) 

# call to render Folium map in Streamlit
st_data = st_folium(m, width=725)

제 2 - 3 장. 동그란 마커 클릭시 오른쪽 sidebar open

🎀 ( •̀ ω •́ )✧ Marker Click Event 를 먼저 처리해보자 !
🎀 ( •̀ ω •́ )✧ 그냥 onClick Event 를 추가하면 되는데 뭐가 문제냐 ! 라고 물어보신다면 다음과 같은 문제가 있다.

  • streamlit folium 의 Marker 는 click event 를 지원하지 않았다.
  • folium.Element 로 script 를 추가하는 것이 동작하지 않았다.

실행되지 않는 코드

click_js = """function onClick(e) {
                 var point = e.latlng; alert(point)
                 }"""
                 
e = folium.Element(click_js)
html = m.get_root()
html.script.get_root().render()
html.script._children[e.get_name()] = e

🎀 ( •̀ ω •́ )✧ Marker 의 template 에 들어가는 javascript 를 변경하면 click event 를 추가할 수 있다.

실행 이미지

구현 코드

import folium
from jinja2 import Template
from folium.map import Marker
import streamlit
from streamlit_folium import st_folium


# Modify Marker template to include the onClick event
click_template = """{% macro script(this, kwargs) %}
function onClick(e) {
                 let point = e.latlng; alert(point)
                 }
                 
    var {{ this.get_name() }} = L.marker(
        {{ this.location|tojson }},
        {{ this.options|tojson }}
    ).addTo({{ this._parent.get_name() }}).on('click', onClick);
{% endmacro %}"""

# Change template to custom template
Marker._template = Template(click_template)

location_center = [51.7678, -0.00675564]
m = folium.Map(location_center, zoom_start=13)

e = folium.Element(click_js)
html = m.get_root()
html.script.get_root().render()
html.script._children[e.get_name()] = e

#Add marker (click on map an alert will display with latlng values)
marker = folium.Marker(location_center).add_to(m)

st_folium(m, width=750)

제 2 - 4 장. 상단 메뉴바에 시/군/구 선택

선택된 구 정보를 활용해서 Backend 에 매물 정보를 요청한다.

selected_gu = st.selectbox(
    label = "구",
    options = GU_INFO,
    label_visibility=st.session_state.visibility,
    disabled=st.session_state.disabled,
    index = selected_idx
)

st.session_state['center']['coord'] = [GU_INFO_CENTER[selected_gu]["lat"],GU_INFO_CENTER[selected_gu]["lng"]]
url = ''.join([BACKEND_ADDRESS, DOMAIN_INFO['map'], DOMAIN_INFO['items']])
res = requests.post(url,data=json.dumps(user_info) )
st.session_state['item_list'] = [*res.json()['houses'].values()]

제 2 - 5 장. 선택시 중심으로 이동

선택시 반환되는 좌표 정보를 map 의 location 에 넣어주면 된다.

Folium documentation 바로가기

class folium.folium.Map(location=None, width='100%', height='100%', left='0%', top='0%', position='relative', tiles='OpenStreetMap', attr=None, min_zoom=0, max_zoom=18, zoom_start=10, min_lat=- 90, max_lat=90, min_lon=- 180, max_lon=180, max_bounds=False, crs='EPSG3857', control_scale=False, prefer_canvas=False, no_touch=False, disable_3d=False, png_enabled=False, zoom_control=True, **kwargs)

제 2 - 6 장. Streamlit 내 html 요소에서 발생한 interation 받아오기

Link 가 걸린 element 의 id 를 가져오는 멋찐 기능..
jsx, typescript 로 멋찐 걸 만들어 낼 수 있다니
공부 욕구가 뿜뿜했다.

import streamlit as st
from st_click_detector import click_detector

content = """<p><a href='#' id='Link 1'>First link</a></p>
    <p><a href='#' id='Link 2'>Second link</a></p>
    <a href='#' id='Image 1'><img width='20%' src='https://images.unsplash.com/photo-1565130838609-c3a86655db61?w=200'></a>
    <a href='#' id='Image 2'><img width='20%' src='https://images.unsplash.com/photo-1565372195458-9de0b320ef04?w=200'></a>
    """
clicked = click_detector(content)

st.markdown(f"**{clicked} clicked**" if clicked != "" else "**No click**")

제 2 - 7 장. 버튼으로 다음 페이지 넘어가기

비록 우리는 if else 로 처리했으나... 언젠가 개선할 날을 위해 소스를 남긴다.

def switch_page(page_name: str):
    from streamlit import _RerunData, _RerunException
    from streamlit.source_util import get_pages

    def standardize_name(name: str) -> str:
        return name.lower().replace("_", " ")
    
    page_name = standardize_name(page_name)

    pages = get_pages("streamlit_app.py")  # OR whatever your main page is called

    for page_hash, config in pages.items():
        if standardize_name(config["page_name"]) == page_name:
            raise _RerunException(
                _RerunData(
                    page_script_hash=page_hash,
                    page_name=page_name,
                )
            )

    page_names = [standardize_name(config["page_name"]) for config in pages.values()]

    raise ValueError(f"Could not find page {page_name}. Must be one of {page_names}")```

🎀 ( •̀ ω •́ )✧ 모든게 준비됐다 ! 잘 조합해서 만들면 된다.
🎀 ( •̀ ω •́ )✧ 그런데..! 엄청난 산이 남아 있었으니....

📑 바야흐로 링크 공개 하루 전 , 팀원들과 열심히 기능 테스트를 진행하는데....

👩 : 누가 내 아이디로 로그인 했어 ? 나 막 내가 선택 안한 지역 구로 가는데!?
😸 : 어머 ! 난가봐, 아까 빌려 했는데 로그아웃을 안했네 ! 깔깔깔

??? 생각해보면 살짝 말이 안된다.. Session 이란게 뭔가.
컴퓨터와 사용자 간의 연결..! 하다못해 새로운 탭도 새로운 세션으로 쳐질텐데 !?
하지만 그 땐 발견하지 못하고..

📑 링크 공개 직전, 다섯 명의 유저가 유입되었다고 생각하고 마구 마구 interaction 을 만드는데..!!
🙄 단체로 동기화가 된다. 하하. 웬걸... session state 가 공유되는 것이다. 말도 안돼..

제 3 장. Session State 가 서로 다른 유저임에도 공유되는 건에 대하여

📢 주의 ! 해당 문제는 미해결 상태입니다. 추측성 발언이 담겨 있습니다.

제 3 - 1 장. 문제 상황 시나리오

A 유저 👩 가 서초구를 선택하여 매물을 보고 있던 상황
B 유저 😸 가 강남구의 매물을 보기 위해 구를 변경
👩 와 😸 모두 강남구에 있게 된 상황..!

제 3 - 2 장. 시도한 방법

❌ 1 ) 로그인 후 지도 페이지 진입할 때 session_state 에 존재하는 key 지우고 새로 설정
❌ 2 ) streamlit version 을 upgrade ( 1.16 {\rightarrow} 1.17 )
❌ 3 ) streamlit version 을 downgrade ( 1.16 {\rightarrow} 1.9 )
⭕ 4 ) streamlit version 을 downgrade 하고 SessionState.py 파일 추가

제 3 - 3 장. 문제 원인 파악하기

📢 주의 ! 해당 문제는 미해결 상태입니다. 추측성 발언이 담겨 있습니다.

Fact Check

Streamlit 의 Session State 는 in-memory 에 저장된다.

  • browser 를 닫거나 session 이 만료되었을 때 session 이 삭제되는 이유

~~📢 추측 ) 사용자들이 공유 네트워크 환경에서 애플리케이션을 사용하거나 애플리케이션이 shared hosting service 나 cloud-based platform 같은 multi-user 환경에서 동작하는 경우 session state 를 공유하는 듯하다.

📢 추측 ) 해결하려면 unique session key, cookies, 또는 각 user 의 session 을 확인할 수 있는 token 을 사용하는 방법이 있다. ~~

Streamlit 에서 session 으로 저장되지 않은 데이터들은 유저 간 공유된다.

KEYWORKD : NAS, SAN, load balancer, session key

제 3 - 4 장. SessionState.py 파일이 뭐길래 나를 구제해줬을까 ?

Session Object 를 가져와 유저 별로 session state 가질 수 있도록 한다.

제 4 장. 결론

한 줄 평 : 빠르지만 제약이 좀 있는 녀석

제 4 - 1 장. Streamlit 장점

💖 1 ) 개발 속도가 빠르다. Python 덕분인지, layout 에 대한 고민을 할 필요가 없어서 인지, 간단한 웹 페이지 구성하기 정말 좋다고 느꼈다.
💖 2 ) 빠른 개발 속도 덕분에 모델링, 분석에 집중할 시간이 늘어난다. 내가 분석한 내용을 간단히 보여줄 일이 있을 때 좋을 것 같다고 생각했다.
💖 3 ) 사람들이 React 와 Typescript 로 다양한 기능을 만들어두었다. 내가 원하는 것을 직접 만들어 사용할 수 있음이 매력적으로 다가왔다.

제 4 - 2 장. Streamlit 단점

💔 1 ) 편리한 만큼 제약사항이 있다. Style 적용이나, Streamlit 과 Web 의 상호작용이 단방향이기 때문에 오는 제약 등이 있다.
💔 2 ) Session State 공유의 건..ㅎ ( streamlit 단점이라고 할 수 있을진 모르겠지만.. )

제 4 - 3 장. 소감

Demo 용 웹을 만드는 툴 ! 로 배운 뒤 꽤나 오랜 시간을 걸쳐 베타 버전 웹 페이지를 공개했다.

말 그대로 간단한 웹을 만드는 도구인데 시간을 너무 많이 쓰고 있는건지 걱정이 됐다.
제약사항을 발견할 때 마다 아, 리액트할걸 하는 생각도 종종 들었다.
물론 리액트 쓴다고 더 빨리 한다거나 더 멋졌을 거란 말은 아니다.
그래도 문법이 단순하고 layout 잡기가 쉬워서 꽤나 빠르게 백엔드와 모델의 결과를 확인하기 좋았다.

앞으로도 분석 결과를 시뮬레이션 해서 볼 일이 있다면 streamlit 을 애용할 것 같다.

부록 : HTML, CSS 로 좋아요 버튼 만들기

하트를 누르면 빨간색으로 변한다 ! 한 번 더 누르면 원래대로 되돌릴 수도 있다.
마음에 들었는데 깜빡임 문제 때문에 아쉽지만 이모지로 바꾸었다.

#heart{
    font-size: 25px;
}
#heart:hover{
    color:red;
}

<!DOCTYPE html>
<html lang="en">
    <head>
        <script src="https://kit.fontawesome.com/3929e16ef5.js" crossorigin="anonymous"></script>
        <script src="{% static 'network/functions.js' %}"></script>
    </head>
    <body>

      <div>
            <i id="heart" class="far fa-heart"></i>
      </div>
    </body>
</html

0개의 댓글