ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Clientside Callbacks
    plotly dash 2023. 12. 13. 00:38

    개요

    dash 에서 javascript 로 작성된 라이브러리를 사용하기 위해 dash 의 Clientside Callback 을 활용 할 수 있다.

     

    데모 페이지

    https://wefree-tistory.fly.dev/javascript

     

    Clientside Callback 동작

    사용할려는 javascript library 를 다운 받아 src/assests 디렉토리 하위에 저장해 둔다. (Dash 에서 자동으로 import 해 줌)

    dash 의 Clientside Callback  은 @callback 대신에  clientside_callback 을 사용하는데, 다음과 같이 동작한다.

    1. (python) dash.Input(id, property) 로 callback 이 trigger 될 조건 지정 
    2. (javascript) javascript function 코드를 python string 으로 작성하는데, 이 javascript function 이 리턴하는 값이 dash.Output(id, property) 을 업데이트 한다.
      • javascript function 에서 Promise 로 리턴되어도 좋다. (dash 도움말 참고)
      • javascript library 가 callback 함수 기반이면, Promise 로 만들어 리턴되도록 하자. (참고)

     

    문제 상황

    javascript Sortable library 를 Clientside Callbacks 을 이용해 dash 에 포함시킬려고 하니, 아래와 같은 문제가 있었다.

    • dash 의 Clientside Callback 은 기본적으로 dash 에서 만든 Html element (or component) 가 dash.Input() 으로 들어가야 하는데, drag & drop 할 element 를  Sortable library 에서 만들고 이벤트도 다 처리 하도록 되어 있다. 아래는 Sortable library 사용법이다.
    <ul id="items">
    	<li>item 1</li>
    	<li>item 2</li>
    	<li>item 3</li>
    </ul>
    
    let el = document.getElementById('items');
    Sortable.create(el, {
      animation: 150,
      onEnd: function (/**Event*/evt) {
    		...
    	}
    });
    • 그리고 Sortable.create() 는 한번만 호출 되어야 한다.

     

     

    해결 방안

    • dash 는 기본적으로 app 시작시 모든 callback 이 한번씩 호출 되므로, dummy element 에 Clientside Callback 을 걸어 Sortable.create() 는 한번만 호출되도록 한다.  
    from dash import Dash, html, dcc, Output, Input, State, callback
    from dash import clientside_callback
    
    
    clientside_callback(
        '''
        (value) => {
           Sortable.create(items, {
            animation: 150,
            onEnd: (evt) => {
                const msg = [evt.oldIndex.toString(), evt.newIndex.toString()]
                console.log(msg);
                sessionStorage.setItem("session", JSON.stringify(msg));
                document.getElementById("hidden_sync_button").click();
            }
          }); 
        }
        ''',
        Output("dummy-element", "style"),
        Input("dummy-element", "value"),
    )
    • javascript 로 작성된 onEnd 에서 이벤트를 받는데, 이벤트 결과를 dash 로 전달해 python 코드로 가공할 수 있도록 해야 한다. 이를 위해 브라우저 Session Storage 에 이벤트 결과를 저장하고, dash 의 dcc.Store() 와 연동 될 수 있도록 한다. layout 에 dcc.Store() 를 추가한다. dcc.Store() 는 데이터를 json 으로 저장하고 읽기 때문에 javascript 에서 저장하거나(sessionStorage.setItem) 읽을 때(sessionStorage.getItem)도 이에 맞추어야 한다.
    app = Dash(__name__)
    
    app.layout = html.Div(
        [
            dcc.Store(id='session', storage_type='session'), # 목적에 따라 storage_type='memory' 로 변경
            dcc.Textarea(id="my-sortable-output"),
            html.Button("Sync", id="hidden_sync_button", hidden=True),
            html.Ul(
                [
                    html.Li("item 1"),
                    html.Li("item 2"),
                    html.Li("item 3")
                ],
                id="items",
            )
       ]
    • clientside_callback() 에서 호출되는 sessionStorage.setItem() 에 의해 dcc.Store(id='session') 이 trigger 되거나, sessionStorage.setItem() 에서 저장한 데이터를 dcc.Store 를 이용해 읽을 수 있으면 좋겠지만.. client 와 react 간의 sync 이슈 때문에 그렇게는 되지 않는다. (이것 때문에 고생했다.ㅠ) - 참고
    • 위의 문제 때문에 onEnd: sessionStorage.setItem() 에서 저장한 데이터를 다시 dcc.Store(id='session')  에 저장하는 프로세스가 필요했다. 이를 위해 아래와 같이 꼼수를 썼다. - 참고
      • javascript 로 작성된 onEnd 에서 sessionStorage.setItem() 으로 데이터를 저장하면서 dash element (hidden_sync_button) 에 click event 를 발생 시켰다. 
        코드: document.getElementById("hidden_sync_button").click();
      • dash 에서는 hidden_sync_button click() 이벤트가 발생하면, clientside_callback 을 통해 sessionStorage.setItem() 으로 저장한 데이터를  sessionStorage.getItem() 으로 읽어 dcc.Store(id='session') 에 저장되도록 한다.
    clientside_callback(
        '''
        (n_clicks) => {
           let json = sessionStorage.getItem("session");
           return JSON.parse(json);
        }
        ''',
        Output("session", "data"),
        Input("hidden_sync_button", "n_clicks"),
    )

     

    • 최종적으로 dcc.Store() 에 저장된 데이터는 아래처럼 사용할 수 있다.
    @callback(
        Output("my-sortable-output", "value"),
        Input("session", "modified_timestamp"),
        State("session", "data"),
        prevent_initial_call=True
    )
    def action(_, data):
        data = data or ["0", "0"]
        return ' -> '.join(data)

     

     

    주의 사항

    dash 의 Clientside Callback  이용시 python string 으로 javascript 코드를 넣는 대신에, 따로 javascript 파일을 만들어 활용할 수 있다. 이때 외부 라이브러리를 사용할 필요가 있는데, 따로 만든 javascript 파일에서 외부 라이브러리를 import 하면 정의한 function 을 찾을 수 없다는 에러가 발생한다. 이를 위해 다음과 같이 개발한다.

    1. 외부 라이브러리(예: Sortable.min.js) 파일을 다운로드 후 src/assests 디렉토리 하위에 저장
    2. 로컬 개발 환경에서 개발 편의를 위해 npm 으로 사용하려는 외부 라이브러리 인스톨(예: npm install sortablejs)
      • 이제 외부 라이브러리에 대한 code auto-complete 이 가능
      • (다시 강조하지만) clientside callback 을 위해 따로 만든 javascript 파일에서 외부 라이브러리를 import 하면 안된다.

     

    결론

    왠만한 javascript 라이브러리는 모두 dash 에서 쉽게 사용 가능할 듯?

    'plotly dash' 카테고리의 다른 글

    html.Div 에 간단한 데이터 저장하기  (0) 2023.12.15
    admin (multi-page) 화면 개발  (0) 2023.07.15
    Advanced Callback  (0) 2023.06.15
    Interval  (0) 2022.06.14
    Tab  (0) 2022.06.14

    댓글

Designed by Tistory.