golemでShinyアプリを作る

[R Advent Calendar 2021](https://qiita.com/advent-calendar/2021/rlang) の14日目の記事です。

データをウェブブラウザなどで見る、つまり data dashboard を実現する方法として、 R と Shiny でアプリを作ることがあります。 Shiny アプリを作るためのツールキットとして、 R の golem パッケージがあります。 golem を使うと、 Shiny アプリをRパッケージとして実装するために必要なファイル群を、整合性を取って管理できて便利なので今回紹介します。

Shiny について詳しく知りたい方は、 [Mastering Shiny](https://mastering-shiny.org/) をご一読ください。 golem について詳しく知りたい方は、 [Engineering Production-Grade Shiny Apps](https://engineering-shiny.org/) をご一読ください。これらの文献を参考に私見を交えて、以下 golem を使って Shiny アプリを作る方法を手短に説明します。

## Shiny とは

データを可視化してウェブブラウザ上で見られるようにするための、Rパッケージです。サーバ (Shiny Server) 上で Shiny を使用したRスクリプトを実行すると、ウェブブラウザ上で以下の図のようなユーザインタフェースを実現できます。この図の例では、負の二項分布のパラメータを与えると確率密度を表示します。ソースコードは、 [私のGitHubレポジトリ](https://github.com/zettsu-t/nbinomPlot) にあります。

![Sample Page](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2351078/da366a8b-0c04-fa4c-01f8-5962b4c18838.png)

## golem とは

golem とは、Rパッケージとして配布可能な Shiny アプリを開発するためのツールキットです。Rスクリプトをスクラッチから手書きするのに比べて、以下の利点があります。

+ Shiny アプリの雛形、つまり Shiny アプリをRパッケージとして構成する一連のファイル群を簡単に作れます。 Shiny アプリの成果物がRパッケージなので、 Shiny アプリをリリースするのも容易です。
+ Rパッケージを作るときの開発手法が、 Shiny アプリの開発にそのまま使えます。 RStudio の Build タブや `R CMD check` からパッケージを検査できますし、ドキュメント生成もテストも、雛型に沿ってすらすら書いて実行できます。
+ golem が生成する Shiny アプリの雛形は、以下の部品で構成します。コードを細かく分けているので理解しやすく、テストも書きやすいです
  + Shiny アプリの部品: ユーザインタフェース (UI) と ユーザインタフェースに表示するもの (server) 、および UI と server の対をまとめたモジュール (module) からなる
  + Shiny とは独立な部品: 表示するデータを可視化する主機能 fct (function) と、共通部品 utils からなる
+ Rパッケージのメタデータ、例えば依存する他のパッケージの一覧や、名前空間の export, import を簡単に更新できます。手作業だと更新し忘れますからね。
+ Shiny アプリの設定ファイル(YAML形式)と、その設定ファイルを用いたアプリの初期化コードがついてきます。環境変数によって実行時のパラメータを変えたり、ロケールに応じてユーザインタフェースの文字列を変えたりするのに活用できます。
+ Shiny アプリが依存する他のパッケージとバージョンを調べて、 Dockerfile を作ってくれます

## golem を使う準備

golem パッケージと shiny パッケージおよび、Shiny アプリの開発にお勧めのRパッケージをインストールします。 Shiny と Shiny Server について詳しい説明は省略します。

``` r
install.packages(c("golem", "shiny", ...))
```

|パッケージ名|用途|
|:------|:------|
|golem|golem 本体|
|shiny|Shiny 本体|
|shinipsum|Shiny アプリのモック画面を作る|
|shinytest|Shiny アプリを自動テストする|
|gargoyle|Reactivity に基づいた更新通知を出す|
|R6|アプリに表示するデータを包むクラスを定義する|
|rmarkdown|Shiny アプリの説明を書いてHTML文書に変換する|

## golem を用いた開発手順

golem を使って Shiny アプリを開発するには、以下の手順を実行します。

1. RStudio上で golem プロジェクトを作る
1. golem プロジェクトのメタデータを記述する
1. UI と Server を作る
1. UI と Server に表示するものを作る
1. Shiny アプリとして全体を結合する
1. Shiny アプリを検査、ビルド、インストールする
1. Dockerfile など実行環境を整える

### RStudio上で golem プロジェクトを作る

R に golem パッケージをインストール済の状態で、以下の手順で golem プロジェクトを作成します。 Rパッケージとしての Shiny アプリを作成するプロジェクトができます。

1. RStudio上で、 File → New Project を選ぶ
1. New Project ダイアログで、 New Directory を選ぶ
1. Project Type として、 Package for Shiny App using golem を選ぶ。スクロールして下の方にみつかるかもしれません。
1. プロジェクトを作成するディレクトリを指定し、 Create Project ボタンを押す

プロジェクトのトップディレクトリ(上記で指定したディレクトリ)に、以下のファイルが生成されます。今後の説明でRパッケージとは、今回配布する Shiny アプリを、Rパッケージとして作成して配布するものを指します。

|ファイル|用途|
|:------|:------|
|*.Rproj|Rパッケージを開発するためのRプロジェクト|
|.Rbuildignore|配布するRパッケージに含めないファイル|
|DESCRIPTION|Rパッケージのメタデータ|
|NAMESPACE|Rパッケージの名前空間|
|dev/*.R|Shiny アプリの開発に使うスクリプト|
|inst/golem-config.yml|Shiny アプリを実行するときに読み込む設定ファイル|
|inst/app/www/favicon.ico|golem パッケージのアイコン|
|man/*.Rd|Rパッケージのマニュアル|
|R/*.R|あらかじめ用意された、 Shiny アプリのコード|

### golem プロジェクトのメタデータを記述する

dev/01_start.R に書いてある内容を一通り記述して、 Shiny アプリのメタデータを設定します。具体的にはRパッケージの名前、説明、作者、ライセンスなどを記述します。一通り記述したら、 RStudio 上で Ctrl-Alt-R を押して 01_start.R を実行します。

01_start.R を実行すると、01_start.R の内容が DESCRIPTION に反映され、LICENSE, LICENSE.md などを生成します。 01_start.R は何回実行してもよく、Rパッケージのファイルの更新が必要な箇所だけ更新します。

### UI と Server を作る

最初にユーザインタフェースの構成を決めます。ここでは表が1個表示されるだけで他には何もない Shiny アプリを作ることにします。次に、ユーザインタフェースを構成する Shiny の部品 (module つまり UI と serverの組) を決めます。ここでは table というモジュール名にします。モジュール名を決めたら、 dev/02_dev.R に `golem::add_module` 呼び出しとして記述します。

```r:02_dev.R
golem::add_module(name = "table")
```

RStudio 上で Ctrl-Alt-R を押して 02_start.R を実行すると、 R/mod_table.R を生成します。モジュールを実装する .R ファイルの名前には、 mod_ という接頭辞がつきます。生成されるコードは以下の通りで、モジュールに対応する UI と serverができます。ここに Shiny アプリとしてのコードを書いていきます。すでに R/mod_table.R があれば、 02_start.R は内容を変更しませんので、モジュールを増やすたびに 02_start.R に追記して 02_start.R を実行すればよいです。

```r:mod_table.R
mod_table_ui <- function(id) {
  ns <- NS(id)
  tagList()
}

mod_table_server <- function(id) {
  moduleServer(id, function(input, output, session) {
    ns <- session$ns
  })
}
```

まずこの雛形をそのまま実行しましょう。RStudio 上で Ctrl-Alt-R を押して dev/run_dev.R を実行すると、 Shiny アプリが起動します。生成された R/app_ui.R には、パッケージ名を表示するだけのユーザインタフェースが実装されているので、その通り表示されます。

![First](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2351078/017d41b5-7c49-84e5-8b38-b7c795a3db2b.png)

UI にユーザインタフェースの部品を作り、 server にユーザからの入力に対する応答を作ります。とはいえ、表示するものがまだ無いので、 shinipsum パッケージに作らせます。 例えば `shinipsum::random_table` はランダムにテーブルを生成して返します。こうしてユーザインタフェースの良しあしだけを実行して試すことができます。

```r:app_ui.R
app_ui <- function(request) {
  shiny::fluidPage(
    tags$style(".container-fluid {background-color: #f0ffff;}"),
    mod_table_ui("sample")
  )
}
```

```r:app_server.R
app_server <- function(input, output, session) {
  mod_table_server("sample")
}
```

```r:mod_table.R
mod_table_ui <- function(id) {
  tagList(shiny::tableOutput(shiny::NS(id, "tbl")))
}

mod_table_server <- function(id) {
  moduleServer(id, function(input, output, session) {
    output$tbl <- shiny::renderTable({
      shinipsum::random_table(nrow = 3, ncol = 4)
    })
  })
}
```

ユーザインタフェースができたので、 dev/run_dev.R を実行して起動します。このような表が1枚あるだけのページが表示されます。

![Shinipsum](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2351078/f7b0245c-843b-09e1-0a9d-a1c47c2662e3.png)

### UI と Server に表示するものを作る

ユーザインタフェースができたら、ユーザインタフェースに表示させる中身を作ります。例えばデータやデータの回帰結果を図やテーブルとして返す、といったことです。

実際の開発では、データの可視化や回帰が上手くいったことを図示して確認したから Shiny アプリを作ろう、という順序にすることが多いでしょう。この処理は Shiny とは無関係で、ユーザインタフェースを伴わない普通のRの関数です。ですから Shiny アプリを作り始める前に、先に以下ができていると思います。

+ データの可視化や回帰を行うコードを、Rスクリプト(.R)として実装する
+ R Markdown 文書にこのコードを埋め込んで、結果を表示するHTML文書を生成する
+ このコードに対するユニットテストを書いて、自動的にテストする

コードはそのまま流用できますし、R Markdown 文書はそのまま Shiny アプリの説明に使えますので、Rパッケージに含めます。ユニットテストも流用できます。 02_start.R に `usethis::use_test("テスト対象")` を記述して 02_start.R を実行すると、 tests/testthat/test-テスト対象.R を生成します。

これらのコードに対して、 Shiny アプリの server から呼びだせるようなインタフェースを作ります。 server が図やテーブルを取得する処理を R6 class としてラップ(wrap)した上で Shiny アプリの内部状態を持たせて、 get_chart() とか get_data_frame() といったメンバ関数を用意すればよいでしょう。 02_start.R に以下のように記述して、.Rファイルを生成します。テストのファイル名は、テスト対象と1対1対応させるとわかりやすいです。

|生成関数|生成するファイル|用途|
|:----------------------|:----------------------|:----------------------------------------------------------------|
|golem::add_fct(名前)|R/fct_名前.R|Shinyとは無関係な処理一般と、Shiny アプリの内部状態(R6 class)を定義する|
|golem::add_utils(名前)|R/utils_名前.R|Shiny アプリに共通な関数を定義する|
|usethis::use_test(名前)|tests/testthat/test-名前.R|R/*.Rに対応するテストを作る|

golem とは直接ありませんが、ユーザインタフェースをユーザが更新した → それに基づいてユーザインタフェースを更新する、という関係を、前者から後者への通知として実装できると便利です。そのために gargoyle パッケージが使えます。 `gargoyle::watch` で通知を待ち、 `gargoyle::trigger` で通知を出します。詳しくは [gargoyleパッケージの説明](https://cran.r-project.org/web/packages/gargoyle/vignettes/gargoyle.html) をご覧ください。

### Shiny アプリとして全体を結合する

golem プロジェクトを作成したときに出力した app_server.R を、以下のように改良します。

1. Shiny アプリの設定(golem-config.yml)を読み込む。 `get_golem_config` が用意されている。
1. バックエンドの状態を、先の R6 class のインスタンスとして作成する。コンストラクタの引数を変えることで、データの入手先やデータの編集方法を変更できる。
1. バックエンドの状態を、引数として server に参照渡しする

これで {UI}-{server}-{R6 class (Shiny アプリの内部状態)}-{Shinyと無関係な処理} という連携が取れます。試しに、表示するテーブルの行数を先ほどの4列から、YAMLファイルに記述した ncol 属性の値 (6列) に変えてみましょう。

```yaml:golem-config.yml
default:
  ncol: 6
dev:
  golem_wd: !expr here::here()
```

```r:fct_helpers.R
AppState <- R6::R6Class("AppState",
  public = list(
    #' @description Initialize default parameter(s)
    #'
    #' @param ncol The number of columns
    initialize = function(ncol) {
      private$ncol <- ncol
    },
    #' @description Get the number of columns
    #'
    #' @return The number of columns
    get_ncol = function() {
      private$ncol
    }
  ),
  private = list(
    # The number of columns
    ncol = 1
  )
)
```

```r:app_server.R
app_server <- function(input, output, session) {
  ncol <- as.numeric(get_golem_config(value = "ncol"))
  state <- AppState$new(ncol = ncol)
  mod_table_server("sample", state)
}
```

```r:mod_table.R
# UIは無変更
mod_table_server <- function(id, state) {
  moduleServer(id, function(input, output, session) {
    output$tbl <- shiny::renderTable({
      shinipsum::random_table(nrow = 3, ncol = state$get_ncol())
    })
  })
}
```

dev/run_dev.R を実行して Shiny アプリを起動すると、以下のように列が4つから6つになりました。表の中身が先ほどと異なるのは、 shinipsum パッケージがランダムに生成しているからです。

![Config](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2351078/d227d98a-9c10-8e3a-ee01-c9792d57d178.png)

### Shiny アプリを検査、ビルド、インストールする

今回作成した Shiny アプリはRパッケージなので、一般的なRパッケージと同様に検査、ビルド、インストールできます。

+ RStudio の Build タブから、Check ボタンを押す。 Build タブが表示されていないときは、Rプロジェクトを開き忘れていますので開きます。
+ RStudio の Build タブから、Install and Restart ボタンを押す
+ コマンドラインからは `R CMD check`, `R CMD build`, `R CMD INSTALL` を実行する

Rパッケージをインストールできたら `パッケージ名::run_app()` を実行すると、今回作成した Shiny アプリを起動します。

ちなみに Shiny アプリのユーザインタフェースをテストするには、 shinytest パッケージと PhantomJS を使えばよいです。 golem とは直接関係ありませんので、説明は省略します。

### Dockerfile など実行環境を整える

dev/03_deploy.R を実行すると、Shiny アプリを検査、ビルドしたあと、 Dockerfile を作成します。 03_deploy.R にある記述のうち、不要なものがあれば (`rhub::check_for_cran()` など) 適宜削除しておきます。

Shiny アプリが必要とする外部パッケージとそのバージョンを調べて、外部パッケージのインストール手順を列挙した Dockerfile を作成します。以下が Dockerfile の出力例です。外部パッケージのバージョンが指定されているので、自分で調べる必要がありません。便利ですね。

```Dockerfile:Dockerfile
FROM rocker/r-ver:3.6.3
RUN apt-get update && apt-get install -y  git-core libcairo2-dev libcurl4-openssl-dev libgit2-dev libicu-dev libssl-dev libxml2-dev make pandoc pandoc-citeproc zlib1g-dev && rm -rf /var/lib/apt/lists/*
RUN echo "options(repos = c(CRAN = 'https://cran.rstudio.com/'), download.file.method = 'libcurl', Ncpus = 4)" >> /usr/local/lib/R/etc/Rprofile.site
RUN R -e 'install.packages("remotes")'
RUN Rscript -e 'remotes::install_version("rlang",upgrade="never", version = "0.4.11")'
RUN Rscript -e 'remotes::install_version("glue",upgrade="never", version = "1.4.2")'
RUN Rscript -e 'remotes::install_version("R6",upgrade="never", version = "2.5.1")'
RUN Rscript -e 'remotes::install_version("processx",upgrade="never", version = "3.5.2")'
RUN Rscript -e 'remotes::install_version("testthat",upgrade="never", version = "3.0.2")'
RUN Rscript -e 'remotes::install_version("htmltools",upgrade="never", version = "0.5.2")'
RUN Rscript -e 'remotes::install_version("attempt",upgrade="never", version = "0.3.1")'
RUN Rscript -e 'remotes::install_version("DT",upgrade="never", version = "0.19")'
RUN Rscript -e 'remotes::install_version("shiny",upgrade="never", version = "1.7.1")'
RUN Rscript -e 'remotes::install_version("config",upgrade="never", version = "0.3.1")'
RUN Rscript -e 'remotes::install_version("spelling",upgrade="never", version = "2.2")'
RUN Rscript -e 'remotes::install_version("thinkr",upgrade="never", version = "0.15")'
RUN Rscript -e 'remotes::install_version("shinipsum",upgrade="never", version = "0.1.0")'
RUN Rscript -e 'remotes::install_version("golem",upgrade="never", version = "0.3.1")'
RUN mkdir /build_zone
ADD . /build_zone
WORKDIR /build_zone
RUN R -e 'remotes::install_local(upgrade="never")'
RUN rm -rf /build_zone
EXPOSE 80
CMD R -e "options('shiny.port'=80,shiny.host='0.0.0.0');golemsample::run_app()"
```

### 指摘事項に対処する

golem とは直接関係ありませんが、 Shiny アプリの実行時エラーを防ぎ、 Rパッケージの検査で warning も note も指摘されないようにするには、以下の対策が必要でしょう。

+ `%>%` が何か分からない、という実行時エラーが出ることがあります。 ``@importFrom magrittr `%>%` `` をRファイルの docstring に書く必要があります。
+ コードに US-ASCII ではない UTF-8 文字があります、と指摘されることがあります。非US-ASCII文字は、YAMLファイルなどに書いて実行時に読み込み、ソースコードに直接書かないようにします。
+ tidyverse を使っていて、列名の指定に `rlang$.data` が必要になることがあります。詳しくは [こちらの記事](https://dplyr.tidyverse.org/articles/programming.html) を参照してください。

## さいごに

golem を用いた Shiny アプリの作り方について簡潔にまとめました。詳細な説明を省きましたが、RStudio上で golem プロジェクトを作って、出力された雛形を見ながら実際に手を動かす方が理解しやすいでしょう。 golem を使うとRパッケージの作り方や配布方法に悩まなくて済むので、これから Shiny アプリを作るなら golem がお勧めです。