shiny module – Module 사용방법 – Communication

Published 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

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

Module은 특정 NameSpace 내의 함수일 뿐이므로, reactive 및 non-reactive 인수를 모두 받아 return할 수 있으므로,
사용자의 요구 사항을 처리하기 위한 다양한 도구 세트를 제공합니다.

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

숫자 및 문자열 값 인수

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

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

ex) UI의 버튼의 Label(custom_label)과 카운터의 시작값(starting_value)을 설정할 수 있는 카운터 Module
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)
https://shiny.posit.co/py/docs/module-communication.html

UI element 인수 (*arg)

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. Module에 reactive 인수로 전달

기본 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. Module에 callbacks 인수로 전달

일반적으로 Module이 가진 문제는 Module내에서 애플리케이션 state의 일부를 변경하는 것입니다.
이를 위한 직관적인 방법 중 하나는 애플리케이션 수준에서 “상태 수정 함수”를 정의하고, 해당 함수를 모듈로 전달하는 것입니다.
모듈 코드 내에서 함수가 호출되면 전역 애플리케이션 상태가 업데이트됩니다.

예를 들어, 세션의 총 버튼 클릭 수를 합산하는 텍스트 출력을 추가해 보겠습니다.
이를 위해 reactive.value와 해당 값을 1씩 증가시키는 함수를 생성합니다.
그런 다음 이 함수를 모듈에 전달하고 모듈 버튼이 클릭될 때마다 호출합니다.
이렇게 하면 애플리케이션 수준에서 reactive.value가 업데이트됩니다.

반응형 값 자체를 모듈에 전달하여 동일한 작업을 수행할 수도 있지만, 이 방법은 효과가 있지만 좋은 아이디어는 아닙니다. 반응형 값을 전달하면 모듈과 모듈이 호출된 특정 컨텍스트 간에 긴밀한 결합이 이루어집니다.
모듈은 특정 유형의 반응형 값을 기대할 것이고 다른 어떤 것에도 작동하지 않을 것입니다. 또한 업데이트 로직이 애플리케이션 컨텍스트와 모듈 간에 분할되어 추론하기가 더 어려워집니다. 콜백을 전달하면 모듈이 다양한 작업을 수행하는 데 사용될 수 있기 때문에 더 유연합니다.

예를 들어 다른 콜백을 전달하면 버튼이 클릭되었을 때 다른 작업을 수행하는 다른 애플리케이션에서 동일한 모듈을 사용할 수 있습니다.

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

반응형 객체를 모듈에 전달하여 모듈 코드 내에서 사용할 수 있는 것처럼 모듈에서 반응형 객체를 반환하여 애플리케이션에서 사용할 수도 있습니다. 예를 들어 동적 사용자 인터페이스의 일반적인 형태 중 하나는 다른 드롭다운을 기반으로 드롭다운 메뉴를 채우는 것입니다. 사용자가 주를 선택할 수 있는 메뉴가 하나 있고 해당 주의 도시만 표시하는 메뉴가 또 하나 있을 수 있습니다. 이것은 매우 일반적인 구성 요소이므로 다른 애플리케이션에 쉽게 추가할 수 있도록 모듈로 추출하고 싶을 수 있습니다.

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

Multiple returns

모듈 컨텍스트에서 여러 개의 리액티브 객체를 검색하고 싶을 때가 있습니다. 이를 위해 튜플 또는 네임드 튜플을 사용하여 모듈에서 다른 컨텍스트로 여러 개의 리액티브를 보낼 수 있습니다. 예를 들어 모듈에서 도시와 주 리액티브를 모두 검색하려는 경우 모듈에서 반환(input.cities, input.state)을 통해 두 개를 모두 반환하도록 할 수 있습니다. 그런 다음 이 튜플을 애플리케이션 컨텍스트에서 도시, 주 = city_state_server(“cities”)로 언패킹할 수 있습니다.

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

Categories: shiny

onesixx

Blog Owner

Subscribe
Notify of
guest

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