shiny module – Module 사용방법 – Communication

Published by onesixx on

Module은 반복해서 사용하는 특적 UI 블럭를 효율적으로 관리하기 위해 사용한다.

Module을 사용하면 App을 실행 가능한 부분으로 나누고, 이러한 부분이 서로 통신하는 방식을 정의하고, 애플리케이션 전반에서 컴포넌트를 재사용할 수 있습니다.
모듈을 마스터하는 데는 상당한 시간이 걸리지만, 아래 4 가지 패턴을 사용하면 거의 모든 것을 달성할 수 있습니다.

  1. non-reactive 인수를 받는 Module
  2. reactive 인수를 받는 Module
  3. callback 인수를 받는 Module
  4. reactive 인수를 return하는 Module

이제 서로 다른 Module과 Application의 나머지 부분 간에 통신하는 방법을 알아보자.

Module간 Communication

  • Module <=> Application
  • Module A <=> Module B

Module을 사용하여 App을 의미있는 단위로 나누기 시작하면, Module과 기존의 App 간에 “value”을 전달할 필요가 있다.
예를 들어, App에서 입력 label을 정의하고, 해당 label을 module에 전달하거나,
어떤 Module의 UI가 다른 Module의 Output에 영향을 미치도록 구성해야 하는 경우도 있다.

Module은 단지 특정 NameSpace 내의 함수일 뿐이고, reactive 및 non-reactive 인수를 모두 받아 return할 수 있으므로,
이를 기반으로 사용자의 요구사항을 해결하기 위한 다양한 방법을 제공합니다.

1. Non-reactive 인수로 Module에 전달

App —(Non-reactive)—> Module

숫자 및 문자열 값 인수

Module은 Non-reactive 값을 인수로 받는다.

이는 Module과 정보를 주고받는 가장 쉬운 방법이고,
(Python) 함수에 인수를 전달하는 것과 같으며, 특정 Module 옵션을 설정할 수 있습니다.

ex) UI의 버튼의 Label(custom_label)를 module에 넘겨줌,
Server에서 카운터의 시작값(starting_value)을 설정해줌

https://shiny.posit.co/py/docs/module-communication.html


Module의 ui함수 : 버튼레이블(custom_label)을 설정하는 인수를 추가
Module의 server함수: 카운터의 시작값(starting_value)을 지정하는 인수를 추가.
App: 앱에서 Module을 호출할 때, 옵션을 설정할 수 있습니다.
(위에서 넘겨주는 인수와 상관없이, Module함수에는 항상 ID를 제공해서, NameSpace를 정의해야한다!).

아래와 같이, 인수를 활용하면 Module을 훨씬 더 유연하게 만들 수 있고,
Application에 필요한 유연성을 유지하면서 일부 로직을 캡슐화할 수 있습니다.

# counter_m.py is a module that can be reused in other apps.
from shiny import module, ui, render, reactive, event, App

@module.ui
def counter_ui(custom_label="Increment counter"):
    return ui.card(
        ui.h2("This is ", custom_label),
        ui.input_action_button(id="button", label=custom_label),
        ui.output_text_verbatim(id="out"),
    )

@module.server
def counter_server(input, output, session,     starting_value=0):
    count = reactive.value(starting_value)

    @reactive.effect
    @reactive.event(input.button)
    def _():
        count.set(count() + 1)

    @output
    @render.text
    def out():
        return f"Click count is {count()}"

from shiny import App, ui
from module.counter_m import counter_ui, counter_server

app_ui = ui.page_fluid(
    ui.layout_columns(
        counter_ui("cnt1", "Counter 1"),
        counter_ui("cnt2", "Counter 2"),
        col_widths={"sm":(12,12), "md":(6,6), "lg":(6,6)},
    )    
)

def server(input, output, session):
    counter_server("cnt1", starting_value=5)
    counter_server("cnt2", starting_value=3)

app = App(app_ui, server)

UI element 인수 (*arg)

App —(*arg)—> Module

Module에 숫자 및 문자열 값을 전달하는 것 외에도, 원하는 수의 UI요소를 전달할 수 있습니다.
이를 통해 임의의 Shiny요소를 받아 원하는 방식으로 배열할 수 있는 ui.sidebar_layout()과 유사한 레이아웃 Module을 만들 수 있습니다.

Module에 여러 UI요소를 전달하는 방법에는 크게 2 가지가 있습니다.

1. Module이 인자 중 하나로 List을 받아, 다른 컨테이너 함수에 전달하도록 할 수 있습니다.

이 방법은 부모 컨텍스트에서 Module에 원하는 수의 요소를 전달할 수 있으므로 편리하지만,
Module에 요소를 전달하기 전에 요소를 반드시 List으로 래핑해야 합니다.

@module.ui
def mod_ui(elements):
    return ui.div(elements)

ui = ui.page_fluid(
	mod_ui(
		[ui.h1("heading"), ui.p("paragraph")]
	)
)

2. Module이 키워드가 아닌 인수를 *args로 받아들이도록 하는 것입니다.

이것이 Shiny의 컨테이너 함수가 설계된 방식이며, 이 패턴을 사용하면 다른 Shiny함수와 마찬가지로 Module UI를 호출할 수 있습니다.

ex) Standard Table을 표시하는 Card와 임의의 UI element들의 집합을 표시하는 Card를 표시하고 싶다
이를 위한 한 가지 방법은 한 카드에 표를 렌더링하고, 두 번째 카드에 *args를 전달하는 Module을 작성하는 것입니다.

@module.ui
def mod_ui(*args):
    return ui.div(*args)

ui = ui.page_fluid(
	mod_ui(
		ui.h1("heading"), ui.p("paragraph")
	)
)
참고.

Python에서 *args

*args는 함수에 가변 개수의 위치 인자(Positional argument)를 전달할 때 사용하는 문법입니다.
함수에 몇 개의 인자가 전달될지 미리 알 수 없을 때 사용되며,
*args는 이러한 인자들을 하나의 튜플로 묶어서 함수 내부로 전달합니다.
예를 들어, 여러 개의 숫자를 입력받아 그 합을 계산하는 함수는 다음과 같이 작성할 수 있습니다:

def sum_numbers(*args):
    return sum(args)

print(sum_numbers(1, 2, 3))  # 출력: 6
print(sum_numbers(1, 2, 3, 4, 5))  # 출력: 15

Python에서 **args

**args는 함수에 가변 개수의 키워드 인자(Keyword argument)를 전달할 때 사용하는 문법입니다.
이는 *args와 유사하지만, *args가 위치 기반 인자들을 튜플로 묶는 데 사용되는 반면, **args는 키워드 인자들을 딕셔너리로 묶어서 함수 내부에 전달합니다.

**args는 함수 정의에서 마지막 매개변수로 위치해야 하며,
함수 내부에서는 해당 인자들에 대해 딕셔너리 연산을 수행할 수 있습니다.
이를 통해 함수는 예상치 못한 키워드 인자들을 유연하게 처리할 수 있게 됩니다.

예를 들어, 여러 키워드 인자를 받아서 그것들의 키와 값을 출력하는 함수는 다음과 같이 작성할 수 있습니다:

def print_kwargs(**kwargs):
    for key, value in kwargs.items():
        print(f"{key}: {value}")

print_kwargs(first_name="John", last_name="Doe")

2. reactive 인수로 Module에 전달

App —(reactive)—> Module

기본 Module은 코드 기반을 정리하는 강력한 방법이지만 앱의 반응형 프레임워크에 통합하는 것은 어려울 수 있습니다. 예를 들어 애플리케이션의 모든 카운터를 리셋하는 전역 버튼을 만들고 싶다면 어떻게 해야 할까요?
이를 위해 반응형 객체를 전달하고 앱에서 사용하는 것과 마찬가지로 모듈 내부에서 사용할 수 있습니다.

중요!! input.n vs. input.n() reactive object itself vs. reactive object
input.n은 반응형 객체이지만,
input.n()을 호출하면 해당 객체의 현재 값이 return됩니다.

# app.py
from shiny import App, module, reactive, render, ui
from modules import counter_ui, counter_server

app_ui = ui.page_fluid(
    ui.input_action_button("clear", "Clear counters"),
    counter_ui("counter1", "Counter 1"),
)

def server(input, output, session):
    counter_server("counter1", starting_value=5, global_clear=input.clear)

app = App(app_ui, server)

module

from shiny import App, module, reactive, render, ui

@module.ui
def counter_ui(label: str="Increment counter"):
    return ui.card(
        ui.card_header("This is " + label),
        ui.input_action_button(id="button", label=label),

        ui.output_text_verbatim(id="out"),
    )

@module.server
def counter_server(input, output, session, 
    global_clear, starting_value=0):

    count =  reactive.value(starting_value)

    @reactive.effect
    @reactive.event(global_clear) # input.clear
    def clear_all():
        count.set(0)

    @reactive.effect
    @reactive.event(input.button)
    def iterate_counter():
        count.set(count() + 1)

    @output
    @render.text
    def out():
        return f"Click count is {count()}"

위 예의 App은 다른 앱과 동일한 리액티브 규칙을 따르고 있습니다. input.clear를 global_clear 인수로 모듈에 전달하면 다른 리액티브 객체를 사용할 때처럼 모듈 내부에서 사용할 수 있습니다. global_clear()로 값을 검색하거나 @reactive.event(global_clear)와 함께 사용하여 side effect을 trigger할 수 있습니다.
모든 모듈 인스턴스가 동일한 리액티브를 수신하고 있으므로, 해당 리액티브가 변경되면 해당 모듈 내의 요소가 업데이트됩니다.

3. callbacks 인수로 Module에 전달

App —(callback) —> module —(callback)—> App

일반적으로 Module이 가진 기본적인 문제는 Module내에서 application state의 일부를 변경하는 것입니다.
이를 위한 가장 직관적인 방법 중 하나는 app 에서 “state-modifying function”를 정의하고, 해당 함수를 Module로 전달하는 것입니다.
Modul 코드 내에서 해당 함수가 호출되면, global application state가 업데이트됩니다.

예를 들어, session의 총 버튼 클릭 수를 합산하는 텍스트 출력을 추가해 보겠습니다.


이를 위해 reactive.value와 해당 값을 1씩 증가시키는 함수를 생성합니다.
그런 다음 이 함수를 Module에 인수로 전달하고, Module내에서 정의된 버튼이 클릭될 때마다 이 함수가 호출합니다.
이렇게 하면 App에 정의된 reactive.value가 업데이트됩니다.

다른방법으로 reactive value 자체를 Module에 전달하여 동일한 작업을 수행할 수도 있지만, 좋은 생각이 아니다.
reactive value 을 전달하면 module과 호출된 특정 컨텍스트 간에 tight coupling이 생성된다.
이 Module은 특정 유형의 reactive value을 기대할 것이고, 다른 어떤 것에도 동작하지 않을 것입니다.
또한 업데이트 로직이 application context와 Module 간에 분할되어 추론하기가 더 어려워집니다.

따라서, callback을 전달하는 것이 Module이 다양한 작업을 수행에 더 유연하게 사용될 수 있다.
예를 들어, 다른 callback을 전달하면 버튼이 클릭되었을 때 다른 작업을 수행하는 다른 App에서 동일한 Module을 사용할 수 있습니다.

4. reactive 인수를 return하는 Module 전달

Module —-(reactive)—-> App

reactives를 Module에 전달하여, Module의 코드 내에서 사용할 수 있는 것처럼,
Moudule에서 reactives를 return하여 App에서 사용할 수도 있습니다.

예를 들어, dynamic UI 의 일반적인 형태 중 하나는 하나의 dropdown을 기반으로 다른 dropdown 메뉴를 채우는 것입니다.
사용자가 특정 “주”를 선택할 수 있는 메뉴가 하나 있고, 해당 주의 “도시”만 표시하는 메뉴가 또 하나 있을 수 있습니다. 이것은 매우 일반적인 구성 요소이므로 다른 애플리케이션에 쉽게 추가할 수 있도록 모듈로 추출하고 싶을 수 있습니다.

이를 위해 모듈의 서버 함수가 모듈에 정의된 리액티브 객체 중 하나를 반환하도록 할 수 있습니다.
그러면 이 반응형 객체는 다른 반응형 객체처럼 애플리케이션 컨텍스트에서 사용할 수 있습니다.

Multiple returns

module context에서 여러 개의 reactive objects를 검색하고 싶을 때가 있습니다.
이를 위해 tuple 또는 namedtuple을 사용하여, module에서 다른 context로 여러 개의 reactive를 보낼 수 있습니다.

예를 들어, Module에서 도시와 주 reactive를 모두 검색하려는 경우,
모듈에서 반환(input.cities, input.state)을 통해 두 개를 모두 반환하도록 할 수 있습니다. 그런 다음 이 튜플을 애플리케이션 컨텍스트에서 도시, 주 = city_state_server(“cities”)로 언패킹할 수 있습니다.

반환 값의 구조가 좀 더 복잡하다면 네임드 튜플을 반환하는 것이 좋습니다. 명명된 튜플은 특정 명명된 속성을 설정할 수 있다는 점을 제외하면 튜플과 유사하지만, 명명된 튜플에 올바른 속성을 전달하지 않으면 조기에 큰 소리로 실패하기 때문에 데이터 유효성 검사에 유용합니다.

Categories: shiny

onesixx

Blog Owner

Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x