現在開発担当している AbemaTV で、昨年末にパナソニックさんのスマートテレビ VIERA 向けにアプリケーションをリリースしました。AbemaTV リモコンボタンで AbemaTV にアクセスできます。今回このアプリケーションの UI を開発するにあたって、新しいデザインワークフローを導入しました。
本記事では、 Story-Assured Design というデザインワークフローを、現在プロダクト開発で直面しているデザインの課題とともに紹介します。
Story-Assured Design はその名前が示しているように、ユーザーストーリーを保証することを目的とした UI デザインのワークフローです。UI デザインを3つのステップに分けて行います。
一番重要なユーザーストーリーのデザインから始めて、それを保証するようにフローを進めていきます。大袈裟な名前が付いていますが、奇抜なことをやっているわけではなく、デザインする上でやった方が良いことを明示的なデザイン工程として分割しているだけです。しかし、分割することによって規模が大きくなってきたサービスが抱えがちなデザインに関する問題にアプローチすることができます。逆に言えば、規模が小さく収まるサービスやプロダクトを作り上げるだけであれば、目的に対して複雑な手順を取り過ぎるかもしれません。
今回このワークフローで1つのプロダクトの初期リリースを終えてみて、兼ねてから課題に思っていたことが解決できたので、それを共有したいと思います。
デザインに関する話を書いていますが、このプロジェクトでの私の役回りはテックリードで、プロダクトのオーナーシップを持ったエンジニアとして開発の進行、エンジニアリング、デザイン、ビジネスとの調整を行ううちに得た副産物になります。
Story-Assured Design の実践方法の説明に入る前に、まずこのデザインワークフローで解決しようとした課題と導入の経緯について触れたいと思います。
従来のデザインのやり方でもアプリケーションを作ることはできますが、将来的にデザインを改善し続けることを考えると下記4点のことが課題でした。
逆にこれら4つの課題が解決できれば、組織が大きくなっても、プロダクトの年齢が上がっても、自分たちの力でデザインを1歩ずつ確実に前に進化させることができるのではないかと思います。そして、この課題解決をデザイナーやチームのメンバーの力量に左右されることなく、仕組みによって大部分を解決できればと思い、ワークフロー化しました。
プロダクトが飯の種になっている以上、どんなデザインが良くて、どんなデザインが悪いのか、アプリケーションの開発に携わる人なら誰でも多かれ少なかれ関心があるテーマだと思います。目的や文脈が異なればデザインの善し悪しもそれに応じて変わりますが、それでもデザイン1つで売り上げが変わったり、ユーザーの使用頻度に影響が出てしまったり、プロダクトの寿命が左右されてしまうことを思うと、自分たちのプロダクトのデザインが間違いなく良い方向に進んでいることを確信したいと思っています。
しかし、デザインを評価することは難しいです。誰もが目で見て触る部分なので、簡単に意見できたりしますが、1ユーザーとしての意見を言うことはできても、プロダクトもしくはサービスにとっての正解は誰も知らないことがほとんどです。それに比べるとエンジニアリングは評価しやすいと感じます。ソフトウェアエンジニアリングであれば、実際の処理時間やパフォーマンスを計測して評価したり、コードの設計が拡張しやすくなっているかを評価したり、など多少の好みや比重の置き方に違いはあっても、何となくみんなが共通で考えている正解が存在します。
デザインにもみんなが共通で考えることができる正解が存在すればいいなあ、とかねてから思っていました。そうすれば良いデザインをちゃんと評価できます。
デザインコンサルタントの長谷川恭久氏のブログ記事「デザインシステムの最初の一歩として原則を作る理由」で「自分たちにとっての良いって何だっけ?」を考えることについて触れられています。この記事の中で
お互いが考える『良い』をぶつけ合う
という一文がありますが、デザインをみんなで考えたり、振り返ったり、評価しようとすると、まさに「お互いが考える『良い』をぶつけ合う」状態になりがちだなと思います。
Story-Assured Design では3つのステップに分けて UI デザインを進めますが、1つ1つのステップにおいてチームで共有する『良い』が定義されます。これにより、少なくとも「お互いが考える『良い』をぶつけ合う」状態は避けることができました。
何人かのデザイナーと一緒に仕事していると、当然のことですが、デザイナーにもいろいろなタイプの人がいることが分かります。あるデザイナーはグラフィックが得意だったり、情報整理が得意だったり、サービスのブランディングが得意だったり、アイデアの引き出しが多かったり、分析が得意だったり、いろいろな人がいます。でも、デザインの仕事にはこういったこと全てが求められます。
どのプロジェクトでもアサインされるデザイナーの数は多いわけではないので、場合によっては担当デザイナーが不得意な領域でもデザイン作業としてお願いしなければならない場合があります。今までさまざまなチームでアプリケーションを開発してきましたが、どのチームでもデザイン作業はデザイナーがやるものでした。でも、明らかに情報整理が苦手なデザイナーは、ブレインストーミングで好き勝手に出たアイデアを整理する作業に苦しみますし、やっぱり何度もやり直しを繰り返すことになります。こういうとても非効率に感じる場面もたくさんありました。
そういうとき、苦手な領域のデザイン作業をデザイナー以外でも担当できたり、時にはチーム全員の知恵を借りてデザインに関する課題を解決できたらいいのに、と思います。そこで、Story-Assured Design では、デザイン作業のステップを分割することで分担しやすくします。
私が経験してきたプロジェクトでは、ユーザビリティの本格的な検証は開発終盤に行われることが多かったです。デザインがある程度プロダクトに実装されてからでないとテストしづらかったからです。早期にユーザビリティを検証する方法としては、ペーパープロトタイピングなど方法はあるのですが、やはり実機で早期に検証できる方が確実です。最近だと、ペーパープロトタイピング自体がしづらい特性のプロダクトもあります。
また、開発経験がない分野だと、ある程度のベストプラクティスさえもイメージできないので、実機で確認するまで気付かないデザインの落とし穴もあります。
現在私は未着手のデバイスを開拓することをミッションに持った開発をやっています。未着手なので、当然その領域の知見はほとんどない案件ばかりです。未着手といっても、世の中的に未着手というわけではなく、あくまでも自分たちにとって未着手なので、もちろん調べれば知見に巡り合えることもあります。ただ、経験値がない状態では知見の適用するにも見様見真似感が否めません。基本的に経験がない分野でのプロダクトデザインは難易度が高いと感じます。
本記事のプロジェクト以前に、今年始めに AbemaTV VR という AbemaTV の Google Daydream 版アプリの開発を担当しました。そのプロジェクトでは私はテクニカルディレクターという立ち位置でしたが、開発を進める中で、同じアプリケーションでもスマホや PC アプリと同じ感覚でデザインしていては全く使いやすくなっていかないことを痛感しました。このあたりの奮闘は当時の担当デザイナーの解説記事にもあります。
AbemaTV VR の開発では結果的にリリース日を少し延期して UI デザインの品質を上げるという決断に至りました。知見がない新しい分野の開発にも関わらず、私たちは技術的な課題ばかりに目がいき、完全に UI や体験に関するデザインをなめていたために発生した失敗でした。
過去の一見似ている経験を過信したことを反省しました。普通に考えれば、自分たちはスマホと PC 向けのアプリケーションのデザイン経験しかないのだから、それ以外のデバイス向けにプロダクトを開発するときは初心にかえってデザインするのが正しい姿です。開発の早いタイミングからプロダクトを触り始められるデザインワークフローにしてデザインの問題点を早々に発見することが大事だと痛感しました。
この VR プロジェクトでは、新規分野ではデザイナーもデザイン未経験だということを忘れてはいけない、ということも痛感しています。開発初期はちゃんとしたデザイナーにデザインを任せれば、デザインはなんとかなるだろうと思ってしまっていました。
VR のプロジェクトでは、このデザイナーはさまざまな試行錯誤を繰り返し、最後まで諦めずに UI の品質を追い求めました。最終的に良い品質のものができることに何の疑問もありませんでしたが、そんな彼でも身近な人物にユーザーテストをしてその反応を見るまでは VR 特有の穴に気づくことは難しいものでした。
VR 上の UI 要素は画面内に入っていてもユーザーが気付かないことがあります。VR の場合は焦点が合っている対象物の臨場感が大きいため、それ以外の要素の存在感が雑音になりやすいのです。
そのため、たとえばサイドメニューのような普段は使わないため閉じておきたい要素をユーザーに開かせる場合、画面の端っこの方に開くボタンを配置しても想定以上にユーザーが気付かないことに後になって気付きます。スマホアプリの場合だと画面右上端にハンバーガーメニューがあれば、気付いてもらえると思います。その感覚からすると、何となく VR 画面でもデザインカンプ段階だと良いデザインに思えてしまいます。(しかし、実際に実機で見ると全然気付かないのです。)
それらの経験を踏まえて、新規分野の場合は特にデザインに対してしっかりと早めに説得力があるフィードバックを得られることが大事だと実感しました。プロダクトのリリース前であれば、一番説得力があるフィードバックは、やはりデザインに対する自分たちの直接的な感触です。
そこでユーザビリティに関わる要素の中でも検証しやすい部分だけ分離してデザインし、先に実装することで、プロダクト開発の流れの中であまり寄り道することなくプロトタイプを作ることができます。そして、プロトタイプとして自分たちで触り検証しながらそのまま改善していくとプロダクトに昇華できるので、開発におけるプロトタイプ開発工数が最小にすることができます。
Story-Assured Design ではユーザーストーリーを構成する UI 要素を先にデザインすることで、ストーリーを開発フェーズの早期から検証しながら、そのままシームレスにプロダクトへと昇華していきます。
解決したい課題の最後になりますが、「デザインの負債による改善速度低下を防ぎたい」、個人的にデザインフローを変えた一番の理由はこれでした。
現在私が担当している AbemaTV というサービスは開局3年が経とうとしています。サイバーエージェントに入社してから、どちらかというと新規プロジェクトの立ち上げを多く担当してきたので、リリース後に3年も同じサービスの運用開発していること自体が初めての体験です。以前は正直3年も同じサービスを担当していたら飽きそうだなと思ってましたが、意外なことに飽きていません。新規開発では経験しなかった2種類の課題に挑戦できるからだと思います。
1つ目はサービス自体が成長し続けていることで生まれた課題に挑戦できる点です。私個人の話でいうと、サービスリリース当初は AbemaTV Web 版 を立ち上げていましたが、組織的な動画技術力進化が課題になり、半年渡米して動画技術研究し、帰国後は未着手デバイス開拓と動画再生技術のエンジニアリングに軸足を置いて技術的な挑戦を続けられています。ほかにも動画技術エバンジェリスト※1 ※2 のような立場で変則的なアプローチでの技術的な挑戦ができるのも、サービスが成長し続けているおかげだと思います。
2つ目は長く運用開発しなければ直面できなかったと思える課題に挑戦できているからだと思っています。そんな課題の1つに「 デザインの負債 」という概念があります。私自身は、新規プロジェクトの立ち上げをメインにやっているときは、ほとんど意識しなかった概念です。というのも、デザインの負債は変更し続けることを前提としたプロダクトのデザインでしか生まれない概念だからです。
開発に携っている人なら「負債」という言葉を普段から耳にすると思います。私もエンジニアなので負債といえば「技術的負債」という言葉が真っ先に頭に浮かびます。技術的負債といえば、いきあたりばったりのソフトウェアアーキテクチャによって既存コードに対する機能追加や変更のコストが高くなることを指しますが、「デザインの負債」は簡単に言えば、そのデザイン版です。いきあたりばったりのデザイン改修の繰り返しによって将来的なデザイン変更がしづらくなっていくことを指します。
私がデザインの負債を明確に意識し始めたのは Google のプロダクトデザイナーである Austin Knight 氏の『 Design Debt 』という記事を読んでからでした。この記事で、デザインの負債とは、新しいユーザー体験のためのデザインが既存デザインに統合できなくなってしまう状態のことだと言っています。
負債を持つことは誰しも好まざるところですが、良いデザインを生むためには直感ドリブンとデータドリブン両方のデザイン手法が必要です。直感ドリブンだけでは目の前のコンテキストや人の瞬間の感覚に左右されすぎますし、データ・ドリブンだけでは現状に対する改善はできても、まだ現実に存在していないものはデザインできません。両方をうまく組み合わせてデザインする必要がありますが、一見相反する性質の2つを組み合わせるので、どうしてもデザイン的には矛盾がある瞬間が生じてしています。
このような矛盾は明らかに「デザインの負債」と呼べるものですが、負債は返済するものなので、放っておいて大きくなる前に返済するようにする必要があります。Austin 氏は小さなループで負債を生んで、大きなループで負債を返済するといいよ、とも言っています。
それで当然ながら、AbemaTV もデザインの改修を繰り返しているので、いろんなデザインの負債があります。小さなループで負債を生んで、大きなループで返済するといっても、返済するためには負債に明確に気づく必要があります。
サービスの成長速度を加速するためには、複数のプロジェクトが平行して走る必要があります。プロジェクトはそれぞれ自分たちが達成すべきゴールを持っているので、デザインの最適解もそれに応じて変わります。デザインは目に見えて触れることができるという特性上、ゴールが異なるプロジェクトからは矛盾するフィードバックをもらうこともあります。あるプロジェクトからは良いと言われたデザインが別のプロジェクトからは悪いと評価されたりです。全て折り合いをつけようとすればデザイン上に何かしら無理が生じ、それが後でデザインの負債としてのしかかります。また、デザイナー自身が特定のプロジェクトにコミットする体制を取っている場合は、プロジェクトの文脈に最適化されたデザインになってしまうリスクもあります。
どのプロジェクトも一番目立つところに自分のプロジェクトの数字を上げるための導線を配置したいと思うでしょうし、全部のプロジェクトの導線を目立つところに置けばレイアウトの優先順位は崩壊し、ユーザー体験を激しく損ねるでしょう。たとえプロジェクトに優先度が付いていたとしても、既存のデザインはそのために作られたわけではないので、その優先度を表現できるように変更されるためには大きな工数がかかります。さらに悪いことには、複数のプロジェクトが平行して走っているときは大抵の場合、全てのプロジェクトの優先度が「S :最高」だったりします。
それでもプロダクトの数が1つであれば良いですが、インターネット上でサービスを展開するのであれば、iOS、Android、Web などメジャーどころの複数の媒体にプロダクトを展開していることが一般的です。スモールスタートで Android だけ先にリリースしていたり、ある技術のエンジニアが他方に比べて不足している場合など、さまざまな理由でプロダクトごとに成熟度が違う場合は先行して特定のプロダクトにだけプロジェクトが走ったりすることもあります。すると、プロジェクトを実現するためのデザインがそのプロダクトに最適化されてしまい、追いかけてリリースするプロダクトにデザインが適用できなかったり、適用しても不自然さが残り負債になるでしょう。
複数のプロダクトに複数のプロジェクトが走っている状況では、いわゆるリードデザイナーが人力で全てのデザインを見て品質を担保することは不可能です。組織体制の変更やワークフローの工夫など仕組みとしての解決が必要になります。
複数の平行プロジェクトが走っているということは、1つ1つのプロジェクトのデザインにかけられる絶対的な時間が少なくなるということです。デザインは開発の最初のプロセスなので今後の開発に大きく影響するような決断を迫られることが多いと思いますが、ここに時間がかけられないということはプロジェクトとプロダクトの品質に関わる判断をミスるリスクが大きくなるということです。
「あのときもう少し時間をかけて考えていれば。。」と思うことは誰しもあることだと思います。とはいえ、時間をかけることが許されない状況は変わりません。そもそもデザインは決断の連続なので、プロジェクトの度に考えて判断していてはどれだけ時間があっても足りなくなります。プロジェクトの度に考えなくてもいいように、明文化された判断基準が共有されていると、何を考慮しなくてもいけなくて何は考慮しなくてもいいのか迷うポイントが減り、判断をミスるリスクも小さくなります。
デザインは複数の問題を解決する仕事です。改善を積み重ねていると、目の前にある課題のコンテキストに左右されすぎて、解決しようとしているもの以外の問題が見えなくなってしまうことがあります。
特に好ましくないのが、1つの問題の解決した副作用でこれまで解決してきた問題が再発することです。長くプロダクトを改善していると、1つの画面デザインはさまざまな問題を解決した後の絶妙なバランスで構成されています。プロダクトの年齢が高くなればなる程デザインの難易度が上がっていきます。長く担当しているデザイナーでも大昔に解決した問題を忘れてしまうこともあるので、新しくジョインしたデザイナーにこの何層にも積み重ねられたコンテキストを読み解くことを期待することは無謀なことです。
たとえば、上図のようなあるグローバルナビゲーションのデザイン改善の履歴では、「サービスのメイン機能を目立たせたい」、「推したい機能をよりアクセスしやすい距離に配置したい」、「文字文字しい見た目をスッキリさせたい」、「アイテムを優先度によるグルーピングをしたい」、「配置のバランスを整えたい」などの課題が発見され、その都度コンテキストが積み重ねられています。しかし、このコンテキストは Slack 上で展開された議論を遡ってみれば分かるのですが、Sketch のデザインデータ上から読み取ることは困難です。積み重ねたコンテキストを忘れてデザインに修正を加えた場合、解決された問題が再発する可能性もあります。
改善の繰り返しによってコンテキストが積み重なったとしても、今までデザインが解決しようとした問題を振り返ることができるようになっていれば、目の前の改善案によって過去に解決した特定の問題がデグレーションを引き起していないかを確認することができます。
デザインはパターンが統一されている方がユーザーにとっては使い方に迷うことが少なくなります。しかし、あるデザインパターンを導入するとき、特定の条件の画面だとそのデザインが破綻してしまうリスクがあるときあります。
そんなとき「今このデザインはここでしか使ってないから大丈夫」と導入すると、近い将来高い確率で負債になります。導入のタイミングでは、破綻したときにデザインを見直すことにして一旦これでいこうと思ってしまうことも多いですが、その後の改善でそのデザインの上にもいくつもコンテキストが積み重ねられていきます。そうなるとそのデザイン修正は積み重ねられた複数の問題を一気に解決しないとデグレーションになってしまう難易度が高いものになります。多くの場合は「ここのデザインを修正するのはデメリットが多すぎるからやめよう」ということになります。その場所のデザインを修正できんなくても、解決しなければならない問題は残り続けるので、別のアプローチでのデザイン修正で解決を試みるのですが、よほど筋が良いアプローチでないと無理が生じて更にデザインの負債が積み上がることも少なくありません。
この場合の「ここでしか使っていないデザイン」は「ここでしか使えないデザイン」なので、その時点でも負債です。少なくともその場の人間がパッと思い付くような条件で破綻するようなデザインは、将来的にも導入される場所になる可能性が高いので導入は避けるべきだと思います。
大抵の場合、1つの問題を解決する手段はいくつもあります。しかし、コンテキストが重なっていくとさまざまな制限により取れる手段が少なくなっていきます。長くサービスを運用するのであれば、同じ問題を解決するにしても選択肢ができるだけ減らないような手段を選ぶ必要があります。それができるのは選択肢が多いうちだけです。
運用期間が長くなると開発メンバーも入れ替わります。私も AbemaTV 立ち上げ時にいた開発チームにはもういませんし、ほかのエンジニアやデザイナーも入れ替わっています。入れ替わってもチームに同じ職種のメンバーが複数いる場合は、順々に入れ替わるので、仮に明文化されていない知見が存在しても、一緒に開発している間に受け継がれたりします。
最近のプロダクト開発ではエンジニアは複数人開発していることが多く、総入れ替えは発生しづらかったので、これにより知見の暗黙化が防がれて助かっている部分がありました。それに対して、デザイナーの方が人数が少なく、基本的に1プロダクト1デザイナーになりがちでした。必然的にデザイナー総入れ替えパターンは発生しやすくなっていました。
そうすると、前のデザイナーの意図が正しく受け継がれないことも多く発生します。「このデザイン、何でこうなってるんだっけ?」と一番長くいるデザイナーに尋ねることも多いです。尋ねる分にはまだいいですが、「既存のデザインがそうなっていたので合わせました」と意図が理解されないままデザインが継承されることもあります。もし元々のデザインの意図が適さないものだったら、継承先のデザインはどこかのタイミングで思わぬ形で矛盾を露呈することになります。
デザインはデータで管理されることが多いと思います。AbemaTV も Sketch のデータとして管理しています。データは使いやすくするために用途に応じて整理されると思いますが、チームで整理方法の基準が決まっていなければ、整理した人によってデータの構造はバラバラになるでしょう。AbemaTV も初期は作業速度重視でプロダクトことで担当デザイナーが決まっていたので、プロダクトごとにデザインデータの構造はバラバラです。
プロダクトごとにデザイナーが固定の場合はこの場合でも最速だと思いますが、デザイナーが変われば効率は一気に落ちるリスクがあります。サービスのフェーズが変わると、プロジェクトごとに最高の成果にコミットできる組織構造にするためにプロジェクトごとにデザイナーをアサインするように変更したくなる場合もあります。しかし、プロダクト間でデザインデータの構造が統一されていなければ作業効率が下がるため、組織構造にチームがついていけなくなります。
デザインをリリースした後そのデザインの評価ができないこともデザインの負債の要因です。新しいデザインに対するユーザーからのフィードバックをリリースした後すぐにもらえるのは稀だと思います。大体の場合はフィードバックは遅れて開発者に届くものです。でもそうすると、リリースしたデザインが良いのか悪いのか分からない状態で次の開発に進むことになります。
遅れてフィードバックをもらえたとしても、矛盾するフィードバックやターゲットユーザーではない人からのフィードバックもあったりします。フィードバックを鵜呑みにしてもプロダクトデザインが改善されるとは限りません。
こういった状況でデザインの変更を積み重ねていくと、時間差で届いたフィードバックを効率的に取り入れることができなかったり、取り入れるべきでないフィードバックを間違って入れてしまう可能性があります。そうなればデザインの負債です。
ユーザーの直接的なフィードバック以外にも、Google Analytics などの解析ツールを使うことでファクト(事実)として自分たちのデザインを評価することはできます。しかも、ファクトはリリース後すぐに数字などの変化により収集することができます。ただし、ファクトが収集できるのは、ファクトが収集できるように考慮されたデザインのみです。解析することを何も考慮されていないデザインから正しいファクトを取り出すのはものすごく難しいです。
積極的に直接的なフィードバックを取得する方法としては「ユーザーテスト」などの手段もあります。しかし、ユーザーテストは解析などにより収集されたファクトがある前提で、そのファクトになっている理由を調査する方法であって、ファクト自体を収集する方法としては適していません。
「A/B テスト」などの手段でデザインを定量的に評価することもできますが、これもテスト対象のデザインがきちんと分離されていないと、正しい結果を導くことは困難です。
カリスマ的なデザイナーが全てのデザインを評価する、ということもできます。しかし、これはスケールしないので破綻も時間の問題です。
デザインを評価するためには、評価できるようにデザインすることが必要になります。エンジニアがテスタブルなコードを書くようにするのと同じです。
前述の「デザインの評価ができない」に似ていますが、デザイン改善の優先度が測れないためにデザインの改善速度が下がることがあります。リリース後もプロダクトは改善のために更新され続けますが、デザイン改善以外にもプロダクトの改善要素はたくさんあります。事業インパクトが大きい追加機能やプロモーションイベント、実装やメンテナンス効率を上げるためのリファクタリングやパフォーマンスチューニング、さまざまな種類の改善タスクがある中でデザイン改善に関するタスクが優先度でほかのタスクに負けて改善速度が下がってしまうのです。
事業インパクトが大きいことが分かっている追加機能やプロモーションイベントは優先度の高さが分かりやすいですが、リファクタリングやデザイン改善といった一見効果が目に見えずらい改善タスクは気にしている本人たち以外にはその優先度が理解できません。コードのリファクタリングなどはエンジニアが気にしていることなのでエンジニア自身が作業の合間を見て実施したりもしますが、デザイン改善はデザイナーが気にしていてもエンジニアが実装しなければプロダクトに反映されません。デザイン改善の優先度が気になっている本人以外にその優先度を伝えることができなければ、ほかのタスクの優先度に永遠に勝てなくなることもあります。
デザインの評価ができていれば、デザイン改善の優先度は高くなるかもしれません。しかし、既に評価が高いことが分かっていたら、逆に「これ以上改善することもない」と思われてしまうかもしれません。デザイン改善の優先度を伝えるには、改善の重要度が伝わるようにしなければいけません。
これらのデザイン課題を解決するために Story-Assured Design では下記6つのことを行っています。
今回のプロジェクトで幸運だったのは、プロジェクト開始時に「 とにかくリモコンでボタンを押す回数を少なくしてほしい 」と明確に要望されたことでした。
皆さんもテレビを使ったことがあると思うのでイメージしてもらえると思いますが、多くのテレビのリモコンにはタッチパッドのようなインターフェースはないので、十字キーを押して操作します。画面上に見えていても端の方にある番組を選択するためには何度も同じ方向の十字キーを押すことになります。
ユーザーが少ないリモコン・ボタン操作で自分がやりたいことができるなら、それだけでもかなり使いやすい UI と言えるはずです。そこで、「プロダクトを使いやすいものにする」という KGI(Key Goal Indicator)に対して、「リモコンでボタンを押す回数」という KPI(Key Performance Indicator)を設定し、「リモコンでボタンを押す回数を各画面3回以内」になるように目指しました。
KPI を設定したことにより、チームの誰もが同じ目線でデザインを評価できるようになりました。
デザイン作業の種類に得意・不得意がある場合でも作業自体を分担できるように、Story-Assured Design ではデザインの作業工程を分割します。冒頭に述べた通り3ステップに分割しました。
アプリケーションのデザイン作業は、ユーザーに意図した動作をしてもらいやすいストーリーを考え、そのストーリーを画面における情報要素の配置(レイアウト)を考え、魅力的な見た目を作り上げることをしなくてはいけません。しかし、そのうち、グラフィック能力に長けていないと作業できないのは、最後の魅力的な見た目を作り上げる工程で、そのほかの工程は意外とデザイナー以外も作業に参加できます。ただ、これらの一連の作業をデザインカンプの上で、一人のデザイナーが一気にやってしまうとチームのほかのメンバーが参加するのが難しくなってしまいます。
今回のプロジェクトでは、それぞれのデザイン工程でチーム全員でデザインを考え、レビューをし、時には作業自体をデザイナー以外にも分担しました。特にユーザーストーリーをデザインする作業はディレクターの方が得意なことも多く、その部分はデザイナーとディレクターが分担してデザイン作業しました。
作業工程を分割して細分化したことにより、副産物としてデザインに対するチームの理解度が上がりました。後述しますが、作業工程自体が「Why? How? What?」の順で成果物を作るように分割されています。これによりなぜこのデザインの変更が重要なのかがチームメンバー全員に伝わりやすくなる効果もあります。
従来のデザインワークフローでは、事業利益を上げるためのデザインとユーザー満足度を上げるためのデザインを一緒にレビューしていました。しかし、この形式だとレビュースコープが広すぎるため、レビューワーによって見ることができる問題に差ができてしまいます。レビューワーによってはユーザー体験に関わる問題は指摘できるけど、事業利益に関わる問題を指摘できない、などです。議論に発展しても収拾がつかないことも多く、最終的に決裁権を持つ人や声が大きな人の好みで方向性が決定してしまうリスクもあるでしょう。
当然ですが、どれだけユーザー満足度が高くても事業利益が上がらなければ、サービスは潰れます。逆にどれだけ短期の事業利益が上がっていてもユーザーが満足していないサービスはいづれユーザーが離れていくので、やはりサービスは潰れます。この両者を高い品質で成立させるようにデザインすることは難しいですが、サービスが大きく成長しながら長く生き残るためには必須です。
そこで Story-Assured Design では初期工程ではより事業利益に関わる要素にできるだけスコープを絞りデザインを評価し、後の工程でユーザー満足度に関わる要素に絞ってデザインを評価します。
アジャイル開発の基本は「不確実性を取り除くこと」です。どんなに初期にデザインレビューをしても、考えたデザインは人の頭の中でシミュレーションされているだけなので多分に不確実性を含んでいます。しかし、実機でデザインに触れることができるようになると、不確実な要素は一気に具体的な体験として解消されます。それとともに具体的な課題も明らかになります。
不確実性をできる限り取り除くために、開発の早い段階からユーザビリティテストで評価できるようにモック実装に必要な要素から集中してデザインします。これによりプロダクトが完成に近い形になるかなり前から致命的なデザインの不具合を発見し、リリース前でも確実なデザインの改善を進めることができます。
デザインにおいて特に不確実性が高いのは、ユーザー満足度に関わる要素だと思います。デザインされた細かな要素がユーザーの最終的な満足度を向上させます。理想を言えば、実装したデザインをさまざまなユースケースでテストしてからリリースしたいところですが、現実的には時間にも限りがあります。
Story-Assured Design では、事業利益に関わるベースデザインを作成した後は、モック実装しながらユーザー満足度を向上させるデザインを平行して進めます。これにより限られた時間内でできる限り不確実性を取り除いたデザインを実現します。
モックを作って、ユーザビリティを徐々にテストできるフェーズになると、具体的な改善案が出るようになります。このフェーズでは細かい改善案が多く出ます。細かいデザイン修正によってプロダクトのデザインはより洗練されていきます。
しかし、これら細かいデザイン修正は先述した直感的な改善案であったり、コンテキストに強く依存した改善案であることも多いため、目の前の気持ち悪さを改善した結果、最初にデザインしたユーザーストーリーに影響を与えてしまう場合もあります。更に悪いことに、この変更をテストするときは出した改善案が適用されていることをテストしがちなので、既存のユーザーストーリーがデグレードしてることに気付かないことも多いです。
「木を見て森を見ない」状態にならないように、変更が既存のデザイン構造に影響を与えていないかをテストする仕組みがあれば、デグレードは防ぐことができます。Story-Assured Design ではこの仕組みのためのデザイン工程があります。
デザインは、考案され、実装され、リリースされたら初めてユーザーに届きます。これらの作業は1人では完結しません。複数の人の手を介することになります。あるデザインを考えたデザイナーはそのデザインの重要性を肌で感じていると思いますが、リリースまでに携わる人がその重要性が理解できるとは限りません。
ある機能をユーザーに届けるために全くデザインされていない状態ではリリースすることができないため、デザインの優先度は絶対的なものです。しかし、一度ある程度のデザインが実装されたりリリースされている場合は、改善するためのデザインの優先度はその重要度が考案者以外にも理解されていなければ下がります。そのため、ワークフローの途中で考案者が意図しない形で優先度が下げられてしまうリスクもあります。
デザイン改善の優先度を伝えやすく、リリースまでに携わるより多くの人を巻き込めるようなデザインワークフローであれば、そのリスクを下げることに貢献します。後述しますが、「Why? How? What?」の形式に沿って途中経過の成果物を残すことでデザイン改善の優先度をよりチーム全体で共有しやすくします。
こうしてできあがったデザインワークフローが Story-Assured Design です。冒頭にお話した通り3ステップで UI デザインを作っていきます。
先程、Story-Assured Design では「Why? How? What?」の順で成果物が作られると書きました。これは Simon Sinek 氏が TED の「How great leaders inspire action」というトークでも語っているゴールデンサークル理論です。この理論は人を動かすためには「Why? How? What?」の順に伝えることがとても重要だと言っています。Story-Assured Design の3ステップはまさに
というステップに呼応しています。
Story-Assured Design で一番重要なステップがストーリーデザインです。なので最初にデザインします。その名前の通り、ユーザーストーリーをデザインする工程です。先程のゴールデンサークル理論でいうところの Why をデザインする工程 です。
このステップでは、 画面ごとにユーザーに得てもらう情報とその情報を得たことによって起こすアクション をデザインします。
ほとんどのサービスは何らかの形で事業利益を生み出すために提供されていると思います。ストーリーデザインでは、ユーザー満足度に直接関わる部分というより、事業利益に貢献する部分に比重を置いてデザインします。つまり、使ってもらうことで事業利益を生む出すアクションまでのストーリーをデザインするのです。どんな情報をユーザーに与えれば次の情報に繋がるアクションを起こし、最終的に事業利益に繋がる行動や習慣に導きます。
このステップでストーリーを確立させた上で、以降のステップでより満足度高くストーリーに沿ってもらえるようにデザインしていきます。
デザインしたものがデザイナーの頭の中だけで完結してしまっては、このステップを分割した意味がなくなってしまいます。アウトプットとして管理しましょう。このステップのアウトプットは、次のようにワイヤーフレームにとても近いものが良いです。
AbemaTV の開発ではデザインデータは Sketch で作られているので、今回のプロジェクトでもストーリーデザインのアウトプットは Sketch ファイルです。画面単位でワイヤーフレームのように画面に配置する要素をレイアウトしていくのですが、ユーザーストーリーが確実にデザインされていることを検証するために、このステップでは先述した KPI に対する数値も明記します。
例ではワイヤーフレーム図の下にユーザーストーリーに対するリモコンボタンの押下数を明記しています。これでデザインしたユーザーストーリーが KPI を達成しているかを確認します。
余談ですが、この KPI の表現方法は Google I/O 2018 でのセッション『An accessible process for inclusive design』からヒントを得ています。
とはいえ、画面にはいくつもの機能と選択肢があります。そこでストーリーデザインにおいてストーリーを作る手法としてオススメしたいのが、次に紹介する「プライマリーストーリーメソッド」です。
プライマリーストーリーメソッドは、1画面に1つだけ1番重要(プライマリー)なユーザーストーリーを設定することで、ストーリーデザインする手法です。とても単純明快なためチームメンバー全員が目線を合わせてデザインに参加することが容易なので新規プロダクトの初期デザインにオススメな手法です。
たとえば、この画面ではユーザーに過去に視聴した番組一覧を情報として表示して、そのうちの1つの詳細画面に遷移することをプライマリーストーリーに設定しています。この画面には視聴履歴とした記録されている番組を履歴から削除する機能やマイリストというお気に入りとして登録した番組を確認する機能を使うこともストーリーとして存在しますが、まずは1番多くのユーザーに良い体験を与えることができるストーリーを設定します。そしてそのストーリーに対して1番ボタン押下数が少ないカーソル移動経路をデザインします。
プライマリーストーリーメソッドは開発する側にとってもメリットがある手法ですが、ユーザーにとってもメリットがあります。ユーザーにやってもらいたいことが1画面に複数あるデザインは新規ユーザーを迷わせます。それでも、サービスを提供する側の心理としてはユーザーが離脱しないうちにできるだけいろいろなコンテンツや機能を知ってもらいたいので1画面に複数のストーリーを詰め込んでしまいがちです。プライマリーストーリーメソッドは1つの王道のストーリーを明確に優先することになるため、ユーザーは迷うことなくそのストーリーを選択することができます。
そして、ストーリーデザインの評価もしやすくなります。画面ごとに明確なストーリーがあるので、Google Analytics などを使ったユーザー行動計測との差分を確実に取ることができます。デザインの評価が難しいという話を先述しましたが、プライマリーストーリーメソッドによって、意図したデザインが数字として結果を出しているかがしっかり評価できます。
そして、意図したストーリーと違った結果が出たとしてもガッカリすることはないと考えています。ユーザーが想定と違う行動をしているのが事実だと確信できたら、自信を持って改善案を考えればいいのです。リリースする前のデザインはどんなものでも「ユーザーはそのように行動するはず」という仮定でしかありません。その仮定が肯定されればもちろん嬉しいですが、逆に完全に否定してくれれば、未練なく別アプローチでデザイン改善できます。
プライマリーストーリーがないと、画面においてユーザーに与えられた行動の選択肢が多すぎて、考えうる仮説も多くなりがちです。仮説が多いと考慮しなくてはいけないことが増え、ユーザーストーリーの設計も難しくなってしまいます。
デザインを作成することになると、視覚的に美しく細部に拘ったデザインを作りがちです。しかし、ストーリーデザインではそれらの情報は邪魔になります。
Story-Assured Design では、チームメンバー全員でデザインを考えるので、作成したストーリーデザインも全員でレビューします。その際に視覚的に完成されたデザインを目の前にすると、色が見ずらさやレイアウト的な細かいズレなどの詳細に注意を奪われてしまい、ストーリー自体のデザインレビューに集中できなくなります。
ストーリーデザインでは、ユーザーがその画面で次のアクションを起こすために何の情報を得るように画面を構成していく必要があるかが分かるだけで十分です。情報がどのように伝えられるかがない方がレビューしやすくなります。
ストーリーデザインという工程を明確に区切ることでデザインレビューのスコープを小さくすることができます。
現在は Abstract などのツールが一般的になってきて、デザイン工程の進捗を詳細に可視化することができます。
これによりエンジニアが Git や GitHub などを使ったときのように、プロダクトに対する変更をブランチ単位で可視化することができるようになりました。エンジニアがプロダクトに変更を加えたい場合、コードを変更してマージリクエストやプルリクエストすると思いますが、基本的にレビュースコープが小さくなるように変更範囲を工夫することでマージまでの時間は短かくすることができます。Abstract でも同様にプロダクトのデザインデータに変更を加えてマージするときにレビューリクエストすることができます。このときのデザインレビューのスコープをストーリーに限定すれば、同様にマージまでの時間が短かくなります。
また、複数人の目でデザインレビューをすることも可能になり、デザインの精度も上がります。しかも、ストーリーのデザインはデザイナーに限らずレビューできるので、より他面的に問題を指摘することができます。
チームで納得できるストーリーをデザインした後は、後のデザイン工程でそのストーリーが壊れないようにします。まさに Story-Assured Design の名前の由来である ストーリーを保証するステップ です。 How? の部分を作る ステップでもあります。
ストーリーデザインでは「ユーザーが画面からどんな情報を得ることで、次にどんな行動をするか」をデザインしました。デザインの構造化のステップでは「情報をどのように得るか」と「次にどんな手段で行動するか」をデザインします。
たとえば、上図では、この「この番組の詳細情報へ(仮)」というボタンっぽいものが配置してありますが、実際に手段としてデザインされたのは下図です。
ストーリーデザインの段階では「ユーザーが番組タイトルという情報を受け取って、番組のことをもっと知りたいと思って詳細情報に遷移する」というストーリーにフォーカスを絞ってデザインを考えます。でも、どんな手段で番組タイトルという情報を知って、どんな手段で詳細情報に遷移するか、は詳細に詰める必要はありません。
デザインを構造化するこのステップで、どんな情報の提供方法が一番良いかを詰めます。
手段をデザインするとともに、このステップではストーリーを保証します。ストーリーを保証する、というのはストーリーが壊れていないことを保証する、ということです。デザインされたストーリーが壊れる、というのはどういうことかを説明します。これを説明するにはステップ3の視覚情報デザインについて少し話す必要があります。
視覚情報デザイン、というのは簡単に言うと、Sketch などのデザインツールでプロダクトの画面の見た目の最終形を作ることです。いわゆるデザインカンプです。たぶんデザインデータと言われて一番頭に思い浮かぶものだと思います。
いざデザインカンプを作っていく段階になると、視覚的なバランスを調整することになります。このとき、画面上で目に入る要素数が多いから、バランスを取るために UI 要素を減らしたり、バランスを保てる場所に要素を移動させるなどの調整することもあります。このとき視覚的な問題ばかりに気を取られていると、プライマリーストーリーとして決めた一番ユーザーにしてもらいたい行動までの距離が遠くなっていることに気付かないレイアウト修正を行ってしまうことがあります。
こういった意図しないストーリーデザインの破壊を防ぐため、ストーリーを構成する要素を構造化します。構造化というのは、そのものの構造を見て分かるようにすることです。つまり「デザインの意図を理解しやすくする」ための作業です。構造化によってデザインの意図が見えやすくなると、デザインに変更が入ったときに影響範囲も人目で分かるようになります。
Story-Assured Design では、ストーリーに対する影響と見た目(視覚情報)に対する影響を分離するようにデザインを構造化します。
デザインの構造化の作業では、デザインを構成する要素を意味ある単位に分解して整理していきます。具体的な整理の仕方はどんな方法でも構わないのですが、Sketch などのデザインデータ上と実装コード上で表現できるものにします。この2つで表現可能なものでないと仕組みとして変更に対する保証を作ることが難しいからです。
引き出しの中の道具を整理するとき、仕切りを作って道具の種類ごとに分類すると思います。これはどこにどんな道具が入ってるか理解しやすくするための構造化です。そのように整理された引き出しは使いやすいはずです。
引き出しは整理されていなくても、文章は書けるし、絵も描けます。なので、作品作りに影響がないという人もいると思います。ただ、その引き出しを個人ではなく複数人で共用で使うことを考えると、整理されていないと道具を探すことに時間がかかりすぎて、何人かの制作作業に支障が出ているかもしれません。新しく引き出しを使う人は確実にすぐには作業に取り掛かれないでしょう。
デザインの構造化も引き出しの整理と基本的な目的は同じです。複数人で作業しても制作作業に支障が出ないようにします。引き出しの中にある鉛筆や消しゴム、定規などの道具は個体として識別できるので、個々の配置を変えたり、分類したりすることで整理できますが、デザイン自体はそのままでは整理することができません。そこでデザインを構成する要素を識別可能な形でコンポーネント化すると整理がしやすくなります。
今は Sketch などのデザインツールがコンポーネント化を意識した機能を提供しているため、以前よりコンポーネントベースでデザインすることがかなり一般的になってきて多くの人がイメージしやすくなっています。
そして、何よりソフトウェアのコード設計自体がコンポーネントベースであることが多いので、一番理に適っています。デザインにおける検証と実装を繰り返して改善していくのであれば、デザインの構造とコード設計の差をできる限り無くすことが1番デザインからデプロイまでのリードタイムを短くすることができます。
デザイン要素をコンポーネント化した、いわゆる UI コンポーネントの話とよくセットで話題に上がるのは、再利用性という言葉です。しかし、Story-Assured Design の中では再利用できるかどうかは UI コンポーネントの必須条件とは考えないようにします。デザインデータの構造化のためにコンポーネント化しているので、再利用できなくてもコンポーネントとして整理します。
デザイン要素をコンポーネント化しようとすると、どのような要素をどういう単位でまとめておくか悩むと思います。私のオススメは「関心の分離」を基準にコンポーネントにまとめるのが良いと思います。そのコンポーネントがどんなデザイン的な課題を解決することに関心(責任)を持たせたいか、という視点でまとめておくと、デザインの意図が構造により理解しやすくなります。
そして、ソフトウェアのコンポーネント化と同様に、単一責任原則に従うとデザインの変更がしやすい構造になるでしょう。「変更する理由が同じものは集める、変更する理由が違うものは分ける」ことで将来的なデザイン改善速度は加速するはずです。
デザインにおける関心の分離の例は、拙著『Atomic Design ~堅牢で使いやすいUIを効率良く設計する』にも書きましたが、UI をコンポーネント化する基準として1番理に適っていると思っています。デザインの構造化に Atomic Design のような階層化された構造を採用するのも良いでしょう。
データ構造デザインが完了した時点でエンジニアは UI 実装に着手できるようになります。今回のプロジェクトでは Sketch のデータ構造どおりに UI をコードに実装していきました。
ここで実装された UI は視覚情報がまだデザインされていないので、実装されたアプリケーションはまだモックのようなものですが、デザインしたユーザーストーリーはそのまま反映されているはずです。なので、このモックではデザインされたユーザーストーリーが正しく機能しているかを評価することができます。
また、実装と平行して、次のステップで紹介する視覚情報デザインを進めることができます。これにより、実装においてデザインを待つ時間が短かくなります。そして、ユーザーストーリーを自分たちで先行して触って評価することもできるので、視覚情報のデザインを詰めすぎる前にフィードバックを得ながらユーザーストーリーに改善していき、最終的なデザインへと詰めていくことができます。
Story-Assured Design の最後のステップは視覚情報のデザインです。視覚情報のデザインというとまどろっこしいですが、一般的に多くの人が思い浮かべるであろうデザイン作業です。私たちのチームではよく「デザインカンプの作成」と言っていました。 ユーザーが見る画面に限りなく近い最終形のデザインを Sketch データとして作ります 。プロダクトデザインにおける What を作る 工程です。
ストーリーデザインでは事業利益に貢献することに比重を置いてデザインしましたが、このステップでは、ユーザーの満足度を最大限高めることに比重を置いてデザインします。
視覚情報デザインはその名前の通り、ユーザーに視覚的に伝えるあらゆる情報をデザインすることを指します。ストーリーでデザインした情報の流れがユーザーにどのように見せられて、どう伝わるかが改めてデザインされます。細やかな工夫で直感的に情報が伝わり、ストレスない使用感に繋ります。この工程次第でユーザーの満足度は大きく変わります。
Story-Assured Design において視覚情報をデザインする際は、前のステップで作った構造を保ったまま見た目だけをデザインしていきます。前述したとおり、構造デザインはデザインしたストーリーが壊れていないことを保証するためのステップです。その構造に変化を加えてしまうとストーリー自体を破壊してしまう可能性があります。Story-Assured Design では構造化されたデザイン自体をストーリーデザインに対するリグレッション(退行)テストだと捉えることができます。
とはいえ、視覚情報をデザインしていると、どうしても構造に変更を加える必要が出てくる場合はあります。そんなときは、構造に変更を加えます。ただし、先程構造化されたデザインはストーリーデザインに対するリグレッションテストだと書いた通り、構造に変更を加えた場合はストーリーデザインから見直しを行います。構造の変化により、デザインされたストーリーに矛盾があったり、成り立たなくなっていることが確認された場合はストーリーデザイン自体をやり直します。そして、デザインの構造化を再度行い、最後にデザインカンプに戻って修正を反映します。
先述した通り、視覚情報デザインだけは作業にもレビューにも、どうしてもグラフィック能力に長けている必要がある場合が多いです。この領域はデザイナーが担保します。視覚情報デザイン以外のステップでは、チームメンバーの意見は常に平等に扱われることが大事です。明文化された KPI があるため、誰でも同じゴールを向いた意見を言うことが可能だからです。しかし、視覚情報デザインのステップだけはデザイナーの意見を優先させることが大事だと個人的には思っています。視覚情報デザインは非言語的な要素を扱うことが多く、言葉だけでレビュー観点を合わせることが困難です。
言葉だけでレビュー観点を合わせることが困難だと言っても、視覚情報デザインをデザイナーだけに任せてしまうということではありません。視覚情報デザインにおいても言葉にしきれないギリギリのところまではチーム全員が良さを評価できる指針を作ります。
ユーザー満足を追求するときに一番身近なユーザーは自分です。そのため、このステップでは特にお互いが考える『良い』をぶつけ合いやすくなります。ぶつけ合いを防ぐために自分たちが考える良いユーザー体験をしっかり定義することが大事です。実際にユーザーがどういう体験を得ることを期待するのかを言語化します。
そのためには、自分たちが開発するプロダクトやサービスにとってのデザイン原則を作るのがオススメです。この原則を視覚情報デザインに限らず、全体のデザイン指針にします。
上記は一部ですが、ユーザー体験を考える際に「Aの案の方がユーザー体験が良い」というより「Aの案はカーソルの移動距離が小さく、ユーザーは目線を移動させる必要がないため、楽に操作ができる」というように原則に沿っているかという目線で話し合いができる方がチームが考える『良い』への結論に早く導くことができます。
作って思いますが、最初から完璧なデザイン原則を作ることは難しいと思います。ただそれでも、何かを言葉にしてから先に進む、ということが大事です。決定が間違っていれば、少なくともどこかで間違いに気付くことができ、修正することもできます。修正すれば前進しています。しかし、難しいからと言って何も決めなければ、いろいろな人にとっての良いユーザー体験が詰め込まれ、結果的に、誰にとってもどのように使ってもそこそこな体験を提供するデザインになってしまいます。
ここまでストーリーを明確にしてデザインしてきたので、リリース後はそのデザインが本当にワークしているかが分かります。アクセス解析ツールでユーザーがストーリーデザイン通りに行動をしているかを定量的に評価することができるからです。今回のプロジェクトでは Google Analytics の「行動フロー」でユーザーの実際の行動とストーリーデザインとの差異を確認しました。
ユーザーの行動を解析していくとストーリーデザインで描いたものと異なる結果も出てきます。解析ツールで確認できるユーザーの行動は紛れもないファクト(事実)なので、リリース後はこのファクトを明確に変化させるようにストーリーデザインを改善していきます。
効果的に意図する方向にファクトを変化させるためにはユーザーの行動理由を分析する必要がありますが、解析ツールで得られる数字だけでユーザー行動の全てを理解することは困難です。ある程度仮説を立てた後は、実験的にストーリーをリリースしてファクトの変化を観測しながら分析する方が間違いもなく確実です。ファクトからユーザーの行動理由を推測できるものもありますが、どうしても推測できないものもあると思います。ユーザーテストなどのより直接的な手段も活用していきます。
より分析しやすくなるようにデザインすることも重要です。「デザインの構造化」で触れた「デザインにおける関心の分離」がうまく設計されていると、分析したい行動に関わる要素だけを変更させることが簡単にできるため、有意な差異に辿り着きやすくなります。「デザインの構造化」によりコード側の構造もデザインの構造と同じになっているため、実装も容易なはずです。
組織やチームが大きくなってくると、大人数で分業するために作業を多数のタスクに分割して管理することになりますが、分割されたタスクが「それをやる理由」と切り離されて管理されてしまうことがあります。タスクには最低限「何をやるか」が記されていれば実行可能なため、「なぜやるか」までは記されずに担当者に渡されることがいつしか普通になってしまう現象です。
タスク自体が「何をやるか」だけ理解された上で実行されてしまうと、意図と異なった仕上がりになってしまうリスクがあります。担当者にとってもただの作業となってしまい工夫の余地もないため、せっかくチームに高い能力を持ったメンバーがいても、力を活かしきることができなくなります。
これを防ぐため、タスクは基本的にストーリー駆動で管理するようにします。プロジェクト管理ツールなどを使ってタスクをチケット管理している場合は、ストーリーチケットを常にタスクチケットの親として管理するのがオススメです。
ストーリーに紐付くタスクは、「仕様化」、「ストーリーデザイン」、「デザインの構造化」、「視覚情報デザイン」、「実装」、「ログ設計」、「ログ実装」などになります。「ログ設計」、「ログ実装」以外はストーリーをリリースするために常に必要なタスクです。ストーリーは常にユーザーの行動に変化を与えるために作られるため、KPI を持ちます。そのため、その KPI が新しいストーリーデザインによってどう変化したか計測する必要があるので、既存のログ設計で計測できない場合は「ログ設計」と「ログ実装」がタスクとして追加されます。
それぞれのタスクのレビューは、「紐付いているストーリーが持つ KPI を効果的に改善できるように工夫されているか」という視点でチーム全員が目線を合わせて行うことができます。
Story-Assured Design のワークフローを実践した結果、初期リリース直前のデザインのブラッシュアップ速度は凄まじいものになりました。ストーリーが構造化によって保証されている状態なので、ギリギリのタイミングまで細かいユーザビリティ改善を心理的安全性が高い状態で行うことができました。
また、構造化されたデザインがコード設計と同期しているため、どの要素をいじれば実装にどの程度影響があるかも分かるのも大きかったと思います。影響範囲が大きいことが分かった場合にすぐに同じ問題を解決する別のデザイン手段に切り替えることができます。
そして何より、チーム全員がデザインに取り組んでいる状態は、それぞれが事業利益とユーザー体験を向上するために考えて工夫することを可能にするため、相乗効果を生みました。初期リリース直前の KPT ではデザインに関する多くの Keep や Problem が出たことは、1つの成功体験になったと感じます。
またこのワークフローで作られたデザインデータも思わぬ副産物を生んでいます。ストーリーデザインの Sketch データは見ることでプロダクト全体のユーザーストーリーを把握できるため、テスト設計者やログ設計者もこれを参照してテスト設計やログ設計を行いました。ユーザーストーリーに対する理解があることで、テストやログ設計者も柔軟に設計しやすくなりました。
このデザインワークフローはプロダクトの成長に伴なって確実なデザインの改善スピードを上げていくことが目的です。ここからの道程が本番なので、品質が高い改善スピードを向上させられるように進めていきたいと思います。
現在も Story-Assured Design のワークフローで改善を繰り返しています。しかし、サービスはどんどん成長していき、組織規模のスケールに伴なった課題の難易度も増していっています。そのため、次のアプローチとして DesignOps を模索しています。このまま組織が大きくなったとしても、デザインに関するオペレーションを正しく効率化でき、クリエイティビティを常に最大限にできたら最高です。その中で Story-Assured Design のようなワークフローもその1つの構成要素として更に上手く機能させることができたらなと思っています。
]]>担当している AbemaTV が Amazon Alexa に対応しました。今回 Alexa スキル(Alexa に機能を追加するためのアプリをスキルと呼びます)の開発にあたって、課題感があったチームによる平行開発、継続的インテグレーションおよびデプロイ、多言語対応についてのメモを残したいと思います。
最近は Web で使われてきた技術がさまざまなデバイス用のプラットフォームでも利用できるようになり、Web エンジニアがこういった PC やスマートフォン以外のデバイス向けのアプリを開発することも多くなってきました。たとえば Amazon Echo などの Alexa 搭載端末や Google Chromecast 用アプリも Web 技術を使って開発できるので Web エンジニアが参入する障壁はかなり低いのですが、既存の Web アプリケーションと大きく違うのが、実機で動作確認するのが一苦労な点です。
Web アプリケーションであれば、実装している PC 上でビルドして同じ PC で Web サーバを立てるだけでほかの PC やスマートフォンからアクセスして動作確認できます。しかし、Amazon Echo や Google Chromecast のようなデバイスは、各々のアプリ開発者ポータルに登録されたアプリにしかアクセスできないようになっています。もちろん PC に直接デバイス接続するような機能もプラグもありません。両者とも物理的なインターフェースがシンプルすぎます。
実機で自作アプリの動作確認をするには毎回オンラインでアクセスできるどこかにデプロイする必要があります。アプリを実装する場所(PC)から動作テストを行える場所(オンラインのどこか)までがやたら遠いので、どうしても開発効率が悪くなりますし、複数人で平行して開発しようとすると人数分のテスト環境をどこかにホストする必要があり、そういった環境を別途管理するなど煩雑さも増します。
そんな中、現在サービス開発を担当している AbemaTV で、いわゆるスマート・デバイス向けのアプリ開発を専門で担当するチームを立ち上げました。Alexa スキルをチームで開発するとなっても、平行開発しづらいこの環境ではマンパワーのメリットが活かせません。平行開発を可能にするために以下の要素を満たす必要があります。
この 2 点を実現する手段について紹介するのですが、Alexa スキルの開発が初めての方のために一般的な開発手順について認識がない方のために、まず Alexa スキルの概要と開発手順をざっくり説明します。
Alexa スキルと呼ばれるものは、家電製品などを制御するためのスマートホームスキル、ニュースなどを読み上げるためのフラッシュブリーフィングスキルなど用途が特化しているものもありますが、今回は用途が汎用的なサービスを作ることができるカスタムスキルについてお話します。
カスタムスキルの開発手順は大まかに以下のようになります。
具体的な実装手順自体は Amazon Alexa が公式に公開しているチュートリアル Build An Alexa Fact Skill を一通りやると大体分かります。あと、昨日クラスメソッドさんが投稿している 【祝Alexa日本上陸】とりあえず日本語でスキルを作ってみる もスキルの作り方がとても分かりやすいのでオススメです。
Alexa スキルを開発するには、普段聞き慣れない Alexa 特有の概念をいくつか理解する必要があります。個人的には最初分かりづらかったので、補足がてら簡単に説明します。
Voice User Interface(以後 VUI)というのは、その名前の通り、声で操作するユーザー・インターフェースです。PC デスクトップ・アプリやスマートフォン・アプリでいうところのボタンとかテキスト入力ボックスなどにあたります。PC やスマートフォン上のアプリケーションはマウスやタッチパッドで操作するので、ボタンがクリックされたりテキストが入力されたときにアプリは特定の処理を実行します。しかし、Alexa の場合は操作手段が声です。どのように話しかけたときにどんな処理につなげるかを橋渡しする存在が VUI です。
VUI の少し深い話はこちらの記事など参考になりますが、とても抽象性が高い概念なので、ここでは以下の Alexa の VUI を構成する具体的な要素 3 つがどのようにユーザーの声とサービスをつなげているかを見ていきます。
Invocation Name はいわゆるスキルの呼び出し名です。つまり、Alexa から特定のスキルを使いたいときに指定する名前です。スマホだとホームスクリーンでアプリを起動するときにアイコンをタップすると思いますが、Invocation Name に相当します。たとえば、AbemaTV スキルを呼び出したい場合であれば、AbemaTV が Invocation Name なので、「Alexa、AbemaTV を開いて」と話しかけると AbemaTV スキルが起動します。
Sample Utterance は、発話のサンプルです。ユーザーが実際に発話した文言をどんな意図(Intent)として受けるかを判断するための要素です。たとえばユーザーがランキングを知りたいときに、質問の仕方は何パターンもあります。ある人は「ランキングを教えてー」と言うかもしれませんし、別の人はランキングという言葉を使わず「いま人気の番組は何?」と尋くかもしれません。ただ、厳密な発話の仕方が異なってもユーザーが聞きたいことは結果同じだったりします。こういった異なる発話パターンをスキルがどんな意図として解釈するのかをマッピングするのが Sample Utterance の役割です。
上記のような Sample Utterance を書いた場合、「Alexa、AbemaTV のランキングを教えて」 と言っても、 「Alexa、AbemaTV のランキングが知りたい」 と話しかけても 「Alexa、AbemaTV でいま人気の番組は何?」 と訊いても全てランキングを知りたいという意図と解釈して処理するようになります。意図には名前が付けることができ、ここでは RankingIntent
という名前にしています。この名前を指定することは、次に説明する Intent に処理を接続するために重要です。
Intent は Sample Utterance によってマッピングされたユーザーの意図に対して実際のどんな処理を行う部分です。例の RankingIntent
の場合はユーザーがランキングを知りたいという意図に対する処理なので、実際に現在のランキング・データを取得して、それをユーザーに回答するための文章を作成します。
Amazon Echo などの Alexa 対応デバイスはユーザーの発話音声をクラウド上の Alexa サービスに送り音声からユーザーの意図を解釈した後は、具体的なサービス・ロジックの処理依頼を AWS Lambda のような別サービスにリクエストします。Alexa が Amazon のプロダクトなので、チュートリアルにあるように AWS Lambda 上に処理を実装すると連携も簡単ですし、Amazon が用意している SDK の恩恵にあずかることができます。このフローは Alexa 公式ブログの記事 Alexaスキル開発トレーニングシリーズ 第1回 初めてのスキル開発 の下記スキル実行仕組みの図が分かりやすいです。
Alexa サービスから Intent が指定されて AWS Lambda にリクエストが飛んできます。その Intent に対するサービス・ロジックを Lambda Function として実装します。Amazon が提供する Alexa Skills Kit SDK for Node.js では、Alexa をトリガーに呼び出された AWS Lambda のイベント情報を SDK を通じて Intent ごとのハンドラに振り分けてくれます。
この Lambda Function のエンドポイントを自作のスキルから接続するように設定することで Alexa にユーザーと対話させることができるようになります。
余談ですが、2017 年 11 月現在 AWS Lambda の Node.js サポート・バージョンが v6.10
なので、本記事での JavaScript コードは全て v6.10
用になっています。
ここまでが Build An Alexa Fact Skill チュートリアルに載っている内容です。この内容をカスタマイズすれば、Alexa スキルを開発することはできます。しかし、前述したように、このスキルを動作確認するには毎回 AWS Lambda 上にコードをデプロイする必要があります。これでは開発効率も悪いです。しかも Alexa スキルは Lambda Function に紐づけて管理するので、チームで開発するとなると人数分のスキル設定と Lambda Function も別途用意しておく必要があります。しばらく開発してると、少々手間がかかりすぎるのでちょっと辛くなり、次のようなことができる環境が欲しいなと思い始めます。
結論を言うと、前者は alexa-app というサードパーティーの Alexa スキル用フレームワークを利用することで解決して、後者は Amazon が提供する ASK-CLI という Alexa スキル管理のためのコマンドラインツールを CI ツールで走らせることで解決しました。
ローカルで Alexa スキルを動作確認したいと思っている人は多いだろうなと思い、ネット上で記事を漁っていると、やはり同じことを考えている人がちょこちょこいるようです。書かれたのが 2016 年 3 月と少し古いですが、ローカルで Alexa スキルを開発するための詳しい手順が書かれた Big Nerd Ranch さんによる Developing Alexa Skills Locally with Node.js: Implementing an Intent with Alexa-app and Alexa-app-server という記事を見つけました。
alexa-app は、Alexa スキルを開発するためのサードパーティ製のフレームワークです。基本機能としては Alexa からの JSON リクエストを簡単に扱うための API や Alexa へ返すレスポンスを簡単に生成するための API を提供してくれます。実装が少し楽になるにはなるのですが、ただそれだけのメリットだと、サードパーティ製ということもあり将来的なメンテナンスとか考慮すると使うのを躊躇するところです。しかし、これを使いたく一番大きな理由は、今回の課題である「ローカルでのスキル・テスト」と「スキル設定と Lambda Function の継続的インテグレーション」を実現するために必要な次の 2 つの機能を提供してくれるからです。
実機である Alexa 端末から Intent 処理のリクエストを AWS Lambda で受ける必要があるので、alexa-app で実装したスキルを AWS Lambda のハンドラとして連結することは当然可能ですが、このフレームワークは同じ実装コードを任意の Express サーバに接続することも可能です。これで Alexa スキルを Express アプリのようにテストできます。
たとえば簡単な Alexa スキルを alexa-app で書いてみます。
これは「Alexa、AbemaTV を開いて」と話しかけたときに Alexa に「人気の番組は何?、と訊いてください」と答えさせる処理を alexa-app で書いたものです。このテストを Jasmine で書くと:
const app = require('./app');
で読み込んでいるのが alexa-app のインスタンスです。これを Express に接続することで SuperTest などを使って通常の Express HTTP サーバをテストするのと同じ感覚でテストを書くことができます。
一点、注意なのですが、alexa-app インスタンスはそのままだと AWS Lambda に接続できないので、別途 index.js
などのエントリーポイントを作成して以下のようにハンドラに渡すようにしておきます。
alexa-app の Express 用のインターフェースを利用すればローカルでのデバッグ作業もかなり楽になります。
任意のリクエストに対するデバッグを行うときに、毎回リクエストを生成するコードを書くのは手間です。そこで alexa-app で書いたスキル用の Web サーバ alexa-app-server を使うと Web ブラウザから GUI で簡単にリクエストを生成できます。
alexa-app-server の設定は簡単です。まずプロジェクトに alexa-app-server を npm
か yarn
でインストールします。
プロジェクトのルート・ディレクトリ直下に apps
というディレクトリを作成して、そこに作成した alexa-app アプリのプロジェクトを移動します。(シンボリックリンクでも構いません。)
alexa-app-server は alexa-app インスタンスのモジュールを探すときに package.json
の main
プロパティの値をパスとして確認します。もしプロジェクトに package.json
がない場合や main
プロパティの値が alexa-app インスタンスのファイル・パスを指していない場合は変更します。
サーバの設定を記述します。alexa-app-server をインストールした方のプロジェクトのルート・ディレクトリに index.js
という名前でファイルを作成し次のように記述します。
保存したら起動してみます。
http://localhost:8080/alexa/sample-alexa-skill にアクセスすると自分が作ったスキル向けの JSON リクエストを生成できるインターフェースが表示されます。ここでスキルに実装済のインテントをプルダウンで設定したり任意の値を入力できるので、ローカルで効率的にスキルをデバッグすることが可能です。
スキルを多言語対応する場合、Alexa からのリクエストにロケール情報が入っているので、それを使って地域/言語別にレスポンスを変更できます。
alexa-app-server はロケール別のリクエスト切り替えがとても簡単なため多言語対応に関しても重宝します。通常、ロケールを頻繁に変更しながらのテストは大変です。Amazon Echo などの端末は Alexa の管理コンソール での登録時にしかロケールを変更できないように見えますし、Amazon 開発者コンソールのシミュレータも言語ごとに分けられているため、ロケールを頻繁に変更するテストには向いていません。
alexa-app-server のインターフェース上、まだ ja-JP
ロケールがオプションから選択できません。フォークして ja-JP
をオプションに追加したものを使っています。こちらプル・リクエスト中。
alexa-app を使うことで Express に連結してローカルで擬似的にテスト・デバッグできる範囲が広がり、複数人での平行チーム開発も可能になりました。しかし、チームで平行開発できるようになると今度はインテグレーションが問題になってきます。特に Alexa スキルの場合、Lambda Function とは別にスキル設定を Amazon 開発者コンソールで管理しているので、Lambda Function 用の最新コードにスキル設定が一致しないことが発生します。そういった不一致を発生させないために:
などが実現できれば嬉しいです。前者については、スキル情報とスキルに紐づいた Lambda をまとめて操作できるコマンドラインツールの Alexa Skill Kit Command-line Interface (ASK CLI) を使うことで同期を取ることができます。後者については、ソースコードのバージョン管理に Git/GitHub を使っているのであれば、GitHub との連携が簡単な CI ツールで ASK CLI を走らせれば実現できます。本記事では、CircleCI を使います。
ASK CLI を使うために Amazon Developer アカウント と AWS ユーザーの認証が必要です。今回は AWS Lambda 用のコードも含めてスキル管理したいので、ASK CLI から AWS を使える状態にする必要があります。
ASK CLI の認証時に AWS の認証情報を紐づけたいので、先に AWS ユーザーを認証します。(AWS CLI を既に使ったことがある方で認証済のプロファイルがある場合は、ここは読み飛ばしていただくのが良いでしょう。)
まず AWS CLI をインストールします。Python パッケージで提供されているので、 pip
などでインストールします。
インストール完了後、 which aws
などでパスが表示されることを確認できたら、次に AWS のユーザー認証を行います。AWS のアカウントがまだない場合は アマゾン ウェブ サービス(AWS) で作成します。AWS のアカウントを持っている場合は、ログインして IAM Management Console サービスの Users で AWS CLI 用のユーザーを作成します。作成時に表示される AWS Access Key ID と AWS Secret Access Key をメモしておきます。
Alexa スキルに紐づける Lambda Function を作成したり、IAM の操作も許可する必要があるので、作成したユーザーに下記のポリシーを追加します。
作成した AWS CLI 用のユーザーで AWS CLI を認証します。認証は aws configure
コマンドで行います。
Default region name
ですが、Alexa Skills Kit のドキュメントに以下のように書いてあり、限られたリージョンでしか AWS Lambda の Alexa Skills Kit のサポートをしていないので注意が必要です。
Lambda functions for Alexa skills can be hosted in either the US East (N. Virginia) or EU (Ireland) region. These are the only regions the Alexa Skills Kit supports.
これで AWS CLI が認証できました。認証情報が ~/.aws/config
と ~/.aws/credentials
に保存されていれば OK です。
次に ASK CLI を Amazon Developer アカウントに認証します。 ask init
コマンドを使って AWS CLI でユーザー認証したプロファイルと紐づけながら Amazon Developer アカウントへの認証手順が進みます。
初めて実行する場合は下記のようなダイアログが表示されますが、既にデフォルトのプロファイルがある場合は、新規プロファイルを作成するのか、既存プロファイルを上書くのかを尋かれるダイアログが表示されます。
ここで紐づけたい AWS のプロファイルを選択します。例では、先程 aws configure
でユーザー認証したときプロファイル指定をしていないので、 default
という名前で保存されているので、 default
を選択します。
Web ブラウザが起動し、「Login with Amazon」のページで表示されるのでログインします。
次に権限の確認をされるので、問題なければ「Okay」ボタンをクリックします。
無事ログインが成功すると、Sign in was successful. Close this browser and return to the command line interface. というメッセージでブラウザを閉じろと言われるので閉じます。
ASK CLI の認証情報に関しては ~/.ask/cli_config
に保存されています。
ユーザー認証が通ったので、ASK CLI でスキル全体を管理できるようにプロジェクトを新規作成します。
これで新規の Alexa スキル・プロジェクトの雛形が作成されます。tree コマンドを実行すると次のようなディレクトリ・ツリーが表示されるはずです。
この中で重要な各要素の役割はざっくりと次の通りです。
要素 | 役割 |
---|---|
lambda | Lambda 用のコードを格納するディレクトリ |
models | Intent Schema や Sample Utterance などを格納するディレクトリ |
skill.json | スキルの申請に必要な情報を記述するファイル |
雛形ができたので、スキルに必要な情報を設定していきます。
まず Alexa は Apple の App Store などと同様にスキルを公開するのに申請が必要なので、skill.json
にこのスキルの名前やこのスキルを使うためのフレーズ例など、申請に必要な情報を記述します。
次に lambda
ディレクトリには、今回 alexa-app で作った AWS Lambda 用モジュールを格納します。生成された lambda
ディレクトリ以下の雛形は必要ないので custom
ディレクトリごと削除してしまい、代わりに AWS Lambda 用モジュールのディレクトリを custom
という名前でここに移動します。
最後に models
ディレクトリにスキルの Intent のデータ構造を示した Intent Schema と Sample Utterance の情報を格納する必要があるのですが、これらは alexa-app フレームワーク上の実装コード内に記述されています。なので、フレームワークの API を使って JSON ファイルとして出力するスクリプトを書きます。
このスクリプトを実行した出力をロケール ID をファイル名にした JSON にパイプします。日本語であれば models/ja-JP.json
にパイプします。
ja-JP.json
の中身はこんな感じです。
多言語対応する場合は、必要な分、別のロケール ID の JSON ファイルにパイプします。ロケール ID をファイル名にした JSON ファイルを複数 models
ディレクトリに入れておくことにより、ASK CLI が言語別のスキル情報として登録してくれます。ここでは日本語と英語に対応するために ja-JS.json
とは別に en-US.json
を書き出します。
ここでは、環境変数 APP_LOCALE
に応じて Sample Utterance が切り替わるように alexa-app の Intent を実装しました。 en-US.json
の Sample Utterance 部分などが差し替わって出力されます。
言語に関する設定は models
ディレクトリのほかに skill.json
ファイルにも記述する必要があるので、必要に応じてロケール情報を追加しましょう。
これで alexa-app で実装した多言語対応スキルを ASK プロジェクトとして管理できるようになりました。
作成した ASK プロジェクトを Amazon Echo などの実機で試すためには、Alexa スキルとしてデプロイする必要があります。ASK CLI の deploy
コマンドを使うだけです。
Alexa スキルがデプロイされたことを確認するため、Amazon 開発者コンソール に行き、Alexa Skills Kit のスキル・リストに sample-alexa-skill
が登録されているか確認します。
ASK CLI で Alexa スキルをデプロイできるところまで来たので、あとはこの手順を CI ツールに設定すれば継続的にテストしたりデプロイしたりすることができます。もちろん CI ツールは何でも構いませんが、ここでは GitHub に簡単に連携ができる CircleCI を使って、GitHub Flow ベースで単純で DevOps な感じの運用ができればいいなというイメージ。
CircleCI に処理を書いていく前に、スクリプトの実行手順に若干の依存関係ができてしまったので、明示的に手順を示す意味で Makefile にまとめます。
先程も説明した通り、多言語対応する場合は、環境変数別に gen-interaction-model.sh
を走らせて言語別の Model を書き出す処理もまとめておきます。
デプロイ時に必要な処理もまとまったので、CircleCI プロジェクト用にインテグレーション処理とデプロイ処理を書いていきます。ASK プロジェクト・ディレクトリ直下に .circleci/config.yml
ファイルを作り、以下のような YAML でジョブを記述します。
テストの実行やらコード・カバレッジの取得やらしていますが、前述したようにデプロイに関しては
のように自動化したかったので、CircleCI で GitHub の master
ブランチへのマージのタイミングで下記 3 点の処理を実行するようにタスクを記述しています。
ローカル同様、CircleCI 上でも AWS と ASK の認証が必要です。それぞれ CircleCI 上でインストールして認証情報を設定します。
まず ASK CLI で AWS Lambda をデプロイするために必要なので AWS CLI をインストールします。AWS CLI を認証するために環境変数 AWS_ACCESS_KEY_ID
と AWS_SECRET_ACCESS_KEY
を参照しています。なので、CircleCI 側の環境変数に両者を登録しておきます。
ASK CLI に関しては、先程の認証情報が ~/.ask/cli_config
に記述してあるので Base64 にエンコードしてクリップボードにコピーします。Mac 系の OS なら pbcopy
できるのでこんな感じです。
クリップボードの中身を CircleCI 側の環境変数 ASK_CLI_CONFIG
として設定します。これで GitHub にホストした master
ブランチに変更をマージする度に Amazon 開発者コンソールの Alexa スキル情報と紐づいた AWS Lambda Function コードが最新状態に更新されるようになりました。
Alexa スキルを実際に公開するためには、スキルを申請して審査を通過する必要があります。ASK CLI は申請もコマンドラインで送信できるので、それも自動化したい人は ask api submit
コマンドなど必要な手順を CI プロセスに追加してもいいかもしれません。
Alexa スキルは Web 技術を使って簡単に開発を始めることができます。新しいデバイスなので勝手が掴めず、動作確認等々が大変な部分もあって最初はちょっととまどいましたが、探せば開発を助けるツールの恩恵を受けることができ、少しずつ開発しやすい環境を構築できるようになってるなと感じます。本記事がこれから Alexa スキルを開発をする人の効率化の参考になれば幸いです。
本記事の内容のサンプルコードは下記に上げてあります。
前回「フロントエンドエンジニアのための動画ストリーミング技術基礎」では HTTP ベースのストリーミング技術に関して勉強会を実施しました。視聴者に映像を届けるためのストリーミング技術に関してのお話でした。
本記事は、AbemaTV の生放送番組で撮影機材から送られた映像がエンコーダーを介してリアルタイムに放送する部分について勉強会を実施した際の資料です。
AbemaTV では生放送で撮影した動画データのやりとりに RTMP というプロトコルを利用しています。
RTMP は Real-Time Message Protocol の略で、その名前の通りリアルタイムにコミュニケーションを行うためのプロトコルです。Web 業界では Photoshop などでお馴染の Adobe Systems 社が開発しています。Adobe Flash Player がメディア配信サーバーとの間で音声や動画などのデータをやりとりするためのストリーミングのためのプロトコルとして開発されました。
前回紹介した HLS や MPEG-DASH もストリーミングプロトコルでしたが、RTMP はこれらと異なり、HTTP ベースではありません。ですので、HLS や MPEG-DASH のように通常の Web サーバーでコンテンツを配信できるわけではなく、専用の RTMP サーバーが必要になります。
HTTP における通信は必ずクライアントのリクエストから始まります。そのため、動画をストリーミングしようとする際、クライアントはサーバーに対して任意のインターバルで動画のセグメントをリクエストし続ける必要があります。HLS や MPEG-DASH におけるストリーミングはこの方式ですが、クライアントのタイミングで動画データをリクエストするため、本当の意味でのリアルタイム性はありません。動画データを生成しているサーバー側が送信したいタイミングでクライアントにデータをプッシュできる方が遅延が発生することがなく、効率的です。
それに対して、RTMP におけるデータ通信は持続的に接続した状態で双方向に行われます。そのため、サーバーがクライアントに送信したいタイミングでプッシュ送信することができ、遅延が発生しません。
また HTTP の場合、HTTP レスポンスヘッダーは冗長で一般的に数百バイトになります。返したいペイロードサイズに対してのオーバーヘッドを大きくしてしまっています。それに対し、RTMP パケットのヘッダーは固定長で 12 / 8 / 4 / 1 バイトのうちどれかになり、ペイロードサイズに対するオーバーヘッドは小さいです。
AbemaTV の生放送の現場では撮影機材で撮れた映像を Wirecast などのエンコーダーでエンコードし、それを RTMP 通信で Wowza などのメディアストリーミングサーバーに届けています。そして、メディアストリーミングサーバーに届けられた映像を確認しながら、生で撮影しているその映像に対して遅延を極力少ない状態で CM 入りや視聴者参加型コンテンツなどのトリガーとなるシグナルを通信できる環境を構築しています。
RTMP にはいくつかの派生種があります。
RTMP は一般的に 1935
ポートを使用します。しかし、セキュリティの厳しい環境ではこの 1935
ポートが使えないこともしばしばあります。そのため、HTTP( 80
ポート)や HTTPS( 443
ポート)を装って通信するという手段を取ることが可能です。それが RTMPT と RTMPS になります。
RTMP は TCP を利用したプロトコルですが、別に RTMFP という UDP を利用したプロトコルもあります。UDP を利用するプロトコルは TCP を利用するプロトコルと比べて通信速度面において利点があります。TCP はパケット・ロストに対して再送する仕組みですが、UDP はパケット・ロストに対して再送することはありません。その分再送のオーバーヘッドなく通信することができます。
RTMP は双方向のデータ通信が可能なプロトコルですが、両方向とも送信するデータ量が大きな通信サービスを構築する場合は UDP ベースの RTMFP が好まれます。たとえばテレビ会議やビデオチャットなどは双方が送信するデータが動画のため、データサイズが大きいにも関わらず、スムーズなコミュニケーションのためにリアルタイム性が求められます。
TCP で パケット・ロストによる再送で遅延の頻度が高まると、音声や映像が遅れた状態になる可能性が高くなります。特にテレビ会議やビデオチャットなどはデータの抜け落ちが発生したとしてもノイズ程度の劣化として許容できる場合がほとんどなため、UDP での通信が向いています。
RTMP でストリーミング配信するには、専用の RTMP サーバーが必要です。ここでは RTMP をサポートする代表的なメディアストリーミングサーバーを3つ紹介します。
Adobe Systems 社が開発しているメディアストリーミングサーバーです。Flash 技術の総本山である Adobe が開発しているだけあり、ここで紹介するメディアサーバーの中で一番知名度が高く、機能も豊富です。そしてその分ライセンス料も高いです。(RTMFP のサポート有無など機能数に応じて複数のエディションに別れています。)
Wowza Media Systems 社によって開発されているメディアストリーミングサーバーです。元 Adobe Systems の社員がスピンアウトして立ち上げたこともあり、Adobe Media Server との互換性が高く、ほぼ同等の機能を持っています。それにも関わらずライセンス価格は Adobe Media Server と比較するとかなり安価なこともあり、AbemaTV でも生放送のストリーミングサーバーには Wowza Media Server を使用しています。
Java で実装されたオープンソースの RTMP プロトコルをサポートするメディアストリーミングサーバーです。Adobe Media Server にかなり似せて作られていて、単体のサーバーとしては同等の機能を提供してくれますが、クラスタリング構成にあまり対応していないため、大規模な配信サービスを構築する場合には注意が必要です。
Web ブラウザは RTMP をネイティブでは対応していません。ブラウザ上で RTMP を使用するためには、プラグインとして Adobe Flash Player を使用する必要があります。
まずは RTMP ストリーミングサーバーを構築します。先述したメディアサーバーを使用したいところですが、今回は単純なストリーミング機能のみを提供できれば良いので、NGINX をメディアストリーミングサーバーとして使うことができる nginx-rtmp-module を使います。
nginx-rtmp-module を追加してコンパイルした NGINX の Docker イメージを作ります。ベースイメージには Alpine Linux を使います。ここでは RTMP 用に 1935
ポートと Flash アプリケーションを読み込むための HTML を返すために HTTP 80
ポートを開けるようにします。
下記の Dockerfile では、コンパイルに必要なパッケージを apk
でインストールして、任意のバージョンの NGINX と nginx-rtmp-module の Tarball をダウンロードし、コンパイルしています。 ./configure
のパラメータがやたら多いですが、必要ないモジュールを除外しているだけです。
イメージにコピーする nginx.conf
は下記のように設定します。 http
コンテキストに加えて nginx-rtmp-module
で使用できるようになった rtmp
コンテキストに設定を追加しています。
この rtmp
コンテキストの設定により、ローカルに Docker コンテナを立ち上げたとき rtmp://localhost:1935/live
という URL で RTMP サーバに接続が可能になります。
次に RTMP プレイヤーとそれを表示する HTML を作成します。RTMP プレイヤーは Flash アプリケーションとして実装するので ActionScript で書きます。まず新規ファイル Player.as
を作成します。
Player.as
に Player
クラスを作成します。動画を表示するための Video
オブジェクトも追加したいので、 Sprite
クラスを継承しておきます。
次に Stage
の設定します。Flash コンテンツを左上に整列する設定だけします。
NetConnection
クラスを使い、クライアントとサーバー間の双方向の接続を作成するための準備をします。 NetConnection
オブジェクトのステータスが変化したタイミングで、メディアサーバーからのデータを再生できるように、 NetStream
クラスを使ってストリームチャネルを開きます。開いた後ライブストリームを再生するために ns.play()
メソッドを実行します。このときストリーム名として "test"
を渡していますが、これは後程ライブストリームを作成する際にも使う名前になります。
作成したストリームチャネルからの動画を表示するために Video
オブジェクトに取り付けます。
この Video
オブジェクトもコンストラクト時に作成し、ステージに追加してきます。
NetConnection
の準備が整ったので、最後にメディアサーバーに接続します。URL は先程の NGINX の RTMP メディアサーバーの live
application に向けています。
これで RTMP サーバーの実装はできたので、次は ActionScript をコンパイルします。コンパイルには Apache/Adobe Flex SDK の Node.js モジュール版である node-flex-sdk を使用します。まずは NPM でインストールします。
無事インストールできたら、mxmlc
というコマンドを使って、 Player.as
から Player.swf
をコンパイルします。
作成された Player.swf
を表示する HTML を作成します。
Docker コンテナの構成に必要なファイルが揃ったので、これでイメージを作成します。
イメージが作成できたか確認しましょう。
無事に作成できたら、そのイメージから Docker コンテナを起動します。
Web ブラウザで http://localhost/ にアクセスしてみましょう。
黒いボックスが表示されたと思います。このボックスが RTMP プレイヤーなのですが、今は配信するストリームが存在していないため、何も再生できず黒い状態です。ですので、次は再生するストリームを作成します。
ここでは、Open Broadcaster Software (OBS)というオープンソースのライブストリーミング用のツールを使用してストリームを作成します。
OBS を起動して、「Settings」ボタンをクリックします。
「Settings」ダイアログが表示されるので、左側のペインから「Stream」を選択します。すると「URL」と「Stream key」を入力する画面に切り替わりますので、「URL」に NGINX の RTMP サーバーの live
application の URL を入力し、「Stream key」には先程 RTMP プレイヤーを実装したときにストリーム名として指定した test
を入力します。
「Settings」ダイアログで「OK」をクリックしたら、次に「 Sources」の「+」をクリックして適当なメディアソースを追加します。プルダウンメニューが表示されるので「Media Source」を選択して任意の動画ファイルを追加します。
メディアソースが追加されたら、「Start Streaming」ボタンをクリックしてストリーミングを開始します。
ストリーミングが開始されたら、Web ブラウザに戻ります。すると OBS でストリームしている動画がブラウザの方でも再生されていることが確認できます。
ここまでで RTMP でサーバー側からプッシュされたデータをクライアントで再生する実装をしてきました。しかし、RTMP は双方向のデータ通信が可能なので、クライアント側からサーバー側のメソッドを呼ぶことも可能です。 nginx-rtmp-module
では難しいですが、メディアサーバーに Adobe Media Server や Wowza Media Server を利用して開発をした場合、クライアント側からサーバー側のメソッドを呼ぶことが可能です。
ActionScript の場合、クライアントとサーバー間の双方向の接続が作成した後( NetConnection
オブジェクトが nc.connect()
して、 NetStatusEvent
が "NetConnection.Connect.Success"
になった後)であれば、 nc.call()
でサーバー側のメソッドを呼ぶことができます。 nc.call()
の第1引数がメソッド名なので、 nc.call("doSomething")
のようにクライアントから実行した場合、メディアサーバーに実装した該当のメソッドが実行されます。
たとえば Wowza Media Server の場合であれば、実装は Java なので下記のようなメソッドを実装することで、クライアントから Wowza Media Server のコンソールに doSomething is called
と表示させることが可能です。
AbemaTV の生放送番組では、RTMP の双方向通信を利用して、Web ブラウザから Wowza Media Server のメソッドを呼ぶことで、番組の進行具合に合わせて CM 入りのタイミングや視聴者参加型のインタラクションコンテンツのトリガーを最小限の遅延で放送に挿し込んでいます。
メディアサーバーはエンコーダーから RTMP 通信で送られた動画をそのまま RTMP でクライアントにプッシュ送信する以外に HLS や MPEG-DASH でストリーミングできるように変換することも可能です。たとえが nginx-rtmp-module の場合は先程作成した nginx.conf
を編集して、 application live
コンテキストに HLS に関する以下のディレクティブを追加します。
すると、Live セッションの m3u8 プレイリストと 1 秒感覚のセグメントファイルを /usr/local/nginx/html/hls/live
に出力してくれます。この nginx.conf
を反映した Docker コンテナが起動している状態で、http://localhost/hls/test.m3u8 に Safari でアクセスすると HLS でストリーミング再生ができます。(Safari でアクセスする理由は前回書いた通り、HLS をネイティブサポートしている Web ブラウザが Safari だけだからです。)
普段の Web フロントエンドの開発では、RTMP や ActionScript を扱う必要があることはあまりありません。しかし、こと動画やストリーミング領域となるとまだ Flash テクノロジーの安定性にお世話になることも多いように思います。WebRTC や WebSocket などの技術の組み合わせでこのあたりの事情もどんどん変化していきそうです。
Web サービスが HTML を介して提供するコンテンツはテキスト、画像、音声、動画などいろいろありますが、テキスト以外のデータは HTML にインラインで返したりせず、基本的には外部ファイルとして非同期に取得されることがほとんどだと思います。
HTML 内の img
要素の src
属性に表示したい画像ファイルのパスを指定することで、Web ブラウザはその画像をリクエストし、ダウンロードしたデータをデコードして画像として表示します。
動画の場合も同じです。video
要素を使って img
要素と同様に src
属性に動画ファイルのパスを指定します。
画像と違い、動画コンテンツはデータ容量がとても大きいため、データをダウンロードして再生するまでに待ち時間が発生します。
動画のデータ容量が大きい理由はとても単純で、動画は画像データが集合したものだからです。静止画像を人間の目が滑らかに感じられる速さで切り替えて表示することで絵を動かすという表現を実現しています(よくパラパラマンガに例えられますが、そんな感じです)。この人間の目が滑らかに感じる速さというのが 1 秒間に 30 枚だったり 24 枚を切り替えることになります。29.97 (≒30) fps とか 24 fps とかの数字を耳にしたことがあるかと思いますが、24 fps の場合は 1 秒間(s)の間(p)に 24 フレーム(f)を切り替えることを意味します。
データを全て自分の端末にダウンロードしてから再生しようとすると、かなり長い待ち時間が発生してしまいます。もし 2 時間の映画を見ようと思ったら 172,800 (= 24 フレーム * 60 秒 * 60 分 * 2 時間) 枚の画像をダウンロードするのを待つことになります。しかも動画を構成する要素は画像だけではなく、音声データも含まれるため、純粋な情報量としてはそれ以上になります。
動画データを全てダウンロードしてから再生するのではなく、ダウンロードしたデータで再生できる部分から再生を始め、同時に残りのデータをダウンロードしていく方式を、ストリーミング再生といいます。長時間の動画でもダウンロードしながら再生することができるので、再生するまでの待ち時間を短かくすることができます。
また、ストリーミングでは動画を途中から再生することも可能にします。2 時間映画のたとえば 1 時間経ったあたりから見たいとき、1 時間経過した部分からデータをダウンロードし始め、再生を始めることができます。
ストリーミング再生は、映像を配信する側と映像を再生する側で、データをどのような手順で通信するかをあらかじめ決めて、その手順通りに両者がデータを処理することによって実現します。その通信手順のことをストリーミングプロトコルと呼びます。ここでは AbemaTV で使用しているはストリーミングプロトコルを 2 つ説明します。
HTTP Live Streaming はアップル社が自社プロダクトである QuickTime、OS X、iOS、Safari 向けに開発したストリーミングプロトコルです。略して HLS と呼ばれるので、この記事でも HLS と表記します。その名前の通り、通信は HTTP で行われます。専用のプロトコルが必要ないため、通常の Web サーバーを用意するだけで配信ができてしまいます。
HLS を配信するために必要なファイルは、動画を数秒ごとの「MPEG-2 TS」形式のファイルに分割したセグメントファイル、それらをどの順番で再生するかを記したプレイリストだけです。
まず、プレイリストとセグメントファイルを作成します。ここでは ffmpeg というツールを使い、 input.mp4
というファイル名で保存されている動画から output.m3u8
というプレイリストと分割されたセグメントファイルを作成します。
すると、 output.m3u8
と output_****.ts
というファイルが作成されます。 output.m3u8
の内容は下記のようになります。
Web サーバーを起動します。ここでは Mac OS X にプリインストールされている Python2 を使用します。
Python2 の SimpleHTTPServer モジュールはデフォルトで 8000
番ポートを使用するので、Safari で http://localhost:8000/output.m3u8 にアクセスします。
ここでアクセスする Web ブラウザに Safari を指定しているのは、Safari 以外のメジャーブラウザでは HLS をネイティブサポートしていないためです。Safari 以外のブラウザで HLS を再生するには、Flash などのプラグインを使用するか、後述する Media Source API を使用して、JavaScript で追加実装する必要があります。HLS はアップル社が開発したということもあり、Safari だけは m3u8 をロードしてそのまま再生することができます。
MPEG-DASH は HLS と同様に通信に HTTP を使用したストリーミングプロトコルです。DASH は Dynamic Adaptive Streaming over HTTP の略です。Apple 社が開発した HLS のほかに Microsoft 社が開発した Smooth Streaming や Adobe が開発した HTTP Dynamic Streaming など HTTP ベースのストリーミングプロトコルがいくつかありますが、残念ながら各々互換性がありません。MPEG-DASH は ISO 国際標準規格 (ISO/IEC 23001-6) としてリリースされています。
MPEG-DASH も HLS 同様、通常の Web サーバーと動画のセグメントファイルとプレイリストを用意するだけで配信ができてしまいます。MPEG-DASH ではセグメントファイルは fragmented mp4 もしくは ts 形式、プレイリストは MPD(Media Presentation Description)と呼ばれる XML で記述されたファイルを用意します。
MPEG-DASH 用のセグメントファイルとプレイリストを用意します。今回はセグメントファイルは fragmented mp4 を使用することにします。まず、ffmpeg を使って動画を fragmented mp4 で映像の圧縮に使う「H.264/AVC」と音声の圧縮に使う「AAC」というコーデックでリエンコードします。コーデックについては後述します。
次に MP4Box というツールを使って、動画を分割してセグメントファイルとプレイリストを作成します。
プレイリスト output.mpd
と output.m4s
と連番になったセグメントファイル郡が作成されます。 output.mpd
は下のようになっています。
再び Web サーバーを起動します。
HLS とは違い、残念ながら MPEG-DASH をネイティブでサポートしている Web ブラウザはありません。後程 Media Source Extensions を説明するときに MPEG-DASH プレイヤーを作成するので、そこで確認したいと思います。
HLS と MPEG-DASH は HTML5 用の JavaScript API である Media Source Extensions を利用することで追加でプラグインをインストールすることなく、ストリーミング再生が可能です。
Media Source Extensions は MSE と呼ばれていますので、本記事でも MSE と表記します。MSE は W3C によって標準化されている HTTP ダウンロードを利用してストリーミング再生するために作られた JavaScript API です。
MSE で扱うメディアデータは、W3C で定められている仕様に従って、短い時間で区切ったデータ構造にセグメント化されている必要があります。MSE では、セグメントを 2 種類に分けて扱います。
MSE は最初に初期化セグメント、その後にメディアセグメントを順番にソース・バッファに渡すと、そのメディアセグメントの順番で再生していきます。
ここでは XMLHttpRequest
と MediaSource
API を使用して簡単な MPEG-DASH プレイヤーを作成して、先程 ffmpeg と MP4Box で作った MPEG-DASH コンテンツを再生してみます。
最初に id
をつけた video
要素を用意します。
次に XMLHttpRequest
で MPD を取得します。MPD は XML ファイルなので、パースして Representation
要素から MIME タイプやコーデックの情報を取得しておきます。
次に MediaSource
API で最初に用意した video
要素を拡張し、ソースとしてダウンロードした動画のセグメントを追加できるようにします。
ソース・バッファを作成し、初期化情報が入ったセグメントとメディア本体のセグメントを追加できるように準備します。
先に取得した mpd
から Initialization
要素の sourceURL
の値を取得し、 XMLHttpRequest
で取得します。セグメントファイルはバイナリデータなので、 responseType
をarraybuffer
に指定しておきます。
そしてセグメントをロードしたタイミングでソース・バッファに追加します。
初期化情報がバッファに追加されソースが更新されたら、続けてメディア本体のセグメントファイルを取得し、ソース・バッファに追加します。この処理をメディアセグメントの数だけ繰り返します。
HLS や MPEG-DASH などのストリーミング配信では、セグメントファイルが実際の動画データになります。本記事の最初に書いた通り、動画データの容量は大きいです。ストリーミング配信の仕組みだけでは、動画の再生開始までの待ち時間は短かくすることはできても、再生を続けるために必要な1秒あたりのデータ量は減らすことはできません。ストリーミング再生では 1 秒あたりに必要なデータ量を少なくとも 1 秒以内に取得し続ける必要があります。でないと再生を継続できません。
同じ情報量を表現するデータの容量を小さくしたい場合、データに圧縮処理をかけます。圧縮のアルゴリズムはいくつもありますが、動画は映像と音声で構成されているため、映像の圧縮に適したアルゴリズムと音声の圧縮に適したアルゴリズムは異なることを考慮する必要があります。映像圧縮に適したアルゴリズムで処理した映像ファイルと音声圧縮に適したアルゴリズムで処理した音声ファイルを1つのファイルとしてまとめたものが動画ファイルです。
動画ファイルは映像ファイルと音声ファイルをまとめたものと説明しましたが、このまとめ方の形式のことをコンテナフォーマットといいます。また、映像データや音声データを圧縮するアルゴリズムのことをコーデックといいます。
コンテナフォーマットは一般的にコンテナと略します。コンテナと呼ぶと難しそうですが、コンテナはファイルフォーマットの1種なので、私たちが普段動画ファイルとして意識している単位と一致します。ファイルフォーマットとはファイルの保存形式のことです。以下にリストしたものが代表的なコンテナですが、聞いたことがある名前が多いと思います。
コンテナは映像と音声データがどのように格納されるのかを定義しています。また動画は映像と音声を同時に再生する必要があるため、両者の同期を取るための情報もコンテナが格納しています。ほかにも動画タイトルや説明などのメタ情報、字幕などの情報もコンテナが格納されている場合があります。
コンテナは対応しているコーデックの映像と音声データのみ格納することができます。1つのコンテナがいくつかのコーデックに対応している場合も多々あるので、コンテナの種類が分かっても格納されているコーデックの種類は分かりません。そのため、動画プレイヤーが同じコンテナで保存された2つの動画ファイルのうち、片方だけ再生できるということもあります。
たとえば Flash 動画のコンテナである FLV は映像コーデックとして「Sorenson Spark」と「H.264/AVC」を格納できます。もし動画プレイヤーが「Sorenson Spark」には対応していても 「H.264/AVC」には対応していなかった場合、「Sorenson Spark」を格納している FLV ファイルは再生できても、「H.264/AVC」を格納している FLV ファイルはコーデックエラーが発生して再生できません。
映像や音声は圧縮する必要があります。特にストリーミング再生などのデータ通信と再生を同時に行うような場合は必須です。コーデックはその圧縮のアルゴリズムです。
なぜ映像を圧縮する必要があると言うと、映像はたくさんの静止画をパラパラマンガのようにめくって人間の目に物体や背景が動いているように見せているので、このたくさんの静止画は情報量として膨大なのです。
映像を構成する画像データはラスタという色のついたピクセルの集合で表現します。1 ピクセルの情報量は 24 bit で表現できます(24bit フルカラーの場合、R -赤- G -緑- B -青- の各色成分につき 256 段階の指定ができるため、1 ピクセルは Math.log2(256 * 256 * 256) = 24 bit
の情報量が必要)。
そうすると例えば、フル HD の 1 フレームを構成する 1920 * 1080 ピクセルの情報量は 49,766,400 (= 24 * 1920 * 1080) bit になります。これはまだ 1 フレームなので、24 fps の動画の場合、1 秒間に 1,194,393,600 (= 49,766,400 * 24) bit が必要になります。
これは 1 秒間に 1,194 Mbit のデータを通信を介して取得する必要があるということになります。しかし、例えば受信実効速度が 76.6Mbps と記載されているソフトバンク提供の超高速データ通信サービス SoftBank 4G LTE でデータ通信をした場合でも、 1 秒間に取得できるデータ量は 76.6 Mbit なので、先程の 1,194 Mbit に遠く及びません。
しかし、この 1,194 Mbit の映像データは「H.264/AVC」というコーデックで圧縮した場合、典型的な圧縮率としては 1/100 のデータ量に圧縮することができます。すると 12 Mbit 程度になるので、76.6 Mbps のデータ通信速度でも視聴が可能になります。
この「H.264/AVC」は AbemaTV でも映像コーデックとして使用していますが、映像コーデックにはほかにも以下のような種類があります。
ここでは映像コーデックしか取り上げませんが、音声コーデックは代表的なものに「AAC」や「MP3」があり、AbemaTV では「AAC」を使用しています。
コーデックはデータ量を圧縮するものですが、ただデータ量を減らせればいいのではなく、人間が知覚できる範囲の画質や音質を落とすことなく圧縮しなくてはいけません。なので、選択するコーデックが悪いと画質や音質を落とすことになります。
「MPEG-2 TS」は MPEG-2 システムのうち放送・通信用のコンテナです。地上波デジタル放送でも使用されているコンテナですが、HLS でも「MPEG-2 TS」を使用します。DevTools の Network パネルを開いた状態で AbemaTV の動画を視聴しているとたくさんの **.ts
という拡張子のデータがリクエストされるのが確認できます。これが「MPEG-2 TS」のファイルです。
「MPEG-2 TS」は放送・通信用に作られたコンテナのため、通信途中でデータが途切れたとしてもちゃんと再生できるように設計されています。「MPEG-2 TS」では動画を 184 バイト単位のデータに分割し、それに 4 バイトの TS ヘッダと呼ばれるデータを付加して計 188 バイト固定長のパケットを連続で転送することでデータ伝送を行います。4 バイトの TS ヘッダのうち最後の 4bit は巡回カウンターと呼ばれるデータを持っていて、これがパケットごとに 1 ずつカウンターするため、これを検査することでパケットの欠落がないかを確認できるようになっています。
MPEG-2 システムには蓄積メディア用のコンテナとして別に「MPEG-2 PS」がありますが、こちらはデータが連続していることが前提なので、ランダムアクセスなどに優れた設計になっています。
AbemaTV では「MPEG-2 TS」コンテナに「H.264/AVC」コーデックで圧縮した映像データを格納しています。「H.264/AVC」は正式名称を「H.264」もしくは「MPEG-4 Part 10 Advanced Video Coding」といいます。(正式名称が2つあるのは ITU-T と ISO/IEC という2つの組織が共同で策定したものをそれぞれの名称をつけているだけです。)「MPEG-4」という名前が付けられている通り、その圧縮アルゴリズムの原理は、従来方式の「MPEG-1」、「MPEG-2」を継承しています。ここでは「MPEG」の圧縮アルゴリズムの原理を学んでいきます。
データを圧縮する基本は
です。単純な例で見ていきます。
たとえば、文字 a-d があったとき、それらを識別する符号を下記のように表現できます。
文字 | 符号 |
---|---|
a | 00 |
b | 01 |
c | 10 |
d | 11 |
文字列「bbabcbdbaacba」は「01 01 00 01 10 01 11 01 00 00 10 01 00」という符号で表現されます。この文字列を表現するのに必要なデータ量は 26(=2*13)bit です。この文字列にて、各々の文字の出現回数は均一ではありません。
文字 | 出現回数 | 出現率 |
---|---|---|
a | 4 | 0.31 |
b | 7 | 0.54 |
c | 2 | 0.15 |
d | 1 | 0.08 |
そこで出現回数が 1 番多い b に 1 番短い符号、2 番目に多い a に次に短い符号を割り当ててみます。
文字 | 符号 |
---|---|
a | 10 |
b | 0 |
c | 110 |
d | 111 |
すると先程の文字列「bbabcbdbaacba」は「0 0 10 0 110 0 111 0 10 10 110 0 10」と表現されますが、データ量が 23bit に減りました。このように、データの出現頻度が均一ではなく偏りがあると、異なる長さの符号を割り当てることによりデータ量を圧縮することができます。
このように可変長の符号を出現頻度に応じて割り当てることエントロピー符号といいますが、その割り当てパターンを作成する方法の 1 つにハフマン符号があります。
この図のように出現確立が高いものからツリー上に符号を割り当てていきます。これにて全ての文字が一意かつ瞬時に解読できる少ないデータ量の符号を作成することができます。
一見出現率に偏りがない場合でも情報の表現方法を変えることでデータの出現頻度に偏りを持たせることができます。
たとえば、「1 2 3 2 1 0 -1 -2」のような数列はそのままだと下記のような出現回数ですが、
数字 | 出現回数 | 出現率 |
---|---|---|
-2 | 1 | 0.125 |
-1 | 1 | 0.125 |
0 | 1 | 0.125 |
1 | 2 | 0.25 |
2 | 2 | 0.25 |
3 | 1 | 0.125 |
これを前の数字との差分として表現すると「1 2 3 2 1 0 -1 -2」→「0 +1 +1 -1 -1 -1 -1 -1」となり、データの出現頻度に大きな偏りを作ることができました。
差分 | 出現回数 | 出現率 |
---|---|---|
0 | 1 | 0.125 |
+1 | 2 | 0.25 |
-1 | 5 | 0.625 |
これをハフマン符号することでデータを圧縮することができます。
文字列や数列データの圧縮の例について見てきましたが、動画圧縮の場合も基本的な考え方は同様です。しかし、MPEG の場合は動画特有の性質を利用して圧縮率を高める工夫をしています。
MPEG の圧縮アルゴリズムは静止画の圧縮と映像の圧縮で構成されています。
MPEG の静止画の圧縮アルゴリズムの基礎は画像圧縮規格である「JPEG」です。**.jpg
の拡張子で馴染のアレです。
静止画の圧縮アルゴリズムは簡単に以下のようなことを行います。
たとえば空の写真を撮影した場合、その画像を構成するピクセルの多くは空の青色と雲の白色の微妙な色味の変化になると思います。空ほど色数が少なくない写真や絵の場合でも、基本的に画像は色が段階的にしか変化していないピクセルの方が出現頻度が圧倒的に多く、急な変化の頻度は少ないはずです。
この画像の性質を利用して、画像データの出現頻度に偏りを作って符号化することを DPCM 符号化といいます。
MPEG は離散コサイン変換という演算を行うことで、人間の目にあまり目立たない細かい情報をデータから取り除いてしまうことで圧縮率を上げています。離散コサイン変換は英語では Discrete Cosine Transform というので DCT と略されます。
DCT では画像を波形として扱い、フーリエ変換のように周波数ごとの波の強度で画像を表現します。ここで高い周波数の波は人間の目にあまり目立たない情報となるので、省略してしまうことで画質への影響を最小限に抑えながら圧縮率を高めることが可能になります。
ここまで静止画の圧縮について、DPCM 符号化と DCT の処理を見てきましたが、これらで求められた値をエントロピー符号することで更に圧縮効率を高めます。
静止画の圧縮では、映像における 1 枚 1 枚のフレームのデータ量を削減しました。映像の圧縮では、時間の流れを利用してデータの圧縮率を高める工夫をしています。
MPEG では画素情報を RGB ではなく、輝度信号(Y)、 色差信号(Cr)(Cb) で表現します。RGBの各成分、輝度信号(Y)、色差信号(Cr)(Cb)の関係は下記です。
人間の目は明るさの変化に対しての方が色の変化に対してより敏感です。MPEG ではその人間の視覚の癖を利用し、フレームごとに Cr と Cb 信号を画素の情報から省いています。Cr と Cb が少々省かれたとしても 明るさの情報である Y が省かれていなければ、人はそれ程違和感を感じないのです。
静止画の場合と似ていますが、映像の場合も時間的に隣合うフレームが持つ画像は似ているはずです。MPEG はその映像の特徴を利用して、映像のフレームにその画像を表示するための全ての情報を持たせません。MPEG には 3 種類のフレームがあります。
I ピクチャを除いて、他のフレームが持ってる情報と自身が持ってる情報を合わせて画像を表示することができるようになります。この 3 種類はそれぞれ役割りが違います。I ピクチャは画像を表示するための全ての情報を持っています。P ピクチャは過去に表示したI ピクチャもしくはP ピクチャが持っていたデータとその差分データを使用して画像を表示します。B ピクチャは過去だけではなく未来のI ピクチャもしくはP ピクチャが持っているデータを利用することでより圧縮率を高めます。
動画は昔からある技術分野ですが、Web のフロントエンドエンジニアだった自分には足を踏み込んだら分からないことだらけの難しい分野だと感じました。しかし、最近はストリーミング関連の技術も進み、Web においても動画を扱った事業に関わることが増えてきています。本記事は社内勉強会向けですが、フロントエンドエンジニア視点から動画を学んでいくスタートポイントになればと思います。
HTTP Live Streaming
https://en.wikipedia.org/wiki/HTTP_Live_Streaming
H.264
https://ja.wikipedia.org/wiki/H.264
MPEG-2システム
https://ja.wikipedia.org/wiki/MPEG-2%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0
HLSとは
http://qiita.com/STomohiko/items/eb223a9cb6325d7d42d9
ffmpeg で mp4 をiPhone用のストリーミング(HLS)に対応させる。
http://takuya-1st.hatenablog.jp/entry/2016/04/06/034906
MPEG DASHを知る
http://qiita.com/gabby-gred/items/c1a3dbe026f83dd7e1ff
MPEG-DASH content generation with MP4Box and x264
https://bitmovin.com/mp4box-dash-content-generation-x264/
Media Source Extensionsを使ってみた (MP4編)http://qiita.com/tomoyukilabs/items/54bd151aba7d3edf8946
動画・音声の規格について ~コーデック・コンテナ~
http://michisugara.jp/archives/2011/video_and_audio.html
VIDEO-ITを取り巻く市場と技術
http://www.mpeg.co.jp/libraries/video_it/index.html
動画形式の種類と違い(AVI・MP4・MOV・MPEG・MKV・WMV・FLV・ASF等)【コンテナ】
http://aviutl.info/dougakeisiki-konntena/
【動画が再生できない!?】そんなときに必ず役立つ5つの知識
http://smarvee.com/column/can-not-play/
「映像がH.264/AVCでエンコードされたFLV」を「FLV5」と呼ぶのは間違い
http://goldenhige.cocolog-nifty.com/blog/2009/10/h264avcflvflv5-.html
量子化行列のナゾ~その1
http://www.nnet.ne.jp/~hi6/lab/quantize/
モニタ解像度 図解チャート&一覧 / monitor resolution data sheet&chart
]]>コンポーネントリストやスタイルガイドは、フロントエンドエンジニアとデザイナーが協業して作る必要がありますが、この協業がなかなかうまくいかず、スタイルガイド運用をワークさせるのは難易度が高い印象がありました。
自分自身、今まで携ってきた開発プロジェクトにおいて、うまくワークしたと思えるスタイルガイド作りができなかったのですが、現在のプロジェクトで開発初期からスタイルガイドを作り、リリース後、運用フェーズまでワークさせることができました。
本記事ではフロントエンドエンジニアとデザイナーが協業してスタイルガイドを作り始めるにあたって気をつけたことについて書いていきます。
開発において、スタイルガイドが欲しい理由は、開発中のコミュニケーションにおいて、デザインを言語化できないことが多いからです。スタイルガイドの役割はデザイナー以外の職種にも通じる(デザイン要素に関する)共通言語として働くことです。そしてたぶんメインデザイナーとそれ以外のデザイナー間での共通言語としても働いてくれると思います。
1つの作業を2者で行う場合、2者の間に共通言語がないと、コミュニケーションコストは一気に高くなります。今の職場では、基本的にデザインとフロントエンドエンジニアリングは分業しているので、デザイナーがエンジニアリングを理解するか、エンジニアがデザインを理解するか、いずれかの状況でない限り(もしくはその状況であったとしても)、コミュニケーションコストは高いです。それを下げる意味でスタイルガイドを作ることは開発において大きな意味を持ちます。
しかし、たとえ本格的に開発が始まる前にスタイルガイドを作ったとしても、スタイルガイドがプロダクトのデザインで使えるものになっていなければ、いずれ誰からも参照されない状態になってしまいます。
過去に別のサービスを作っていた際、開発速度を上げるため最初にスタイルガイド/コンポーネントリストを定義し、 それを PSD ファイルで管理するようにしました。以降、画面デザインはそのコンポーネントを使って行う手法を試みました。しかし、残念ながらそのコンポーネントをそのまま使うことは少なかったです。
デザイナーがプロダクトの画面デザインを先に行うことなく、プロダクトの画面上で実際に起こる問題に対して、デザイン的な解決を汎用的なコンポーネントに落とし込むことができなかったからです。実際に画面上に定義したコンポーネントを置いたときに、解決すべき問題が解決できないことが頻発しました。
問題の解決を行うに足りるコンポーネントがないと、新しいコンポーネントを別途作るしかないので、コンポーネントは延々と増え続け、管理できなくなり、そのサービスをリリースする頃には誰もスタイルガイドを見ることはなくなっていました。
タイトなスケジュールでスタイルガイドを見直す時間もなかったのと、見直すこと自体を開発フローに組み込んでいなかったため、スタイルガイドは作っただけで意味のないものになってしまいました。
レギュレーションをしっかり決めた方が今後のデザインにブレがなくなるだろうという想いもあり、最初からスタイルガイドにいろいろと定義をつめこみすぎたために(そしてその定義が完璧とは程遠いため)、デザイナーも窮屈になってしまったのだと思います。デザイナーが早々にスタイルガイドを参照することをやめてしまっていました。
自分たちが思っていた以上にスタイルガイドを最初から完璧に作ることは難しかったのです。
そしてエンジニア主導でスタイルガイドを作った点もスタイルガイドが死んでいった要因だったように思います。そのときのデザイナーはスタイルガイドを作ることにあまり利点を感じていなかったように思います。今思えば、自分たちエンジニアも、デザイナーにスタイルガイドの利点を伝えきれていなかったと思うので、当然かもしれません。スタイルガイドがある開発フローに対して成功体験がなかったので、伝えきれるわけもないとも思います。
特に開発初期は、エンジニアが主導になってスタイルガイドを完璧に作ることはほぼ無謀に近いでしょう。これから実際にデザインを考えていくのはデザイナーです。しかし、スタイルガイドを作るというモチベーションはエンジニア側にあることが(少なくとも自分の周りでは)多いように感じます。
スタイルガイドを作るモチベーションがデザイナーよりエンジニア側にある場合、エンジニアは主導になるのではなく、ファシリテーターとして動くように意識すべきだったなと思います。
もろもろの後悔もあって、今のプロジェクトで開発を開始した時、なんとかリリースまで生き続けるスタイルガイドを作ろうと思いました。とりあえずちょっとした成功体験があるだけでも今後の視野が変わりそうだなと。
フロントエンドエンジニアの視点で言うと、デザインに関して、デザイナーしか判断できない事柄が多すぎるのは大きなツラミです。デザイナーしか判断できない状況は、Photoshop や Sketch のデータから読み取れない事柄において、全てデザイナーに判断を仰ぐ必要がある状態です。それはとてつもなくコミュニケーションコストが高いのです。コミュニケーションコストが高いからと言って、エンジニアが勝手に解釈して実装してしまえば、デザイナーに意図に沿わず、結局実装後に修正することになり、更に工数が膨れます。
コミュニケーションコストを下げるためにも、デザイナーの頭の中がプロジェクト全体に共有され、簡単なことであれば徐々にエンジニアでもデザインに関する判断を下せるようになっていけるのが理想です。生きたスタイルガイドがあれば、デザイン要素に関する最低限の判断は誰でもできる状態にすることができます。仕組みによって、判断しなくて良い状況を増やすからです。
そしてスタイルガイドを作る過程では、デザインを論理的な言葉に置き替えていく必要もあるため、その作業をエンジニアが一緒に行うことでデザイナーがビジュアルを通じてユーザーに伝えようとしている意図を言葉で理解する手助けになります。
過去の後悔を踏まえつつ、デザイナーとエンジニアが無理なく協業できる方法を工夫しようと考えました。特にデザイナーに負担が少ないように、できるだけ最初は既存のデザインフローを変えないように心掛けました。
開発開始時はスタイルガイドには何も定義がされていない状態にしました。ただ、これから定義していく項目だけ決めておくことにしました。中身がない箱だけ用意したイメージです。決める項目というのは、一般的な Web アプリケーションに最低限必要そうな要素です。
などです。今後決める、となっているのは、背景色やフォントサイズなどの値はこの時点で決めないからです。この時点では今後決める項目だけを決めます。
このタイミングで挙げた項目のほかにも必要になる項目は出てくるとは思いますが、必要になったときに追加すれば良いと割り切りました。定義する項目の粒度がパターンとして分かる程度にしておきます。こうしておくことで後から追加する項目の粒度も揃いやすくなります。
上記の項目を箱として用意し、実際の値としては定義していない状態で、スタイルガイドを Sketch データにします。
上の画像で NO SET
となっているところが実際の値が定義されていない項目です。この時点で決まっている値があれば定義し、決まっていない、分からない値については無理に決めず、 NO SET
としておきます。
そして、Sketch で設定した値を CSS のカスタムプロパティとして設定します。(今回のプロジェクトでは cssnext を使用しているため、カスタムプロパティとして設定しました。Sass や Stylus などでも変数で設定しても良いかもしれません。)
上のコードで none
となっているところは、Sketch で NO SET
としたところと同じ意味です。定義できている値のみ設定して、定義できていないものはプロパティ名だけ用意しておいて none
を設定しておきます。
プロダクトの UI コンポーネントにあてる CSS は基本的にこのカスタムプロパティを参照し、特別な理由がない限り、値をハードコードすることを避けるようにします。
ここで、最初に Sketch でスタイルガイドを作るのは、Sketch から始める方がデザイナーが心理的な障壁なく作業に入りやすいためです。最終的にスタイルガイドは HTML で管理する方がプロダクトのコードと同期しやすいので、本当は Sketch を介さず、最初から CSS のカスタムプロパティと HTML でドキュメント化できると、効率は良さそうにも思います。ただ、ステップをファシリテートするという意味では、Sketch や Photoshop などから始める方が良いように個人的には思い、今回のプロジェクトでは Sketch から始めました。
ここまでで、今後決める項目は決めることができたので、ここからは項目に実際の値を入れていきます。箱を埋めていく作業です。箱を埋める作業は各画面のデザインカンプを作りながら、必要に応じて進めていきます。
前回の投稿で、Interface Inventory をゆるく実践したと書きましたが、このように、デザイナーにはデザインカンプを通常通り作ってもらいながら、カンプ上で使用した色やフォントサイズなどで、定義していない値を使うたびに適切な項目への値として埋めていくようにしてもらいました。
フロントエンドエンジニアもカスタムプロパティを参照して CSS を書くので、定義されていない値がカンプに出現した場合は、デザイナーと話してその値をどの項目として定義するかを決めてから実装することにします。これを繰り返してスタイルガイドに定義する値を徐々に埋めていくことになります。
デザイナーの中には画面上に色などのデザイン要素を配置するとき、感覚で配置されている方もいると思います。そういったデザイナーとの協業だった場合でも、最初に埋めるべき箱がデザイン要素としての意味を持った名前とともに用意されているので、名前の意味と異なるカラーコードが出てきたら、そのカラーコードを見直すきっかけになります。
そして、既存のどの箱にも適切に入らない値が出てきたタイミングにのみ、新しい箱を用意します。
新しい箱に入れる必要がある新規の値がデザインカンプ上に出現した時に意識したいのが、箱を本当に新規で追加する価値があるのかを再度デザイナーと考えてみることです。今回も、既存の箱に入っている値では本当に画面上の問題を解決することができないのかということをデザイナーと時間をかけて一緒に考えました。
箱が増えるということは、プロダクトを通してのトンマナがブレるリスクがあるだけでなく、UI を通して送るユーザーへのメッセージの種類が増えることになります。基本的にユーザーへのメッセージの種類はシンプルな方が良いので、増やさなくて良いのであれば、それに越したことはありません。意図したメッセージが適切に伝わりづらくなるというリスクが増すからです。
それでも新規の箱が必要になった場合、箱の名前は適切な抽象度を保っているかを注意します。新規の箱は、それが必要になったデザインカンプの画面のコンテキストにすごく影響されて命名してしまうことが多いです。例えば、コメント一覧 UI の画面デザインで新規に必要になった背景色のカラーコードに対する箱の名前を --bg-comment
と命名するなどです。
「コメントの背景」という名前では画面の UI 上でどんなデザイン的にどんな働きをするのかが分かりません。もちろん、コメント系モジュールの背景はどんな画面にレイアウトしたとしても同じ色で統一するのであれば問題ないかもしれません。その色がコメントを表す特別な色という意味を持っているということであれば、 --bg-comment
という名前も適切かもしれませんが、実際は別の画面になればコメントより目立たせたい要素が別にあるなど、コメント系モジュールであっても、別の色が適切な場合は多いはずです。
その場合はデザイン的な働きを適切な抽象度を持った名前で表現することが大事だと思います。その画面上でユーザーに最も注目してもらいたい要素であれば、 --bg-highlight
という名前で コメント一覧モジュールの背景を塗るのが適切でしょう。
新規の箱に具体的な名前をつけて別項目として設定しまうことは簡単です。なので、その誘惑に駆られることは何度もありました。しかし、そうやってつけられた名前は適切な抽象度を持たないため、別の画面では使われない可能性が高くなる上に、別の箱を作る要因になるので、あっという間に人が管理できない個数へと箱が増えてしまいます。
適切な抽象度とは、色でいうと、基本色や強調色、警告色といったアプリケーションにおいて普遍な要素名に対してサービスのキャラクターづけがされた値を設定するようにします。これより具体性が高い要素名は、カンプ上に配置されたコンテキストに強く影響されている可能性が高いので、デザインレビュー時に再度名前の見直しをするようにしました。
サービスにおいてコアバリューに近いものに関しては具体性が高い要素名が適切な場合があるかもしれません。先程の例で言うと、コメント機能がサービスのコアバリューでほかの要素とは完全に差別化し、どの画面にいてもコメントが特別だと分かるように色を一色に統一したい、という場合は具体的な名前が適切でしょう。ただそういった名前は、サービス1つにつき多くても1、2個のように思います。
プロダクトの強調色や警告色など、全ての色は2パターン決めておくとよいです。スタイルガイドは背景色を白で用意することが多いと思いますが、もしプロダクトのベースが白であっても、配置するモジュール郡の中には暗い色のものもあると思います。
スタイルガイドで定義したカラーコードが、これら暗い色のモジュールを考慮していないと、明度差が足りなくて視認性や可読性が下がったりして、強調色が強調色として働かない可能性が出てきます。
そこでスタイルガイドには最初から明るいパターンの背景上に乗るもの用と暗いパターンの背景上に乗るもの用を用意しておき、色系の要素には常に2パターンの箱がある状態にしました。
CSS のカスタムプロパティも2パターン用意します。
最終的にHTML (というか React と JSX)で作ったコンポーネントリストでは、実際に明るい背景と暗い背景の上にコンポーネントを置いて管理しました。これにより明るい背景では使えるけど、暗い背景ではそのまま使えないコンポーネントを一目で確認できるようになります。
空き箱を適切な抽象度を意識して用意することで、スタイルガイドの運用はうんと楽になりましした。以前は自分の中に、スタイルガイドは(ガイドというくらいなので)UI デザインを先導するものであるべきという意識がありました。先にスタイルガイドがあって、デザインがそのガイドに従って作られるべきと。でも、自分たちはある意味未知のものを創っているのに、それを先導するものを作る難易度はとても高いです。今回は作ってみたデザインカンプからガイドとなるパターンを見つけていく仕組みを緩く作ることで、スタイルガイドが以前よりもワークするようにできました。
今回のプロジェクトでスタイルガイドに助けられた部分は大きいです。スタイルガイドがあることでデザイナーとの会話はしやすくなりましたし、今まで Photoshop や Sketch を通してしかできなかった部分のコミュニケーションを大きく補足してくれました。そしてエンジニアがデザインについてすこし深く考えるためのツールにもなったと思います。
そして「こんな感じでスタイルガイド作っていきたいんだけど」って相談したら一晩で Sketch のテンプレート作ってくれた @pekep に感謝。
]]>Atomic Design の基本的な概念に関して知りたい方は Brad Frost 氏の原文、もしくは私の以前の記事↓を参照できます。
最近よくクリエイターが移住するカナダで Atomic Design を学ぶ
結論から書くと、今回 Atomic Design を導入したことは正解でした。コンポーネントの粒度を論理的に説明できるガイドラインとして十分すぎるほどの役割を果たしてくれました。
このガイドラインがあることで、デザインに関してさほど関心がない人(たとえばデザインよりもエンジニアリングが好きなフロントエンドエンジニアなど)でも、UI コンポーネントの粒度を考えるフレームワークとなります。
もちろん、Atomic Design でなくても、コンポーネントの粒度を決定する基準が個人の感覚に依存することが避けられれば、どんなものでも構わないと思います。
Atomic Design を導入する目的は、デザイン仕様の変更に強く、再利用性がコンポーネント郡を揃えることです。当然のことですが、粒度が小さく特定のコンテキストに依存しないコンポーネントは少しくらい UI デザインに変更が入ったとしても、そのまま再利用することが可能です。
内製で自社サービスを開発していることもあり、今回のプロジェクトではデザインの変更は日常的に発生しました。そのためコンポーネントは将来的なデザイン変更を見越した上で、現在のデザインカンプ上に見えるコンテキストにできる限り依存しないように注意してコンポーネントを作ることが重要です。
Atomic Design の 5 つのカテゴライズの中で Atoms や Molecules はコンテキストが極めて薄いコンポーネントです。コンポーネントを作る際に常に Atoms や Molecules の粒度で作るとどういうコンポーネントが理想かを意識することが変更に強い UI コンポーネント郡を備えていくことにつながっていきます。
ここまで書いてきたとおり、コンポーネントベースの開発はフロントエンドにとってはメリットが多いですが、Atomic Design はその名前の通りデザイン手法なので、導入においてもフロントエンドだけで完結するものではなく、デザイナーに協力してもらうことが不可欠です。(今回のプロジェクトでは、デザインとフロントエンドエンジニアリングは分業でした。)
しかし残念ながら、コンポーネントベースのデザインというのは一般的にマークアップをしないデザイナーにとっては作業しづらいものです。画面上でほかのコンポーネントと一緒にレイアウトされたときに、そのコンポーネントがどのように見えるのかをイメージすることは、かなりベテランで腕の良いデザイナーでも難しいです。
Atomic Design のようなコンポーネントベースのデザインプロセスをデザイナーに協力してもらうのは大きな課題です。
この課題を解決するために、開発の初期段階から Brad Frost 氏が提唱している Interface Inventory をゆるく実践していきました。
Interface Inventory を実践したと言っても変わったことや難しいことをやったわけではなく、デザイナーには画面ごとのデザインカンプを通常通り作ってもらい、フロントエンドはそのカンプからいきなり画面を実装するのではなく、まず必要なコンポーネントを切り出して、コンポーネントリストを作成しました。
このコンポーネントリストは Pattern Lab のように Atoms や Molecules といったコンポーネントの粒度でカテゴライズされた状態で、デザイナーに共有します。(Git 上で例えば master にマージされたら、自動で最新のコンポーネントリストが開発チーム全体で共有されるようデプロイされるようにしておくと良いかと思います。)
デザイナーには次からの画面デザインを考える際、リストにあるコンポーネントを見て可能な場合はそれらを使ってもらうよう協力してもらいます。UI のトーンがブレないようにスタイルガイドを作っているデザイナーの方もいると思いますので、スタイルガイドと同じように使用してもらえると一番良いです。
コンポーネントが再利用できた方が実装工数が減り、コード量も少なくなるためバグも減り、プロダクトのパフォーマンスも上がるのでプロダクトにとってのメリットも大きいので、その点も伝えるのですが、デザイナーにとってはイメージがつきにくい場合もあるので、まずはスタイルガイドと同じように使ってもらうように促すのが一番なのかなと思います。
前述したとおり、Atoms や Molecules はデザイン仕様の変更に強いので、コンポーネントリストでも Atoms や Molecules を増やし、Atoms や Molecules から Organisms を構成することを意識します。
作ったコンポーネントたちがカテゴリー別に一覧化されていると、粒度のバランスが取れているかを意識しやすくなります。例えば Atoms や Molecules にそぐわない、ある一定のコンテキストに強く影響されたコンポーネントがないかレビューしやすくなります。
デザインカンプからコンポーネントを抜き出すので、実装者はその画面デザインが意図しているコンテキストに非常に影響されやすいです。そのコンテキストの一層下に UI コンポーネントが共通で持っている目的があるはずなので、その共通の目的以上のものが機能に含まれていないかをレビュー時に確認し、適切に Organisms などにコンポーネントを分離することが大切です。
実際に開発中盤以降もデザイン変更が頻発しましたが、Organisms 以上のコンポーネントはその変更により修正が発生しますが、Atoms や Molecules に関しては、ほとんど発生しませんでした。
コンポーネントリストやスタイルガイドを運用する上で陥りやすい問題として、開発が佳境に入るとコンポーネントリストが更新されないことが多々起こりえます。プロダクトの方に開発の意識が集中しているので、それは当然ですが、コンポーネントリストが常に更新されなければ、いづれデザイナー含めチーム全体がコンポーネントリストを参照しなくなります。
コンポーネントリストを「生きたドキュメント」として保ち続けるために、コンポーネントリストの中のコンポーネントとプロダクトで使われているコンポーネントの実装が同じソースを見ていることが重要です。
今であれば、コンポーネント化を助けるライブラリとして React などを利用できると思います。今回のプロジェクトでは、 React で作成したコンポーネントをコンポーネントリストにもレンダリングしているため、プロダクトのコンポーネントとずれることがありません。
今回のプロジェクトでも、React + alexlande/react-style-guide を利用してコンポーネントリストを作成し、プロダクト上にあるコンポーネントと同期されるようにしました。
Atomic Design に限らず、コンポーネントを正しく分離するということは、デザインとして正しく論理的な意味を持ったコンポーネントとして設計することになります。サービス上で統一された正しい意味合いの色が適用されていて、コンポーネント単体が必要以上でも以下でもないマージン情報を持ち、どこにレイアウトされても自分以外を影響することのないように心掛ける必要があります。
スタイルがそのコンポーネント以外に影響しない状況が保持されていれば、アプリケーションの実装全体をそれほど把握していなくても、安心してコンポーネントに変更をかけることができます。それはフロントエンドエンジニアでなくても、デザイナーやほかの職種の人が実装に関わることができることを意味しています。
頻繁に変更するデザインには、簡単な色やラベリングの変更、余白の調整などが含まれます。これらの変更は作業的にはとても簡単なものですが、デザイナーがデザインカンプに対して変更したものをフロントエンジニアに渡して作業する場合は、そこそこの工数がかかります。
まずフロントエンドが実装したものをデザイナーに確認してもらう作業は二者のコミュニケーションが発生するため、確認が必要なときに他方の時間が空いているとは限りません。簡単な修正なので、デザイナーの確認が必要でないと思っていても、デザインカンプ上では良く見えていたものが、実際に実装されたら調整が必要だったということは多々起こりえます。残念ながら、デザイナーにしか気づけないもの点が存在するのは事実です。
また、実装したものを Pull Request すれば、ほかのフロントエンドエンジニアがレビューすることになると思いますが、その際にレビューできるのはコードに関することのみだったりします。(そして、これらのデザイン修正ではコードに関しては指摘する必要がないことの方が多いと思います。)
上記の 2 点は、デザイン変更に関する実装についてデザイナー自身が Pull Request することができると、工数をぐっと削減できます。デザイナー自身が実装しているため、実装後のデザイン確認作業は 1 者のみで済みます。また、Pull Request に対して、フロントエンドはコードのみをレビューすれば良くなります。
基本的な Git の使い方をデザイナーに学んでもらう必要はありましたが、デザイナーにとっても自分の意図を他人にフィルタリングされることなくプロダクトに反映できるので、デザイナー自身が Pull Request できることは大きなメリットがありました。
Brad Frost 氏の Atomic Design では 5 つのカテゴリー( Atoms / Molecules / Organisms / Templates / Pages )が紹介されていますが、今回のプロジェクトでは最初の 3 カテゴリー( Atoms / Molecules / Organisms )のみを管理しました。
今回のプロジェクトでは、Templates のような大きな粒度のものを再利用することがありませんでした。Templates と Pages については、何百や何千とページを量産するような案件の場合はコンポーネントリストなどで管理した方が良いように思いますが、そうでない場合は Atomic Design のカテゴリーから Atoms / Molecules / Organisms だけを採用するのも良いかと思います。
もちろん実際には Templates の役割をしている大きなコンポーネントは存在しているのですが、プロダクト上のみで確認できるだけで事足りるように個人的には思いました。
Atomic Design の良いところは、名前が分かりやすく、概念的にも難しいところがないことだと思います。(逆に悪いところは、カテゴリーの名前が若干恥ずかしいことかなと思います。「この Molecule が...」とかチームメンバーに言うのは最初は抵抗がありました - これについては t32k さんも予想してましたが。)
今回のプロジェクトでは、チームの半分弱がフロントエンド経験がないメンバーでしたが、Atomic Design の基本的なコンセプトはすんなり取り込めたようでした。
Atomic Design 自体は特に斬新な考え方なわけではなく、従来のコンポーネントを作る上でのふわっとした考え方に面白い名前がついただけですが、名前がついていることで認識の共有が加速する良い例だと思います。
Atomic Design の考え方を借りて、コンポーネントの粒度を決定していった結果、以前より再利用性が高いコンポーネントを作ることができました。もちろん、Atomic Design だけではなく、コンポーネントベースのデザインプロセスに協力してくれたデザイナーのおかげもあります。(今回前向きにコンポーネントベースのデザインプロセスに協力してくれた @pekep に感謝。)
そして、適切なコンポーネントを分離できた結果、デザイナーがデザイン後の開発に参加できるという副産物も得ることができました。
]]>しかし、アプリケーション固有の UI コンポーネントをある程度の大きさの粒度で作っている場合、必要十分な汎用性を予測することは非常に難しいことが多いため、必要になった時点で機能的な分岐を追加することが多いです。多くの場合、既存でそのコンポーネントを使っている箇所に影響を与えないように、分岐するためのフラグを要素の属性値として渡します。
ある程度の大きさの粒度というのは、例えば投稿フォームのような複数の input 、button 要素をテンプレートの中に持ち、バリデーションや Ajax などの機能を提供するくらいの粒度を想定しています。例えば、この投稿機能を提供する UI を下記のように使っている場合、
label
という属性の値として変更したいラベルのテキストを渡すことで、属性値が設定されていた場合だけデフォルトとは違うラベルに変更できるようにします。
ただし、変更が必要になったときにこれを繰り返し続けると、次のようなことにもなりかねません。
さすがにこの例のような場合はデザイン的にも問題があるとは思いますが、運用を長く続けていると起こり得る自体です。
Gilgamesh は、Zhenyu Hou 氏が開発している JavaScript フレームワークを拡張するライブラリ集です。
JavaScript フレームワークを拡張する、とありますが、現在のところサポートしているフレームワークは、AngularJS のみです。今後 Polymer と React もサポートされる予定みたいです。
Gilgamesh には今のところ大きく2つの機能があります。
今回は、1つ目の「テンプレート拡張ライブラリ」としての機能を利用して AngularJS の Directive
で作った UI コンポーネントの拡張を試してみます。
Gilgamesh のリポジトリ を clone してきて、任意の HTML で jQuery と AngularJS を読み込ませた上で下記の script 追加します。
ちなみに、jQuery を外して実行してみたところ、エラーが出たので現状は jQuery に依存しているのかもしれません。(あまり深く追ってないです。)
AngularJS の Directive 機能でコンポーネントを作るのとほぼ同様に、Gilgamesh のコンポーネントを作ることができます。Angular モジュール・オブジェクトの directive
メソッドを component
に置き替えるだけです。
これで下記のように <div post-form></div>
をマークアップに配置するとこのコンポーネントを使うことができます。
本当は、restrict: 'E'
で、Element Derective にしたかったのですが、現時点では <div post-form></div>
を配置するとエラーが出てうまく動作しませんでした。
Gilgamesh で作ったコンポーネントに対して、マークアップ側から変更を加えていきます。中止ボタンだけ別の見た目のボタンに変更します。
まず、コンポーネントで使っているテンプレートの中止ボタン要素に役割名を指定します。ここでは cancel
という名前を指定します。
次にコンポーネントを使用する側で、中止ボタンだけ置き替えたい要素に上書きする記述をします。コンポーネントの要素に gm-tpl-partial
という属性を書き加えて、子要素として gm-role="cancel"
という属性を付けた要素を記述します。これが中止ボタンを上書きする要素になります。
また、部分的にではなく、テンプレート自体を全体的に別のものにしたい場合は、gm-tpl-partial
属性を設定しないで、下記のようにコンポーネント要素の中身を上書きするだけで UI 全体が上書きされます。
部分的に上書きもできれば、部分的に取り除くこともできます。今度は中止ボタンをコンポーネントから取り除きます。gm-tpl-exclude="cancel"
という属性をコンポーネントの要素に追加するとテンプレートで gm-role="cancel"
属性を与えられた要素だけ除外されます。
例えば、中止ボタンをテンプレートのマークアップ外に配置したい場合は、Gilgamesh のコンポーネントの外にある要素をコンポーネントのパーツとして扱うことができる機能が有効です。まずコンポーネントの要素に id
を設定します。
そして HTML の任意の場所に中止ボタンとして機能させたい要素を gm-import="postForm"
属性を加えて配置します。属性値を、先程の id 属性の値と同じにすることで、postForm コンポーネントの外にある要素をコンポーネントの中のものとして扱うことができるようになります。
コンポーネントの外にあっても、gm-import
属性で紐付けられた要素内は postForm コンポーネントの scope に紐付きます。なので、要素内に書いた Angular 式は postForm のコンテキストで展開されます。
あるコンポーネントの link
に設定した機能を継承して別のコンポーネントを作ることができます。component
メソッドで新しいコンポーネントを作るときに extend
キーを追加します。値には継承したい親コンポーネントの名前を指定します。
これで postForm コンポーネントを継承して追加で iScope.user.subscription = true;
という処理を postFormSubscribe コンポーネントにだけ実行できます。iScope.user.subscription = true;
を実行したことにより、postFormSubscribe コンポーネントでは、購読チェックボックスがデフォルトでオンになるようにしました。
この拡張機能は残念ながら、現時点で自分が試した範囲では link
を継承することしかできないようでした。親のテンプレートを継承できるともっと使い道が広がりそうです。
Gilgamesh のテンプレート拡張機能を試してみました。少しまだバグが多い印象なのでプロダクトにはまだ導入できないと思っています。しかし、予期できなかったけれど、必要になった汎用性を復活してくれるライブラリとして、機会があれば使ってみたいと思っています。
また本記事の内容については、こちらのスライドでも同様の内容を話しています。
Gilgameshhttp://sskyy.github.io/Gilgamesh/
Gilgamesh: bring Angular to the next levelhttp://www.reddit.com/r/programming/comments/2s5exu/gilgamesh_bring_angular_to_the_next_level/
]]>2014年 12月8日〜12月14日の間、カナダに滞在してきました。目的は12月9日〜12日に開催される Smashing Conference 2014 へ参加するためです。
海外のカンファレンスに参加するのはおろか、海外旅行もしたことがないので、今回1人でカナダに行くということで終始緊張の連続でした。(Frontrend の @hiloki さんが「僕も行こうかな」と言っていたので、心強く思っていたのですが、残念ながら叶わず。)
本記事は、カンファレンスが開催されたカナダという国にまつわるお話を Frontrend の話題を交えながら書きつつ、このカンファレンスで一番楽しみにしていたトピックである「Atomic Design」について書いていきます。
今まで海外カンファレンスなど参加したことがない自分が今回参加を決意した理由は、興味があるスピーカー陣が多くいたことがもちろん1番なのですが、2番目の理由として、今年に入ってからカナダという国そのものに興味を持つようになったからです。
興味を持ったキッカケは、カンファレンス募集期間の前後、立て続けに日本からカナダに移住する Web クリエイター・エンジニアたちに会ったことです。そのうちの一人が、Mr. Frontrend である @t32k さんでした。彼のブログ記事「Webエンジニアからみたフィリピン語学留学」で書いている通り、彼はフィリピンで語学留学した後、現在カナダでエンジニアとして働こうとしています。
「偶然なのか、なんだか最近多くの人がカナダに行っている気がするな。」となんとなく思っていました。IT 業界でエンジニアとして働いている者であれば、一度はシリコンバレーで働きたいと願う人も少なくないでしょう。だから、アメリカ合衆国で働きたい、というのは分かります。しかし、なぜカナダなのでしょうか。
そんな疑問に答えるかのように、Frog というクリエイターのためのカナダ留学支援団体のサイトにはカナダのバンクーバーについてこんなことが書かれています。
なんともアメリカンドリームに近づけそうなカナダの魅力がそこにはありました。もし普通の日本人がアメリカで働きたいと思ったとしても、アメリカの就労ビザ取得は難しく、働きたくても外国人にあたる日本人はなかなか働く許可を得ることが難しいでしょう。それと比較すると、カナダは就労ビザの制度に融通が利きやすいということのようです。
実際、現地で出会った人の中にもいずれはシリコンバレーで働くことを目標にしている人もいました。
上記のような理由は、カナダ自身というよりはアメリカへのアクセスに対してのカナダの魅力しか示していませんが、実際カナダのバンクーバーに行ってみると、人は優しく、街は綺麗でとても魅力的な国でした。
私は学生時代に約6年アメリカに住んでいた記憶もあって、カナダに着いたとき、街中の標識や店頭の商品を見てアメリカととても似ているな、と感じたのですが、実際に人と触れ合うと(言い方は悪いですが)アメリカのようなドライさはあまり感じられません。むしろ優しさに溢れていました。
たとえばロックが開かなくなった荷物について私が空港の人に助けを求めると「私には開け方が分からない」と言いながらもあの手この手で開けようと頑張ってくれたり、カナダに来たばかりで電車の乗車券の買い方1つ分からない私に通りすがりの人がとても丁寧に買い方を説明してくれたり、気候とは裏腹になんだかとても暖かい国だなと感じました。
そして、移民も多いためか外国人に対して慣れている感じも見受けられます。物価が若干高いところと、気候が寒いところを除けば、日本人にとっても住みやすそうな街に感じました。もちろん1週間しか滞在していないので実際のところは分かりませんが、The Japanese Community in Canada が示すようにバンクーバーの人口の 1.3% が日系であることからも日本人にとっての住みやすさが想像できます。
カナダで開催されるカンファレンスということもあり、やはりコミュニケーションは英語になります。長く英語を話す機会から離れているので、この点も今回の旅において心配する要因の1つでした。
少し前に、エンジニアミーティング vol.6-4 エンジニアの英語戦略 というポッドキャスト番組でも話があったように、特にここ1、2年はまわりにいるエンジニアが英語を話せるようになる必要性を強く感じてきています。
実際、職場の友人も、English Lunch という呼ばれる英語オンリーで会話するランチを定期的に開いたり、Frontrend の @1000ch が「2014年の振り返りと人気記事まとめ」で触れているように レアジョブ英会話 で毎日レッスンを受けたりしている人がたくさんいます。
私も同様に、生の声を聞ける機会に英語で恐れず話ができることは大事だと今回の旅で思いました。(恐れず、を強調したのは、別に流暢でなくてもコミュニケーションを取ることができれば良いと思っているからです。)私は漠然と開発における技術やワークフロー、考え方など欧米は日本より進んでいるように思っていましたが、実際にカンファレンス参加者と話すと日本で私たちが課題に持っているのと同じような課題を抱えていて、相手の話が身近に感じたり私の話に共感してくれたりするのがとても新鮮でした。
さて、今回参加した Smashing Conference ですが、ウィスラーというバンクーバーから車で2時間程離れたリゾート地で開催されました。(ウィスラーは2010年冬季オリンピック競技が行われたくらいのスキーリゾートなので、正直スキーやスノボというオマケにつられてカンファレンスに参加した、という人もいました。)
参加者は、やはりカナダ人、とりわけバンクーバーから来ている人が多かったのですが、アメリカ人も多くいました。逆に考えると、カナダからアメリカのカンファレンスなどのイベントに参加することもそのくらい気軽なのだろうと感じます。(日本からアメリカのイベントに参加しようと思うとウン十万円と費用がかかるので、この点においては羨ましい限りです。)
今回のカンファレンスで一番楽しみにしていたのは、Brad Frost 氏の Full-day ワークショップを受講することでした。Brad Frost 氏は Responsive Design のパターンやニュースなどを集めたサイト This Is Responsive を公開し、Adaptive Design に関する実用的で深い見識を持っています。
私がこのワークショップを楽しみにしていた理由は、彼がレスポンシブな Web をコンポーネントベースでデザインする方法論として、Atomic Design というものを提唱していて、このワークショップはその考えを学ぶことができる場だったからです。
ここ1年、私は2つのプロジェクトでフロントエンドの開発をしました。どちらのプロジェクトでもコンポーネントベースでの UI 開発を意識してきましたが、コンポーネントをどんな粒度で作るのが1番再利用性が高くなるのか、なかなか明確な基準をチームの中で共有することができないでいました。彼のワークショップを通して、コンポーネント開発に対する考え方を磨きたいと思いました。
ここからは、そのワークショップおよび、カンファレンスの1セッションでもあった Atomic Design について書きたいと思います。
Atomic Design はデザインシステムを作るための1つの手段です。ざっくり言うと UI コンポーネントを粒度に応じたカテゴリーに明確に分ける手法です。Atomic Design では下記のようなカテゴライズのレベルが示されています。
これらのカテゴリーは上から下に行くにつれて、粒度は大きくなり、抽象度は下がっていきます。下位カテゴリーのコンポーネント(例えば原子)を組み合わせて上位のコンポーネント(分子)を構成するようにデザインを考えていきます。
We’re not designing pages, we’re designing systems of components.
まさに Stephen Hay 氏の上記の言葉にあるように、ページをデザインするのではなく、コンポーネントで構成するデザインシステムです。
私が Atomic Design が手段としてとても優れていると思う点は、カテゴリーの名称がコミュニケーション・ツールとしても柔軟なところです。
5つのカテゴリーのうち、原子、分子、有機体に関しては、開発サイドにメリットがあるカテゴリー名です。この3つの名称は、およそ Web デザインには関係が無さそうな名前です。しかし、原子と原子が結合して分子になることから連想すると、コンポーネントを組み合わせて新しいコンポーネントを作り出すという特性をとても正確に表しています。
それに対してテンプレートとページという2つの名称はとても一般的です。前者3つのコンポーネントより具体性が増して、このレベルではクライアントやプロデューサーに見せるアウトプットになるので、彼らと会話するときに余計な違和感を与えることなくコミュニケーションできる名称になっています。
5つのカテゴリーを1つずつ説明する前に、Atomic Design を実践するために便利なツールである Pattern Lab を紹介します。Pattern Lab は Brad Frost 氏と Dave Olsen 氏の両名が作ったパターンライブラリかつ静的サイト・ジェネレータです。
パターンライブラリとしては、原子や分子、有機体といったコンポーネントパターンをコードベースで管理することができます。また静的サイト・ジェネレータとしては、ページやテンプレートと言ったサイトレベルのデザインを原子、分子、有機体から生成します。
Pattern Lab は Atomic Design のパターンを作るスターターキットとしてとても優れているので、ここからの5つのカテゴリの紹介は Pattern Lab で説明していきたいと思います。
原子という名前の通り、これ以上分割することができない基本的な要素がこのカテゴリに含まれます。これ以上分割することができない要素なので、ラベルやインプット要素、ボタンなどの HTML タグは原子としてカテゴライズされます。カラーパレットやフォント、アニメーションなどもこれ以上分割することができない要素として原子に含まれます。
原子にカテゴライズされる要素は、基本的に単体では UI として意味を成さないものばかりになりますが、この後に紹介していくカテゴリのコンポーネントも、ここの要素にあてられたスタイルがベースとなっていきます。この要素たちを集めて一覧化すれば、Dan Mall 氏の ELEMENT COLLAGES のような感じで、サイト全体のトンマナを要素レベルで俯瞰できるはずです。
分子もその名の通り、原子がくっついてできる最小の単位です。ラベルやインプット要素、ボタンなどのコンポーネントを合わせて、例えば検索フォームという形にすることができます。原子は単体ではあまり意味をなさないものでしたが、組み合わせることで目的を持ったコンポーネントになることができます。
ただ、このコンポーネントが持っている目的はまだ汎用的なもので、コンポーネントとしての再利用性が5つのカテゴリの中で最も高くなるようにデザインしていく必要があります。これが Atomic Design システムの根幹を担います。この分子をいろいろな UI パーツに上手に組み込むことが Atomic Design のコツとなるように思います。
Pattern Lab では、このように既存で作った原子を組み合わせて分子としてのコンポーネントを作れるように、別のコンポーネントをインクルードすることができるようになっています。
Pattern Lab はテンプレートエンジンとして Mustache を使っており、{{> そのコンポーネントの名前}}
という記述でほかのコンポーネントをインクルードすることができます。上記のコードでは、原子のコンポーネントである atoms-square
をインクルードしています。原子から分子というように小さいコンポーネントを使って大きいコンポーネントを作っていくことが簡単にできます。
有機体は原子や分子を組み合わせて、さらに複雑なコンポーネントを構成します。このレベルでは、インターフェースのパーツとして人が意味のある名前を付けるコンポーネントとなります。例えば、ヘッダーやフッター、記事一覧などのパーツが有機体に含まれます。
テンプレートはその名前の通りで、具体的なコンテンツを持っていないページのテンプレートです。ヘッダーや記事一覧などの有機体、ページネーションなどの分子をレイアウトして構成します。
最後がページです。テンプレートに画像やテキストなどの具体的なコンテンツが流し込まれて完成します。
Pattern Lab では、コンテンツは JSON によってテンプレートに適用され、ページとなります。ここではここまで作ってきたコンポーネント現実のコンテンツに耐え得るかを確認します。サンプルテキストやアテの画像ではないコンテンツを流し込んだときにレイアウトが崩れることなく、コンポーネントとして成り立つことができているかをテストするカテゴリーでもあります。
私も過去のプロジェクトで、デザインシステムらしきものを自分で考えて、そのシステムに従ってコンポーネントを管理していました。いわゆるオレオレデザインシステムです。そのデザインシステムで作ったコンポーネントの粒度が適切だったかどうかはさておき、ディベロッパー間で共通言語として使うところまでは上手くいったように思いましたが、コンポーネントを組み合わせて新しいコンポーネントを作成していくため、粒度の小さいコンポーネントに修正が入ったときの影響範囲が分かりづらくメンテナンス性にずっと課題を感じていました。
コンポーネントベースのデザインシステムを運用で実践していくにあたっては、コードのメンテナンス性を保つために最低限下記の機能を持ったパターンライブラリが必要だと思います。
「粒度の小さいコンポーネントをインクルードしてより大きなコンポーネントを構成する機能」に関しては、細かいコンポーネントたちを1ソースにまとめるために必要です。やはりコンポーネントに修正を入れるときは1ヶ所だけ修正すれば良い状態が理想です。
「インクルードされる側の粒度が小さいコンポーネントがどこで使用されているかを把握できる機能」に関しては、コンポーネントを削除する時に便利です。長く開発・運用を続けていると、定期的にコードの大掃除する必要に迫られると思いますが、そのコンポーネントを削除してもほかのコンポーネントに影響が出ないか確認できれば不必要なコンポーネントの掃除が容易になります。
Atomic Design 実践用のサポートツールである Pattern Lab は、「粒度の小さいコンポーネントをインクルードしてより大きなコンポーネントを構成する機能」を Mastache を採用することにより実現し、「インクルードされる側の粒度が小さいコンポーネントがどこで使用されているかを把握できる機能」を Lineage という機能で実現しています。
コンポーネントベースでのデザインを実践するときは、どうしてもそのデザインシステムに合ったコンポーネント管理機能を持つツールが必要になります。デザイナー・ディベロッパーは自分のプロジェクトに合ったデザインシステムを採用する必要がありますが、そのデザインシステムをサポートするツールがあるとは限りません。むしろ、ないケースの方が多いでしょう。Atomic Design の場合は、そのデザインシステムに特化した Pattern Lab というツールがすでに用意されていることが、これを採用する強い理由になるように思います。
]]>本記事では、Controller / Service / Directive / Filter の役割のポイントを整理したいと思います。
他の MVC デザインパターンと同様、ビジネスロジックとプレゼンテーションロジックを分離することが基本です。
ビジネスロジックを Service が担当し、Controller でそれを紐付けてテンプレートに共有します。プレゼンテーションロジックは Directive と Filter が担当し、DOM 操作処理やデータ整形処理をテンプレートに共有します。
Controller ではビューで表示するデータとユーザーアクションに対するメソッドを定義します。
AngularJS では $scope
オブジェクトを介してデータやメソッドをテンプレートで共有することができます。Controller には、ビジネスロジックを $scope
オブジェクトに書き込んでいき、テンプレートでは $scope
オブジェクトで共有されたデータとメソッドを参照します。
共有される $scope
オブジェクトがカオスな状態になるのを避けるために、Controller では $scope
オブジェクトを書き込み専用として、テンプレートでは読み取り専用として扱うと良いです。
また、直接 DOM を参照することなどは行わなないようにします。DOM 操作処理が Controller に入ってしまうと、デザイン変更などでテンプレートの HTML に変更を行わなければいけない場合に、Controller も書き変える必要が出てくる可能性があります。
そのため、DOM 操作処理は Controller 内では極力行わなず、Directive にその役目を渡しましょう。
ビジネスロジックとプレゼンテーションロジックを Service、Directive、Filter に分離して Controller を簡潔に保つことができると理想的です。
ビジネスロジック担当です。ビューに依存しない処理を記述します。
また、各 Service はシングルトンとして存在するため、異なる Controller や Directive 間で共有するモデルとして使用できます。
HTML を拡張する機能です。前述の DOM 操作が必要になる場合を含み、プレゼンテーションロジックを記述します。
自身で Controller を持ち、単一で完結するコンポーネントを作ることもできますし、属する scope で公開されているデータと振舞いをテンプレートに紐付ける役割を担います。
データを整形する処理を記述します。
Directive と同様にプレゼンテーションロジックを記述しますが、Directive と違い、直接 Scope にアクセスすることはできません。モデルを変更することなく表示フォーマットのみを変更します。
ここでは、フォームから ユーザーデータを追加する処理を例に説明します。
この例では、FormCtrl
という Controller、noHyphen
という Directive、User
という Service を組み合わせて実装しています。
まずモジュールを宣言します。
このようなテンプレート用意した場合、各々の役割は以降のようになります。
ここでのポイントは $scope
オブジェクトの設定だけを記述している点です。DOM にイベントハンドラを紐付ける $('button').on('click', function () { ... })
などの処理はビルトインの Directive である ngClick
に任せてあります。
また、バリデーション機能は、特定の DOM の値を取得する必要があるため、Controller 内には記述しません。後述する noHyphen
という Directive を実装して機能を実現します。
このフォームは送信ボタンを押された時に Ajax 処理も実行しますが、その処理も後述する User
という Service に実装を切り分けています。
ここでのポイントは、ngModel
を介して DOM 操作をしている点です。Controller で必要だった DOM にイベントハンドラを紐付ける処理は Directive に記述します。
処理した結果を ngModel
ディレクティブを介して FormCtrl
の $scope
に渡しています。
今回は、factory
メソッドを使用します。
ここでのポイントは、ビューに依存しない処理のみを記述している点です。
ここでは、新規ユーザーデータの送信に使われる Ajax 処理を実装しているので、FormCtrl
はこの User
Service をインジェクトすることで自身の $scope
オブジェクトに持っているデータを送信することができます。
特有の概念が多いため AngularJS での役割の分担は分かりづらいですが、自分は上記のように処理を分けるようにしています。
]]>本記事は、2012年までjQueryだけで開発していたフロントエンドエンジニアの自分が、Frontrendな方々に影響を受けたのをきっかけに、とあるコミュニティ系WebサービスでAngularJSを導入するまでの過程で影響された記事やイベントや人を時系列で紹介すると共に導入後AngularJSの開発でハマった10のことを紹介します。
AngularJSと言えば、MVC的なアーキテクチャをJavaScript開発に取り入れるためのフレームワークで現在は業務でも使用させていただいています。
しかし、振り返ってみると、2012年はMVなんちゃらなフレームワーク等には、全く無縁の生活を送っていました。
そんな2012年の7月頃に、FrontrendのYuya SaitoさんにCode SchoolのAnatomy of Backbone.jsを紹介していただいて、BackboneをはじめとするJS開発におけるMVC的な発想を知ります。
ご存知の方も多いと思いますが、Code Schoolはステップごとに用意されている講義形式の動画で学び、そのまま出題される課題をブラウザ上のエディタでコードを書いて解答して、実践的にプログラミングを学ぶことができるサービスです。
Anatomy of Backbone.jsを受講した印象としては、とても初心者が学習しやすいチュートリアルです。JSライブラリと言えばjQueryしか使ったことがなかった自分はここでBackboneの基本の基本を学べたと思います。
基本のチュートリアルをこなしたとは言え、実際のプロジェクト(リリース済)で使うことに敷居の高さとリスクを感じ、なかなか導入することもできないまま2013年になってしまいました。
しかし、2月9日に開催されたFrontrend Vol.4でのahomuさんセッション「jQuery to Backbone – アーキテクチャを意識したJavaScript入門」で、まさに「自分みたいにjQueryくらいしかライブラリ触ったことない人でもBackboneとか使えるかもー」と思い始めます。セッションの内容を参考にさっそく自分がフロントエンドを担当しているWebサービスでBackboneの導入を始めました。
このセッションは、当時のJS開発におけるjQueryが解決しない問題とBackboneを導入することで得られるメリットが分かりやすく説明されている上に、jQueryベースで記述されたコードをBackboneの構造に徐々に移していく具体的なコーディング手順まで紹介されています。自分はまさにその手順に従って、プロジェクトにBackboneを導入できたように思います。
Backboneを使い始めて半年くらい経ち、自分が担当しているサービスでは巡るめくアップデートとプロモーション施策の実装をしていかなければいけませんでした。そしてフロントエンドエンジニアが自分だけ、という状況だったこともあり「何か劇的に開発を高速化できる方法はないかな」と日々模索していました。
そして、Shine Technologiesのブログ記事「Backbone Is Not Enough」を読み、AngularJSに興味を持ち始めます。
記事では、Backboneでの大規模なSPA開発において、ネストされるViewをうまく構成する難しさ、Viewをテストする難しさ、メモリ管理の難しさ、容易に遅くなってしまうレンダリングへの対策やデータバインディングの重要さなどに関してEmberJSやAngularJSと比較して書かれていますが、中でも「Backboneと比較したEmberJSとAngularJSのコード記述量がかなり少ない」という1点に惹かれて、EmberJSとAngularJSが気になり始めます。
EmberJSとAngularJSが気になり始めましたが、これらのフレームワークはそれぞれ何が違うのかは正直よく分からないでいました。Backbone単体では自分でこつこつ設定するデータバインディングを簡単に設定できる点に関しては、EmberJSもAngularJSも同じです。
その疑問に関しては、2013年10月に公開されたJSConf EU 2013(9月開催)のMarius Gundersen氏セッションの動画「A comparison of the two-way binding in AngularJS, EmberJS and KnockoutJS」で解決されます。
このセッションでは、AngularJS、EmberJS、KnockoutJSの双方向データバインディングにおける挙動の違いが分かりやすくまとめられています。
このセッションにおいてAngularJSは、Dirty Checkingという仕組みと非同期でデータをバインドをしているため、
などの特徴が説明されています。
担当サービスにおいて、Modelはそこまで複雑かつ巨大にならないと思った点と単純なレンダリングの速さが気に入り、AngularJSの導入を決めました。
導入を決めたら、次はAngularJSについて学習しなければいけません。
「Anatomy of Backbone.js」のような効率が良い(そしてできれば無料の)ラーニングリソースを探していたら(残念ながら、Code SchoolにAngularJSのコースはありませんでした。)、「A Better Way to Learn AngularJS」というラーニングリソースを見つけました。
Code Schoolのようにコーディングで課題を問いていくリッチな機能は無いですが、初心者にも分かりやすく説明されたAngularJSのチュートリアルを動画で見ることができます。無料で学べるのですが、これを一通りこなすだけでAngularJSの基本的な使い方に関しては網羅できるように思います。
もちろん基本的な使い方しか学んでいない自分は、AngularJSでサービスの開発を始めると、いろんなところでハマりました。Backboneを利用していた時と比べ、確かにコード記述量は減りましたが、AngularJSについて調べている時間は増えました。
そんなわけで「Backbone Is Not Enough」に載っていたAngularJSのラーニングカーブをしみじみ実感しましたが、同時にノウハウが溜まった後は劇的に楽になるはず、という期待でいっぱいでした。
本記事のタイトルにある通り、ここからはAngularJSでの開発で最初の頃に自分がハマった点を回避策とともにリストしていきたいと思います。
AngularJSでは、Controllerの書き方にパターンがいくつか存在しますが、そのパターンのうちminifyするとJSが正しく動作しなくなるものがあります。
AngularJSにはDI (Dependency Injection)と呼ばれる仕組みがあります。Controllerで使用するserviceを指定する際に、functionの引数に指定された変数名から自動的に必要なserviceを決定できるすごい機能を持っているために、起こってしまうのがこの問題です。
回避策は2点。
1点目の回避策は、引数にわたすserviceの名前を文字列で明示することです。
minifyすると動かない書き方(functionの引数だけでserviceを指定)
minifyしても動く書き方(functionの引数の前に文字列でserviceを指定)
2点目の回避策は、使用するpre-minifierをngminにすることです。
こちらだと動かない方の書き方をしても、ngminが動く方の書き方に変えてminifyしてくれます。これでキーの打数は減り、楽して開発できるでしょう。
Backboneを使っているときは、モジュールごとに処理を分けてRequireJSで遅延ロードさせたりしていましたが、AngularJSで同様のことをしようと思うとモジュールのロードが完了する前にAngularの起動が行われて、意図した挙動をしなくなることがあります。
回避策は、手動で起動することです。
SPAで作っている場合には問題ないかもしれませんが、そうでない場合、先述した手動での初期化などを行うとAngularJSの起動が遅くなり、ページロード時にngHideディレクティブなどを指定しているDOMが一瞬表示されてしまうことがあります。
angular.cssを読み込んでおき、対象の要素のclass属性にng-hideを足しておくことで回避できます。
ngHideなどの処理は、内容的には**display:none;**を適用するために、ng-hideというクラスを要素に付けているだけです。
angular.cssを読み込まなくても、上記のようなcssが入っていれば大丈夫です。
上記と同様に、AngularJS起動前の評価されていないexpressionも生のテキストとしてページロード時に一瞬表示されてしまいます。
このような場合は、
で回避することができます。もしくは、
でも可能ですが、ngCloakを使用する場合はngHide同様angular.cssを読み込んでおく必要があります。
この問題を回避するという用途的には、ngCloakを使う方が正しいようです。
textarea要素にデフォルト値を設定したいと思い、
上のように設定してもテキストエリア内にもcomment Modelにも反映されません。
回避策は、ng-initで明示的にcomment Modelをそのデフォルト値で初期化することです。
AngularJSでHTMLにバインドすると自動的にエスケープがかかってしまいます。サーバサイドでエスケープ処理を施している場合は、場合によっては2重エスケープ状態が発生してしまいます。
そんなときは、ngSanitizeをインストールして、
DOMには
と書く代わりに
のようにしてエスケープ処理をしないようにできます。
ここからは、AngularJSで一番素敵な機能だと思っているDirectiveについてのネタが続きます。
このようなElement DirectiveをJS側で定義したいとき、
のように、ハイフンつなぎ(x-switch)→キャメルケース(xSwitch)とする必要があります。
当然と言えば当然なのですが、テンプレートを指定したDirective DOMの内側にあらかじめコンテンツを入れておいてもテンプレートに置き変えられてしまいます。最初はこれに気づきませんでした。
Directiveの内側に入れたコンテンツをテンプレート内の特定の場所で利用したい場合は、JS側のDirective定義でtranscludeをtrueに設定し、template側のコンテンツを利用したい要素にng-transclude属性を設定する。
カスタムDirectiveはとても便利で、HTMLを綺麗にしておけるし、処理も独立させられるのでついついたくさん作ってしまいます。そのときに使用するテンプレートHTMLを外部においてtemplateUrlで読み込ませると、そのテンプレートの数分だけリクエストが別途走ってしまいます。
回避策は、外部テンプレートHTMLをtemplateCacheを使用してJSにキャッシュしてくれるgrunt-angular-templatesを使用することです。
こちらに関しては、別記事参照。
これは、ハマったことでも何でもないですが、AngularJSのデバッグをする際にDev ToolsとDOM自体にプロパティ表示用のコードを書いたりしていました。
AngularJSのデバッグ用Chrome Extensionに「Angular Batarang」というものがあります。これの存在をもっと早く知っていたらデバッグがもっと楽だっただろうと思います。
BackboneにもFirebug Extensionの「Backbone-Eye」などがありますし、やはり専用のデバッグツールがあると開発も快適です。
開発にハマっては調べハマっては調べしている日々の中、FrontrendのHiroki Taniさんに何気なく「ブログとか書いてみたらどうですか」と言われたのをきっかけに、どうせならAngularJSで調べたことでも書いてみようと思い、このブログを始めました。あまり継続して書けていないので、来年はもっと頑張ろうと思います。
Frontrend Advent Calendar 2013という場を借りて、とても個人的な振り返りをさせていただきました。今日この記事を書けるのも、先に書いた通りFrontrendの方々にいただいたきっかけが大きく影響しています。
明日は、ysugimoto さんです。
]]>通常はstableな「1.0.8」を使用すればいいのですが、たとえばngAnimateなどの機能は「1.2.0」から提供されているので、ngAnimateを使いたくてアップデートすることもあるかと思います。
SPAで開発する場合など$routeProviderサービスを使用してルーティング処理を行っていると思いますが、1.0.8では本体に組み込まれている$routeProviderが1.2.0ではngRouteモジュールのサービスとして分離されています。
なので、$routeProviderを使用しているアプリで「1.2.0-rc」にアップデートした場合、下記のようなエラーがコンソールに表示されます。
Uncaught Error: [$injector:modulerr] Failed to instantiate module angularApp due to: Error: [$injector:unpr] Unknown provider: $routeProvider
対応方法は簡単で、ngRouteモジュールを提供するangular-route.jsを別途インストールします。例えばBowerで
でインストールした後、HTMLファイルでangular-routeを読み込み、
依存するモジュールとしてngRouteを追加します。
これでエラーは消えて正常な動作に戻るはずです。
]]>AngularJSのElement Directiveは便利でHTMLも綺麗になるので好きなんですが、テンプレートをDirectiveの中に書くのだけ好きになれません。
JavaScriptファイル内に文字列として書くためエディタのカラーリングも効かず、書くにくいのが嫌でした。
もちろんtemplateの代わりにtemplateUrlで外部HTMLを使うこともできますが、別途AJAXしてしまうので無駄にファイルのリクエスト数が増えてしまうのも好ましくありません。
よく使うテンプレートたちは外部HTMLで書いておいて、リクエスト数は抑えたい。
それの1つの解決方法として、今回はgrunt-angular-templatesを使いました。
grunt-angular-templatesは、Eric Clemmons氏のGruntプラグインで、指定したHTMLファイル郡をミニファイ・結合して1つのJavaScriptファイルとして出力してくれます。
そのJavaScriptファイルには、指定したHTMLファイルを$templateCache.putするように記述してあるので、後はそのURLをtemplateUrlで指定すると別途AJAXすることなくキャッシュしたデータを使用してくれます。
インストールはnpm。
そしてGruntfile.jsでロードを有効化。
まずは一番シンプルな形を試します。AngularJSのテンプレートとして書いたHTMLファイル全てを$templateCacheにキャッシュさせるJavaScriptを出力させます。
Gruntfile.jsで以下の最低限のオプションだけ記述します。
"myApp"はテンプレートを使うモジュール名に合わせます。
実行します。
もし下記のような名前のHTMLファイルを入れていれば
js/templates.jsには以下のようなコードが出力されます。
あとはこのjs/templates.jsを
と読み込めば、btnLike.htmlとbtnFollow.htmlの内容は読み込まれてキャッシュされるので、使うときは$routeProviderやDirectiveのtemplateUrlで
という感じで使うか、またはHTMLに直接
とngIncludeで指定して使うことができます。
もしテンプレートが置いてあるパスと実際にアプリ内で指定するURLが違う場合は、cwdオプションを使って下記のようにカレントディレクトリを別途指定します。
また、テンプレート用JSに変換する際にhtmlminを利用することができるので、
オプションでhtmlminを指定して出力時のファイルサイズを最適化できました。
grunt-angular-templatesのおかげでテンプレートの外部HTML化が実現し、リクエスト数を抑えることもできました。
grunt-angular-templateshttps://github.com/ericclemmons/grunt-angular-templates
]]>