てくすた

ピクスタ株式会社のエンジニア・デザイナーがつづるよもやまテクニカルブログです

ピクスタにおける最近のReactコンポーネント開発

この記事は、PIXTA Advent Calendar 201718日目の記事です。

概要

こんにちは。開発部で技術基盤を担当している id:yszk0123 です。 ついこの間新しい自転車を買い、サイクリングに行くぞ!と思っていたら、いつの間にか冬を迎えており、時の流れの速さを痛感しています。

ここ最近、React を使った独立して使える UI コンポーネントを作りつつ、開発環境の整備を行っていました。 本エントリでは、その際に気をつけたことや自分なりに考えたことなどをまとめて、パターン形式で紹介しようと思います。

今回の環境整備で導入したフレームワークやライブラリは以下のとおりです。

  • UI の手動確認: Storybook1
  • 自動テスト: Jest, Enzyme, StoryShots2

対象読者は、React を使って開発をしているエンジニアです。

補足

PIXTA では、サーバーサイドは Rails、フロントエンドは JavaScript を webpack と Babel を利用してビルドしています。 以下に登場する JavaScript のコード例は ES2017 を想定しています。

パターン1「小さく始める」

目的

技術を段階的に取り入れていく。

背景・制約条件

新しい技術やライブラリを試験的に導入したい。今後本格導入されるかはわからない。

解決策

ライブラリなどの導入は、最初は最小限に。

適用例

I18n 対応におけるライブラリの導入。

  • I18n-js (Ruby の Gem とセット)
    • Rails と分離されているため、i18n-js のような gem と連携するライブラリを使う必要はない。
    • i18n-js は今のところ npm パッケージとして利用できない3
  • React Intl
    • ファイルサイズが大きい。
    • 必要な知識が多い。
    • 主要な開発者が抜けてしまったようで、更新もやや停滞している。利用者は多いので、当分の間、使えなくなることはないはず。
  • react-i18next
    • React Intl とほぼ同様。
  • Polyglot.js
    • ファイルサイズが小さい。
    • ドキュメントが簡潔。
    • メンテナンスも行われており、安心して使えそう。
    • i18n-js とインタフェースがほぼ同じなので、問題が起きたらすぐに乗り換えることができる。
    • 複雑な機能は求められておらず、機能的には十分。

以上を踏まえて、Polyglot.js を採用しました4

関連

  • スケジュールを小分けにする (『組織パターン』5 4.1.2)
  • 1歩ずつ少しずつ (『プリンシプル オブ プログラミング』6 5.5)

パターン2「既製のドキュメントを利用する」

目的

新しく学ぶ人や、メンテナンスする人の負担を軽減する。

背景・制約条件

社内にフロントエンド専門のエンジニアが少ない。 アルバイトやインターンの人の教育に使える時間も限られている。 ドキュメントを書いて共有することもできるが、フロントエンドの移り変わりは激しく、その度に更新する労力に見合わないことが多い。

解決策

ドキュメントが整備されたライブラリやツールを優先的に採用する。

適用例

テストフレームワーク導入における Jest と Mocha の比較。

Mocha の場合、Sinon.JS など追加のライブラリを入れることはできるが、ライブラリ毎にドキュメントが異なり、品質もまちまち。 ライブラリ選択の柔軟性はあるが、要求される知識も高くなる。

Jest のドキュメントは完成度が高く、モックなどのテストで必要になる機能があらかじめ揃っている。 一箇所にまとまっており、よくメンテナンスされたドキュメントがあるという安心感は大きかった。 設定にやや癖があるが、細かなカスタマイズはほぼ公式ドキュメントを見れば設定できた。 全部入りフレームワークの弱点はあるが、依存関係地獄からの開放や、「何故このライブラリを入れたのか」という説明を省けるといったメリットもある。

以上を踏まえて、Jest を採用7

関連

パターン3「構造より意図」

目的

テストを変更に強くする。

背景・制約条件

ユーザーインタラクションに関するプロダクションコードは変更が多く、変更の度にテストコードの修正が必要なケースが多い。

解決策

HTML 構造によらない手がかりを使ってテストする。

適用例

Enzyme で DOM 要素を指定する方法の比較 (具体例は下記コード参照)。

  1. HTML の基本要素で指定
    • 分かりやすい。
    • React に限らず使える。
    • HTML の書き方によっては、常に使えるとは限らない (例: a タグがボタンとして使われている)。
  2. HTML のカスタムデータ属性 (data-*) で指定
    • こちらのブログで紹介されている方法。
    • DOM 要素を指定するためのテスト環境以外では取り除きたい。
      • babel-plugin-react-remove-properties という Babel のプラグインで取り除ける。
    • React に限らず使える (date-test を削除できる場合)。
  3. 指定するコンポーネントを export しておく
    • react-markers のようなライブラリもある。
    • React 限定。
    • コンポーネント化されていない要素は指定できない (例: div タグなど)。
  4. className (class) で指定
    • デザイン変更により名前が変更される可能性が高い。
    • デザイン用なのかテスト用なのか判別しにくい。
    • "test-foobar" のようなクラスは付けられるが、デザイン用のクラスと区別がつけにくい。
    • data-test のような、簡単に取り除ける仕組みが今のところない。
  5. Enzyme のメソッドチェーンで指定 (下記コード参照)
    • HTML 構造の変更に弱い。
    • 読みにくい。
// test.js
import { shallow } from 'enzyme';
const wrapper = shallow(<TestForm />);

// 1. HTML の基本要素
expect(wrapper.find('input[type="number"]')).toBe(...);

// 2. HTML のカスタムデータ属性 (data-*)
expect(wrapper.find('[date-test="phone-number"]')).toBe(...);

// 3. 指定するコンポーネントを export しておく
import { PhoneNumberInput } from '/path/to/component';
expect(wrapper.find(PhoneNumberInput)).toBe(...);

// 4. className (class)
expect(wrapper.find('.phone-number')).toBe(...);

// 5. Enzyme のメソッドチェーン
expect(wrapper.find('form').at(2).find('li').first()).toBe(...);

以上を踏まえた結果、data-test を採用。

関連

  • コードは必ず変更される (『プリンシプル オブ プログラミング』 5.5)
  • アプリケーションの境界はテストにより境界づけられる (『組織パターン』 4.2.30)

パターン4「ストーリー駆動で開発する」

目的

テストを書く負担を減らし、段階的に増やしていく。

背景・制約条件

なるべくテストは書きたいが、慣れていない人が多い。テストしやすさは実装箇所により異なる。

解決策

Storybook Driven Development

適用例

今回 UI コンポーネントを作成した時の手順。

複雑なロジックのテストには Enzyme を使う。この場合はなるべくコンポーネントのツリー構造に依存しないテストを書く。 見た目のチェックは Storybook で手間を掛けずに行う。

最終的なデザインが固まり、実装も終わったら StoryShots でスナップショットを保存して、コンポーネントのツリー構造に対する回帰テストを行えるようにする。story をきちんと用意していれば、(ほぼ) 自動的に snapshot テストも追加できるので手間もかからない。

これよりも粒度の大きなテストが必要な場合は、JavaScript 単独でテストする利点が薄いため、Rails の feature spec などで担保する。

よく書かれたテストや story は、それ自体がドキュメントの役割を果たしてくれるという効果もある。古くなればテストが落ちるので、同期が取れなくなる心配も少ない。

関連

パターン5「処理の外部化」

目的

独立性の高いコンポーネントを作る。

背景・制約条件

コンポーネントで外部との連携処理を行いたいが、テスト自体はそのコンポーネントで完結させたい。

解決策

外部との連携処理を外に出す。

適用例

外部の API 呼び出しを伴うコンポーネント。 素直に実装すると、テストが面倒になる。

import { shallow } from 'enzyme';

it('should fetch ...', () => {
  const onFetch = jest.fn((name) => Promise.resolve({ hello: name }));
  const wrapper = shallow(<Component onFetch={onFetch} />);
  expect(onFetch).toBeCalledWith('world');
  // ...
});

大規模な開発になってくると、モックサーバーを用意するなどの必要が出てくるかもしれないが、小規模な場合はだいたいカバーできた。

関連

  • アプリケーションの境界はテストにより境界づけられる (『組織パターン』 4.2.30)

おわりに

感想

今回の UI コンポーネント開発は主に1人で進めていたということもあり、その中で得た経験を他人と共有することができていなかったのが反省点でした。

これは書いた後で気づいたことですが、このようにパターン化しておくと、他の人との共有も随分と楽になると思いました。

今後の課題

共同作業を快適に行うために、いろいろな対策を行ってきましたが、まだまだ改善の余地があります。 例えば Storybook を導入したものの、コードレビューを行う際、レビュアーが自分の開発環境確認したい場合に Storybook を立ち上げるのは面倒です。

幸い Storybook では、静的サイトを生成することができます。 この機能を利用すれば、CI で生成した静的サイトへのリンクを GitHub のプルリクエストにコメントすることで、レビュアーはリンクをクリックするだけでコンポーネントの確認ができるはず。というところまで調べたので、この記事を投稿したら導入しようと思います8 9

ピクスタでは、半年ほど前から社内用チュートリアルを作って JavaScript の普及を行っており、チュートリアルで足りない部分は、ペアプロを通して実践形式で補ってきました。 その効果もあってか、Storybook を使って開発を行うエンジニアも少しずつ増えてきました。 来月はベトナムのエンジニアにも普及を行う予定です!

19 日目の記事は masahiro ogawa が担当します。お楽しみに!

*****

ピクスタでは、サービスを盛り上げていきたいデザイナー・エンジニアを募集しています。 recruit.pixta.co.jp

参考リンク


  1. 類似したツールに React Styleguidist があるが、こちらは Markdown 埋込み型なので、Lint などのツールと組み合わせるのがやや面倒だと感じた。また、プラグイン (addon) の充実度をみても、今のところ Storybook が有利だった。求めていたのは、スタイルガイドというよりは、テストの色合いが強いため、今回は Storybook を選択。

  2. Storybook には様々な機能拡張を行う addon という仕組みがある。Storyshots は Jest と組み合わせて snapshot テストを行うための addon。

  3. npm パッケージ化が進行中のようだが、更新が停滞している (2017年12月15日時点)。

  4. ピクスタにおける Polyglot.js の運用方法は、Rails エンジニアが理解しやすいように locales/ フォルダに JSON を配置している。不要なパッケージ依存を避けるため yaml にはしなかったが、不便であれば yaml 化も検討。翻訳が必要な要素が少ないため、現状はすべての翻訳ファイルを含めている。増えてきた段階で遅延読み込みに変更予定。

  5. 『組織パターン』翔泳社

  6. 『プリンシプル オブ プログラミング 3年目までに身につけたい 一生役立つ101の原理原則』秀和システム

  7. Mocha のスナップショットテスト対応に不安。ライブラリは存在したがあまり使われていない。

  8. Storybook Hub というサービスがあるらしいので調べてみたが、サイトにアクセスできなかった (2017年12月15日時点)。

  9. Qiita に投稿済み → コードレビューに役立つ React Storybook の閲覧環境を作る - Qiita