開発プロセス
reactjs
redux
react-storybook

React Storybook で変わるUI開発フロー (Redux Flavor)

More than 1 year has passed since last update.

React Storybook で変わるUI開発フロー (Redux Flavor)

SPAを考える会 (D3勉強会 2016.10.06)

by @kitaly (twitter: @kita_ly)


自己紹介 @kitaly

twitter.com/kita_ly

Twitter: @kita_ly

スクリーンショット 2016-10-05 13.16.39.pngスクリーンショット 2016-10-05 13.16.05.png


はじめに

React / Redux / Webpack 前提の話ですが

他のコンポーネント志向FWなどでも、ユースケースやワークフローは応用可能だと思っています

新しいツールも多く、実践"未"投入な構想段階のアイディアも多いです


今日紹介するコード例は大体ここにあります:

https://github.com/k-italy/react-storybook-demo-plus


目次

  • [導入] 動的UIの開発フローと課題
  • [導入] 提案する解決策の概要
  • [導入] Component as API
  • [本題] Component開発環境としての Storybook
  • [本題] Componentテスト環境としての Storybook
  • [本題] Componentドキュメントツールとしての Storybook
  • [Redux Flavour] Redux と Peudo-Local ライブラリ
  • まとめ

[導入] 動的UIの開発フローと課題

動的UI開発の登場人物とスキルセットの違い

  • ES君

    • 強み 「元々サーバーサイドで大規模アプリケーション組んでたし、EcmaScriptの最新仕様もバッチリ」
    • 弱み 「HTMLはなんとか書けるけど、CSSはちょっと無理ぽ…(´・ω・`)」
  • W3C君

    • 強み 「HTML・CSS・JQueryなら爆速開発まかせろ」
    • 弱み 「複雑なアプリケーションは経験無いな。Immutable/Reactive/MVCとかよくわからん」
  • REST君

    • (サーバーサイドの Rest API 開発してる人。今日はあんまり登場しない)

[導入] 動的UIの開発フローと課題

HTML納品スタイル

  1. マークアップエンジニアがHTML納品
  2. アプリケーションエンジニアがテンプレートに書き直しアプリに組み込む流れ
  • HTMLがデータ構造とか条件分岐を考慮しきれてなかったりする
    • HTML納品し直してもらうキャッチボール
    • アプリケーションエンジニアが限られたHTML/CSSの知識で修正

[導入] 動的UIの開発フローと課題

ViewModel/ViewHelper納品スタイル

  1. アプリケーションエンジニアがViewModel/ViewHelperを納品
  2. マークアップエンジニアがHTMLテンプレートを書く
  • 画面に必要なデータやHelperが足りないことが途中で判明したりしがち
    • ViewModel/ViewHelper の実装し直し
    • マークアップエンジニアがテンプレートのシンタックスを覚える必要(なおかつ汎用的な知識じゃなかったり)

[導入] 動的UIの開発フローと課題

Angular とか React 使うとどうよ?

→ 何も考えなかったら同じようなことになる


[導入] 解決の道筋

解決策: 「お前ら黙って勉強しろ」

→ というのは乱暴な正論というやつ


[導入] 解決の道筋

UI開発とアプリケーション開発の境界線付けをする

制御系の実装者とAPI提供者の区別をつける
- REST君: Rest API を提供する
- W3C君: Component API を提供する
- ES君: 上記を材料としてWebアプリケーションを組み立てる

スクリーンショット 2016-10-06 12.55.11.png


[導入] 解決の道筋

開発フロー

  1. 開発者間でI/Fを握る
    • ES君とREST君が Rest API の仕様で合意
    • ES君とW3C君が Component API (後述)の仕様で合意
  2. API提供者が並行で開発
    • REST君が Rest API の実装
    • W3C君が Component API の実装
  3. ES君が制御系を実装
  4. Webアプリケーションが完成

スクリーンショット 2016-10-06 15.49.30.png


[導入] 解決策の概要

React/Redux アーキテクチャ上だと

スクリーンショット 2016-10-06 12.52.05.png
※ なお、私のプロジェクトでは Middleware部分 は Redux-Saga を使ってます

Redux-Middlewareに何を使うか、Flux系FWは何を使うか、等は差し替え可能かも
※ 今日は UI Component 側の話がメインなので詳しく話さないです

View部分も、React以外のコンポーネント志向FWに差し替え可能かも


[導入] Component as API

Reactとその周辺ツールで可能になったことをおさらい


[導入] Component as API

React の力

  • Component as 変換器 / Component as Pure Function
    • 望ましいのは入力(Props)を受けて、出力(仮想DOM)する純粋関数
    • class でなく function を使うべき、という話ではないです
  • 制御側でうまく利用しやすいよう、Component を副作用のないAPIとして提供可能
// components/People.js

export default function People(props){
  const { users } = props;
  const items = users.length ? users.map(u => <li>{u.name}</li>) : <li>No Users</li>
  return (
    <ul>{ items }</ul>
  );
}
// 利用の例

// 入力
const users = [ { name: "Taro" }, { name: "Jiro" }, ...]; 
const people = <People users={users} />;

// 出力 (イメージ)
<ul>
  <li>Taro</li>
  <li>Jiro</li>
  ...
</ul>

[導入] Component as API

Redux(Flux) の力

Flux的機構により多くのComponentをStatelessにすることが可能に
(後述するが、StatefulComponentをゼロにはできないと思う)

  • Container Component & Presentational Component
    • Presentational Component はUIのみに集中できる
// Container Component
const TodoListContainer = connect(
  (storeState) => ({ todoList: storeState.todoList }),
  (dispatch) => ({
    onTodoComplete: (todoId) dispatch({ type: 'TODO_COMPLETE', payload: todoId })
  })
)(TodoList)


// Presentational Component
function TodoList(props){
  const { todoList } = props;
  return (
    <ul>...</ul>
  );
}

[導入] Component as API

CSS-Modules & Webpack

  • CSS-Modules: CSSにローカルスコープを(擬似的に)もたらす仕組み
  • Webpack: JSファイルからCSSや画像ファイルを require/import できる仕組み

これらによって Component の利用方法(I/F仕様)がシンプルになる
- (Before) <Hoge/> を正しい見た目にするには、 hogeWrapper というクラスを付与して…
- (After) <Hoge/> とさえ書けば Hoge 内部で閉じたCSS定義が正しい見た目にしてくれる

import * as styles from './TodoItem.scss';

function TodoItem(props){
  return (
    <div className={styles.todoItem} />
      ...
    </div>
  );
}

[本題] Component開発環境としての Storybook

「でも、アプリケーションに組み込まずにどうやって開発すんの???」


[本題] Component開発環境としての Storybook

React Storybook 概要

react-storybook

React Storybook is a UI development environment for your React components. With it, you can visualize different states of your UI components and develop them interactively.
It runs outside of your app. So you can develop UI components in isolation without worrying about app specific dependencies and requirements.
(https://getstorybook.io/docs)

公式ドキュメント: https://getstorybook.io/

※ React/Webpack 環境を前提のツールだと思われる


[本題] Component開発環境としての React Storybook

React Storybook 始め方 (詳しくは公式ドキュメントまで)

Quick Start

npm i -g getstorybook
cd my-react-app
getstorybook

create-react-app で作ったプロジェクトなら簡単にいけるっぽいが、独自にビルド組んでる場合は?

Slow Start

自分がStorybook導入した時(v1.36.0)はこれしかなかった

  • npm i -D @kadira/storybook
  • 設定ファイルをゴニョゴニョ書く

[本題] Component開発環境としての Storybook

React Storybook の基本的な使い方

まずコンポーネント自体の実装する (前述のPeopleと同じ)

// components/People.js

import React from 'react';

export default function People(props){
  const { users } = props;
  const items = users.length ? users.map(u => <li>{u.name}</li>) : <li>No Users</li>
  return (
    <ul>{ items }</ul>
  );
}

Storyを実装する

// components/stories/People.js

import React from 'react';
import People from '../People';
import { storiesOf } from '@kadira/storybook';

storiesOf('People', module)
  .add('with non-empty list', () => {
    const users = [ { name: 'Taro' }, { name: 'Jiro' } ];
    return (
      <People users={users} />
    );
  });

[本題] Component開発環境としての Storybook

React Storybook の基本的な使い方

コマンド実行すると、何かビルドが走ってWebサーバー立ち上がる

$ npm run storybook 

// package.json 内はこんな感じ
// "scripts": { "storybook": "start-storybook -p 9001" }

localhost:9001 にアクセスすると、Storybookが表示される

スクリーンショット 2016-10-05 18.03.50.png


[本題] Component開発環境としての Storybook

React Storybook の基本的な使い方

当然ですが、Componentが受け取るPropsは色々ある。

... you can visualize different states of your UI components and develop them interactively.
https://getstorybook.io/docs

前述のPeopleコンポーネントの場合、空配列が渡された場合も動作確認したい。

// 前述の  People.js から抜粋
...
const items = users.length ? users.map(u => <li>{u.name}</li>) : <li>No Users</li>
...

空配列の場合のStoryも追加する

// components/stories/People.js

storiesOf('People', module)
  .add('with non-empty list', () => {
    ...
  })
    /* ここから追記 */
  .add('with empty list', () => {
    const users = [];
    return (
      <People users={users} />
    );
  });

Webpack の HMR のパワーで、コードを修正するとページのリフレッシュ無しに画面が更新される


[本題] Component開発環境としての Storybook

React Storybook の基本的な使い方

スクリーンショット 2016-10-05 18.10.19.png


[本題] Component開発環境としての Storybook

Add-Ons

Storybook に追加機能をもたらすExtensionの機構。
デフォルトで linkTo/action などが提供されている。


[本題] Component開発環境としての Storybook

linkTo add-on

// components/stories/TodoItem.js

storiesOf('TodoItem')
  .add('completed', () => (
    <TodoItem onClick={linkTo('TodoItem', 'not completed')} />
  ))
  .add('not completed', () => (
    <TodoItem onClick={linkTo('TodoItem', 'completed')} />
  ));

スクリーンショット 2016-10-05 18.29.46.png
クリックすると Story間を移動できる
スクリーンショット 2016-10-05 18.29.52.png

ユーザのインタラクションに反応してStory間を遷移、状態遷移しているかのように見せられる。


[本題] Component開発環境としての React Storybook

action add-on

ユーザのインタラクションをLoggingしてくれる

add('xxx', () => (
  <TodoFooter onClick={action('onClearCompleted')} />
));

スクリーンショット 2016-10-05 18.25.22.png


[本題] Component開発環境としての React Storybook

Decorators

ファイル単位もしくは全ファイルで共通的に、Storyを囲う要素を差し込める。

  • Storyをセンタリング表示する
  • Storyの見た目が白系なので、わかりやすいように黒背景の上に載せるようにする
  • Redux を動かす (後述)
const CenterDecorator = (story) => (
  <div style={{ textAlign: "center" }}>
    {story()}
  </div>
);

storiesOf('Hoge', module)
  .addDecorator(CenterDecorator)
  .add('with text', () => (
    <Hoge />
  ))

[本題] Component開発環境としての React Storybook

Head.html

head.html というファイルを定義しておくと、グローバルに適用したいCSSファイルやFontファイルをStobybook全体で読み込める。


[本題] Componentテスト環境としての Storybook

API提供するなら、ちゃんとテストされたものを提供したい!

でも、こういうやつは相当クリティカルなものじゃなきゃメンテナンスできる気がしない…

スクリーンショット 2016-10-05 19.08.21.png
https://github.com/airbnb/enzyme


[本題] Componentテスト環境としての Storybook

Storyshots で Structural Testing しよう

Snapshot testing is a way to test your UI component without writing actual test cases.
https://voice.kadira.io/snapshot-testing-in-react-storybook-43b3b71cec4f#.ndvrs9qmy

テストケース書かずに UI Component をテストできる?


[本題] Componentテスト環境としての Storybook

Structural Testing (Snapshot Testing) とは

With Snapshot testing, we keep a file copy of the structure of UI components. Think of it like a set of HTML sources.

Then, after we've completed any UI changes, we compare new snapshots with the snapshots that we kept in the file.

If things are not the same, we can do two things:

We can consider new snapshots that show the current state, and then update them as new snapshots.
We can find the root cause for the change and fix our code.
https://getstorybook.io/docs/testing/structural-testing


[本題] Componentテスト環境としての Storybook

Storyshots

  • https://github.com/kadirahq/storyshots
  • 内部的には Jest の Snapshot Testing 機能を利用している模様
  • 2016年9月にリリースされたばかり!(この発表のネタ考えてる時に見つけた)

インストール

// Storybook の 2.17.0 以上が必要なので 古いVersionのStorybook をアップデートする
$ npm i -D @kadira/storybook@2.17.0 

$ npm i -D @kadira/storyshots

package.json に script 追加

"scripts": {
  "test-storybook": "storyshots"
}

[本題] Componentテスト環境としての Storybook

Storyshots の使い方

$ npm run test-storybook

スクリーンショット 2016-10-05 19.21.29.png

Snapshotファイルが追加される。これを git commit しておく。

スクリーンショット 2016-10-05 19.22.58.png


[本題] Componentテスト環境としての Storybook

既存のComponentに変更が入ったら

スクリーンショット 2016-10-05 19.26.34.png


[本題] Componentテスト環境としての Storybook

既存のComponentに変更が入ったら

再度 storyshots を実行

$ npm run test-storybook

前回までのSnapshotとの差分をチェックします。当然差分が検知され結果はエラーに。
スクリーンショット 2016-10-05 19.27.33.png

修正対象のコンポーネント自体に加え、それに依存している他のコンポーネントのStoryが意図せず変更されても気づける。
気づいたら、実際にそのStoryを目視でチェックしにいけば良い。


[本題] Componentテスト環境としての Storybook

既存のComponentに変更が入ったら

全ての差分が問題ないことが確認できたら、Snapshot更新

$ npm run test-storybook -- -u

1Storyずつインタラクティブに判定していく場合はこちら

$ npm run test-storybook -- -i

(差分表示)
Update snapshot? (Y/n) 

※ 通常の単体テストと異なりコマンド一つでテストをパスするようになるので、ノールックで更新しない最低限の倫理が必要。

もし、ローカル環境でのSnapshotの差分チェックを怠っていたら、Pull Requestをフックに自動テスト→エラーとすることも可能。


[本題] Componentテスト環境としての Storybook

実際のDOMのスナップショットを保持しているわけではないので注意

// .storybook/__snapshots__/TodoItem.snap

...

<label
  onDoubleClick={[Function bound handleDoubleClick]}>
  Hello Todo
</label>
<button
  className="destroy"
  onClick={[Function onClick]} />

...

出力されるVirtualDOMの構造が変わらなければ、CSS定義だけが変わってる場合などは気づけない。
CSS-Modulesなどをうまく利用し、そもそも副作用の少ないComponent開発を頑張る必要がある。
また、後述する CSS/Style Tesing も検討する。


[本題] Componentテスト環境としての Storybook

他のComponentテスト手法

Interaction Testing

Enzymeなどを利用し、実際にユーザがクリックしたりタイプして状態変化した結果までテストする

CSS/Style Testing

実際にDOMのレンダリングまで行った結果のスナップショットを保持し差分検知する。
StorybookではStoryごとにURLが発行される。
BackstopJSやGeminiなどのCSS/Style TesingツールにそのURLを渡すことで実現可能(らしい

http://localhost:9001/?selectedKind=People&selectedStory=with

[本題] ドキュメント生成ツールとしての Storybook

実装もテストもアプリケーションから切り離された環境でできるようになった

これでドキュメントも作れたら嬉しい

  • Component API の利用者である ES君や他のW3C君向けの APIドキュメント
  • 他のW3C君向けのスタイルガイド
  • デザイナや企画職の人に確認してもらう成果物(特に開発環境が無い人達)

[本題] ドキュメント生成ツールとしての Storybook

Storybookを静的サイトにする

react-storybook が内包している build-storybook コマンドを利用する

npm run できるように package.json に追加

// c: 設定ファイルのディレクトリ o: 生成したファイルの出力先ディレクトリ
"scripts": {
    "build-storybook": "build-storybook -c .storybook -o .out"
}

実行すると、 .out ディレクトリ配下に html/js ファイル等が出力される

$ npm run build-storybook

スクリーンショット 2016-10-06 10.30.13.png

これをCIツールなど利用して、任意のサーバーにデプロイするようにしておけば、デザイナや企画の人など開発環境が無い人達でもさくっと確認可能

(手元でサクッと試したいなら、下記のような感じ)

$ cd .out
$ python -m SimpleHTTPServer

[本題] ドキュメント生成ツールとしての Storybook

ただ、APIドキュメントやスタイルガイドと呼ぶには不十分

スクリーンショット 2016-10-06 10.34.06.png


[本題] ドキュメント生成ツールとしての Storybook

addon-infoでAPIドキュメントっぽくする

インストール方法は割愛(詳しくはリポジトリを参照)

addWithInfo メソッドを使って、第二引数部分に任意のテキストをMarkdownで記述する。

storiesOf('Header', module)
  .addWithInfo(
    'default view',

    /* ↓↓↓↓↓↓↓↓↓↓↓↓ */
    `
    これはTodoアプリのHeaderですよ。
    Props:
      - addTodo: (todo: string) => void

    利用例:  
    \`\`\`jsx
    const addTodo = (todo) => dispatch({ type: 'ADD_TODO', payload: todo });
    <Header addTodo={addTodo} />
    \`\`\`
    `,
    /* ↑↑↑↑↑↑↑↑↑↑↑↑ */

    () => {
      return (
        <div className="todoapp">
          <Header addTodo={action('Add Todo')}/>
        </div>
      );
  });

[本題] ドキュメント生成ツールとしての Storybook

addon-infoでAPIドキュメント化

Storybook上に入力したMarkdownが表示される。

さらに Story のソースコードや、PropTypesの定義を自動でドキュメントに載せてくれるので、
Componentの利用コード例を自分で書く手間が省けそう。

スクリーンショット 2016-10-06 10.49.48.png

※ 自分で書くマークダウンに関しては、シンタックスハイライト未対応っぽい。
※ ドキュメント部分が前述のStoryshotsのSnapshotファイルに含まれてしまい test-storybook で差分検知されてしまう


[本題] ドキュメント生成ツールとしての Storybook

addon-notes

インストール方法は割愛(詳しくはリポジトリを参照)

WithNotes コンポーネントを利用して、ノートを記載。

import { WithNotes } from '@kadira/storybook-addon-notes';

...

  return (
    <WithNotes notes={'左側の丸をクリックしたら完了・非完了状態が切り替わるよ'}>
      <div className="todoapp">
        <div className="todo-list">
          <TodoItem
            todo={todo}
            editTodo={action('editTodo')}
            deleteTodo={action('deleteTodo')}
            completeTodo={linkTo('TodoItem', linkOnClick)}/>
        </div>
      </div>
    </WithNotes>
  );

スクリーンショット 2016-10-06 10.59.50.png


[本題] ドキュメント生成ツールとしての Storybook

その他ドキュメント系ツール

  • Storybook Deployer

    • GitHub Pages にコマンド一発でデプロイ可能(今後他のホスティングサービスにも対応予定?)
  • Storybook.io (有償サービス)

    • Storybookのホスティング環境を提供
    • Pull Request 毎に Storybook を自動デプロイ
    • PRに対応するStorybook上にコメントなどして、レビュープロセスを効率化できるっぽい

気づけばStorybookの内容ばかりになってしまいました

(それぐらい、個人的に考えていたUI開発のあり方にドンピシャなソリューションを提供してくれてる)

活発に機能追加をやってるようなので最新情報をキャッチアップしたい方は:


[再掲] Storybook で Component開発の独立化

スクリーンショット 2016-10-06 12.55.11.png

スクリーンショット 2016-10-06 15.54.06.png


[脱線] Redux と Stateless Component

「本当に全て Stateless Component にすべきか? できるか?」

→ 無理ですし、バランス感が必要だと思う

→ isMenuOpen 程度の状態管理まで分業したくない

例えば下記のような場合は Component 側で完結したい:
- 選択中のタブとか、ちょっとした閉じ開きの状態
- フォームのバリデーション等、実際に触ってみないとイマイチ動作確認にならない

同時に、Reduxの "Single Source of Truth" の概念やタイムトラベルはなるだけ殺したくない。


[脱線] Redux と Stateless Component

Pseudo Local系のライブラリ

スクリーンショット 2016-10-06 15.50.23.png

https://github.com/reactjs/redux/issues/159


[脱線] Redux と Stateless Component

Redux-UI

選択中のタブとか、閉じ開きのフラグのようなComponentのLocalな状態で良さげなものに利用。
状態を持たない Presentational Component を実現しつつ、reducer をイチイチ記述必要が無い。(内部的に redux-ui が state管理してくれる)

// containers/ToggleParagraphContainer.js

import ui from 'redux-ui';

const ToggleParagraphContainer = ui({ 
  state: { isOpen: true }
})(ToggleParagraph)
// components/ToggleParagraph.js
function ToggleParagraph({ ui, updateUI }){
  const onClick = () => updateUI({ isOpen: true })
  return (
    <div>
      <button>{ui.isOpen ? 'Close' : 'Open' }</button>
      { ui.isOpen && <p>Storybook 最高!</p> }
    </div>  
  );
}

[脱線] Redux と Stateless Component

Redux-Form

基本的に redux-ui と同じような発想。
Form の入力やバリデーションに特化している。

import form, {Field} from 'redux-form'; 

const UserEditFormContainer = form({ 
  form: 'userEditForm',
  initialValues: { userName: 'kitaly' },
  validate: validateFn,
  onSubmit: onSubmitFn
})()

function UserEditForm(props){
  return (
    <form>
      <Field name="userName" component={UserNameInput} />
      <input type="submit" value="更新"/>
    </form>  
  );
}

function UserNameInput(props){
  return (
    <p>
      <input type="text" {...props.input} />
      {props.meta.error && <p>{props.meta.error}</p>
    </p>
  );
}

[脱線] Redux と Stateless Component

Redux-UI/Redux-Form と Storybook

これで状態管理をReduxに任せつつ、コンポーネント開発側だけで完結できる。

// .storybook/config.js

import {reducer as uiReducer } from 'redux-ui';

setAddon({
  addWithRedux(storyName, storyFn) {

    const store = createStore(
      combineReducers({
        form: formReducer,
        ui: uiReducer
      }),
      {}
    );

    this.add(storyName, (context) => (
      <Provider store={store}>
        {storyFn(context)}
      </Provider>
    ));
  }
});
// containers/stories/DropdownMenuContainer.js

storiesOf('DropdownMenuContainer', module)
  .addWithRedux('normal', () => (
    <DropdownMenuContainer />
  ));

[脱線] Redux と Stateless Component

Pseudo Local 系ライブラリの注意

不用意に汎用的な Component まで Redux依存になっていくのも好ましくない

→ 適宜 React標準の setState を利用するなどバランス間を持って使い分ける

また Stateful な Storyは、前述の Structural Testing ではカバーしきれなくなる

→ Container Component のStoryで目視で動作確認しつつ、Presentational Component についても色々なケースのStoryを作る

→ Enzymeなどを利用して Interaction Testing を実施する


[まとめ] 感想

  • Storybook 素敵すぎる
    • 気づいたらほとんどStorybookのドキュメントで書いてあることばかりになった…orz
    • 苦でない人は公式ドキュメントやDemoプロジェクトを見ましょう
  • 分業とは言ったものの、この体制をつくるためにES君とW3C君のハイブリッド人材は1人は必要かも
  • W3C君はAPIを使う側から提供する側へ、I/F設計やテスト等を考える必要性、やはり変化無しにできるわけではない
  • もちろんES君もCSS勉強しましょ

ご清聴ありがとうございました。

  • はじめに
  • 目次
  • React Storybook 始め方 (詳しくは公式ドキュメントまで)
  • React Storybook の基本的な使い方
  • React Storybook の基本的な使い方
  • React Storybook の基本的な使い方
  • React Storybook の基本的な使い方
  • Add-Ons
  • linkTo add-on
  • action add-on
  • Decorators
  • Head.html
  • Storyshots で Structural Testing しよう
  • Structural Testing (Snapshot Testing) とは
  • Storyshots
  • Storyshots の使い方
  • 既存のComponentに変更が入ったら
  • 既存のComponentに変更が入ったら
  • 既存のComponentに変更が入ったら
  • 実際のDOMのスナップショットを保持しているわけではないので注意
  • 他のComponentテスト手法
  • Storybookを静的サイトにする
  • addon-infoでAPIドキュメントっぽくする
  • addon-infoでAPIドキュメント化
  • addon-notes
  • その他ドキュメント系ツール
  • [再掲] Storybook で Component開発の独立化
  • 「本当に全て Stateless Component にすべきか? できるか?」
  • Pseudo Local系のライブラリ
  • Redux-UI
  • Redux-Form
  • Redux-UI/Redux-Form と Storybook
  • Pseudo Local 系ライブラリの注意
  • [まとめ] 感想