GameWith Developer Blog

GameWith のエンジニア、デザイナーが技術について日々発信していきます。

GameWith の Core Web Vitals(Cumulative Layout Shift) 改善! #GameWith #TechWith

初めに

こんにちは。GameWith のエンジニアの tiwu です!

今回のブログは GameWith の Core Web Vitals(Cumulative Layout Shift) 改善について書いていこうと思います!

Core Web Vitals

developers-jp.googleblog.com

Core Web Vitals とは優れたユーザーエクスペリエンスを提供するための3つの指標を指します。

  • Largest Contentful Paint (LCP)
    • ページの主要コンテンツが読み込まれるまでの時間
  • First Input Delay (FID)
    • 最初の入力までの遅延時間
  • Cumulative Layout Shift (CLS)
    • 表示されるページコンテンツにおける予期しないレイアウトのずれの量

Core Web Vitals の詳細についてはこちらを御覧ください。

web.dev

Core Web Vitals は 2021年5月から SEO の要因に追加されことが発表されています。

developers.google.com

計測

GameWith では Core Web Vitals を以下のツールを利用して計測をしています

Search Console

Search Console の「ウェブに関する主な指標」から Core Web Vitals の改善が必要な URL や件数が確認できます。

Search Console のレポートのデータは Chrome UX Report から取得しており、実際にユーザーがページにアクセスをした際のデータになります。

support.google.com

f:id:tiwu:20210122155956p:plain

f:id:tiwu:20210122155920p:plain

なんと、今年の1月時点では17万以上の記事が不良 URL として判定されています!(多い!)

WebAutoPerf

Search Console ではサイトの全体的な件数などは確認できますが、指定の URL の日毎の推移などは確認できません。

GameWith では WebAutoPerf というツールを利用し、日毎の推移を計測しています。

web.dev

Chrome UX Report からデータを取得し、Spreadsheet に保存後、DataStudio で可視化しています。

f:id:tiwu:20210226191736p:plain

調査

GameWith では Cumulative Layout Shift のスコアが基準に達していないことがわかったので、Cumulative Layout Shift の改善を行っていくことにしました。

実際に改善をしていく際は Chrome DevTools を利用して、修正すべき箇所を調査していきます。

Performance タブでページを計測し、Experience 行が赤くなっているところを確認します。

f:id:tiwu:20210122162544p:plain

スクショのように赤くなっているところをクリックすると、実際にページのどの部分で問題が起こっているか青色でハイライトしてくれます。

このスクショの例では画像が読み込まれるまで高さを確保していないので、読み込み後テキストが下にずれてしまうという状態でした。

f:id:tiwu:20210122163642p:plainf:id:tiwu:20210122163651p:plain

改善

修正箇所がわかったので改善をしていきます。

今回の箇所は画像が読み込まれるまで高さを確保していなかったのが問題なので、読み込まれる前から高さを確保しておきます。

ここの画像はサイズが固定(600 * 315)なので、高さを計算して設定します。

.article-ogimage {
    width: 100%;
    height: calc(100vw * 315 / 600);
}

vw は viewport's width といって、1vw = ビューポート幅の 1% となります。

developer.mozilla.org

このように予め高さを指定して置くことで、Cumulative Layout Shift が改善されます。

f:id:tiwu:20210122165726p:plainf:id:tiwu:20210122163651p:plain

結果

Cumulative Layout Shift を改善したことで視覚的なズレが無くなり、ユーザー目線で見た時に体験がとても良くなりました!

また、いろいろな箇所を改善したことによって、ほとんどのページで良好 URL になりました 🎉

f:id:tiwu:20210304105904p:plain

f:id:tiwu:20210304105917p:plain

17万以上あった不良URLはほぼなくなり、WebAutoPerf のグラフもしきい値の 0.1 を大きく下回っています!

f:id:tiwu:20210304105928p:plain

終わりに

Core Web Vitals の改善を行う際に、Yahoo! さんの下記記事がとても参考になりました。ありがとうございます!

techblog.yahoo.co.jp

引き続きより良い体験を提供していくので、これからもよろしくおねがいします!

Twitter

Twitter にてテックブログの投稿など呟いていますので、よろしければフォローをお願いします!

twitter.com

Wanted!

一緒に働く仲間を募集しています!

www.wantedly.com

ダッシュボードに ESLint を導入した話 #GameWith #TechWith

はじめに

こんにちは!Incremental Stream Team の @53able です!

今回はダッシュボードに ESLint を導入した話を書いていこうと思います!

ESLint について

ESLint は JavaScript の構文チェックツールです。

eslint.org

導入の背景

GameWith のサービス側には2年前に ESLint が導入されています。

GameWith のダッシュボード側には導入されていなかったです。

ダッシュボードの開発が複数人で活発に行い、振り返りをしたところ構文チェックツールがないせいで、人によって微妙な書き方の差異が発生しており、開発速度が遅くなっているので、今回導入することにしました

コードレビューで、レビュワーが構文チェックをしてコメントするのはかなりコストが高く、この課題も解決したかったです 例:var 使っているので let, const 使って欲しいなど

やりたいのは「構文チェック」と「自動整形」だったため、ESLint を導入しました

ESLint の設定

ESLint の設定を最初に紹介したいと思います!

{
    "extends": [
        "eslint:recommended",
        "plugin:prettier/recommended"
    ],
    "env": {
        "browser": true,
        "jquery": true,
        "es2021": true
    },
    "parserOptions": {
        "sourceType": "module"
    },
    "rules": {
        "prettier/prettier": ["error", {
            // prettierのルールをカスタマイズ
        }]
    }
}

Prettier

Prettier 関連のプラグインについていくつか紹介します。

eslint-plugin-prettier

Prettier のルールを ESLint のルールとして実行できるようにするプラグインです。

www.npmjs.com

eslint-config-prettier

ESLint にもコードの整形についてのルールがいくつかあります(例えば、インデントは半角スペース2つにするか4つにするか)

こういった Prettier とコンフリクトするルールについて、Prettier の設定で ESLint を上書きするようにするプラグインです。

www.npmjs.com

導入作業

1. 現在のエラーの調査

まずはじめに、GameWith のサービス側に既に ESLint が導入されているので、まず最初にダッシュボードの JavaScript に ESLint を実行し、どんなエラーがどれくらい出るのか確認しました。

確認する際はダッシュボードの JS のディレクトリにはライブラリの JS を直接ダウンロードして配置してあったので、上記 ESLint 実行時には無視するように設定をしました。

また、今回のダッシュボードへの導入のタイミングで、ESLint の enves6 から es2021 に引き上げました(es2021 は Chrome 85 から対応しており、問題ないと判断をしました)

ESLint を実行すると、ファイルごとにエラーが表示されます。

今回知りたかったのは、ファイルごとのエラーではなく、全体を通してどんな種類のエラーが出ているかを知りたかったので eslint-report-by-rule というモジュールを利用しました。

www.npmjs.com

eslint-report-by-rule は下記のように実行します。

eslint-report-by-rule 'eslint dashboard -f json'

実行すると下記のように ESLint のルール別のエラー数が表示されます(下記は実際に図った数値で合計 4000 over のエラーが出ています!)

※ダッシュボードの JavaScript は量が多く、このコマンドは15分ほどかかりました笑

{
    "no-unused-vars": 141,
    "prettier/prettier": 3646,
    "no-undef": 545,
    "no-redeclare": 5,
    "no-useless-escape": 53,
    "no-const-assign": 1,
    "no-empty": 11,
    "no-prototype-builtins": 5,
}

2. エラーの修正(自動修正)

修正作業としてはまず ESLint の fix オプションを利用し修正をしました。

eslint --fix dashboard

この fix オプションよって、4000 件以上あったエラーが約 800 件になりました!

3500 件以上あった prettier/prettier のエラーは全てなくなりました(すごく助かります💦)

{
    "no-unused-vars": 141,
    "no-undef": 545,
    "no-redeclare": 5,
    "no-useless-escape": 41,
    "no-const-assign": 1,
    "no-empty": 11,
    "no-prototype-builtins": 5,
}

3. エラーの修正(手動修正)

fix オプションでも修正できなかったエラーに関しては手動で修正しました。

  • no-unused-vars
    • 使われていない変数は、削除したりコメントアウトしたりして修正をしました
  • no-undef
    • var, let, const が書かれてない変数に関しては、手動で let, const を付与しました
  • no-redeclare
    • 無駄な再代入は修正しました
  • no-useless-escape
    • 正規表現内の不要なエスケープは削除しました
  • no-const-assign
    • const 宣言している変数に代入をしていたので let 宣言に修正しました
  • no-empty
    • catch 文の中が空だったので修正しました
  • no-prototype-builtins
    • hasOwnProperty の実行方法について修正しました

苦労した点

関数から受け取った変数が利用されていなかったのでその変数と関数の実行を削除したのですが、テスト時にバグが発生し、調査すると消した関数が原因でした。

理由としては、削除した関数が副作用のある関数だったためです(具体的には関数内で DOM の操作を行っていました)

サンプルコード

if (hoge) {
  const piyo = fuga(elementId);
}

また、宣言子なしで定義されている変数はグローバルスコープになりどんな箇所からも参照することができるので、let, const の付与でスコープが変わってしまい参照できなくなる可能性もあったので、注意深く作業しました。

ダッシュボードには webpack や gulp といったバンドラーを利用していないため、ライブラリなど依存関係は <script> の読み込み順で解決していました。

そのため、ライブラリなどの別ファイル定義の関数やクラスなどを利用している箇所で no-undef エラーが発生していました。

今回はバンドラーなどは使わず import/export を利用し no-undef エラーを修正しました。

現代の主要なブラウザは import/export に対応しているので、バンドラーの導入など不要で修正できるため採用しました 👍

サンプルコード

import hoge from 'hoge.js';

const piyo = new hoge(elementId);

import が書かれている JavaScript は type=module で読み込む必要があるため、ダッシュボードに記述されている <script> を修正しました。

import/export を書く場合、ESLint で下記オプションを設定する必要があります。

"parserOptions": {
  "sourceType": "module"
},

import を使ってるので、jQuery の getScript の処理は全て import に書き換えました。

4. 完了

手動で修正したことによって、エラーは 0 になりました🎉

終わりに

今回はダッシュボードへの導入だったため、import/export はバンドラーを通さずに採用しています。

ダッシュボードは Chrome で開かれることが多いため、採用もしやすかったです!

導入は完了したのですが、実は PHP Template の <script> に直書きされた JavaScript が存在しているため ESLint による構文チェックができていません。

ちなみに直接書かれた JavaScript の先頭には下記コメントアウトを書き残してあります笑(約100箇所)

<?php //TODO このスクリプトに関連する作業を行う場合は、JSファイルにして ESLint を通すようにしてください。 ?>

現在はテストツールを導入しているので、次回はテストツールの導入について紹介できればと思います!

Twitter

Twitter にて GameWith のエンジニア向け情報発信をしています!

twitter.com

GoogleAnalyticsの 目標到達プロセスの分析 によって課題発見、改善できた話 #GameWith #TechWith

あいさつ

こんばんは、 @peka3 と申します。

今回は、以前の記事で紹介した、あつ森交換掲示板の改善についての話になります!

課題の発見から実装、その後の効果検証まで通して書きたいと思います。

以前の記事は↓↓↓からどうぞ。

tech.gamewith.co.jp

課題の発見

あつ森交換掲示板をリリース後、認知度が増すにつれて各数値は順調に伸びていったのですが、思ったほど投稿数が増えませんでした。

投稿の部分は入力項目が多く、ユーザーに操作を強いる部分です。

今回は、投稿画面に入って、離脱せずに投稿完了した人 の数値を調べてみました。

このような特定の手順で操作をした人を調べるにはGoogleAnalyticsの「目標到達プロセスの分析」が便利です。(アナリティクス360左メニューの「分析」から行けます)

f:id:peka3:20201217175931p:plain:h300

投稿画面から投稿完了に行くには、絶対にアイテムの選択をしなければいけません。

どこで離脱していったかを調べるために、 投稿画面に来たあと、アイテムを選択し、投稿完了した。 というイベントの流れをたどった率を分析します。

結果は以下のようになりました。

f:id:peka3:20201218200023p:plain
投稿完了率 改修前

(一部数値はふせさせていただきました!!)

投稿画面に来た時点で、投稿したいモチベがあるはずにも関わらず、投稿完了したユーザーは40%程度 でした。

この数値が多いか少ないかは比較対象がありませんが、感覚としてまだまだ伸ばせそうな手応えのある数値です。

課題へのアプローチ

投稿完了率がどうやら低そうというのはわかりましたが、一体なぜ低くなってしまっているのかがわかりません。

そこで次に打つ手として2つの選択肢がありました。

  • ユーザーにアンケートを取る
  • デザイナーにUI/UX観点で改善案がないか相談してみる

今回は後者のデザイナー相談をすることにしました。

実装を急いだこともあり、まだ改善の余地がありそうだったこと、こちらのほうがアンケートを取るより早く施策実行に移れるからという理由からです。

改修内容

相談後、以下のような改善案を実装することにしました。

  • アイテム選択のモーダルを全画面にする
    • アイテム選択画面ではアイテム選択に集中させたいから
  • アイテム選択したのを分かるように、トースト/snackbar 表示
    • 選択した際のインタラクションがなく、選択済みアイテム欄を見ないとわからなかった
  • アイテム保存ボタンを追従させる
    • 一番下までスクロールしないと保存ボタンが押せなかったため
  • 選択済みアイテム欄の追従
    • スクロールしてしまうと隠れてしまっていたため

f:id:peka3:20201217182033p:plain:w200 トースト追加

f:id:peka3:20201217182113p:plain:w200 選択枠の固定、保存ボタンの固定

どれも数日あれば実装できそうな内容です。

実際に一週間程度の稼働で全ての実装が終わりました!

今回は、ABテストなどはせずに一気に実装しました。 ABテストはもっとわずかな変化を検証したいときや、余計な要素を完全に排除して効果検証をしたいときに使うべきであり、今回はそれに適さないと思ったからです。

その後の変化

改修後、どのような変化があったのかの効果検証もGoogleAnalyticsの「目標到達プロセスの分析」が便利です!

新規ユーザーだけに絞っての調査もできるので「既存のUIで学習してしまったユーザー」を弾くことができるのです。

f:id:peka3:20201218201836p:plain
投稿完了率 UI改修後

各イベントでの離脱率が如実に現象しました!

新規ユーザーの投稿完了率も 65%を超えています

改修前に比べて20%以上の投稿完了率向上となりました!

まとめ

一週間ほどの改修作業で、投稿率20%改善という大きな効果が得られました。

今回は以下の要素がうまく噛み合って速やかに改善できたと思います。

  • 計測
  • 課題発見
  • デザイナーさんの改善案の素晴らしさ

特に「計測」がなければ課題発見も出来ませんでした。

プロダクトはリリースだけが大事ではなく、ちゃんとログを仕込んで正しく計測することも大事だと改めて思いました!

以上です。

  • TwitterにてGameWithのエンジニア向け情報発信をしています! twitter.com

Google Analytics 4 property(GA4)でできるようになったこととTips #GameWith #TechWith

はじめに

こんにちは。GameWith のエンジニアの m です!

このブログはアドベントカレンダーの22日目のブログになります!

qiita.com

今回は Google Analytics 4 property について記事を書きます!

GA4とは

2020年10月14日に正式リリースされた次世代版の Google アナリティクスです。

support.google.com

Firebase向け Google アナリティクス(FA)をもとに、イベントベース測定モデルに変更されています。

個人的に大きく変わったと思う点と、GA4利用上のTipsを紹介します。

従来の Google アナリティクスは、ユニバーサルアナリティクス(UA)と記載して区別します。

変わった点

ウェブとアプリのデータを一つのプロパティでみられるようになった

FAのデータストリームを連携することで、相互にデータを同期することができます。

後述するBigQueryでも同じテーブルとして扱うことができるので、プラットフォームを跨いだ分析が容易になります。

イベントのデータ構造が柔軟になった

UAではカテゴリ、アクションのように予め大きな項目が決められており、カスタムディメンションも事前に定義が必要でした。

GA4では事前定義を必要とせず、各イベントごとにパラメータを自由に設定することができます。

UAのデータ構造

  • イベントカテゴリ
  • イベントアクション
  • イベントラベル
  • カスタムディメンション
    • (プロパティに事前定義が必要な)ユーザー定義のkey value

GA4のデータ構造

  • イベント名
  • イベントパラメータ
    • ユーザー定義のkey value
  • ユーザープロパティ
    • ユーザー定義のkey value

大規模サービスでも無料で利用しやすくなった

UAはヒット数(ページビューやイベントの発火)に制限があり、 月のヒット数が1,000万以上になると、有償版へのアップグレードが必要でした。

GA4にはイベント数(ヒット数)にあたる制限がありません。

今現在、有償版がないので確定ではありませんが、イベント単位のリッチさ(パラメータの定義数、文字数など)が課金のポイントになりそうです。

BigQueryとの連携が簡単になった

UAでは有償版のGA360でしか提供されていなかったBigQueryへのエクスポート機能が利用できます。(BigQueryへの転送やストレージの料金は発生します)

support.google.com

GAのデータはAPI経由でアクセスすると値が丸められる影響で実数値の分析が難しいですが、 BigQueryでは各イベントがレコード単位で分かれているので、加工や分析が容易になります。

スケジュールタスクを作成し、自動でイベント名ごとに別テーブルに分割して参照しやすくすることもできます。

Tips

user idについて

GA4のデータをBigQueryにエクスポートした際のテーブルスキーマには、トップレベルに user_id カラムが存在します。

ウェブでイベントパラメータのキー名に user_id を設定しておくと、自動でトップレベルにも値が入るようになります。

また、user idがない場合でも、 user_pseudo_id というカラムに自動でCookie管理のIDが振られるので、 サービス側に識別IDがない場合は、こちらをもとに行動分析が可能です。

イベントパラメータのBigQueryテーブルスキーマについて

イベントパラメータは event_params という RECORD型(REPEATEDモード) のカラムに格納されます。

support.google.com

クエリをもとにどんなデータ構造になっているかを紹介します。

event_name と event_paramsだけのテストテーブルを用意

イベントパラメータのvalueは内部的に STRING型 INTEGER型 FLOOT型 に分かれています。

今回は簡略化して以下のようにスキーマを定義しました。

f:id:takuya_minami373:20201222161435p:plain

INSERT

RECORD型のデータを挿入する場合は、STRUCT を使って構造を明示する必要があります。

REPEATEDモード なので ARRAY の記載で複数の値の格納が可能です。

INSERT
  `PROJECT_ID.DATASET_NAME.TABLE_NAME` (event_name, event_params)
VALUES
  ("event_name1", ARRAY<STRUCT<key STRING, value STRUCT<string_value STRING, int_value INT64>>>[
    ("key1",STRUCT<string_value STRING,int_value INT64>('value1', NULL)),
    ("key2",STRUCT<string_value STRING,int_value INT64>('value2', NULL))
  ]);

SELECT

SELECT
  *
FROM
  `PROJECT_ID.DATASET_NAME.TABLE_NAME`
WHERE
  event_name = 'event_name1';

普通に抽出すると画像のように1レコードに複数のイベントパラメータが存在することがわかります。 f:id:takuya_minami373:20201222161344p:plain

イベントパラメータのネストを解除するには以下のようにクエリを発行します。

SELECT
  event_name,
  (SELECT value.string_value FROM UNNEST(event_params) where key = "key1") as key1,
  (SELECT value.string_value FROM UNNEST(event_params) where key = "key2") as key2,
FROM
  `PROJECT_ID.DATASET_NAME.TABLE_NAME`
WHERE
  event_name = 'event_name1';

これでフラットなレコードに整形されるので扱いやすくなりました。

サブクエリがある分、参照データが多くなってしまうように見えますが、 1クエリ内であればよしなにキャッシュしてくれるので処理されるデータ容量は変わりません。

f:id:takuya_minami373:20201222161453p:plain

終わりに

GA4がデフォルトのプロパティとなり、今後は集中して機能拡張されていくみたいです。

UAの終了日はまだアナウンスされていないですが、早めに並行稼働を進めていくことが推奨されています。

スムーズに移行できるよう一緒に準備していきましょう!

Twitter

GameWithのDeveloper向けTwitterアカウントも開設しました。

ブログの更新情報などを発信するので良かったらフォロー宜しくお願いします!

twitter.com

2020 年に起きたワークスタイルの変化とこれからについて #GameWith #TechWith

この記事は GameWith Advent Calendar 21 日目の記事です。

最近、ブログ更新を怠っていた @serima です。Splatoon 2 でナワバリバトルをする日々を送っていますが、お仕事では開発マネージャーをしています。

2020 年は新型コロナウイルスの影響で世界的に働き方の変革を求められた年であり、弊社 GameWith も例外ではありませんでした。

このポストではフルリモートワークへの切り替えにおいて気をつけたことや、現在の課題と試みについて簡単に紹介できればと思います。

なお、現時点(2020 年 12 月)においての会社としての対応は全社で原則フルリモートワーク(在宅勤務)という方針を採用しています。

gamewith.co.jp

コロナ以前のリモートワーク運用について

もともと私が所属しているサービス開発部では、週に 2 回までのリモートワークについては事前申請を行えば利用可能という運用を行っており、半数以上のエンジニアやデザイナーがその制度を日常的に利用していました。

リモートワークの目的を下記のように定義しドキュメントにて周知したうえで、リモートワーク利用希望者にはトライアルとしてまずは 1 ヶ月間実施してもらい問題がなければ本運用に入ってもらうという進め方をしていました。

# 目的

- 柔軟な働き方を選択できるようにする
    - 育児・介護等の制約が生まれたとしても、調整して業務ができる
    - 優秀なエンジニアの業務参加機会の増加
        - 副業、フリーランス人材の活用
- 生産性の向上
    - 集中できる環境、通勤時間の短縮

フルリモートワークへの切り替えにあたって

上記の制度を運用していたおかげで、すでに自宅に開発環境が整備されていること、リモートワークにメンタル的に慣れている方が多くいました。 そのため、先陣を切る形でサービス開発部所属のメンバーについてはリモートワークを推奨とするというアナウンスを行うことができました。

ちなみに 2020 年 2 月ごろはまだ新型コロナウイルスの情報が錯綜していたこともあり、報道で出た数字も鵜呑みにせず 1 次ソースに当たったり、信頼のおける知人から情報を頂いたりと気を払いながら意思決定の材料にしていました。

f:id:serimaryo:20201222025616p:plain

ちなみに、これ以降は全社的にフルリモートワークへ強めに舵を切っていったこともあり、この日以来物理的に対面していないメンバーも数多くいるという状況です。

リモートワークの制度があったとはいえメンバー全員が同時に利用したことはなかったので、下記については特に注意してもらうようにしていました。

  1. オンラインのコミュニケーション特性を理解すること
  2. 健康(メンタル・フィジカルどちらも)に気を使うこと

Slack でのアナウンスだけでなく、ドキュメントを書いて展開するなどいま振り返ると割と口を酸っぱくして伝えていたように思います。

f:id:serimaryo:20201222025721p:plain
長期リモートワークに向けた心がけを伝えるドキュメントチラ見せ

オンラインのコミュニケーション特性を理解する

フルリモートワーク前提で入社してきたわけではないので当たり前なのですが、必ずしもテキストコミュニケーションが得意な人ばかりではありません。

気の使いすぎや行間の読みすぎでテキストコミュニケーションに時間がかかり過ぎてしまったり、言いたいことがうまく伝わらず、あらぬ誤解を招く可能性があると感じていました。

そのため、初期のタイミングから「躊躇なく」オンライン MTG を行い口頭でのコミュニケーションもうまく併用して欲しいと考えていました。

「躊躇なく」を手助けするために、会話したいと思ったら即座に場を作れるようにしておくことが重要だと思ったため Slack App の Google+ HangoutsZoom をインストールし、早いタイミングで周知を行っておきました。

f:id:serimaryo:20201222025821p:plain

メンバー全員がフレキシブルに対応してくれたおかげで、現在は躊躇なくオンライン MTG を行うのが当たり前になっています。

健康(メンタル・フィジカルどちらも)に気を使うことの大切さ

以前からチームでの朝会は必ず行うようにしていましたが、フルリモートワークに切り替えてからも継続するようにしています。

フルリモートワークはお互いの顔が見えないが故に、ちょっとしたことで精神的に孤立してしまいがちです。

そして、ちょっとした精神的な孤立をきっかけにメンタル不全に陥ることは誰しもに襲いかかる非常によくあることだと考えています。 朝会をおこなうことで 1 日に 1 度は必ずチームメンバーとは会話をすることになるので、最低限ではありますがそこを担保するようにしています。また、朝会での雑談も推奨しています。

マネージャーとしてメンバーのフィジカル面にアプローチできることは少なく、注意喚起をおこなうことくらいしかできないのですが、ドキュメントでは筋トレやウォーキング・ジョギングを勧めました。

リモートワークを実際に体験した方は実感しているかもしれませんが、よほど意識しない限りはほぼ筋肉を使わずに生活できてしまいます。

私も例外ではなく、油断するとずっと座りっぱなしだったため春頃にスマートウォッチを購入しました。座りっぱなし警告や歩数計機能を利用して最低限は体を動かすようにしています。

ちなみにリングフィットアドベンチャーを持っているチームメンバーはかなり活用しているようでした。

個へのフォーカス

お互いの顔が見えなくなったことで、同僚がどんな仕事をしているのか分かりづらくなった面があると感じ、部署全体会のコンテンツのひとつとして「業務アピール会」というものを実施するようになりました。

"チームが何をしたか"というのはチームごとの定例など情報共有のパスは存在するのですが、"あなたが何をしたか"を他のチームメンバーが知る術がありません。(もちろん積極的に聞きにいくのであれば話は別です!)

弊社の場合は複数チームでひとつのプロダクトを開発していることもあり、個々人の仕事は間違いなく相互に影響しあっているにも関わらず、です。

個へフォーカスする機会が減ってしまったことで、その人の人となりやスキルなどに関心を持つ機会が互いに失われてしまったのなら、それは非常にもったいないことだと思います。同僚として働いているのであれば、フルリモートワークであっても良い関係が築けるといいなと考えています。

そのため、

  • 個のプレゼンテーションスキルの向上
  • プレゼンターやプレゼンターの普段の仕事に関心を持てるようになるためのキッカケづくり

このあたりをねらいとしてさだめ、運営しています。

以下が試験的に導入した Spatial.chat にて実施したときの様子です。

Spatial.chat とは近くの人の声が大きく聞こえるという距離の概念がはいった一風変わったインタフェースのビデオチャットのアプリケーションです。

(こちらのツールについては本ポストではスコープ外とさせてください)

f:id:serimaryo:20201222025921p:plain

楽しめるコンテンツになるように有志にて毎月 KPT を重ねて少しずつ改善しています。

f:id:serimaryo:20201222025953p:plain
業務アピール会の直近のKPTの様子

これからについて

引き続き原則フルリモートワーク(在宅勤務)体制にて運用していく予定です。 今後はさらに、組織としても制度としても新しいワークスタイルに最適化させていきたいと考えています。

フルリモートワークに切り替えたことで、地理的な制限がなくなったことには可能性を感じていまして、採用の幅が大きく拡がったと考えています。

いまは特にサーバサイドエンジニアと開発ディレクターを募集しています。

技術力が大事という価値観も保ちながら、ユーザーに近く強い価値を提供していけるニュータイプのエンジニアを組織、個人問わず作れないかと挑戦しています。

このままで良いのかと悩んでるエンジニアとぜひ一緒にこの殻を破り、更に成長を一緒にしていきたい。その挑戦が出来る環境です。新しい事は失敗を恐れず一緒に挑戦していきましょう。

興味がある方は @serima までお気軽に DM 頂いても構いませんし、Wantedly から「話を聞きに行きたい」ボタンをポチっとしていただいても構いません。

www.wantedly.com

Twitter

GameWith の Developer 向け Twitter アカウントも開設しました。 ブログの更新情報などを発信するので良かったらフォロー宜しくお願いします!

twitter.com

ダッシュボード (Vue.js, Composition API) とテストコード #GameWith #TechWith

はじめに

こんにちは。GameWith のエンジニアの tiwu です!

このブログはアドベントカレンダーの20日目のブログになります!

qiita.com

今回は Vue.js, Typescript で開発しているダッシュボードのテストコードについて書いていきたいと思います!

導入についてはこちらの記事を御覧ください。

tech.gamewith.co.jp

サンプルページ

サンプルページのコードと共に解説していこうと思います。

例としてユーザーの一覧と検索機能を持ったダッシュボードの場合、下記のような構成になります。

- src
  - components
    - pages
      - user
        - list.vue
  - composition
    - user
      - use-list.ts

composition-api.vuejs.org

composition フォルダには Composition API RFC を参考にロジック部分を分けています。

src/components/pages/user/list.vue

検索機能とユーザー一覧を表示するページのサンプルコードです。

コンポーネントライブラリとして BootstrapVue を利用しています 👍

bootstrap-vue.org

表示するデータや取得処理は Composition API 経由で、検索は URL パラメータを書き換え更新することで検索を実現させています。

The サンプルコードと言った感じのシンプルな作りになっています。

<template>
  <div>
    <label for="input-userId">ユーザID</label>
    <b-form-input
      id="input-userId"
      type="number"
      :state="userIdModel.state"
      v-model.number="userIdModel.input"
    >
    </b-form-input>
    <b-button @click="submitSearch" variant="outline-primary">検索</b-button>
    <b-button @click="resetSearch" variant="outline-secondary">リセット</b-button>

    <b-pagination-nav :link-gen="linkGen" :number-of-pages="numberOfPage"></b-pagination-nav>
    <b-table :items="users">
      // 省略
    </b-table>
  </div>
</template>

<script lang="ts">
import { defineComponent, reactive, toRefs, getCurrentInstance } from '@vue/composition-api';
import { useList } from '@/composition/user/use-list';

export default defineComponent({
  setup() {
    const root = getCurrentInstance();
    const reactiveData = reactive({
      page: Number(root.$route.query.page || '1'),
      userIdModel: {
        key: 'userId',
        input: root.$route.query.userId ? Number(root.$route.query.userId) : null,
        state: null,
      },
    });

    const {
      fetchUserList,
      users,
      numberOfPage
    } = useList();

    const linkGen = (pageNum: number): string => {
      return `?page=${pageNum}` + '&' + buildQuery();
    };

    const submitSearch = (): void => {
      const query = buildQuery();
      window.location.href = '/list?' + query;
    };

    const resetSearch = (): void => {
      window.location.href = '/list?';
    };

    (async(): Promise<void> => {
      await fetchUserList(
        reactiveData.page,
        Number(userIdModel.input)
      );
    })();

    return {
      ...toRefs(reactiveData as any),
      users,
      numberOfPage,
      linkGen,
      submitSearch,
      resetSearch,
    };
  }
});
</script>

src/composition/users/use-list.ts

Composition API 側も至ってシンプルで、API 経由でデータを取得する関数と、ユーザー情報を持つ state を管理しています。

API のリクエストは aspida を利用しています。詳しくはこちら 👍

tech.gamewith.co.jp

import { reactive, toRefs } from '@vue/composition-api';
import { RepositoryFactory } from '@/repositories/repository-factory';

export interface User {
  id: number;
  name: string;
}

export interface Users {
  users: User[];
  numberOfPage: number;
}

export const useList = () => {
  const state = reactive<Users>({
    users: [],
    numberOfPage: 1
  });
  const DashboardRepository = RepositoryFactory.getDashboardRepository();

  const fetchUserList = async(
    page: number,
    userId?: number,
  ): Promise<void> => {
    const response = await DashboardRepository.api.user.$get({
      query: {
        page: page,
        user_id: userId,
      }
    });
    state.users = [];
    for (const user of response.users) {
      state.users.push({
        id: user.id,
        name: user.name,
      });
    }
    state.numberOfPage = response.number_of_page;
  };

  return {
    ...toRefs(state),
    fetchUserList
  };
};

テストについて

テストは Jest を利用しています。

Components のテスト

コンポーネントライブラリとして BootstrapVue を採用しているので、まずその設定を行います。

shallowMount() では BootstrapVue のコンポーネントをレンダリングしないので mount() を利用しています

ユーザーの情報をモックを利用して定義し、スナップショットテストを実行します。

import { createLocalVue, mount } from '@vue/test-utils';
import VueCompositionApi from '@vue/composition-api';
import { default as BootstrapVue, IconsPlugin } from 'bootstrap-vue';
import List from '@/components/pages/user/list.vue';
import * as useList from '@/composition/user/use-list';

const localVue = createLocalVue();
localVue.use(VueCompositionApi);
localVue.use(BootstrapVue);
localVue.use(IconsPlugin);

const mockUseList = jest.spyOn(useList, 'useList');

describe('list.vue', () => {
  it('ユーザー一覧', () => {
    const mockValue: useList.Users = {
      numberOfPage: 1,
      users: [
        {
          id: 1,
          name: 'user 1'
        },
        {
          id: 2,
          name: 'user 2'
        }
      ],
    };
    mockUseList.mockReturnValue({
      fetchUserList: jest.fn(),
      numberOfPage: mockValue.numberOfPage as any,
      users: mockValue.users as any,
    });

    const $route = {
      query: {
        page: 1,
      },
    };

    const wrapper = mount(List, {
      localVue,
      mocks: {
        $route,
      },
      stubs: {
        transition: false
      },
    });
    expect(wrapper.html()).toMatchSnapshot();
  });
});

Composition のテスト

aspida のブログでも触れてましたが、aspida のモックを作りテストを書いています。

import { createLocalVue } from '@vue/test-utils';
import VueCompositionApi from '@vue/composition-api';
import { useList, User } from '@/composition/user/use-list';
import { DashboardRepository } from '@/repositories/dashboard/repository';

const localVue = createLocalVue();
localVue.use(VueCompositionApi);

type UserListType = ReturnType<typeof DashboardRepository.api.user.$get> extends Promise<infer T> ? T : never;
const mockUserList = jest.spyOn(DashboardRepository.api.user, '$get');

describe('use-list.ts', () => {
  describe('fetchUserList', () => {
    it('ユーザー一覧の取得', async() => {
      const mockValue: UserListType = {
        number_of_page: 1,
        users: [
          {
            id: 1,
            name: 'user 1'
          },
          {
            id: 2,
            name: 'user 2'
          }
        ]
      };
      mockUserList.mockReturnValue(mockValue as any);

      const {
        fetchUserList,
        users,
        numberOfPage
      } = await useList();

      expect(users).toEqual([]);

      await fetchUserList(1);

      expect(numberOfPage.value).toEqual(mockValue.number_of_page);
      expect(users[0]).toEqual(mockValue.users[0]);
    });
  });
});

小ネタ

検索の箇所に出てきた window.location.href のテストも書いていきます

const submitSearch = (): void => {
  const query = buildQuery();
  window.location.href = '/list?' + query;
};

const resetSearch = (): void => {
  window.location.href = '/list?';
};

こちらを参考にテストを書いていきます 👍

stackoverflow.com

const buildQuery = (): string => {
  return 'username=hoge&userId=1';
};

global.window = Object.create(window);
Object.defineProperty(window, 'location', {
  value: {
    href: ''
  }
});

expect(window.location.href).toEqual('');
submitSearch();
expect(window.location.href).toEqual('/list/?username=hoge&userId=1');
resetSearch();
expect(window.location.href).toEqual('/list');

終わりに

テストできなさそうな window.location.href も意外とテストできたりするのは驚きました!

これからもどしどしテスト書いていきましょう!

Twitter

GameWithのDeveloper向けTwitterアカウントも開設しました。
ブログの更新情報などを発信するので良かったらフォロー宜しくお願いします!

twitter.com

ツール開発で firestore を初めて使って感じた利点と、ハマった落とし穴 #GameWith #TechWith

こんばんは✋

@peka3 です。 ヘムロックがお気に入りです。

ブログは久しぶりに書きます。

最近は攻略ツールを開発をしております。

先日、あつ森でのアイテムを交換するためのツール「あつ森交換掲示板」を作りました。

フロントエンドは GameWithDesignSystem を使っています。 こちらについては以前の記事に詳しく載っているのでぜひ読んでみてください。

tech.gamewith.co.jp

実装側としては Vue + TypeScript で Webコンポーネントを実装していくことになります。

そしてバックエンドなのですが、今回はすべて firestore で実装しました。

firestore については、RDB脳だとハマりどころが多かったので、今回はこちらを詳しく紹介できればと思います!!

まず最初に、あつ森交換掲示板の機能をざっくり紹介いたします。

よくあるスレッド型掲示板とそれほど違いはないので、読み飛ばしていただいても大丈夫です。

あつ森交換掲示板の紹介

あつ森交換掲示板 | あつまれどうぶつの森 - GameWith

一覧画面

f:id:peka3:20201204190004p:plain:h100
あつ森交換掲示板 一覧画面

現在の取引募集を一覧で見ることができます。 アイテムを選択して、検索をすることができます。

投稿画面

f:id:peka3:20201204191730p:plain:h100
投稿画面

欲しいアイテム、譲れるアイテムを選択して投稿します

詳細画面

f:id:peka3:20201204192204p:plain:h100
詳細画面

投稿主に対して、コメントすることができます。 この画面で交換のやり取りが行われます。

アイテム選択モーダル

f:id:peka3:20201204200802p:plain:h100
アイテム選択モーダル

アイテム一覧から様々な条件で絞り込み、アイテムを選択する画面です。

firestoreでのデータの持ち方

今回作ったコレクションは3つになります。

  • アイテム情報
    • アイテム名、サムネイル等
  • 取引情報
    • 欲しいアイテム、譲るアイテム、投稿ユーザーの情報等
    • サブコレクションで 返信情報 も保持しています
  • 削除ログ
    • 規約に反した投稿を削除した際のログ

firestore での DB 設計の勘所

firestoreで大事なところとして 「なるべく1つのコレクションに画面を出力するのに必要なすべてのデータを格納する」 というのがあると思いました。

firestore には join がない

まずfirestoreはjoinがありません。

RDBですと、取引情報には、欲しいアイテムのアイテムID、譲るアイテムのアイテムIDだけ格納し、 実際に画面でアイテム名が必要になったらjoinによってアイテム名を取得してくる、という流れになると思いますが、

firestoreでそれをやろうとすると、joinがないためアイテムID一つに対して1回読み取りクエリを投げることになります。 パフォーマンス的にも料金的にも、とても非効率なります。

そのため、今回は取引情報のなかにアイテム名やサムネイルURL等、画面の描画に必要なデータをすべて盛り込みました。

firestoreの便利だったところ

https://firebase.google.com/docs/firestore/query-data/queries?hl=ja#array_membership

firestore には array-contains 演算子というものがあります。

これが今回の「欲しいアイテム」「譲るアイテム」から特定アイテムを検索する、という要件にぴったりと合致しました。

「欲しいアイテム」と「譲るアイテム」は複数設定できます。 それぞれのアイテム名を配列で持つフィールドを用意し、そこに対して array-contains で where することで、一発で検索できるようになりました。

コード例:

wishItemTradesRef = itemTradesRef.where(
    "items",
    "array-contains",
    selectedItemNames
);

firestoreでの論理削除は非効率

firestoreでも論理削除をすることもできますが、削除フラグを持ってしまうと、削除フラグを含めた複合indexをたくさん作る必要がでてきます。 firestoreはindexも課金対象であるため、できれば避けたいです。

今回の要件として、削除された対象のログをあとから追跡できれば良かったので、削除したログを残すコレクションを別途用意し、取引情報のほうは物理削除することにしました。

firestore で、複数条件での検索、ソートが必要になったら要注意

whereによる範囲比較、orderByによるソートは、同一のフィールドを指定しないと動きません。

たとえば年齢10〜20歳の人で、かつ男性を上位に表示する、というようなクエリは発行できません。 年齢10〜20歳の人で、年齢の昇順に表示する、なら可能になります。

// これはエラーになる
usersRef.where("age", ">", 10).orderBy("gender");

// これはOK
usersRef.where("age", ">", 10).orderBy("age");

これは firestore 特有の制限ですね。

今回はこれを知らずに仕様を決めていたため、あとで調整が必要になりました。

要注意ポイントです。

(前述の array-contains は範囲比較に該当しないため問題なし)

以下、公式ドキュメントへのリンクです

firestore ではセキュリティルールを書く必要がある

firestoreのconsoleからセキュリティルールを書くことができます。

これはバリデーションのようなものであり、これを書かないと、どんな内容でもクエリを受け付けてしまいます。

セキュリティルールを書くことによって、フィールドに対して型を縛ったり、READ/WRITE制限をかけたり、このフィールドのみUPDATE可能にする、というようなこともできます。

セキュリティルールは詳しく書くとこれだけで結構なボリュームになるので割愛します。

まとめ

ちょっと要件が複雑な掲示板でしたが、 firestore のみでバックエンド問題なく実装ができました。

firestore 単体で実装がすむと、API開発をしなくてよい、APIサーバの運用を考えなくてもよいという圧倒的メリットがありますね!

ただ、利用には一癖二癖あるので、何度かfirestoreの実装を経験しないと、工数見積もりなどを正確に出すのは難しそうだと感じました。

しかしAPI開発を一切せずにこういったツールが作れるとなるとは…どんどん便利になってエンジニアとしては嬉しいかぎりですね。

firestore には未来を感じるので、これからも経験を積んでいきたいなぁと思いました。

それでは失礼します👋

GameWith Advent Calendar 2020 の他の記事もよろしくおねがいします!

qiita.com