Streamlitはなぜ“簡単なのに難しい”のか？

先日、Pythonの基礎問題集を解くアプリをPythonで実装しようと試みたのですが、その際使用したStreamlitについて自己理解もかねて解説しようと思います。実際の開発記事とアプリは下記で確認できます。

- https://frazz.hatenablog.jp/entry/2025/04/28/001112
- https://pyquest-mhybpd8dgt6bjxhfs86fpy.streamlit.app


## 本記事の目的

Pyquestの実装にあたり使用したStreamlitですが、ここで一度詳しくこのフレームワークについて解説します。対象読者としては私と同じWebアプリ開発初心者の方で、公式ドキュメントを見たものの理解が追いつかなかった方向けになっています。また、UIが「サクサク出るけど制御が難しい」と感じた方にもその理由が分かるように、Streamlitが初心者にも人気の理由を改めてご紹介できればと思います。

## Streamlitの「再実行モデル」とは何か？

FlaskやReactを用いた通常のWebアプリケーションはイベントドリブンと呼ばれ、クライアントの特定の操作に対してプログラムの特定箇所が実行されます。これに対しStreamlitの再実行モデルは、ボタンを押すなどのイベントが発生するたび、プログラムが上から順に全行実行(シングルスレッドで再レンダリング)されます。

これだけでは高速化しないのではと思われがちですが、ユーザ別に保存された変数やコンポーネントの値などは、「セッション毎に状態管理（Session State）」されています。記憶されたひとつひとつの状態を実行時に巻き戻しているため、動作可能なのです。加えて、これによりイベント発火(onClick等)の記述が不要になります。代わりに、「もしボタンが押されていれば次の画面に遷移する」が記憶されているとすれば、if文ベースの条件分岐でUIが分かれるようなプログラムを書くだけで十分になります。

```python
if st.button("次へ"):
    page += 1  # → これがうまく動くようにするには状態保存が必要
```
これが再実行モデルの実態です。

## アーキテクチャ概観

```
[Frontend (Browser)]
        ↑↓  WebSocket
[Streamlit Server (Backend)]
        ↓
[Script Runner]
        ↓
[SessionState Manager]
```


Streamlitは上記4つのコンポーネントに大きく分かれます。

FrontendはHTML/JSで構成され、ユーザーの操作（ボタン、スライダー等）を検知します。情報の差分はStreamlitサーバへ送信し、またサーバから受信したUI変更をリアルタイムに反映します。通常の開発であればFrontendは自動生成してくれるため、こちらが書くことはありません。

Streamlit Serverはクライアントとの接続をWebsocket通信で維持します。Pythonスクリプトを実行し、出力されたUI情報をフロントに送信しています。スクリプトの実行はシングルスレッド（順次処理）です。

Script Runnerが`streamlit run app.py` の中核部分です。クライアントのインタラクションをトリガーに、スクリプトを最初から実行します。

SessionState Manager（セッション状態管理機能）がセッション（ユーザ別）ごとの変数、コンポーネントの値を保持し、再実行時にこれらの状態を復元してUIの一貫性を保ちます。

## メリット・デメリット

前提として、StreamlitはWeb開発を知らないデータサイエンティストでも使えることを目標に開発されています。複雑な非同期処理やイベントバインドを抽象化し、再実行モデル+セッション管理というアプローチで一貫性のあるUIを提供しています。
メリットとして、Web技術に精通していなくてもインタラクティブアプリを非常に簡単に作成可能になります。しかし複雑なUIになるほど状態管理が煩雑になり、条件分岐が増えるとどこまで再実行されるのか理解しにくくなるともいえます。

## PyQuestの作成にあたって

```python
if "mode" not in st.session_state:
    st.session_state.mode = None
if "q_index" not in st.session_state:
    st.session_state.q_index = save_data["q_index"]
if "score" not in st.session_state:
    st.session_state.score = save_data["score"]
if "answered" not in st.session_state:
    st.session_state.answered = False
if "selected_questions" not in st.session_state:
    st.session_state.selected_questions = []
```

Pyquest作成の際には、冒頭部で使用する問題のjsonデータやセーブデータを別ファイルから読み込み、その下にセッションステートの初期化コードを作成しました。初期化なしにモード選択が行われていては2回目以降そのモードしか使えなくなる為、解答が全て終了した際にはステータスをnoneに戻します。同様に、解いた問題数やスコアなどもその都度リセットしますが、例外として成績表には累計解答数を保持しておかなくてはならない為、q_indexなどは別途savedataへ書出しました。

```python
 if st.button("最初からやり直す"):
            st.session_state.q_index = 0
            st.session_state.score = 0
            st.session_state.answered = False
            st.session_state.mode = None
            st.session_state.selected_questions = []
            save_progress()
            st.rerun()
```

実際に制作を振り返ってみると、この質問進行型のロジックはStreamlitの再実行モデルと相性が良かったなと感じます。何故なら「成績表を読み込んだ」→「モードを選択した」→「問題を解いた」などの状態遷移が、アプリ全体のコードの流れを通して容易にイメージおよび追跡できるからです。

転じて、「タスクを設定した」→「期日を設定した」→「メンバ共有した」→「タスクを完了した」などの状態遷移も作成可能なことから、Streamlitは簡単なタスク管理ツール等の開発にも応用できそうです。

## まとめと応用可能性

Streamlitはイベントハンドラの代わりに再実行モデルを使用しており、セッションステートを保存して実行時に再読み込みすることで高速性を保っています。またそのシンプルさからプログラミング的思考の基礎理解にも向いており、簡易的なツールやプロトタイプの実装、社内向けダッシュボード等に利用用途があります。逆に、複雑な分岐や動的なページ遷移には向いていない為、技術要件に応じた使用可否の見極めが重要です。


## 参考資料
- https://www.tc3.co.jp/graphic-explanation-on-eventdriven-in-cloudnative
- https://qiita.com/Nate0928/items/566c2073358b4df3ceec
-  https://qiita.com/yasudakn/items/089aaf4488fc6a8396ae
- https://github.com/streamlit/streamlit
- https://docs.streamlit.io/get-started/fundamentals/main-concepts
- https://docs.streamlit.io/develop/concepts/architecture/architecture:title
- https://auth0.com/blog/introduction-to-streamlit-and-streamlit-components