1 of 85

品質と開発速度を

両立させるために

捨てたものと守ったもの

Inside Frontend 2019

Abema Towers, Shibuya 2019.05.18

2 of 85

自己紹介

About US

3 of 85

CyberAgent, Inc.�CATS (CyberAgent Advanced Technology Studio)

4 of 85

Tsuyoshi WadaTwitter: wadackel�GitHub: tsuyoshiwada

Soichi MasudaTwitter: masuP9�GitHub: masuP9

5 of 85

はじめに

Introduction

6 of 85

  • 品質を担保するための戦略
  • 捨てたもの
  • 結果
  • これから
  • まとめ

今日お話すること

7 of 85

8 of 85

WinTicket とは

  • 2019年4月にリリース
  • 競輪のインターネット投票サービス
  • 競輪場のレース動画を高画質で観れる
  • AI やタレントの予想した買い目をそのまま買える

9 of 85

競輪ドメインに求められる特性

  • 屋外(競輪場)での使用
    • モバイル回線での利用
    • 音が出しにくい
    • 環境光でディスプレイが見づらい可能性がある
  • 高い年齢層
    • 60代以上の男性がメインユーザー層
    • 50代以上で75%を超える

競輪場来場者の年齢構成

「競輪場来場者」に対するアンケート調査 平成 23 年度 競輪定点観測調査 / 財団法人JKA 2012を元に桝田が作成

60

70

50

40

30

20

10 of 85

高い利用体験

Performance

Accessibility

11 of 85

Web チームの開発メンバー

リリース前

8

リリース後

※完全に分業制で、Webフロントエンド、サーバー、iOS、Android、デザインがチームとして分割されている。

5

12 of 85

初期の開発規模

80〜100 P

想定ページ数

13 of 85

チーム状況

0

競輪経験者

14 of 85

求められるもの

ドメイン知識

LIVE 動画

決済

「競輪」

ナニソレ美味しいの?

たしかにレース観れると

嬉しいよナァー...

クレカ、銀行、Apple Pay

使えるんだ...!?

15 of 85

工数見積もり

見積もり12 ヶ月以上

16 of 85

割くことのできるスケジュール

見積もり12 ヶ月以上

4 ヶ月

17 of 85

/(^o^)\

18 of 85

スケジュール

見積もり12 ヶ月以上

4 ヶ月

この差を埋める戦略が必要

8 ヶ月

19 of 85

開発速度と品質の両立

考える必要があった

20 of 85

ちなみに

21 of 85

最終的な開発期間

開発内外で様々な要因が重なり結果として7ヶ月(2019年4月にリリース)

見積もり12 ヶ月以上

7 ヶ月

22 of 85

最終的な開発規模

TypeScript + CSS

SPA として動作するページ種別

92 P

ページ種別

15 万行

コード量

23 of 85

品質を担保するための戦略

Strategy

24 of 85

Performance

25 of 85

Server Side Rendering (SSR)

  • レース情報の閲覧
  • 競輪場情報の閲覧
  • 動画視聴

メディア特性

  • ユーザー認証
  • 車券の購入
  • 決済管理

アプリケーション特性

26 of 85

Server Side Rendering (SSR)

WinTicket では表示改善を図りメディア特性を最大限活かすための SSR

  • レース情報の閲覧
  • 競輪場情報の閲覧
  • 動画視聴

メディア特性

  • ユーザー認証
  • 車券の購入
  • 決済管理

アプリケーション特性

27 of 85

React

HTTP Framework には Fastify、View Framework には React を採用

Fastify

28 of 85

Fastly を使ったキャッシュの活用

CDN として HTML やアセットの配信を行う。

レスポンス速度向上

オリジンへのリクエスト

29 of 85

ユーザー固有の情報を扱う場合は

プレースホルダーを表示しつつ遅延読み込み

30 of 85

if (req.http.Cookie:token ~ "^.+$") {

declare local var.DecodedPayload STRING;

declare local var.TokenExpiration STRING;

set var.DecodedPayload = digest.base64_decode(regsub(req.http.Cookie:token, "^[^\.]+\.([^\.]+)\.[^\.]+$", "\1"));

set var.TokenExpiration = regsub(var.DecodedPayload, {"^.*?"exp"\s*:\s*(\d+).*?$"}, "\1");

if (time.is_after(std.integer2time(std.atoi(var.TokenExpiration)), now)) {

set req.http.X-Valid-Expiration-Token = "true";

} else {

set req.http.X-Valid-Expiration-Token = "false";

}

} else {

set req.http.X-Valid-Expiration-Token = "false";

}

31 of 85

if (req.http.Cookie:token ~ "^.+$") {

declare local var.DecodedPayload STRING;

declare local var.TokenExpiration STRING;

set var.DecodedPayload = digest.base64_decode(regsub(req.http.Cookie:token, "^[^\.]+\.([^\.]+)\.[^\.]+$", "\1"));

set var.TokenExpiration = regsub(var.DecodedPayload, {"^.*?"exp"\s*:\s*(\d+).*?$"}, "\1");

if (time.is_after(std.integer2time(std.atoi(var.TokenExpiration)), now)) {

set req.http.X-Valid-Expiration-Token = "true";

} else {

set req.http.X-Valid-Expiration-Token = "false";

}

} else {

set req.http.X-Valid-Expiration-Token = "false";

}

32 of 85

  • ユーザーのログイン、非ログインで UI の出し分け
  • Cookie に入っている JWT の有効期限からログイン状態を推測
  • 推測結果を独自 Header に詰める
  • Cookie は破棄してオリジンには渡さない

33 of 85

その他の Fastly 活用

  • UserAgent を元にバリエーションを iOS、Android、デスクトップ、その他の 4 分類まで絞る
  • IE11 等の非モダンブラウザ向け Compatible Build の配信分岐
  • クエリパラメータ制御
    • GTM 用のクエリパラメーターは削除 (utm_*)
    • キーをソートすることで Cache Hit 率を向上
  • etc...

34 of 85

Performance Budget

  • WinTicket の品質定義に Performance が含まれる
  • 予算を決め、維持 / 改善の指標とする
  • リリース前は競合他社との比較を可視化

35 of 85

SpeedCurve を使った Synthetic Monitoring

36 of 85

設定した Budget 超過したら

Slack へ通知

数値がたまに跳ね上がるとめちゃ焦る

37 of 85

Resource Budget

  • size-limit をカスタマイズして使用
  • 基準値を超過した場合は CI で落とす

"resource-budget": [

{

"path": "dist/public/bootstrap.*.js",

"limit": "100KB"

},

{

"path": "dist/public/vendors.*.js",

"limit": "120KB"

},

{

"path": "dist/public/pages-*.js",

"ignore": [

"dist/public/pages-keirin-cups-races-*"

],

"limit": "80KB"

},

{

"path": "dist/public/pages-keirin-cups-races-*.js",

"limit": "125KB"

}

]

38 of 85

Code Splitting & Dynamic Import の活用

  • 92 ページある Route 全てチャンクを分割する
    • 最初の 1 ページは `<link rel="preload" />`
  • サイズの大きいコードは必要になるまで読み込みを遅延
    • firebase-sdk, @sentry/browser etc...

39 of 85

Development

40 of 85

型とテストで変更に強いコードベース

  • 全てのコードが TypeScript
  • 各層毎にテスト方針の使い分け
    • UI Component
      • Visual Regression Testing
      • Unit Testing は最小限
    • Logic
      • Unit Testing がメイン

TypeScript

Visual Regression Testing

Unit Testing

41 of 85

Visual Regression Testing

  • Storybook + zisui + reg-suit で Visual Regression Testing を実施
  • 各 PR 毎のコンポーネント単位での差分比較
  • 一度につき約 900 の Story を検証

42 of 85

1コンポーネントにつき

最大 36 の表示パターンを検証

43 of 85

Protocol Buffers を元にした型定義の使用

  • API が定義した Protocol Buffers を元に pbjs, pbts を使って型定義を作成
  • それらをアプリケーション側で使用する
  • 実際の通信は一般的な HTTP で行う
  • 直接定義を import すると protobufjs がバンドルされてしまうが...
  • TypeScript Compiler API で型定義を使いやすい形に変換し使用

44 of 85

全体を通して

あたりまえを積み重ねることを意識

45 of 85

これからの開発 / 改善に耐えうる

安定した開発基盤を整えた

46 of 85

リリースは最優先だが

リリースがゴールではない

47 of 85

Accessbility

48 of 85

他チームとの連携が大事

自分たちの負債は返しやすいが

他チームに返してもらうのは大変

49 of 85

屋外 + 年齢層高め

クッキリ + 大きく

50 of 85

デザイナーとの密なコミュニケーション

  • コントラスト比
    • 基本は 4.5 : 1で、一部難しいところでも 3:1 を下限に
  • フォーカスリング用のスタイルを統一
  • フォントサイズベースを16pxで

プロジェクトの初期から守りたい点を伝える

3 : 1

5.8 : 1

51 of 85

代替テキストの確保

  • 動的に取得するバナー画像などに必ず代替テキスト用のテキストフィールドを加えてもらう
  • 管理画面の代替テキスト入力欄を必須にする

{

"id": "register_campaign",

"link": "https://support.winticket.jp/~~~",

"image": "https://hayabusa.io/winticket/",

"description": "登録完了で全員必ずもらえる!1,000円分ポイントプレゼント"

}

52 of 85

実装方針の策定

  • 当初は実装の戦略が無く個人任せでスタート
  • 意識が高まりすぎて、時間をかけすぎたり、漏れぬけがあったり
  • どこまでやるかを全員で共有するための方針が必要だった

53 of 85

  • コンテンツ、UIのマシンリーダビリティ(※)
  • 最低限のキーボード操作

Must

  • 適切な要素選択
    • hx, list, table ...
  • フォーカス順のコントロール

Usual

※動画を除く

54 of 85

近日公開

予定

55 of 85

捨てたもの

〜 Performance / Development 編 〜

Compromise

56 of 85

全ページキャッシュ

  • 会員系のページもキャッシュ、ユーザー固有の情報は遅延読み込みしたかった...
  • ページ数も多く、表示の調整にかかるコストが馬鹿にならない
  • 開発工数の兼ね合いでリリースに向けた開発では取り組まないことに

# `/my` 以下、以外の場合は Cookie を削除

# オリジンが直接ユーザー情報を扱う

if (!req.url ~ "^/my") {

unset req.http.Cookie;

}

57 of 85

慣れたアーキテクチャ以外への挑戦

  • 脱 React してみたかった
  • Edge によせたアーキテクチャ
    • ESI の活用
  • 開発体験を損なわずに実現する方法の模索
    • Traditional な MPA では楽しくないかも...という話も出た
  • これから一切チャレンジしない、という訳ではない。部分的な機能追加や改修では挑戦していきたい

58 of 85

負債をある程度許容する

  • 小〜中規模の負債に対しては勇気ある妥協
  • 後々、改修に掛かるコストがあまりに大きくなる負債を抱えるのは🙅‍♀️
  • 後戻りできないものは、ちゃんと前もって手を打つ

59 of 85

ただし、負債は局所化させる

  • 適切な負債のコントロールができる状態を維持する
  • 戦術的
    • レイヤードアーキテクチャで構造化
  • 戦略的
    • 複雑なドメイン部分の実装は局所的に、ある程度負債化することを妥協する。ただしテストをしっかりと書くなどしてあとからの変更に耐えられるように!

60 of 85

結果

〜 Performance / Development 編 〜

Results

61 of 85

Performance

2019/5/12 20:00 ~ 20:30 頃に計測したトップページの Google Chrome の Audits Simulated Fast 3G, 4x CPU Slowdown スコア

WinTicket

競合O

競合K

競合k

62 of 85

属人性についての結果と反省

  • 属人性の高いところが多くなってしまった
  • それ自体はある程度仕方ないけど...
  • ドキュメントを作成したり、実装についてチームメンバーがある程度理解する機会作りができると少しは潰しがききそう
  • しかし、全く手が回らなかった😇

63 of 85

  • スペシャリストが全力で標準化に取り組む
  • ドキュメントを整備する
  • 機械的なテストの質を向上させる

属人性を最小化するために

64 of 85

これから

〜 Performance / Development 編 〜

Vision

65 of 85

LightWallet の導入

  • Lighthouse の�Performance Budget 対応
  • PR 単位で Performance を�より細かく検証できるように

[

{

"resourceSizes": [

{

"resourceType": "script",

"budget": 300

},

{

"resourceType": "image",

"budget": 100

},

{

"resourceType": "third-party",

"budget": 200

},

{

"resourceType": "total",

"budget": 1000

}

],

"resourceCounts": [

{

"resourceType": "third-party",

"budget": 10

},

{

"resourceType": "total",

"budget": 50

}

]

}

]

66 of 85

Runtime Performance の改善

  • ページによってはインタラクションに伴う Performance の問題を抱える
  • 初期描画はネットワーク速度の改善で大部分が古くなっていく
  • 向こう数年は有効だと思うけど...
  • Runtime での Performance もより良くしていくために取り組んでいきたい

67 of 85

prefetch / preload の機構を追加

  • hover, focus, inview など任意のタイミングをフックして
    • Dynamic Import する .js ファイルを先読み
    • ファイルだけではなく API コールまで行う
  • quicklink みたいなイメージで考えている
  • ユーザー行動をログから仮説立てして効果的なものに対して取り組みたい

68 of 85

tsc から Babel への移行

  • 現在アプリケーションの Transpile 相当の処理は tsc を使用
    • リリース当時、namespace を使わざるを得ない箇所があった (Protocol Buffers 由来の enum)
  • エコシステムの活用、最適化系のプラグイン活用
    • React
    • styled-components

69 of 85

捨てたもの

〜 Accessibility 編 〜

Compromise

70 of 85

  • コンテンツ、UIのマシンリーダビリティ(※)
  • 最低限のキーボード操作

Must

  • 適切な要素選択
    • hx, list, table ...
  • フォーカス順のコントロール

Usual

※動画を除く

71 of 85

  • コンテンツ、UIのマシンリーダビリティ(※)
  • 最低限のキーボード操作

Must

  • 適切な要素選択
    • hx, list, table ...
  • フォーカス順のコントロール

Usual

※動画を除く

72 of 85

要素選択の徹底

レビューにかかるコスト、意思決定にかかるスピードから要素選択の徹底を諦めた

  • ページ内見出しレベルの順序
  • <strong>, <em> などのテキストレベルセマンティクスの徹底
  • <ul> or <ol> or <dl> / <section> or <article> などの使い分け

やらないわけではなくて徹底を諦めた

73 of 85

完璧なフォーカスマネジメント

  • 矢印キーによるフォーカス移動
    • キーボードのユーザビリティ的に適用箇所を見定めるのがコストだった
    • Tab で全部移動する
  • ページ遷移やページ内リンクによるフォーカス位置のコントロール
  • 特定の条件下で発生する多重モーダル化におけるフォーカス順
    • セッションエラーとキャンペーンポップアップなど、ユーザーの操作によってモーダルが多重に立ち上がるケースのフォーカスマネジメント

74 of 85

結果

〜 Accessibility 編 〜

Results

75 of 85

70%

コントラスト比 AA 4.5 :1 を達成できているページ

キーカラーを抑えるとカバレッジが高くなる

76 of 85

キーボードで

全部操作できる...ハズ

多重モーダルにも負けなかった

77 of 85

Auditsもハイスコア

93

WinTicket

72

D

60

C

49

K

45

K

37

O

2019/5/12 15:20 ~ 15:30 頃に計測したトップページの Google Chrome の Audits Accessibility / Desktop のスコア

78 of 85

これから

〜 Accessibility 編 〜

Vision

79 of 85

WCAG Test is going on...

80 of 85

品質テストの自動化

Storybook を用いたテストの自動化

  • addon-a11y
  • Accessibility Object Model を用いた name / role / value のテスト
  • フォーカスインジケーターのテスト

81 of 85

Development policy - Accessibility の展開

  • ドキュメントとしての充実
    • 実装例、考え方、根拠などの記載
  • アクセシビリティトレーニングの実施
  • 他プロジェクト / 他職種への展開

82 of 85

まとめ

Conclusion

83 of 85

  • やらないを決めることの覚悟と重要性
  • どこからどのくらい借り入れがあるかは把握する
  • 改善 / 維持ができる基盤は大事
  • あたりまえを積み重ねる

84 of 85

Abraham Lincoln�(1809 - 1865)

If I had six hours to chop down a tree, I'd spend the first four hours sharpening the axe.

“木を切り倒すのに6時間もらえるなら、私は最初の4時間を斧を研ぐことに費やしたい。”

85 of 85

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

👋 thanks!