JSON Schema を作る Presenter で Rails アプリケーション (Qiita Team) とフロントエンドの連携をする

## 話者

- @tomoasleep
    - https://www.wantedly.com/id/tomoasleep
    - Qiita 社のエンジニアです
        - 新卒で Qiita 社にいた → Wantedly で SRE → 最近 Qiita 社に戻ってきた
        - 今は各アプリケーションの開発 & 開発者体験向上 etc... に取り組んでいます
    - Ruby で[変](https://github.com/tomoasleep/yoda)[な](https://github.com/tomoasleep/vhdl_test_script)[も](https://github.com/tomoasleep/ruboty-openai_chat)[の](https://github.com/increments/slacken)を作るのが趣味です
    - 最近の趣味: 散財

https://twitter.com/nemunemu3desu/status/1612393231836385281?s=20

https://twitter.com/nemunemu3desu/status/1614967527540658176?s=20

***

## 話すこと

- Rails アプリケーション (Qiita Team / Qiita Jobs) とフロントエンド (React) の間の連携を、
- JSON Schema を吐き出す Ruby の Presenter を作って使うことで、
- 両者をうまく連携して幸せな開発体験を提供したという話


https://increments.connpass.com/event/274200/

https://nishinipporirb.doorkeeper.jp/events/149198

で話す/した内容です。

***

## Qiita Team の紹介

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7048/ee2b9bab-c79b-88d4-3397-6f8e5f379ffa.png)



- 企業等向け SaaS
    - 簡単に言うと「社内向け Qiita」
- 今年で 10 歳 :birthday: 
- Rails で開発
- Qiita との立ち位置
    - Qiita, Qiita Team は同じ Rails アプリケーションとして開発
        - コア機能 (エディタ, Markdown 処理) を流用
        - その他の機能は徐々に分離を行っている
            - 最初は本当に「Private Qiita」だったが、サービスの成長とともに違いが大きくなり実装の分離を行っていった
    - Qiita 開発で得た経験、資産を取り入れる形で開発


https://teams.qiita.com/

***

## Qiita Team の最近のフロントエンドのアップデート

https://team-docs.qiita.com/QiitaTeam/items/b60ccaa58323e6a0328a

| Before | After |
|:-:|:-:|
|  ![home-before.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7048/259f5818-4bac-46a1-5eec-0d6b4add7094.png) | ![home-after.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7048/d257fab4-a9a4-4d6c-9c17-8474ddc4dab4.png) |



* グループ選択、タグ検索のリストなどをサイドバーから選択可能に
    * チャンネルを見る感覚で記事を探すことがしやすい

***

### デザインリニューアルに合わせて、 View 実装の設計を変更

- View の組み立てを Rails (Slim) → TypeScript (React) に寄せる
    - これまで: View を基本 Rails の View (Slim) で + 部分的に React 化
    - これから: View の一部 (head 要素 etc) 以外は React 側で View を組み立て
        - Qiita Team はクライアントサイドレンダリングを利用
- Why?
    - View を React に寄せることでの開発体験の向上
        - Slim や React を行ったり来たりしなくて良い
        - デザインを CSS-in-JS (Emotion) に寄せられる
        - etc
    - Qiita のフロントエンド資産の流用
- Rails 側から React を利用する方法
    - [React on Rails](https://www.shakacode.com/react-on-rails/docs/) を利用

![rails-mvc-2.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7048/ccab272f-9c01-8a1e-4d06-357ec8608895.png)


↓

![rails-react-mvc-2.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7048/613a8a74-014d-d585-ff6a-1bcbe8954364.png)



*** 

### コードの雰囲気例

* React Component を Rails View (Slim) にマウントしているように書ける

```slim
html
  head
    title
      = content_for(:title)
    = javascript_include_tag
  body
    = react_component("HomeFeed", props: { username: "Mike", articles: @articles.map(&:to_h) })
```

```tsx
export function HomeFeed({ username, articles }) {
  return (
    {articles.map(article => (
       <div css={itemStyle} key={article.id}>
          <ItemHeader article={article} />
          <ItemBody article={article} />
       </div>
    ))}
  )
}
```

***

## Rails バックエンド & フロントエンド連携の難しさ

- テストをするとき大掛かりになる
    - SSR しない場合は、ブラウザテスト (System Test 等) が必要
- フロントエンドの型が信用できない
    - バックエンドのデータと照合する保証がない。。。
- フロントエンドに渡すための Serialize どこでやる？
    - erb か Slim に Hash リテラル直書きはしんどい
    - React on Rails に渡す props を組み立てる場所が定まっておらず、散らかっていた

→ 間を取り持つのが React on Rails だけだと心許ない

![rails-react-react-on-rails.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7048/00fd6f52-0a59-ec84-ae25-36421d7c0c72.png)

***

## この関係図を見ていて思うこと

:thinking: 

![rails-react-react-on-rails.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7048/00fd6f52-0a59-ec84-ae25-36421d7c0c72.png)

***

## View とそれ以外の間を取り持つもの → Presenter 層

* Presenter 層を置く (ViewModel, Serializer, Decorator などとも呼ばれる) 
    * 例: [ActiveModelSerializer](https://github.com/rails-api/active_model_serializers), [Draper](https://github.com/drapergem/draper), [jbuilder](https://github.com/rails/jbuilder), [alba](https://github.com/okuramasafumi/alba) など
    * props どこで組み立てるか問題はこれらを参考にすると解決しそう

![rails-react-presenter (1).drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7048/6ad12119-3ab4-007c-e81f-cec6c1e2e213.png)

***

## 異なる2つのシステムを取り持つもの → スキーマ

* 個別にテストしにくい、型が信用できない → スキーマを作る
    * Web API だと例えば OpenAPI, gRPC, GraphQL などがある

![schema-driven.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7048/fe8624b8-b647-9cac-6fbc-8ac7ccaf9571.png)


***

## この２つをかけ合わせると…？

:eyes: 

![rails-react-presenter (1).drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7048/6ad12119-3ab4-007c-e81f-cec6c1e2e213.png)

X

![schema-driven.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7048/fe8624b8-b647-9cac-6fbc-8ac7ccaf9571.png)

***

## JSON Schema を生成する Presenter を間に挟む


![rails-react-presenter.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7048/7d7ab924-c4b2-e0ba-6757-5c0fca2874e2.png)


- バックエンドからフロントエンドに渡すデータにスキーマ (JSON Schema) を付ける
    - API にスキーマを与える技術 (OpenAPI/gRPC/GraphQL) を View でのデータのやり取りに応用する
    - スキーマには JSON Schema を採用
        - Qiita 内には JSON Schema を扱うための資産がある
            - [Qiita API v2のJSON Schemaを公開しました - Qiita Blog](https://blog.qiita.com/120746062029-2/)
            - [JsonWorldでモデルからJSON Schemaを生成する](https://r7kamura.com/articles/2015-06-05-q)


***

## 作ったもの

- [increments/json\_schema\_view: View framework that brings Schema-driven Development to Rails view](https://github.com/increments/json_schema_view)
    - JsonSchema を生成する Presenter + ReactOnRails とのブリッジ
        - Ruby コードの DSL として JSON Schema を記述
        - [ViewComponent](https://viewcomponent.org/) 風のインターフェイスで ReactOnRails とブリッジ
        - JSON Schema を生成することで、検証/コード生成に利用出来る
            - Ruby 側は JSON Schema に対して値を検証
            - JSON Schema から TypeScript を生成


***

### コード例 (Component 呼び出し)

```slim
= react_component("HomeFeed", props: { username: "Mike", articles: @articles })
```

↓

```slim
= render HomeFeed.new(props: { username: "Mike", articles: @articles })
```

***

### コード例 (生成される JSON Schema)

```json
{
  "title": "HomeFeedProps",
  "properties": {
    "articles": {
      "type": "array",
      "items": { ... },
    },
    "username": {
       "type": "string",
    },
    "required": [
      "articles",
      "username"
    ],
    "additionalProperties": false
}
```


***

### コード例 (Component 定義)


```ruby
class HomeFeed < JsonSchemaView::Component
  renderer_class(:react_on_rails)

  props_class do
    property(:username, type: String)
    property(:articles, type: Array, items: { type: HomeFeed::Article })

    attr_reader :username

    def initialize(username:, articles:)
      @username = username
      @articles = articles
    end

    def articles
      @article.map { |article| HomeFeed::Article.new(article) }
    end
  end
end
```

* ※ [JsonWorld](https://r7kamura.com/articles/2015-06-05-q) を View で render 出来るように & 最近の JSON Schema に対応できるように拡張して利用

***

### コード例 (TypeScript)

[json-schema-to-typescript](https://www.npmjs.com/package/json-schema-to-typescript) を使って TypeScript の型を吐き出せる

```tsx
import { HomeFeedProps, Article } from "./generated-types/home-feed"

export function HomeFeed({ username, articles }: HomeFeedProps) {
  return (
    {articles.map((article: Article) => (
       <div css={itemStyle} key={article.id}>
          <ItemHeader article={article} />
          <ItemBody article={article} />
       </div>
    ))}
  )
}
```

***

## :v: 実際に組み込んだ :v: 

* 導入
    * いくつかのプロトタイプを作り
    * コンセプト的に行けそうと判断したら社内ライブラリ化して Qiita Team に導入
    * (便利に使ってたら他の開発メンバーが Qiita Jobs に組み込んでくれたり)
    * その後 OSS 化
* 効果
    * 信頼性向上 → 安心して開発出来るように
        * TypeScript の型が信用できるものになり、
        * Rails 側の動作検証も Request Spec などで出来るようになる
    * 秩序が生まれた
        * Presenter 層が整ってなかった部分がついでに整備された
        * 必要なデータの問い合わせが Presenter 層に集約されたことで、クエリの効率化
* 課題
    * クライアントが叩く API の部分は未対応 (信用しにくい型定義のまま)
        * 非同期で API を叩く必要がある箇所は実装済みのため後回しに
    * Presenter が DRY になるための整備

https://qiita.com/tomoasleep/items/21a2589317351e784a7c

https://github.com/increments/json_schema_view


***

## おまけ: render 出来るオブジェクトを作ることが出来る

* [ViewComponent](https://viewcomponent.org/) で採用されていた手法
    * Rails 側のアップデートで ViewComponent 以外でも出来るように

```slim
= render HomeFeed.new(props: { username: "Mike", articles: @articles })
```

Ref:

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

***

## おまけ: Action ごとのテンプレート (Slim) を捨てる

1. View の React 化が進むことで Rails 側のテンプレート (Slim) が限りなく薄くなる
1. 使う React Component を宣言するだけのオブジェクト (ReactPage) を作る
1. Controller だけで完結し、 Action 毎のテンプレート (Slim) が消える


```ruby
class HomeController < ApplicationController
  def index
    render ReactPage.new(
       HomeFeed.new(props: { username: current_user.name, articles: Article.page(1) })
    )
  end
end
```




***

## 設計面の FAQ

- Why not OpenAPI/GraphQL/gRPC ?
    - View に必要なデータは、API 経由ではなく、直接渡したい
    - GraphQL は小さくない初期投資をする必要がある
        - GraphQL は Qiita 側で運用されているが、他のサービスでは導入はまだ
        - 土台が出来てくると便利だが、そこまでのコストが大きい
        - GraphQL でモデルの関係をどう表現したり、も慣れが必要
- Why not Rails API mode?
    - 書き換えが大きくなる
        - やりたいことはフロントエンドを React に寄せることだったので、やりたいことに対して労力が大きすぎると判断した
- Why JSON Schema?
    - Qiita 内では一部の箇所 (Qiita API) で JSON Schema が利用されていたため (馴染みのある技術だった)
        - [Qiita API v2のJSON Schemaを公開しました - Qiita Blog](https://blog.qiita.com/120746062029-2/)
        - [JsonWorldでモデルからJSON Schemaを生成する](https://r7kamura.com/articles/2015-06-05-q)
    - 最近の [JSON Schema](https://json-schema.org/) は enum, anyOf など、表現力が十分ありそう
    - OpenAPI / HyperSchema などの転用も将来的に可能 (かも)
- Code First vs Schema First → Code First
    - 社内で採用している技術が Code First なので (GraphQL-Ruby, Qiita API) 馴染む
    - (個人的な考えとして) Ruby は型をチェックする仕組みがまだ弱いので、型定義と実装は近いところにある方が置いたほうが何かと良い
- API 等のスキーマはどうしている？
    - やれてない (今回はスコープから外した)
    - 「JSON Schema を吐き出せる Presenter」 が作れたので、 API と紐付ければ OpenAPI のスキーマを吐き出すことも将来的に可能 (かもしれない)