Opt Technologies Magazine

オプトテクノロジーズ 公式Webマガジン

React & TypeScriptを用いた業務システム開発

alt

先日、オプトからFeed Terminal というツールがリリースされましたので、主にフロントエンドについての大まかな全体像と技術的な拘りをご紹介しようと思います。

あいさつ

こんにちは。uryyyyyyyです。 社内では遊撃手的ポジションにおりまして、今回のReact & TypeScript案件であるFeed Terminalでは、フロントエンドの基盤設計やUIデザインなどを担当していました。

Feed Terminal とは

f:id:opttechnologies2015:20170807162410p:plain

Feed Terminalとはデータフィードマネジメントツールと呼ばれるもので、データフィードというものの運用作業を効率化するためのツールです。 ツールの機能としては、大まかに「基データのインポート → 加工・フィルタ → Googleなど媒体へのエクスポート」という処理を行います。

具体的にどのような処理を行うかは本記事では触れませんが、GUIとしては、これらの処理に置いて設定をするための管理画面を提供しています。 (例えば、どこからデータを取得するか、加工の条件はどうか、などの入力。エラー表示)

本記事では、この管理画面にReact & TypeScriptを導入した事例をお話します。

フロントエンド設計

本ツールにはどんな要件があり、そこからどのように設計したのかを記します。

管理画面の要件

まず、ビジネス要件としてはこのような形でした。

  • 使いやすさが大事(作業工数を出来る限り減らしたい)
    • UIもゼロベースで考える
    • 表示速度の改善や、動的なレイアウトを色々したくなりそう
    • 変更に強い設計にして何度も改善したい
  • 担当者がログインして操作するもの
    • SEOやOGP対応などは不要
    • 対応ブラウザは限定して良い
    • トラフィックも多くないので、アセット配信の最適化なども一旦無視

また、非機能要件として、オプトテクノロジーズで要求される品質や特徴としてこのようなものがあります。

  • チームメンバーはバックエンド(Scala)エンジニアばかり
    • フロントの知識があまりなくても開発しやすいツール選定をしたい
    • 込み入った部分は自分が担当するが、APIは明確にしたい
  • フロントでもテストやlint、カバレッジ計測は必要
  • 要件を満たすのであれば新技術を活用していく文化

アーキテクチャ選定

上記の要件から、当時特に安定していたReactを用いて、Redux & TypeScript を組み合わせたSPA構成で行くことにしました。

実は実装しやすさや可読性については以前から試していて、以前マガジン記事にてご紹介した納会アプリで検証済みだったりします。

tech-magazine.opt.ne.jp

チームへの共有

フロントエンドに不慣れなメンバーにも分かりやすいようにと意識して、ライブラリの使い方や設定項目の意味を記事にしてメンバーに共有しました。

qiita.com

僕の趣味なのですが、余計な要素の無い最小構成から徐々にアプリを組み上げていくのを通じて、各ライブラリの使い方が理解できるようになっています。 本記事ではコードの詳細までは説明しないため、詳しく知りたい方はぜひこちらの記事をご覧ください。 (先日、記事をReact16-beta & TypeScript2.4に更新しました。)

実装・設計の中身の紹介

前置きが長くなりましたが、上記方針を元にした具体的なアーキテクチャ設計についてご紹介します。

全体像(package.json)

まずは全体像を確認しましょう。少々多いですがこのような形です。

//package.json

{
  "name": "feed-terminal-web-console",
  "version": "1.0.0",
  "scripts": {
    "postinstall": "rm -f ./node_modules/immutable/dist/*.d.ts && sh ./moveToPublic.sh",
    "build:watch": "webpack --progress --colors --watch --config ./webpack.config.dev.js",
    "build": "webpack --colors --config ./webpack.config.dev.js",
    "build:prod": "webpack --colors --config ./webpack.config.prod.js",
    "test:ut": "karma start karma.conf.js",
    "test:all": "karma start karma.conf.js **/*.spec.tsx **/*.spec.ts",
    "test:coverage": "tsc && karma start karma.conf.js ./dist/**/*.spec.js",
    "coverage-html": "remap-istanbul -i ./coverage/*/coverage-final.json -o coverage-report -t html",
    "coveralls": "remap-istanbul -i ./coverage/*/coverage-final.json -o ./coverage/*/coverage-final.json && istanbul report --dir ../../target/scala-2.11/coverage-report cobertura",
    "server": "node dev-server.es6",
    "lint:unit": "tslint -c tslint.json",
    "lint:ci": "git diff --stat --name-only HEAD $(git merge-base HEAD origin/master) | grep web-console/front/src | sed -e 's@web-console/front/@@g' | xargs --no-run-if-empty npm run lint:unit",
    "lint:all": "tslint -c tslint.json \"src/**/*.ts?(x)\""
    ...
  },
  "devDependencies": {
    "@types/enzyme": "2.8.2",
    "@types/immutable": "3.8.6",
    "@types/jasmine": "2.5.53",
    "@types/react": "15.0.38",
    "@types/react-dom": "15.5.1",
    "@types/react-redux": "4.4.46",
    "@types/redux": "3.6.31",
    "body-parser": "1.17.2",
    "enzyme": "2.9.1",
    "express": "4.15.3",
    "fetch-mock": "5.12.1",
    "istanbul-instrumenter-loader": "2.0.0",
    "jasmine-core": "2.6.4",
    "karma": "1.7.0",
    "karma-chrome-launcher": "2.2.0",
    "karma-coverage": "1.1.1",
    "karma-jasmine": "1.1.0",
    "karma-mocha-reporter": "2.2.3",
    "karma-webpack": "2.0.4",
    "multer": "1.3.0",
    "react-test-renderer": "15.6.1",
    "remap-istanbul": "0.9.5",
    "ts-loader": "2.2.2",
    "tslint": "5.5.0",
    "tslint-config-standard": "6.0.1",
    "tslint-react": "3.0.0",
    "typescript": "2.4.1",
    "uglifyjs-webpack-plugin": "0.4.6",
    "webpack": "3.2.0"
    ...
  },
  "dependencies": {
    "immutable": "3.8.1",
    "react": "15.6.1",
    "react-dom": "15.6.1",
    "react-redux": "5.0.5",
    "react-router-dom": "4.1.1",
    "redux": "3.7.2",
    ...
  }
}

全部は説明しきれないので、大きく以下の内容を取り上げます。

  • TypeScriptとの付き合い方
  • reduxの使い方
  • 開発時の動作確認の仕方
  • テストやlintとの付き合い方

TypeScriptとの付き合い方

TypeScriptを使うにあたっての懸念点は

  • どこまでECMAScriptの記法を使えるか
  • 型定義がどのくらい信用できるか

でした。

前者については、個人的にはasync/awaitやES Modulesが使えれば問題ないだろうという感覚値でした。 ちなみに、当初はreact-dndのためにデコレータ構文を使っていたのですが、型チェックが上手く利かなかったのでやめました。。

後者については、まずはメンテされうるライブラリを選定した上で、上記Qiita記事のサンプルで動作確認をしています。 たまに上手くいかないこともありますが、そういう時は無理に型定義を使わず、ちょっとラップした層を作って誤魔化しています。 (ImmutableJSについては、公式で配っている型定義が良くなかったため、DTで配布されているものを使い続けています。)

reduxの使い方

状態管理はreduxを使っています。 個人的にはあんまり好きではないのですが、オレオレフレームワークを使うより学習コストが低いかと思って採用しました。 ただ、画面をまたいで共通化できるデータがほとんどなかったため、ほぼページ単位でReducerが存在している形になっています。

Reduxはactionに入ってくるpayloadにどんなものでも詰められますが、TypeScriptを用いて型安全に記述するように心がけています。 (Qiita記事だと このあたり です。)

//module.ts

enum ActionNames {
  INC = 'counter/increment',
  DEC = 'counter/decrement',
}

interface IncrementAction extends Action {
  type: ActionNames.INC
  plusAmount: number
}
export const incrementAmount = (amount: number): IncrementAction => ({
  type: ActionNames.INC,
  plusAmount: amount
})

...

export interface CounterState {
  num: number
}

export type CounterActions = IncrementAction | DecrementAction

export default function reducer(state: CounterState = {num: 0}, action: CounterActions): CounterState {
  switch (action.type) {
    case ActionNames.INC:
      return {num: state.num + action.plusAmount}
    case ActionNames.DEC:
      return {num: state.num - action.minusAmount}
    default:
      return state
  }
}

意図しないActionを飛ばすことも無いし、Reducer側で不適切なプロパティにアクセスしようとしたら型チェックで弾かれるようになっています。

ちなみに、現状reduxのMiddlewareは特に必要性を感じなかったため利用していません。詳しくはReduxでのMiddleware不要論 をご覧ください。

開発時の動作確認の仕方

SPA構成にしたことと、僕はフロントだけいじりたかった(Scalaプロジェクトを立ち上げるのが面倒だった)ので、フロントの動作確認用のダミーサーバーをExpressで用意しました。

APIサーバーからのレスポンスの定義については、サーバー側でSwaggerを導入していて、そこでAPI定義を擦り合わせていますが、サーバーサイドの開発が間に合っていなくてもフロントのみで開発ができるようになっています。 レスポンスをちょっといじればコーナーケースな値を返せたり、ファイルアップロードやリダイレクトなどの動作確認も行えるため重宝しています。

また、ビルド・読み込みに時間がかかる問題があったため、ReactなどStandAloneで動かせるライブラリについてはビルド対象から外すようにしています。

テストやlintとの付き合い方

テストももちろん必要に応じて書いていて、カバレッジは6割は超えているでしょうか。 (Qiita記事だと このあたり です。)

内容としては、

  • Container層にはロジックは持たせてないのでテストしない
  • Component層では、Stateの変化やイベント発火の確認
  • Logic層では、通信部分をmockしつつ適切なActionがDispatchされることの確認
  • Reducer層では、Actionに応じてstateが変化することの確認

といった形で、気になったところだけテストを書いていくようにしています。

また、テスト量が増えてくると全部流すのが大変なので、「特定ファイルのみテストしたい」という需要があったため、 npm run test:ut ./component/__tests__/HogeComponent.spec.tsx のような指定を出来るようにkarmaの設定を組んでいます。

lintについては、プロジェクトがそこそこ落ち着いてから導入したこともあり、そのPRで触ったファイル(masterとの差分)のみチェックすることにして徐々にキレイにしていこうとしています。

まとめ

他にも細かなTipsはありますが、大まかにこのように無理ない形で組んでいます。 React16に向けても、もうwarningは全て潰しているのと、Qiita記事のサンプルで問題なく動いているのですぐに出来るかなと思っています。

オプトテクノロジーズではこのような形で様々なシステム開発を行っています。

  • 最新技術を使ってシステムを作りたい方
  • 型やテストやlintなどを用いて品質を上げたい方

などなど、ご興味を持って頂けた方はぜひこちらへ。

※カジュアル面談ご希望の方は、補足欄に「カジュアル面談希望」とご記載ください