JSON Schema の力でより安全に React on Rails 出来るようにする

この記事は  [Qiita株式会社 Advent Calendar 2022](https://qiita.com/advent-calendar/2022/qiita-inc) 
の 15日目の記事です。

Rails アプリケーションの View として React を用いるライブラリとして、[React on Rails](https://github.com/shakacode/react_on_rails) があり、Qiita 社のプロダクト開発でも広く利用している便利なライブラリなのですが、複数言語をまたぐ故の動作検証等の難しさがあります。
この難しさを解消するために、View を介して言語間で伝搬する情報に JSON Schema でスキーマをあてたという話を書きます。

(今回のアイデアは、Qiita 社の各種プロダクトで実際に利用されており、 [JsonSchemaView](https://github.com/increments/json_schema_view) として OSS として公開しています。興味があればぜひそちらも参照してください。)

https://github.com/increments/json_schema_view

## React on Rails の便利さと難しさ

[React on Rails](https://github.com/shakacode/react_on_rails) は、Rails View 内で、 React Component を利用できるようにするライブラリです。
一度設定してしまえば、以下のようなコードで View 内で React Component を簡単に利用することが出来ます。

```erb
<%= react_component("HelloComponent", props: { name: "Taro" }) %>
```

```tsx
const HelloComponent = ({ name }: { name: string }) => {
  return (<div>Hello, {name}!</div>)
}
ReactOnRails.register({ HelloWorld })
```

非常に便利なライブラリで、複数の言語が絡むゆえの動作検証の難しさという問題がありました。 Rails 側からみた Compoent 名と props は **単なる String と Hash オブジェクトで**、 Rails 側単独ではこれらの正しさを検証する術を持たないということです。

### Rails 側単独では、 Component 名と props の指定が正しいのか検証が難しい

[React on Rails](https://github.com/shakacode/react_on_rails) の大まかなメカニズムは、

* Rails 側
  1. `react_component` メソッドにより、埋め込む予定地に `(利用したい React Component 名, その props)` の情報を含む HTML タグを render する
* JavaScript 側 (React 側)
  1. 埋め込めるようにしたい React Component を予め `ReactOnRails.register` で登録
  1. React on Rails の JS 実装が、 `react_component` が render した HTML タグを発見し、それを対応した React Component に置き換え

という流れになっています。

ここでやり取りする情報を超簡略化して図示すると以下の通りになります。

![react-on-rails-dataflow.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7048/db3124ea-a318-8168-b3f4-0ce93e09352f.png)

ここで Rails 側から JS 側に伝搬している情報は、 `(利用したい React Component 名, その props)` の組です。
ただし、 Rails 側では JS 側にどのような Component があるか (登録されているか) を知りません。指定した React Component 名と props が正しいものなのかは、 JavaScript 側で処理しないとわからないということになります。

そして、渡した React Component 名と props にミスが起こることはありがちで、 Component 名の Typo, React 側の props の変更, View 用のシリアライズ処理の変更, ...etc, 無限に起きるケースがあります。

これらの検証のための方法として、 Server Side Rendering を行うようにしたり、 Feature Spec や System Spec などのブラウザを利用するテストを利用するなどの方法がありますが、実装やテストが必要以上に複雑に大掛かりになってしまうという難点もありました。

## Component とその props に JSON Schema でスキーマを与える

ここでネックなのは、どういう React Component を定義していて、その props として何が期待されているかの情報が JavaScript (TypeScript) 側に閉じてしまっていることです。

これを解消するために、Rails から JavaScript へ伝搬している情報 `(利用する React Component 名, その props)` 
 に対して [JSON Schema](https://json-schema.org/) でスキーマを定義するというアプローチを考えました。

JSON Schema を元に型定義の生成やデータの検証が行えるため、言語間での情報伝播での失敗の可能性を減らし、それぞれの責務の中での正しさを独立して検証することが出来るようになる、というわけです。

![json_schema.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7048/82bd2842-d490-b376-48fe-e8386c7a0ea4.png)


### ※ JSON Schema とは？

https://json-schema.org/

JSON Schema は JSON 形式のデータやドキュメントに対して、そのスキーマ (データの構造) を定義することが出来る、スキーマ記述言語の一種です。 

JSON Schema の強みとして、これを利用した各種実装の豊富さがあります。 [JSON Schema を利用したバリデーター](https://github.com/davishmcclurg/json_schemer) や [型定義生成](https://github.com/bcherny/json-schema-to-typescript)、 [React の Form 定義の生成](https://react-jsonschema-form.readthedocs.io/en/latest/)、[テスト用オブジェクト生成](https://github.com/json-schema-faker/json-schema-faker)、 [Language Server による JSON / YAML ファイルの補完](https://qiita.com/tomoasleep/items/3f334fd13da573246a29) などなど、豊富なツールで様々な活用を行うことが出来ます。ちなみに、REST API の記述フォーマットである、[OpenAPI](https://www.openapis.org/) はこれを拡張する形で定義されています。

Qiita でも JSON Schema は利用されていて、 Qiita API v2 の JSON Schema (http://qiita.com/api/v2/schema) を配布していたり、これを元に [API ドキュメント](https://qiita.com/api/v2/docs) の生成を行っています。

これらの技術的な詳細については以下の記事でまとめられています。

https://blog.qiita.com/120746062029-2/

https://r7kamura.com/articles/2015-06-05-q


### View で伝搬する情報にスキーマを与える JsonSchemaView

ということで、 JSON Schema を使った View 間の連携を行うために必要な実装を [JsonSchemaView](https://github.com/increments/json_schema_view) という OSS として開発しました。

JsonSchemaView では、以下のような Ruby コードで Component とそれの props の JSON Schema を記述します。

```ruby
# クラス名は Component 名と同じになるようにする
class HelloComponent < JsonSchemaView::Component
  renderer_class(:react_on_rails)

  props_class do
    property(:name, type: String)
    attr_reader :name

    def initialize(name:)
      @name = name
    end
  end
end
```

JSON Schema を定義するための DSL としては、 [JSONWorld](https://github.com/r7kamura/json_world) またはその拡張になっています。 [JsonWorldでモデルからJSON Schemaを生成する](https://r7kamura.com/articles/2015-06-05-q) も記述の際の参考になります。

定義した Component は Rails の render メソッドに渡して `react_component` の代わりに利用することが出来ます。

```ruby
render HelloComponent.new(props: { name: "Taro" }) # react_component("HelloComponent", props: { name: "Taro" })
```

render される際に、そのスキーマに適合するか検証が自動で行われ、適合しない場合は例外が投げられます。これで、 Rails 側がスキーマに合わないデータを伝搬してしまうことを防げます。

```ruby
render HelloComponent.new(props: { name: "Taro" }) # valid
render HelloComponent.new(props: { naem: "Taro" }) # invalid なので、 Rails 側の render 時に例外が投げられる
```

また、その JSON Schema からは [json-schema-to-typescript](https://github.com/bcherny/json-schema-to-typescript) を利用して、TypeScript の型を生成出来ます。これを利用することで、 Rails 側と React 側 (TypeScript 側) の持つ定義が JSON Schema を介して一貫したものになります。

```tsx
import { HelloComponentProps } from "./generated-types-from-json-schema/HelloComponent"

const HelloComponent = ({ name }: HelloComponentProps) => {
  return (<div>Hello, {name}!</div>)
}
ReactOnRails.register({ HelloWorld })
```

これにより、各言語でそれぞれ自身の処理の検証を行えるようになり、より動作検証が行いやすく、安全に React on Rails の利用が行いやすくなりました。

### ※設計と導入方針:

#### View を React 化しても、動作確認を行いやすくする

これをどういう意図で設計していたかという点についても軽く触れておきます。

Qiita Team では [ホーム画面リニューアル](https://team-docs.qiita.com/QiitaTeam/items/2e474e23adf4df41d179#%E6%96%B0%E3%81%97%E3%81%84ui%E3%82%92%E6%AD%A3%E5%BC%8F%E3%81%AB%E3%83%AA%E3%83%AA%E3%83%BC%E3%82%B9%E3%81%97%E3%81%BE%E3%81%97%E3%81%9F) などを含めて、使いやすくするための UI のリニューアルを推し進めています。
その際に合わせて、フロントエンドの利用の React 化を推し進めてきました。 View を Rails (Slim) で記述していたものと React で記述していたものが混在していたものを、 大半に React で記述に置き換えを進めています。その際に、 React へのレンダリングの移譲に React On Rails を利用していました。

その際に、ネックだったのは、Rails (React On Rails) から JavaScript (React) 側に渡しているデータの不明瞭さでした。 React On Rails を使って JavaScript にデータを渡す際は、以下のような String, Hash オブジェクトで指定を行っていますが、これが React 側で期待する値と合致しているという保証もなく、(時たま実際にズレが起きていて、)不明瞭なデータが渡っている状態でした。

```erb
<%= react_component("HelloComponent", props: { name: "Taro" }) %>
```

![view.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7048/a28c88ee-b67a-9aa7-281a-076e664a3d37.png)

こういう状況のため、

* フロントエンドで定義している型が信用できない
* テストをするために、 Rails, JavaScript 両方を実行するようなテスト (Feature Spec, System Spec, Server Side Rendering を利用する) が必要になる
    * React を使用する箇所で全てこれになると、テストが非常に複雑化する

という開発での問題がありました。

そこで、改善として行ったのが、今回紹介した React On Rails が伝搬する情報に JSON Schema でスキーマを与える、という手法です。

![json_schema.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7048/9c5eb83c-0412-6db2-3576-7e79e5ef55ad.png)

渡す情報の正しさを JSON Schema で保証できるようにしたことで、Rails, JavaScript 両方の処理系を用意しなくても、それぞれでその責務の正しさをチェックできるようになったのと、Schema から型が生成できるようになったことで、型がある程度信用できるものになりました。

これにより、React に移行してもテストが複雑化せず、また型も信用できる状態になるなど、 React への置き換えと開発が非常にやりやすくなりました。

#### 既存の技術/体験を組み合わせて効率よく体験の向上を図る

JsonSchemaView を開発、チームに導入するにあたって、 Qiita 社内の技術/開発体験と連続性をもたせるようにして、チームメンバーが学習して使いこなすまでのハードルを下げ、**新しいワークフローのメリットを早く感じてもらえるようにする**、**少ないコストでインクリメンタルに開発が行える**という点を強く意識しました。
スキーマを用いた開発ワークフローでは様々なツールが登場したり学習コストが大きくなりやすいので、特に意識が必要なのかなと考えています。

例えば、Qiita 社では、[GraphQL Ruby](https://graphql-ruby.org/) や [JsonWorld](https://github.com/r7kamura/json_world) などを利用していて、いわゆるコードファーストによってスキーマを定義することの経験がある状態でした。
また、 TypeScript の型定義を自動生成する、というアプローチも GraphQL API で行っていたり、 他の API の一部でも JSON Schema からの生成を試験的に行っていました。

また render 出来るオブジェクトというのは、 [ViewComponent](https://viewcomponent.org/) を参考にしたアイデアですが、 ↓ の記事などで、社内にその手法やメリットを紹介したりなど、組み合わせる技術や手法は、どれも見たことか使ったことがあるという状態のものでした。

https://qiita.com/tomoasleep/items/5c1cf71c1148c2737ca2

それらを組み合わせて +α でブラッシュアップするというやり方で導入、開発を行いました。

こうした導入方法を取ったのは、これを開発するタイミングでは業務委託として関わっていて、導入の際に時間を大きく取ってサポートしたりすることが難しい、という事情もあったのですが、こうしたアプローチを取ったことで、社内でも利便性を実感してもらえて上手くユーザーを増やして行くことが出来ました。


## まとめ

この記事では、 JSON Schema と [React on Rails](https://github.com/shakacode/react_on_rails) に組み合わせることで、動作検証を行いやすくするアプローチと、それを実装した [JsonSchemaView](https://github.com/increments/json_schema_view) について紹介しました。

現代における Rails アプリケーションのフロントエンド事情は複雑かつ三者三様なため、今回のアプローチをそのまま活用するというのは難しいかもしれないですが、スキーマを用いた開発は思ったよりも応用範囲が広く、上手くハマれば大きな価値を発揮できると感じています。このやり方と導入のやり方が参考になれば幸いです。

---

Dev トークも公開しているので、もしこの辺の話詳しく聞きたい、と感じていただけた方はオンライン or オフラインでも話せるので是非↓で「話したい」を押してみてください :information_desk_person: 

https://jobs.qiita.com/employers/qiita-inc/dev_talks/542