この記事はDMM.com #2 Advent Calendar 2017の14日目です。

はじめまして、普段はDMMサービスのシステム開発、保守を行なっている@norihです。
SPAの記事を書いておりますが業務的にはバックエンド側に触れることが多いエンジニアです。

今回は書くことは僕が2017年の初旬にリリースしたSPA(シングルページアプリケーション)の失敗談と、そこから学んだことについてです。
基本的なことかもしれませんがよろしくお願いします :grinning:

ほかのカレンダーURLはコチラです。
DMM.com #1 Advent Calendar 2017
DMM.com #2 Advent Calendar 2017

リリースしたSPAの概要

最初に今回リリースしたSPAについて少しだけ紹介させていただきます。
詳しいことは省略させていただきますが概要としては次のようなものです。

  • React + ReduxのSPA
  • ビジネス関係者が利用するアプリケーション
  • 365日24時間利用
  • 各種イベントやタイマーによるREST APIへの通信
  • 開発期間は2人体制で3ヶ月ほど(規模は大きめ)

また僕の所属する部署にはフロントエンドエンジニアがいないため、SPAの設計からリリースまでを自分ともう一人の開発メンバーで行っています。
フロントエンドが好きなため2017年にリリースした案件としては一番記憶に残っているものですが、はじめての大規模SPA開発とリリースだったので失敗も多かったです。

それでは失敗談をさせていただきます!
なお実際に開発したSPAのプログラムコードはES2015以降の文法で実装していますが、本記事内のサンプルコードはES5の文法で書いております。

失敗談1 : クライアントサイドのエラー調査

最初はエラー調査についての失敗です。

まず前提としてSPAはクライアントサイドで処理が行われるためバグを発見するのが難しいです。
今回のSPAもテストをしっかり行なっていましたが、リリース後には原因特定が難しいバグがいくつか出てきました :scream:

当初はそのバグを人海戦術で数日かけて再現させ、その人のブラウザ画面を直接見ることで原因特定していました。
これでもなんとか問題解決しましたが、1つのバグを特定するために多くのコスト払う非常に効率の悪い方法でした。

学んだこと : エラー収集の仕組みは必ず用意しておく

この失敗から学んだことはリリース後もバグがある前提で、事前にクライアントサイドのエラー情報を収集・可視化する仕組みを用意しておくべきということです。
この仕組みは自前で実装することもできますし、サービスを利用することもできます。

自前での実装

自分たちでクライアントサイドのエラー情報を収集するには次のことをすれば良いです。

  • errorイベントの監視
  • エラー情報のREST API送信
  • REST APIの実装
  • エラー情報の保存と可視化

フロントエンド側ではerrorイベントを監視してコールバック関数に渡される情報をREST APIにするということを実装します。
ブラウザでは下記キャプチャのようにコンソールエラーが表示されるときにはerrorイベントが発火します。

コンソールのエラー情報

JS側処理の簡単なサンプルは下記のようなもので難しくありません。
sendErrorLog()ではXHRでREST APIにエラーメッセージなどの情報を送信しているイメージです。

window.addEventListener('error', function(e) {
    var message      = e.message,
        fileName     = e.filename,
        lineNumber   = e.lineno,
        columnNumber = e.colnoa;

    // 自作したAPI送信用の関数やメソッドを実行
    sendErrorLog(message, fileName, lineNumber, columnNumber);
});

バックエンド側のREST APIではPOSTされたデータをDBやストレージに格納するようにします。
あとは管理サイトやメール送信で、エラー情報を可視化できるようにすれば一通りの仕組みができます。

今回のSPAではリリース後のエラー収集めこれらの仕組みを実装して稼働させています。
またREST APIヘの送信処理ができたら、try-catchで拾った例外にも適用させていくと良いです。

パフォーマンス監視サービスを利用する

最近だとJSエラーやパフォーマンスを解析してくれるサービス・ツールも充実しています。
有料プランとなってしまいますがエラー収集だけではなくパフォーマンスチューニングに必要な情報も収集してくれるため、導入を検討する価値は十分あります。

自前でバックエンド機能を用意するのが難しい場合にもパフォーマンス監視サービスを利用してみるのも良さそうです。導入も最短でサービスが提供するjsライブラリを読み込むだけでよかったりするので、途中から組み込む敷居が低いのもポイントです。

主なサービス

エラー情報収集の用途だと基本的に有料プランを選ぶことになりそうですが、例えば開発からリリース後の2ヶ月分だけ有料プランに加入してバグ修正とパフォーマンスチューニングを行い、それ以降は無料プランにするなど柔軟にプランニングできます。

失敗談2 : Node.jsモジュールのアップデートをしていなかった

続いての失敗は依存するNode.jsモジュールを開発期間中にアップデートしなかったことです。

今回のSPAではReduxのミドルウェアの「redux-saga」を導入し、REST APIへの通信処理を実装していました。
開発中は問題なく順調だったのですが、リリース後しばらくしてからredux-sagaのバグで途中からAPIへの通信が実行できなくなるという事象に遭遇しました。

調査の紆余曲折は省略しますが、この原因は自分たちの利用しているredux-sagaのバージョンがかなり古いことでした :cry:
開発当初は「v0.11.0」だったredux-sagaが2017年のSPAリリース時点には「v0.14.3」にバージョンアップされていたのです!

しかもリリース内容を見てみると次のリビジョンにあたる「v0.11.1」の説明にこんなことが!

Fix issues with Error handling

\(^o^)/

自分たちはまさにバグ修正前のバージョンを利用していたようで、結局redux-sagaをバージョンアップすることでバグがほぼ治るという苦い経験をしました。

学んだこと : 開発期間中のモジュールアップデート対応

2つ目の失敗の対処法としては開発中にモジュールのアップデートを行うようにすることです。
当たり前のことかもしれませんが、今回はバージョンアップによって下位互換性が無くなることを恐れて開発中にバージョンアップをしていませんでした。

しかしメジャーなモジュールでさえバグはあります。特にまだ枯れていないものは注意。
アップデートすることによりプログラム修正や動作検証のコストはかかってしまいますが、本番リリース後にバグに悩まされるよりも絶対にマシです。

またトレンドとなるフロントエンド技術は廃れも早いため、開発している最中に利用しているモジュールの既にブームが過ぎて悲しい状況に追い込まれることもあります。
ダメになった場合の代替手段を用意しておいたり、マイグレーションできるようにしておくことも大切です。

失敗談3 : タイマー処理をなんとなく書いていた

最後の失敗は意外なタイマー処理について。これは問題にはなっていませんが備忘録として書きます。

今回実装したSPAには一定間隔でREST APIを実行するタイマー処理(ポーリング)がありました。
最初は何も気にすることなくsetIntervalでタイマー処理を実装していましたが、リリース後にサーバのアクセスログを見ていると実際に設定していた間隔とずれが生じていました :dizzy_face:

SPAの仕様的には問題になからなかったのですが、モヤモヤした気持ちを抱えたままだったので今回の記事を書くにあたってsetIntervalの間隔ずれを確認してみました。

確認用に利用したプログラムは次のように30秒毎に現在時刻を取得して出力するもの。
コールバック関数の実行時間による遅延はありますが、これは許容することにします。

testInterval.js
var interval = 30 * 1000;

// setInterval
window.setInterval(function() {
     console.log(new Date);
 }, interval);

検証結果は次の通り。
Firefox 56.0.2で実行しています。

回数 時刻
01 11:19:22.537
02 11:19:52.549
03 11:20:22.562
04 11:20:52.570
05 11:21:22.580
06 11:21:52.588
07 11:22:23.502
08 11:22:53.513
09 11:23:23.523
10 11:23:53.534

7回目の実行で1秒ほどずれることを確認。
非常に単純なプログラムですが、思ったより早い時点でずれが生じていた結果でした。

調べてみるとブラウザはsetInterval/setTimeoutに設定した一定間隔の精度は保証しないようで、稼働時間に伴いずれも大きくなっていくようです。
またタイマー処理が動いているタブが非アクティブの場合は、ずれがさらに大きくなっていくようです。

stack overflowのスレッドではこのように記載されていました :slight_frown:

setIntervalでは誤差の扱いは定められていません。ブラウザの状態によっては、自動的に頻度を低下させられる可能性もあります。このため、setTimeoutやsetIntervalではまったく要求を達成できません。

引用 : javascript - 0から始まり5までいったらまた0からスタートする - スタック・オーバーフロー

学んだこと : タイマー処理は慎重に考える

これが問題となるのは精度の高いタイマー処理を求められるアプリケーションとなりますが、実装前から作成するSPAの要件に該当するかは気をつけたいところです。
またブラウザによっても間隔の精度が異なるため、クライアントサイドで精度の高いタイマー処理を実装すること自体を立ち止まって考えるべきなのかもしれません :thinking:

応急処置の対処法

ひとまずsetIntervalよりも精度の高いタイマーを作りたい、という場合には応急処置的な対処法はあります。
完全にずれをなくすことはできないのですが、下記のサンプルコードのようにsetTimeoutの再帰処理で毎回ずれ調整すれば改善は見込めます。

var interval = 30 * 1000, 
    initMiliSecond = new Date().getMilliseconds(),
    begin, end, diff, adjustInterval, timer;

timer = function() {
    // コールバック関数の処理開始時刻を取得
    begin = new Date();

    // 処理内容 API実行など
    console.log(new Date);

    // 処理が終了した後でendを取得して差分を算出
    end = new Date();
    diff = end - begin;

    adjustInterval = interval - begin.getMilliseconds() + initMiliSecond - diff;

    // 再帰実行
    setTimeout(timer, adjustInterval);
}

// 関数の呼び出し
setTimeout(timer, interval);

当初のsetIntervalと比較すると開始時刻から1秒ずれることは回避できるかと思います。
なおサンプルコードのため実際にサービス運用に耐えうるコードとしてはもう少し作り込んだ方がよさそうです。

おわりに

2017年初旬頃の出来事でしたがはじめてSPAをリリースした振り返り記事でした。
書いてみると割と当たり前のことができていなかったんだなと反省です。
これからSPAをリリースする方にとって少しでも役に立つ情報を共有できたなら幸いです。

明日は@h_shinonomeさんです。よろしくお願いします!

参考文献

エラー情報収集

setIntervalのずれ