このエントリーをはてなブックマークに追加

こんにちは。フロントエンドエンジニアの松岡です。

この記事では、Webアクセシビリティ向上をスタートするためにカルテット開発部で行った取り組みについて紹介させていただきたいと思います。

Webアクセシビリティ

私が「Webアクセシビリティ」を認識したのは、フロントエンド界隈でアクセシビリティについての注目度が上がり、時おりブログなどでこの言葉を見かけるようになってからです。

歴史に目を向けると、Webの技術の標準を定めているW3Cが20年以上前から WCAG(Web Content Accessibility Guidelines) を公開しています。

当初は障害のある人がWebを使えるようにしましょうねという目的だったものが、昨今では人種やデバイス環境なども含めた幅広いニーズに対するインクルーシブなWebサイト作り、という広がりを見せているようです。

知識ゼロからのスタート

WCAGのドキュメントには知覚可能・操作可能・理解可能・堅牢の 原則 があり、それぞれの原則ごとの ガイドライン が設けられています。そしてガイドラインには A AA AAA達成基準 があります。ガイドラインは数多くあり、一番緩い A だけに注目してみても、かなり頑張らないと実現できないボリュームです。

カルテットコミュニケーションズでは、リスティング広告のレポート作成・予算管理の自動化ツール「 Lisket 」やGoogleアナリティクス自動レポート化ツール「 無限GAレポートメーカー 」をはじめ、いくつかのプロダクトを制作・公開しています。これらのプロダクトのアクセシビリティを一斉に底上げするには相当なリソースを費やす事になりそうです。

アクセシビリティ向上に取り組んでいく上で難しさを感じたのが「どんなユーザーが使いづらさを感じているのか」を可視化できない点でした。年齢や性別のような属性はアクセス解析ツールを使って推測できますが、テキストの読みづらさやボタンの押しづらさといった体感的なものは数値化する事ができません。

さらには「アクセシビリティについて言葉だけは知っている」「具体的な実践方法を少し知っている」などエンジニアにより知識のばらつきがあり、何をどうしていくのかハンドリングできる人材がいないのも悩みの種でした。

社内勉強会の開催

まず始めに取り組んだのは、認識と知識のばらつきを統一するための社内勉強会です。

Webアクセシビリティ | 社内勉強会資料

この勉強会では下記のような事について認識合わせをしました。

  • WCAGとは何か
  • どんな表現がアクセシビリティに反しているのか
  • WAI-ARIAとは何か
  • どんなケースでアクセシビリティが不足しがちなのか

かくいう私もアクセシビリティ向上を実践した事のない初心者です。事前知識として Webアクセシビリティ Advent Calendar 2020 などを読ませていただき、出てくる用語を片っ端からネットで検索して、アクセシビリティってこういう事じゃないかという持論を展開した勉強会でした。

任意参加を呼びかけての勉強会でしたが、びっくりしたのが開発部のメンバーほぼ全員が参加した事です。どちらかというとユーザーとの対話が多いフロントエンド領域の関心事かなと思っていたのですが、エンジニアとして、また人として、興味感心の高いワードなんだなと改めて認識できた機会となりました。

自分たちに今できる事

これからアクセシビリティ向上に取り組んでいく私達が最初から完璧なWCAG準拠を目指すのはとても難しい事です。また、達成基準クリアをゴールとするのか、スクリーンリーダーを各種取り揃えないといけないのか、WAI-ARIAの属性はどう書くのが正解か、書いたコードをどうやってチェックするのか、などなど問題は山積みです。

そこで私達が決めた最初のステップは「自分たちに今できるミニマムなスタートを切る」事です。「しなければいけない」ではなく「何ができるのか」を考える事にしました。

チームガイドラインの策定

勉強会の次に取り組んだのは、フロントエンドを担当するチームでのガイドラインの策定です。

外部公開はしていませんので、一部抜粋したものを紹介させていただきます。

ガイドライン

まず私達が記憶できるボリュームかつ日常的に実践可能なものをピックアップして、できる事(ルール)を取りまとめました。ドキュメントの置き場所はGitHub上のマークダウンファイルです。その後、チームのメンバーにレビューをしてもらい Must(しなければいけない) Should(することが望ましい) Optional(しても良い) の3段階の必須レベルを割り振りました。

ガイドラインのルール

それぞれのルールは、目的・テスト方法・良いコード例・悪いコード例・WCAGの参考リンクといった項目を記載した個々のドキュメントになっています。色に関する表現などはコードだけでは結果が推測しづらいので Stackblitz でサンプルコードを書き、プレビュー画面を見られるようURLを記載しました。

取りまとめたチームガイドラインの適用範囲は、今後作っていくプロダクト、または既存のプロダクトで修正を入れる部分です。チェックツールは開発環境で手軽に利用できるGoogle ChromeのエクステンションやmacOS搭載のVoiceOverを使う事にしました。

チームガイドラインのフォーマットは、Amebaさんの Ameba Accessibility Guidelines を参考にさせていただきました。

その後の反応

勉強会とチームガイドラインの策定を経て変わったのは、個々のエンジニアの意識です。「プロダクトに関わるこのサイトも対象ですよね?」「こういうコードを書いたんですがアクセシビリティ的にどうでしょう?」など自然発生的に問いかけが出てくるようになりました。

ゼロからのスタートなので、もちろん誰も正解を持ち合わせていません。都度ネット検索したり、VoiceOverを起動して画面確認したり、アナログなチェック作業を繰り返しています。

取り組み前は、アクセシビリティという言葉の存在は知っているもののネット上の膨大な情報量に圧倒されてどこから手をつけていいのか分からないというのが正直な気持ちでしたが、チーム全員でやろう!と決めた事で小さな一歩を踏み出せた実感があります。

まとめ

記事執筆とちょうど同じタイミングで、弊社で使用しているフロントエンドのフレームワークAngularの新しいブログ記事が公開されました。

Build more accessible Angular apps

Webアクセシビリティが昨今のフロントエンドのトレンドとはいえ、なんともタイムリーなブログ公開に不思議な縁を感じました 😃

私達のアクセシビリティ向上への取り組みはまだまだスタートしたばかりです。いつの日か、アクセシビリティに配慮したコードを当たり前のように書き、是非についてディスカッションできるよう知識を付けていきたいと思っています。そしてそれらがユーザーのみなさまに体験として届けられる日を、私達自身も楽しみにしています。


このエントリーをはてなブックマークに追加

PHPカンファレンス2021にオンライン参加しました。

弊社からは、菱田が「抽象のはしごの上手な登り方〜使いやすい汎用ライブラリを作るために〜」という題で登壇しました :tada::clap:
動画アーカイブ:https://www.youtube.com/watch?v=xoYamWpmKDo&t=19079s
↑より「抽象のはしごの上り下りの練習にはアウトプットが良い:bulb:」ということで早速アウトプットしますー!

SQL苦手PHPerな私には「PHPer が知るべき MySQL クエリチューニング」のトークが印象的で勉強になりました!
動画アーカイブ:https://www.youtube.com/watch?v=y8AZV1HnieA&t=5876s

そこで今回はトーク内で「EXPLAINの見方」が紹介されていたので、実際に手を動かしてみたいと思います!

[準備]クエリーの見える化

今回は、Symfony + Doctrineのプロジェクトを使い、Doctrineのログのみを出力するdoctrine_only.logを設定していきます。
デフォルトのvar/log/dev.logに出力されるログには以下のように、REQUESSECURIログも含まれるのでDOCTRIだけにしてみます。
(実際にクエリーチューニングする場合は、デフォルトのログの方が便利だと思います。)

var/log/dev.log

[Application] Oct  5 07:08:44 |INFO   | REQUES Matched route "app_home". request_uri="https://127.0.0.1:8000/" route="app_home" route_parameters={"_controller":"App\\Controller\\DefaultController::list","_route":"app_home"}
[Application] Oct  5 07:08:44 |DEBUG  | SECURI Checking for authenticator support. authenticators=1 firewall_name="main"
[Application] Oct  5 07:08:44 |DEBUG  | DOCTRI SELECT t0.id AS id_1, t0.user_name AS user_name_2, t0.full_name AS full_name_3, FROM user t0 WHERE t0.id = ? LIMIT 1 0=1

config/packages/dev/monolog.yaml

monolog:
    handlers:
        main:
            type: stream
            path: "%kernel.logs_dir%/%kernel.environment%.log"
            level: debug
            channels: ["!event"]
...
+       doctrine_only:
+           type: stream
+           path: "%kernel.logs_dir%/%kernel.environment%_doctrine_only.log"
+           level: debug
+           channels: ["doctrine"]

これで、var/log/dev_doctrine_only.logを出力できるようになりました:raised_hands:

参考:https://tech.quartetcom.co.jp/2018/05/31/monolog/

EXPLAINとは?

EXPLAIN は、クエリー実行計画 (つまり、MySQL がクエリーをどのように実行するかの説明) を取得するために使用されます。

引用:https://dev.mysql.com/doc/refman/8.0/ja/explain.html

EXPLAINの主な見方

以下のuserテーブル(全レコード数:1000)があり、(1)と(2)のEXPLAIN結果を元に比較してみます。

mysql> describe user;
+-------------------+--------------+------+-----+---------+----------------+
| Field             | Type         | Null | Key | Default | Extra          |
+-------------------+--------------+------+-----+---------+----------------+
| id                | int(11)      | NO   | PRI | NULL    | auto_increment |
| user_name         | varchar(255) | NO   |     | NULL    |                |
| full_name         | varchar(255) | NO   |     | NULL    |                |
+-------------------+--------------+------+-----+---------+----------------+
3 rows in set (0.00 sec)

mysql> show index from user; # 既存indexの確認
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| user  |          0 | PRIMARY  |            1 | id          | A         |           3 |     NULL | NULL   |      | BTREE      |         |               |
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
1 row in set (0.00 sec)

mysql> select count(*) from user; # 全レコード数の確認
+----------+
| count(*) |
+----------+
|     1000 |
+----------+
1 row in set (0.00 sec)

(1) WHERE句より、PKかつindexのカラム(=id)で絞り込むクエリー

mysql> EXPLAIN SELECT t0.id AS id_1, t0.user_name AS user_name_2, t0.full_name AS full_name_3 FROM user t0 WHERE t0.id = 1;
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type  | possible_keys | key     | key_len | ref   | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
|  1 | SIMPLE      | t0    | NULL       | const | PRIMARY       | PRIMARY | 4       | const |    1 |   100.00 | NULL  |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)

(2) WHERE句より、PKではないかつindexが貼られていないカラム(=full_name)で絞り込むクエリー

mysql> EXPLAIN SELECT t0.id AS id_1, t0.user_name AS user_name_2, t0.full_name AS full_name_3 FROM user t0 where t0.full_name = '志賀彩乃';
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | t0    | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 1000 |    10.00 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

type

  • ALL : フルスキャン、最も遅い。インデックスが全く使用されていない
  • index : インデックスでは対応しきれてない。対象が多い。
  • range : インデックスを使った範囲検索している。
  • const : PK/UKで、1行/0行を取得している。最高速。

key

オプティマイザー(←実行時間が最小になるように処理方法を決めてくれる)が選択したインデックス
→ 狙いと違うインデックスが使われていたら注意する。

rows

検索対象のレコード数
→ 想定より多い場合は、追加の検索条件の検討をする。

比較結果

  • (1)のクエリーはPKかつindexのカラム(id)で絞り込んでいるためにtype=const(最速),key=PRIMARY(インデックス使用),rows=1(対象レコード数を絞り込めている)となっており理想的。
  • 一方、(2)のクエリーはtype=ALL(フルスキャン),key=NULL(インデックス未使用),rows=1000(全レコードが検索対象)となり改善が見込めるクエリーとなりそう。

SQLの実行順序

SQLの実行順序はFROM → ON → JOIN → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMITなので、膨大な量のレコード検索の場合LIMITのみで絞り込んでも負荷が高くなる。
→ SELECTやWHEREなどで絞り込みが可能な場合は絞り込みを行う。

BeforeのようにLIMIT 10のみだと、type=ALL(フルスキャン),rows=1000(全レコードが検索対象)となっている。
一方、AfterのようにWHERE id <= 10を追加するとtype=range(インデックスを使った範囲検索),rows=10となり検索対象のレコード数が小さくなり負荷が低くなる。

Before

mysql>  EXPLAIN SELECT t0.id AS id_1, t0.user_name AS user_name_2, t0.full_name AS full_name_3 FROM user t0 LIMIT 10;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
|  1 | SIMPLE      | t0    | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 1000 |   100.00 | NULL  |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)

After

mysql> EXPLAIN SELECT t0.id AS id_1, t0.user_name AS user_name_2, t0.full_name AS full_name_3 FROM user t0 WHERE id <= 10 LIMIT 10;
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type  | possible_keys | key     | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | t0    | NULL       | range | PRIMARY       | PRIMARY | 4       | NULL |   10 |   100.00 | Using where |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

最後に

今回初めてEXPLAINを真面目に見ることができ、とっても勉強になりました!まみーさんありがとうございます:bow:
その他のトークも面白くて大変勉強になりました!登壇者のみなさん、実行委員のみなさん本当にありがとうございました!
まだ観れていないトークもあるのでアーカイブを漁りたいと思います:fish:
ペチコン全体のタイムテーブルはこちらです。ここからYoutubeでアーカイブも視聴できるので興味がある方は観てみてください。
https://fortee.jp/phpcon-2021/timetable


このエントリーをはてなブックマークに追加

PHPカンファレンス2021にて、「抽象のはしごの上手なのぼり方〜使いやすい汎用ライブラリを作るために〜」を発表します。
YouTube Liveの画面ではスライドが見づらい可能性があるため、スライドを事前に公開しています。
Discordでたくさんの方とお話したいと思いますので、ぜひ感想をお聞かせください。
14:55〜Track3でお待ちしています ☺️