grasys blog

Python でのフロントエンド開発を実現する Reflex のご紹介

Python を使用したフロントエンドのフレームワークが近年いろいろとリリースされてきてますね。今回はそのなかでも、本番環境で運用できるポテンシャルをもちはじめている Reflex についてご紹介します!

Reflex とは?

Reflex はフロントエンドとバックエンドの両方を Python だけで開発できる OSS のフレームワークです。Reflex のフレームワークを使ったアプリケーションの開発言語は Python だけですが、アプリケーションの実態としてはフロントエンドは Next.js、バックエンドは FastAPI を利用して構成されます。フロントエンド / バックエンドともに実態は本番環境で使用されている信頼性の高いフレームワークであり、それらを基盤にしている Reflex にも本番利用の可能性が期待できますね。

インストールからプロジェクト立ち上げの手順

pip 等のパッケージマネージャーで reflex をインストールします。

pip install reflex==0.7.1

reflex のインストールに成功したら reflex プロジェクトの初期化を実行します。

reflex init --name demo_app --template blank

初期化が完了したら以下のコマンドで起動します。

reflex run

起動に成功すると http://localhost:3000/ でフロントエンド、 http://localhost:8000 でバックエンドが起動します。フロントエンドにブラウザからアクセスして以下の画像のようなサンプルページが表示されればプロジェクトの立ち上げは完了です!

Reflex のプロジェクト構成

Reflex プロジェクト構成を理解するためにディレクトリ構造をみます。上記の blank テンプレートを使用したプロジェクトでは一部のディレクトリは含まれないですが、ドキュメント に基づいて簡単に説明します。

Refex のプロジェクトは以下のディレクトリ構造になります。

.
│── .web/
├── assets/
│   └── favicon.ico
├── demo_app/
│   ├── backend/
│   │   └── states/
│   ├── components/
│   ├── pages/
│   ├── templates/
│   ├── views/
│   ├── __init__.py
│   └── demo_app.py
├── uploaded_files/
└── rxconfig.py

.web ディレクトリ

ここに Python のコードをもとにフロントエンドリソースとして Next.js のプロジェクトが作成されます。Reflex にて管理されるため、このディレクトリ中のファイルを操作することはないです。

assets ディレクトリ

フロントエンドで使用する画像等の静的ファイルをここに格納します。

プロジェクト名(demo_app)ディレクトリ

init コマンドで指定したプロジェクト名のディレクトリで、この中にアプリケーションのコードを格納していきます。

プロジェクト名ディレクトリ内にプロジェクト名 .py(demo_app/demo_app.py)を配置すること以外は強制されませんが Reflex のディレクトリ構造に従うことで、Reflex を使用した他プロジェクトを参考にしやすくなるため、強いこだわりが無ければ、従うことをおすすめします。

プロジェクト名 .py ファイル(demo_app.py)

アプリケーションのメインモジュールで app = rx.App() で定義されている app に対してページを登録していきます。

backend ディレクトリ

バックエンド関連のコードをこの中に格納します。

states ディレクトリ

後述する Reflex 固有の概念でフロントエンドとバックエンドで共有される変数である Stete を管理するための実装をこのディレクトリ内に格納します。

components ディレクトリ

サイドバーやボタン等のページの構成要素の実装をこのディレクトリに格納します。components 内の構成要素は State を持ちません。

※ コンポーネント単位で管理される State である ComponentState を持つことはあります。

pages ディレクトリ

アプリケーションの各ページの実装をこのディレクトリに格納します。

templates ディレクトリ

ページのレイアウトを各ページの実装で都度実装することを回避するための実装がテンプレートであり、ページレイアウトの実装をこのディレクトリに格納します。

views ディレクトリ

views は components と同様にページの構成要素です。components と views の違いは views は State を持つページの構成要素であることです。

uploaded_files ディレクトリ

フロントエンドからアップロードされたファイルがここに格納されます。REFLEX_UPLOADED_FILES_DIR 環境変数でディレクトリのパスは変更できます。

rxconfig.py ファイル

フロントエンドのポート番号等の Reflex のフレームワークとしての設定をこのファイルに記述します。

Reflex の基礎

フロントエンドの書き方

Reflex では React ライクにフロントエンドの画面を構築していきます。init でコマンドで作成した Reflex プロジェクトの demo_app.py を開くと index ページが以下のように Python で記述されています。

このように、React ライクに rx.heading などのコンポーネントをネストさせながら画面を構成することができます。Reflex に内蔵されているコンポーネント一覧は ここ から確認できます。基礎的なコンポーネント以外にも チャートコンポーネント も標準で搭載さてているため、必要なコンポーネントがなくて困ることはないと思います。

State クラス 〜バックエンドとフロントエンドの境界〜

Reflex を使ってアプリケーションを実装するときに重要な概念が State でこれがバックエンドとフロントエンドの境界です。バックエンドとフロントエンドを Python で記述するため、記述しているコードがバックエンドで動くコードなのかフロントエンドで動くコードなのかを意識しないと、フロントエンドでは実行できない(Next.js にコンパイルできない)コードを作成してしまいエラーが発生します。

Reflex は State クラスを介してバックエンドとフロントエンド間でデータの受け渡しを行います。このとき双方でやり取りできるデータ型は JSON で扱えるデータ型だけになります。そのため、JSON に変換できない Python オブジェクトをフロントエンドに State経由で送信してもフロントエンドでは実行できません。

ただし、フロントエンドから State クラスのメソッドをコールすることはできます。コールした結果の返り値は JSON で扱えるデータ型に限定されますが、フロントエンドにバックエンドの API をコールするメソッドを作成せずにバックエンドで実装したメソッドを実行することができます。

ここで、Stete を使ってバックエンドとフロントエンドをつなぐ サンプル を見てみます。

※以下はスクリーンショットのためサンプルページで動作を確認しください。

サンプルの Increment ボタンを押下すると数字が増加します。このときの実装は以下のようになっており、バックエンドで管理されている count の値のインクリメントがフロントエンド on_click イベントからバックエンドのメソッドである CounterState.incretemne をコールすることで実現されています。

このように Reflex では State クラスを境界としてバックエンドで動作するコードかフロントエンドで動作するコードかを意識する必要があります。

もう1つ State クラスの重要な特性として、State はページを超えて共有できる(アプリケーション全体で共有されるグローバル変数)ということです。そのため、上記の例の CounterState を別のページで import すると count の値は 0 でなく、すでにカウントが進んだ状態になります。

Component State 〜要素に閉じた状態管理〜

State はアプリケーション全体で状態変数が共有されてますが、画面の要素単位で状態変数を持ちたい場合があります。その場合は、State を使うと、要素毎に State クラスを作成しなければなくなるため、要素単位で状態変数を扱うときは ComponentState を使用します。

ここで、ComponentStete を使った要素単位での状態変数管理の サンプル を見てみます。

※以下はスクリーンショットのためサンプルページで動作を見てください。

サンプルの Decrement / Increment ボタンを押下すると、それぞれの要素に対応した数字が増減します。State クラスで同様な状態管理を実現しようとすると、State クラスを 3 つ作成するか、1 つの State クラス内に counter_1counter_2counter_3 と 3 つの状態変数を持たせた冗長な実装になってしまいますが、ComponentState クラスを使うと以下のようにコンポートと State を対応させて定義できます。ComponentState クラスを使用すると 1 つのクラス内でバックエンドで動くコードとフロントエンドで動くコードが混在することになるため、フロントエンドで使えないデータ型をフロントエンドで動くコードの箇所に渡してしまわないように注意してください。

フロントエンドに ComponentState クラスで作成した要素を表示するときは、以下の通りになります。ここで ReusableCounter.create メソッドを resusable_counter にリネームしているのはフロントエンドの要素だということ視覚的に分かりやすくするためになります。

Reflex デモプロジェクト一覧

Reflex で実装されたプロジェクトのサンプル一覧を ここ から見ることができます。これらのサンプルは init コマンドのテンプレートとして使用したり、GitHub でソースコードを取得したりできます。Reflex を使用した開発をするときは、これらのサンプルプロジェクトをベースにして開発を始めることで定型作業を省略して効率的に開発をはじめることができます。

クラウドへ打ち上げ

上述のようにフロントエンドとバックエンドを意識しながら React ライクに実装することで Web アプリケーションを Python だけで作成することができますが、Web アプリケーションであるからには、クラウドへ打ち上げられる必要があります。Reflex によってホスティングサービス、Reflex Cloud が提供されているため、要件が許せば Reflex Cloud を使用することが最も効率的なクラウドへの打ち上げ方法になります。しかしながら、DB 等の既存リソースへの接続やセキュリティ要件からセルフホスティングができないと本番環境として採用することが難しくなってきます。Reflex は Docker を使用した セルフホスティング にも対応しており、Google Cloud、AWS、Azure 等のパブリッククラウドに打ち上げることもできます。ここでは、Cloud Run を使って Reflex を打ち上げる方法について解説します。

構成

Cloud Run を使用して簡単に Reflex を展開するために以下の図のように 1 つの Cloud Run サービスに複数のコンテナを配置するマルチコンテナ構成を採用します。具体的には、Nginx をメインコンテナ、Reflex バックエンドをサイドカーとして 1 つの Cloud Run サービスを作成します。なお、Reflex フロントエンドは静的コンテンツとして Nginx から配信します。

Nginx + フロントエンド Dockerfile

メインコンテナである Nginx の Dockerfile は以下のように、Reflex フロントエンドのビルドと Nginx へビルドした Reflex フロントエンドを配置するマルチステージビルドにします。

FROM python:3.12 AS builder

WORKDIR /app

COPY . .
RUN pip install -r requirements.txt
# Reflex フロントエンドを静的ファイルとしてビルド
RUN reflex export --frontend-only --no-zip

FROM nginx:stable-alpine
# builder ステージで作成した Reflex フロントエンドを/usr/share/nginx/html にコピー
COPY --from=builder /app/.web/_static /usr/share/nginx/html
COPY ./docker/nginx/nginx.conf /etc/nginx/conf.d/default.conf

Nginx の設定は以下のようになります。Reflex バックエンドは「/_envet」「/ping」「/_upload」の 3 つのパスを使用するため、これらのパスへのリクエストは localhost:8000 に転送します。このとき、「/_event」は WebSocket 接続が使用されるため、WebSocket 接続がプロキシされるようにする必要があります。その他のリスエストは Reflex フロントエンドを配置している/usr/share/nginx/html から読み取るようにします。

server {
  listen 80;
  listen  [::]:80;

  error_page   404  /404.html;

  location /_event {
    proxy_set_header   Connection "upgrade";
    proxy_pass http://localhost:8000;
    proxy_http_version 1.1;
    proxy_set_header   Upgrade $http_upgrade;
  }

  location /ping {
    proxy_pass http://localhost:8000;
  }

  location /_upload {
    proxy_pass http://localhost:8000;
  }

  location / {
    root /usr/share/nginx/html;
  }

}

バックエンド Reflex Dockerfile

サイドカーで起動するバックエンドは単に reflex run ---env prod --backend-only コマンドを実行するだけになります。

FROM python:3.12-slim

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

WORKDIR /app
COPY . .

RUN pip install -r requirements.txt

ENTRYPOINT ["reflex", "run", "--env", "prod", "--backend-only"]

Cloud Run サービス設定

上記の Dockerfile をビルドしたイメージを Artifact Registry のリポジトリに push した後に、Cloud Run サービスを作成します。

Cloud Run のメインコンテナの設定は以下の通りで、Nginx が listen している Port番号 80 をコンテナポートとヘルスチェックのポート番号として指定します。

Nginx の起動後でないと Reflex バックエンド(サイドカー)にはヘルスチェックのためのリクエストが到達できないため、コンテナの起動順序で Nginx に依存するように設定します。また、Reflex バックエンドは Port番号 8000 を listen していますが、ヘルスチェックは Nginx経由で到達するためポート 80 を指定します。

サービスの作成が完了してヘルスチェック通過をすると Reflex の Cloud Run への展開が成功となり以下のように Cloud Run でホストさてている Reflex にアクセスできるようになります。

まとめ

Reflex を使うことで Python だけでフロントエンドとバックエンド開発ができるようになります。Reflex の特徴としてフロントエンドで動く Python コードかバックエンドで動く Pyhton コードかを理解して実装する必要があります。初見では、混乱するかもしれませんが、バックエンド・フロントエンドどちらで動くかを理解できるとバックエンドで定義したメソッドをそのままフロントエンドでコールすることができるため、バックエンドの API をコールするための処理をフロントエンドに実装する手間を省略でき、高速に Web アプリケーション開発ができてしまいます。

また、Reflex の基盤は Next.js と FastAPI で本番環境で使用されているフレームワークであるため、要件次第では Reflex を本番利用することも可能です。

ただし、Reflex のデメリットとして状態変数をバックエンドで保持するため、同時接続数が多い要件のとき単に水平にスケールさせてしまうと状態を保持していないバックエンドに転送されてしまい状態変数がリセットされてしまいます。セッション 永続化をして同じバックエンドに転送されるようにすることで回避することも可能ですが、特定のバックエンドにアクセスが偏ってしまう副作用が発生してしまいます。

Reflex を本番利用するには、2025 年 3 月時点では要件を選んでしまいますが、Reflex を構成するフレームワーク自体は信頼性の高いフレームワークであり、Reflex自体も活発に開発が進められているため、スケーラビリティの課題が解決されるのも時間の問題かもしれませんね。


採用情報
お問い合わせ