shiny module
https://shiny.posit.co/py/docs/modules.html
Module 정의
Module은 재사용가능한 UI를 기반으로 해당 UI의 Server 로직을 하나의 NameSpace에 캡슐화하여 하나의 Container처럼 사용하는 것이다.
특정 NameSpace 내의 함수
Shiny Module은 Python의 그것과 유사하지만 같은 것은 아니다.
Python Module은 NameSpace에서 객체를 구성하는 일반적인 방법인 반면, Shiny Module은 NameSpace에서 reactive components를 캡슐화하는 데 사용됩니다.
Module이 필요한 이유
작은 애플리케이션은 코드 품질에 대해 크게 걱정하지 않고 쉽게 작성할 수 있지만 애플리케이션이 커질수록 복잡성이 커지고 그만큼 유지보수의 이슈가 생기기 마련이다. 이때 shiny의 Module을 사용한 코드 구성 방식을 고려해 볼만하다.
Module을 사용하여, 애플리케이션을 작은 조각으로 나누어 관리하고, 이를 통해 더 큰 애플리케이션을 구성할수 있다.
- function vs. Module
Shiny 코드가 반복적이라는 것을 발견할 때마다, 일부 로직을 함수나 Module로 추출할 가치가 있는지 고려해야 합니다.
Logic –> Functions
코드 품질을 개선하기 위해 할 수 있는 가장 중요한 일 중 하나는 Logic을 Function으로 추출하는 것입니다.
Function를 사용하면 Logic을 한 곳에 관리하므로, 이해하기가 쉽고 실수를 줄일수 있다.
또한 Function내에서 Local변수를 정의할 수 있어, global 환경과의 naming conflicts을 피할 수 있습니다.
많은 사람이 일상적인 프로그래밍 활동에서 함수를 사용하는 데 익숙하지만, 사용자 인터페이스를 구축할 때는 함수를 사용하는 것을 잊어버리는 경우가 많다. 하지만, 함수는 다른 맥락에서와 마찬가지로 강력한 기능을 발휘합니다.
Shiny UI에서 함수를 사용하려면, 유효한 UI 요소를 반환하는 함수를 작성하기만 하면 됩니다.
ex) 거의 동일한 값을 가진 슬라이더가 여러 개 있다고 가정해 봅시다:
이 코드는 반복되는 부분이 많아서 관리하기 어렵습니다. 예를 들어 모든 슬라이더의 최대값을 변경하려면 한 번이 아니라 다섯 번을 변경해야 합니다. 이러한 반복을 허용하는 대신 ui.input_slider를 반환하는 함수를 만들고 다른 ID로 해당 함수를 호출할 수 있습니다. 이 함수를 목록 이해 기능과 함께 사용하면 코드의 반복을 더욱 줄일 수 있습니다.
from shiny import App, ui
app_ui = ui.page_fluid(
ui.input_slider("n1", "N",
0, 100, 20),
ui.input_slider("n2", "N",
0, 100, 20),
ui.input_slider("n3", "N",
0, 100, 20),
ui.input_slider("n4", "N",
0, 100, 20),
ui.input_slider("n5", "N",
0, 100, 20),
ui.input_slider("n6", "N",
0, 100, 20),
)
app = App(app_ui, None)
# ui-generating function 1
def my_slider(id):
return ui.input_slider(id, "N",
0, 100, 20)
multiple calls to the function.
app_ui = ui.page_fluid(
my_slider("n1"),
my_slider("n2"),
my_slider("n3"),
my_slider("n4"),
my_slider("n5"),
)
List comprehension.
ids = ["n1", "n2", "n3", "n4", "n5"]
app_ui = ui.page_fluid(
[my_slider(x) for x in ids]
)
zip
function to turn multiple lists into a list of tuples
# ui-generating function 2
def my_slider(id, label):
return ui.input_slider(id, label + " Number", 0, 100, 20)
numbers = ["n1", "n2", "n3", "n4", "n5"]
labels = ["First", "Second", "Third", "Fourth", "Fifth"]
app_ui = ui.page_fluid(
[my_slider(x, y) for x, y in zip(numbers, labels)]
)
Function : Module과의 차이점 및 단점
이러한 방식으로 Function를 사용하는 것은 애플리케이션 코드를 개선하는 좋은 방법이지만 문제가 있습니다.
- Function 내에서 로컬 범위의 변수를 사용할 수 있지만, 각 입/출력 ID는 전체 Shiny 애플리케이션에 걸쳐 고유해야 합니다.
- 예를 들어 위 예와 달리, function안에 input과 output, 즉 2개의 UI 요소를 반환하는 이 함수를 생각해 봅시다.
io_row() 함수는 정상적으로 작동하지만, 두 번 이상 사용하려고 하면 앱이 제대로 렌더링되지 않습니다. 그 이유는 Shiny에서는 UI의 모든 ID가 고유해야 하며, 이 함수를 두 번 이상 호출하면 text_input ID를 가진 엘리먼트와 text_output ID를 가진 엘리먼트가 여러 개 있기 때문입니다. 이 경우 Shiny는 특정 입력을 특정 출력에 연결하는 방법을 모르기 때문입니다.
이 문제를 해결하려면 함수에 접두사 인수를 추가하여 반환되는 모든 요소의 ID에 추가하면 됩니다.
함수를 호출할 때마다 고유한 접두사를 제공하면 결과 ID는 고유하며 네임스페이스 충돌을 피할 수 있습니다.
그렇지만, 접두사 인수는 반환된 UI ID의 네임스페이스를 효과적으로 생성하며, 작동은 하지만 문제를 해결하는 데 매우 불편한 방식입니다. 접두사를 수동으로 관리하려면 많은 타이핑이 필요하며, ID 중 하나에 접두사를 추가하는 것을 잊어버리면 올바른 동작을 얻을 수 없습니다. 더 큰 문제는 이 네임스페이스에 모든 UI 요소를 포함할 수 있지만 서버 로직을 별도로 정의하고 해당 렌더링 호출에서 올바른 ID를 모두 참조하고 있는지 확인해야 한다는 것입니다.
from shiny import App, Inputs, Outputs, Session, reactive, render, req, ui
def io_row():
return ui.row(
ui.column(6, ui.input_text("text_input", "Enter text")),
ui.column(6, ui.output_text("text_output")),
)
app_ui = ui.page_fluid(
io_row(),
io_row(),
)
def server(input: Inputs, output: Outputs, session: Session):
@output
@render.text
def text_output():
return f"You entered '{input.text_input()}'"
app = App(app_ui, server)
from shiny import App, Inputs, Outputs, Session, reactive, render, req, ui
def io_row(prefix):
return ui.row(
ui.column(6, ui.input_text(f"{prefix}_text_input", "Enter text")),
ui.column(6, ui.output_text(f"{prefix}_text_output")),
)
app_ui = ui.page_fluid(
io_row("first_row"),
io_row("second_row"),
)
def server(input: Inputs, output: Outputs, session: Session):
@output
@render.text
def first_row_text_output():
return f"You entered '{input.first_row_text_input()}'"
@output
@render.text
def second_row_text_output():
return f"You entered '{input.second_row_text_input()}'"
app = App(app_ui, server)
Module은 UI와 서버 로직을 모두 자체 네임스페이스에 캡슐화하여 이 두 가지 문제를 모두 해결합니다.
Module 네임스페이스는 Module의 코드를 보관하는 컨테이너로 Module의 변수, 함수 및 클래스를 다른 Module의 변수와 분리하여 유지하는 데 도움이 됩니다. 이렇게 분리하면 이름 충돌을 방지하고 코드를 더 쉽게 이해하고 관리할 수 있습니다.
Shiny Module의 맥락에서 네임스페이스는 접두사 인자처럼 작동합니다.
이는 Module의 각 인스턴스에 할당하는 고유 식별자로, 입력 및 출력 ID를 다른 인스턴스 및 나머지 Shiny 애플리케이션의 ID와 분리하기 위해 Shiny에서 할당합니다.
Module 사용 방법 – Application에 적용
작성한 애플리케이션에 Module을 포함하는 방법에 대해 알아보자.
본질적으로 Module은 함수에 불과하다. 따라서 함수로 할 수 있는 것은 Module에서도 가능합니다.
Module은 모든 인수를 받을 수 있으며 부모 컨텍스트에 모든 값을 반환할 수 있습니다.
Module은 일반적으로 애플리케이션의 일부를 캡슐화하기 위해 함께 작동하는 UI와 서버 요소를 모두 포함하며,
Module의 UI와 Server는 일반 Shiny 애플리케이션에서와 똑같은 방식으로 작동합니다.
Module의 UI 부분은 UI 요소를 반환하는 함수이며 @module.ui 데코레이터로 장식됩니다.
이 데코레이터는 기본 Module 네임스페이스를 설정하므로 함수에 의해 생성된 각 컴포넌트의 ID에 접두사가 암시적으로 추가됩니다.
Module Server 함수는 @module.server 데코레이터로 꾸민다는 점을 제외하면 Shiny 앱 서버 함수와 비슷하게 생겼습니다.
# my_row_module.py
from shiny import module, render, ui, Inputs, Outputs, Session
@module.ui
def row_ui():
return ui.row(
ui.column(6, ui.input_text("text_in", "Enter text")),
ui.column(6, ui.output_text("text_out")),
)
@module.server
def row_server(input: Inputs, output: Outputs, session: Session):
@output
@render.text
def text_out():
return f"You entered {input.text_in()}"
애플리케이션에서 이 Module을 사용하려면 애플리케이션 UI 및 서버 함수 내에서 Module UI 및 서버 함수를 호출합니다.
모든 Module 호출에는 Module의 네임스페이스를 정의하는 ID 인수가 포함됩니다.
이 ID에는 두 가지 요구 사항이 있습니다.
첫째, 단일 범위에서 고유해야 하며 특정 애플리케이션이나 Module 정의에서 중복될 수 없습니다. 단일 Module의 인스턴스를 여러 개 생성해야 하는 경우 해당 Module의 ID를 목록에 저장하고 목록 이해 기능을 사용하여 UI 및 서버 인스턴스를 생성하는 것이 좋습니다.
둘째, UI와 서버 ID가 일치해야 합니다. 이렇게 하면 UI와 서버 인스턴스가 동일한 네임스페이스에 존재하도록 보장하며, ID가 일치하지 않으면 UI와 서버 Module이 상호 작용할 수 없습니다.
# module_test_app.py
from shiny import App, Inputs, Outputs, Session, module, render, ui
from my_row_module import row_ui, row_server
extra_ids = ["row_3", "row_4", "row_5"]
app_ui = ui.page_fluid(
row_ui("row_1"),
row_ui("row_2"),
[row_ui(x) for x in extra_ids]
)
def server(input: Inputs, output: Outputs, session: Session):
row_server("row_1")
row_server("row_2")
[row_server(x) for x in extra_ids]
app = App(app_ui, server)
Module을 사용하면 UI와 서버 코드를 동일한 네임스페이스에 묶을 수 있으므로,Module 내에 임의로 복잡한 상호작용을 포함할 수 있습니다. Shiny 앱에서 할 수 있는 모든 작업을 Module 내부에서도 수행할 수 있으며 Module 자체에서 다른 Module을 호출할 수 있습니다.
이를 통해 앱을 다양한 크기의 빌딩 블록으로 나누고, 이러한 블록을 구성하여 다양한 애플리케이션을 만들고, 더 나아가 다른 사람들과 공유할 수 있습니다.