Python용 Shiny 대시보드

소개

Shiny 패키지는 Python으로 웹 애플리케이션을 쉽게 만들 수 있게 해줍니다. Quarto 대시보드는 Shiny 구성 요소(예: 입력을 제어하는 슬라이더가 있는 플롯)를 포함할 수 있습니다.

이 섹션은 Shiny 경험이 없는 사용자를 대상으로 하며, Quarto에서 Shiny를 시작하는 데 필요한 기본 개념을 설명합니다.

Python 대신 R을 사용한다면 R용 Shiny 문서를 참고하세요.

NoteShiny 사전 요구사항

Quarto 문서에서 Shiny를 사용하려면 최신 버전의 shiny(>=0.9.0)와 shinywidgets(>=0.3.1) 패키지가 필요합니다. 다음과 같이 최신 버전을 설치할 수 있습니다.

pip install --upgrade shiny shinywidgets

안녕, Shiny

먼저 단일 플롯과 입력으로 구성된 아주 간단한 대시보드에서 시작해 보겠습니다.

Penguin Bills 대시보드 스크린샷. 왼쪽 사이드바에는 Variable과 Distribution 드롭다운, rugmarks 표시 체크박스가 있고, 오른쪽에는 종에 따라 색을 입힌 bill_length_mm 히스토그램이 전체 높이를 차지한다.

아래는 이 대시보드의 소스 코드입니다(오른쪽 끝의 숫자를 클릭하면 추가 설명을 볼 수 있습니다).

---
title: "Penguin Bills"
format: dashboard
server: shiny
---

```{python}
import seaborn as sns
penguins = sns.load_dataset("penguins")
```

## {.sidebar}

```{python}
from shiny.express import render, ui
ui.input_select("x", "Variable:",
                choices=["bill_length_mm", "bill_depth_mm"])
ui.input_select("dist", "Distribution:", choices=["hist", "kde"])
ui.input_checkbox("rug", "Show rug marks", value = False)
```

## Column

```{python}
@render.plot
def displot():
    sns.displot(
        data=penguins, hue="species", multiple="stack",
        x=input.x(), rug=input.rug(), kind=input.dist())
```
1
server: shiny 옵션은 문서 뒤에서 Shiny Server를 실행하도록 Quarto에 지시합니다.
2
2단계 제목에 .sidebar 클래스를 추가하면 사이드바를 만들 수 있습니다. 사이드바에는 코드 셀뿐 아니라 이미지, 설명 텍스트, 링크도 포함할 수 있습니다.
3
Shiny 입력 요소의 모음(이를 조작하면 input 객체가 업데이트됩니다).
4
플롯은 현재 input 값에 따라 렌더링되고 업데이트됩니다.

이 대시보드에서는 왼쪽의 선택 상자에서 값을 고르고, 플롯이 선택에 따라 업데이트됩니다. 체크박스를 클릭하면 rug 표시를 보여주거나 숨길 수도 있습니다. 이제 이 Shiny 대시보드를 단계별로 만들어 보겠습니다.

메타데이터

먼저 프런트매터에 server: shiny를 추가합니다. 이는 Quarto가 문서를 정적 HTML 페이지가 아니라 Shiny 대시보드로 렌더링해야 함을 의미합니다(대시보드가 표시되는 동안 Python 런타임이 필요). server: shiny 문서에 대해 quarto preview <filename>.qmd를 실행하면 Quarto가 Shiny 프로세스를 시작해 유지하고, 브라우저에서 대시보드를 엽니다.

입력 컨트롤 추가

다음으로 ui.input_xxx 패턴에 해당하는 함수로 입력 컨트롤을 만듭니다. 예를 들어 ui.input_select()는 선택 상자를, ui.input_slider()는 슬라이더를 생성합니다. 이 함수들이 반환하는 값은 Quarto가 HTML과 JavaScript로 렌더링합니다.

위 예제는 두 가지 입력만 사용하지만 Shiny에는 훨씬 더 많은 입력이 있습니다. Shiny Component Browser에서 모든 입력을 확인하고, 대시보드에 붙여넣을 수 있는 코드 스니펫을 얻을 수 있습니다.

위 예제에서 입력을 다음 코드로 정의했습니다.

```{python}
ui.input_select("x", label="Variable:",
                choices=["bill_length_mm", "bill_depth_mm"])
```

각 입력 함수는 첫 번째 인자로 입력 ID를 받습니다. 입력 ID는 이 입력을 고유하게 식별하는 문자열로, 간단하고 문법적으로 올바른 Python 변수명이어야 합니다. 이 ID를 사용해 대시보드의 다른 부분에서 입력 값을 가져옵니다.

Warning

Shiny 대시보드에서 각 입력 ID는 반드시 고유해야 합니다. 서로 다른 두 입력에 같은 ID를 사용하면 Shiny가 구분할 수 없어 대시보드가 제대로 동작하지 않습니다.

각 입력 함수의 두 번째 인자는 보통 입력 옆에 표시되는 사람이 읽기 쉬운 문자열입니다. 예를 들어, ui.input_select() 함수는 두 번째 인자로 "Variable:"을 전달하므로 선택 상자 옆에 “Variable:” 레이블이 표시됩니다.

사이드바와 툴바

대부분의 대시보드에서는 입력 컨트롤을 사이드바에 시각적으로 모아두는 것이 좋습니다. 예제처럼 2단계 제목에 .sidebar 클래스를 추가하면 됩니다.

## {.sidebar}

```{python}
ui.input_select("x", "Variable:",
                choices=["bill_length_mm", "bill_depth_mm"])
ui.input_select("dist", "Distribution:", choices=["hist", "kde"])
ui.input_checkbox("rug", "Show rug marks", value = False)
```

사이드바 대신 입력을 가로로 배치하거나, 카드에 직접 연결할 수도 있습니다. 자세한 내용은 입력 문서를 참고하세요.

동적 출력 표시

Shiny에서 대시보드는 플롯, 표, 텍스트 등 다양한 출력이 사용자 입력에 따라 동적으로 업데이트됩니다.

위 예제는 다음 코드로 동적 플롯을 정의합니다.

```{python}
@render.plot
def displot():
    sns.displot(
        data=penguins, hue="species", multiple="stack",
        x=input.x(), rug=input.rug(), kind=input.dist())
```

이 함수의 이름은 displot입니다. 함수 본문은 일반적인 Seaborn 코드로 플롯을 생성합니다. 그리고 @render.plot 데코레이터를 함수에 추가해 Shiny가 이 함수를 플롯 생성에 사용하도록 지정합니다. (데코레이터를 처음 보는 분이라면, 이는 함수에 추가 동작을 부여하는 Python 기능입니다.)

input.x(), input.rug(), input.dist()는 앞서 만든 x, rug, dist 입력의 값을 가져옵니다.

여기서 중요한 점은 우리가 displot() 함수를 호출하지 않는다는 것입니다. 함수 정의와 @render.plot 데코레이터만으로 Shiny와 Quarto는 다음을 수행합니다.

  • 이 위치에 플롯을 삽입합니다.
  • 함수 본문으로 플롯을 생성합니다.
  • 사용자가 input.x(), input.rug(), input.dist() 값을 변경하면 함수 본문을 자동으로 다시 실행하고 기존 플롯을 업데이트합니다.

이 예제는 @render.plot 출력이 하나뿐이지만, Shiny 앱은 여러 출력을 포함할 수 있고 출력 유형도 다양합니다. 사용 가능한 출력 유형은 Shiny Component Browser에서 확인하세요.

반응형 프로그래밍

이전 섹션에서는 displot 함수가 참조하는 입력이 바뀔 때마다 자동으로 다시 실행된다고 설명했습니다. Shiny는 반응형 프로그래밍 프레임워크로, 앱의 입력과 출력 간의 관계를 추적합니다. 입력이 변경되면 그 입력의 영향을 받는 출력만 다시 렌더링됩니다. 이는 사용자 입력에 효율적으로 반응하는 대시보드를 만들 수 있게 해주는 강력한 기능입니다.

Note

input 객체는 Shiny의 반응형 프레임워크가 추적할 수 있도록 설계되어 있습니다.

Shiny는 input 같은 _반응형 인지 객체_의 변경을 추적합니다. 임의의 Python 변수는 추적하지 않습니다. 예를 들어 x = 100을 선언하고 displot에서 x를 사용한다고 해서 x가 변경될 때마다 displot이 자동으로 다시 실행되지는 않습니다.

마찬가지로, Shiny는 @render.plot처럼 반응형을 인지하는 함수만 자동으로 다시 실행합니다. 문서 최상위 코드나 일반 Python 함수의 재실행을 자동으로 도와주지는 않습니다.

추가 기능

다음으로 설정 코드 분리, 반응형 계산, 페이지 같은 고급 레이아웃 구성 등 더 많은 기능을 포함한 심화 예제를 살펴봅니다. 아래는 우리가 만들 대화형 문서입니다.

Palmer Penguins 대시보드 스크린샷. 탐색 바에는 Plots와 Data 두 페이지가 표시된다. 왼쪽에는 펭귄 이미지와 네 개의 입력(종 체크박스, 섬 체크박스, Distribution 드롭다운, rug 표시 체크박스)이 있다. 오른쪽에는 두 행으로 나뉜 밀도 플롯이 있으며, 위는 bill_depth_mm, 아래는 bill_length_mm이다.

아래는 이 대시보드의 소스 코드입니다. 오른쪽 끝의 숫자를 클릭하면 문법과 동작 방식에 대한 추가 설명을 볼 수 있으며, 아래에서 더 자세히 설명합니다.

---
title: "Palmer Penguins"
author: "Cobblepot Analytics"
format: dashboard
server: shiny
---

```{python}
#| context: setup
import seaborn as sns
from shiny import reactive
from shiny.express import render, ui
penguins = sns.load_dataset("penguins")
```

# {.sidebar}

![](images/penguins.png){width="80%"}

```{python}
species = list(penguins["species"].value_counts().index)
ui.input_checkbox_group(
    "species", "Species:",
    species, selected = species
)

islands = list(penguins["island"].value_counts().index)
ui.input_checkbox_group(
    "islands", "Islands:",
    islands, selected = islands
)

@reactive.calc
def filtered_penguins():
    data = penguins[penguins["species"].isin(input.species())]
    data = data[data["island"].isin(input.islands())]
    return data
```

```{python}
ui.input_select("dist", "Distribution:", choices=["kde", "hist"])
ui.input_checkbox("rug", "Show rug marks", value = False)
```

[Learn more](https://pypi.org/project/palmerpenguins/) about the
Palmer Penguins dataset.

# Plots

```{python}
@render.plot
def depth():
    return sns.displot(
        filtered_penguins(), x = "bill_depth_mm",
        hue = "species", kind = input.dist(),
        fill = True, rug=input.rug()
    )
```

```{python}
@render.plot
def length():
    return sns.displot(
        filtered_penguins(), x = "bill_length_mm",
        hue = "species", kind = input.dist(),
        fill = True, rug=input.rug()
    )
```

# Data

```{python}
@render.data_frame
def dataview():
    return render.DataGrid(filtered_penguins())
```
1
server: shiny 옵션은 문서 뒤에서 Shiny Server를 실행하도록 Quarto에 지시합니다.
2
context: setup 셀 옵션은 이 코드 셀이 애플리케이션 시작 시(각 클라이언트 세션 시작 시가 아니라) 실행되도록 지정합니다. 비용이 큰 초기화 코드(예: 데이터 로딩)는 context: setup에 두는 것이 좋습니다.
3
1단계 제목에 .sidebar 클래스를 추가하면 전역 사이드바를 만들 수 있습니다. 사이드바에는 코드 셀뿐 아니라 이미지, 설명 텍스트, 링크도 포함할 수 있습니다.
4
이 체크박스 입력 그룹은 데이터셋의 speciesislands 필드에서 사용할 수 있는 범주를 기반으로 동적으로 내용을 구성합니다.
5
사용자가 체크박스 그룹을 조작하면 데이터셋의 필터링된 뷰가 바뀝니다. @reactive.calc 함수는 필터링된 데이터셋을 다시 계산해 filtered_penguins()로 제공합니다.
6
이 입력은 플롯 표시에는 영향을 주지만, 필터링된 데이터셋의 내용에는 영향을 주지 않습니다.
7
1단계 제목(여기서는 # Plots# Data)은 대시보드 안에 페이지를 만듭니다.
8
플롯은 필터링된 데이터셋(filtered_penguins())과 플롯 표시 관련 입력(input.dist()input.rug())을 참조해 렌더링됩니다. 데이터셋이나 입력이 바뀌면 플롯이 자동으로 다시 렌더링됩니다.
9
Data 탭도 filtered_penguins()를 참조하며, 필터링된 데이터가 바뀔 때마다 갱신됩니다.

설정 셀

정적 Quarto 문서에서 {python} 코드 셀은 렌더링 시에만 실행되고, 문서를 볼 때는 실행되지 않습니다. server: shiny 문서에서는 {python} 코드 셀이 렌더링 시점과 대시보드가 브라우저에서 로드될 때마다 실행됩니다. 이는 각 방문자에게 입력/출력의 독립된 메모리 사본이 필요하므로, 동시에 접속한 사용자가 서로 영향을 주지 않도록 하기 위함입니다.

그러나 모든 사용자마다 실행하기에는 부담스러운 코드도 있습니다. 이때 문서의 Shiny 런타임 프로세스가 시작될 때 딱 한 번만 실행되도록 하고 싶다면 설정 셀을 사용합니다. 예를 들어 위 예제에서는 패키지를 임포트하고 sns.load_dataset("penguins")로 데이터를 로드합니다.

```{python}
#| context: setup
import seaborn as sns
from shiny import reactive
from shiny.express import render, ui
penguins = sns.load_dataset("penguins")
```

이 코드를 설정 셀에 넣는 이유는 각 사용자마다 데이터를 로드하는 것보다, 프로세스 시작 시 한 번만 로드하는 것이 시간과 메모리 측면에서 훨씬 효율적이기 때문입니다.

코드 셀에 #| context: setup을 추가하면 Quarto가 Shiny 프로세스 시작 시에만 해당 코드를 실행하도록 지시할 수 있습니다. 설정 셀은 페이지 로드마다 실행될 필요가 없는 코드를 분리하는 좋은 방법입니다. 설정 셀에서 정의한 변수는 문서의 다른 모든 코드 셀에서 읽을 수 있습니다.

대시보드 페이지

대시보드 상단의 “Plots”와 “Data” 제목은 대시보드 페이지입니다. 대시보드 페이지는 여러 출력이 있는 대시보드를 페이지 단위로 구성하는 방법이며, 마크다운에 1단계 제목을 추가하면 페이지를 만들 수 있습니다. 이 경우 # Plots# Data입니다.

# Plots

# Data

데이터 프레임 출력

Data 페이지에는 동적 데이터 프레임 출력이 있습니다. 이는 다음 코드로 생성됩니다.

```{python}
@render.data_frame
def dataview():
    return render.DataGrid(filtered_penguins())
```

@render.data_frame 함수에서는 Pandas 데이터 프레임을 반환하기만 하면 Shiny가 자동으로 인터랙티브 데이터 그리드로 렌더링합니다. (filtered_penguins() 함수는 데이터 프레임을 반환하는 반응형 계산입니다. 다음 섹션에서 살펴봅니다.)

데이터 프레임 객체를 render.DataGrid 또는 render.DataTable로 감싸는 방법도 있습니다. 이 경우에는 render.DataGrid를 사용했습니다. 꼭 필요한 것은 아니지만, 필터링과 선택 같은 추가 옵션을 지정할 수 있습니다.

render.DataGridrender.DataTable의 차이는 렌더링된 테이블의 외형입니다. render.DataGrid는 더 컴팩트한 스프레드시트형 모양이고, render.DataTable은 전통적인 표 형태를 사용합니다.

반응형 계산

이 예제에서는 사용자가 선택 상자를 통해 데이터셋을 필터링하고, 필터링된 데이터는 두 개의 플롯과 데이터 프레임이라는 세 가지 동적 출력에 표시됩니다. 입력이 바뀌면 Shiny는 @render.plot@render.data_frame 데코레이터가 붙은 함수를 자동으로 다시 실행합니다. 그렇다면 데이터셋을 필터링하는 코드는 어디에 두어야 할까요?

가장 단순한 방법은 세 개의 렌더링 함수에 같은 필터링 코드를 복사해 넣는 것입니다. 하지만 이는 유지보수가 어렵고, 같은 필터링을 세 번 실행하므로 비효율적입니다. 코드를 함수로 추출하면 유지보수는 쉬워지지만, 효율성 문제는 여전히 남습니다.

Shiny는 이를 해결하는 방법으로 반응형 계산을 제공합니다. 반응형 계산은 입력이 바뀔 때마다 다시 실행되는 반응형 함수이지만, 그 반환값은 대시보드에 출력되지 않습니다. 대신 결과가 캐시되어 렌더링 함수(또는 다른 반응형 계산)에서 사용할 수 있습니다. 이를 통해 필터링 로직을 하나의 반응형 계산에 모으고, 세 개의 렌더링 함수가 모두 그 결과를 공유할 수 있습니다.

반응형 계산을 만들기 위해 @reactive.calc 데코레이터를 사용합니다. 다음 코드는 filtered_penguins라는 반응형 계산을 만듭니다.

```{python}
@reactive.calc
def filtered_penguins():
    data = penguins[penguins["species"].isin(input.species())]
    data = data[data["island"].isin(input.islands())]
    return data
```

반응형 계산 값을 읽으려면 함수를 호출하듯 사용합니다. 예를 들어 depth 플롯은 다음과 같습니다.

```{python}
@render.plot
def depth():
    return sns.displot(
        filtered_penguins(), x = "bill_depth_mm",
        hue = "species", kind = input.dist(),
        fill = True, rug=input.rug()
    )
```

filtered_penguins() 호출에 주목하세요. 이 호출이 항상 filtered_penguins 함수를 실행하는 것은 아닙니다. 보통은 캐시된 값을 반환하며, 그 값은 함수가 참조하는 입력이 변경될 때 자동으로 갱신됩니다. 그리고 depth 플롯은 filtered_penguins 계산을 참조하므로, 입력이 바뀌면 플롯도 다시 렌더링됩니다.

더 알아보기

Python용 Shiny 대화형 문서에 대해 더 알아보려면 다음 문서를 참고하세요.

컴포넌트 브라우저는 사용 가능한 Shiny 입력과 출력, 그리고 대시보드에 복사해 사용할 수 있는 코드 스니펫을 정리합니다.

입력 레이아웃는 Shiny 입력을 배치하는 다양한 방법(사이드바, 입력 패널, 입력을 카드에 직접 연결 등)을 설명합니다.

대시보드 실행는 VS Code, Positron, 그리고 명령줄에서 Shiny 대시보드를 실행하는 방법과 사용자에게 배포하는 방법을 더 자세히 다룹니다.

실행 컨텍스트는 코드 셀이 언제 실행되는지(예: 렌더링 vs. 서빙)를 깊이 있게 설명합니다.

Python용 Shiny은 사용 가능한 모든 UI 및 출력 위젯에 대한 심화 문서와 동작 개념을 제공합니다.