Perl のユーザ定義文字特性

めっちゃ久々に Perl5系 書いたのですが、そのときに今更 Perl にはユーザ定義文字特性というものがあることを知りました。

perlunicode - Perl における Unicode サポート - perldoc.jp

ユーザ定義文字特性によって、例えば半角のカタカナだけヒットさせるのを作ろうとなった場合、半角カタカナの範囲をユニコードの表で確認して
https://unicode.org/charts/PDF/UFF00.pdf
`ヲ`である `FF66` から `゚` の `FF9F` までヒットさせようと試みる場合、以下のような文字特性をかけます

package Experopero::Sample;
use utf8;
use strict;
use warnings;

...

# 半角カナ文字 `ヲ` ~ `゚` を指定したユーザ定義文字特性
# see: https://unicode.org/charts/PDF/UFF00.pdf
sub InHalfWidthKatakana {
    return <<END;
FF66\tFF9F
END
}

この文字特性は組み合わせることもできます。自作の文字特性と既存の文字特性を組み合わせてみるとこんな感じ。`return <<'END';` となっている点が注意です。

package Experopero::Sample;
use utf8;
use strict;
use warnings;

...

# 半角カナ文字 `ヲ` ~ `゚` を指定したユーザ定義文字特性
# see: https://unicode.org/charts/PDF/UFF00.pdf
sub InHalfWidthKatakana {
    return <<END;
FF66\tFF9F
END
}

# utf8::InBasicLatin と組み合わせ
sub InHalfWidthKatakanaAndInBasicLatin {
    return <<'END';
+Experopero::Sample::InHalfWidthKatakana
+utf8::InBasicLatin
END
}

`+` は条件の prefix です。他にも -, !, & があります


こういう感じで正規表現で使えます

my @chars = $text =~ m|\p{InHalfWidthKatakana}|g;

Next.js でページを見る権限が無いユーザに対しリダイレクトする処理を集約した

Next.js でユーザ認証付きアプリケーションを作っていると「ページを表示したとき、ユーザの状態によってはリダイレクトして、別のページに飛ばしたい」というケースがあります。単純にログインしていないというものから、ユーザに閲覧権限がないなど、見れないなどいくつかケースがあると思います。


例えば、一般アカウントと管理者アカウントが存在するウェブサービスを考えてみます。これらはどちらも「User」というモデルとすることにします。そしてその「User」は内部に「一般アカウントか、管理者アカウントか」という情報を持っていて、特定の url のページは管理者でないと見れないとします。ページを表示するとき、この「User」を API で叩いて初めてどちらかがかわかる、という状態です。


例えば管理者ユーザ用の「admin/」 パス以下を考えます。これは管理者ユーザしかアクセスできないパスであるので、このパス以下の場合は API で 「User」のデータさえ取得できていれば、他のデータを取るまでもなくアクセスできないということにできるはずです。単純にすると、各ページに以下のような処理を書くことになると思います。

const Index: NextPage = () => {
  const user = useGetUser(); // ここで API を叩いて結果を得ている

  useEffect(() => {
    if (!isAdminUser(user) {
      // true ならリダイレクトする
    }

  }, [user]);
     ...
}

管理者しか見れないページにはこれを書いて、そうでないページには書かない。流石にコピペは辛いのでこういう処理を Hooks にして各所にペタペタ貼っていっても良いのですが、貼り忘れる恐れはあります。また今は管理者か否かだけなので良いですが、他にも「User」モデルの様々な状態をみてリダイレクトする処理が増えていくと、各ページに様々な組み合わせの処理が散らばってしまいます。またテストも当然各ページごとに行う必要があります。

自分が関わったプロダクトでは、こういったリダイレクト処理を一箇所の hooks で行い、また「_app.tsx」でのみ行うようにしています。シンプルに管理者アカウントのケースでのみで表すと以下の通りです。

type RedirectCondition = (user: User) => boolean;

const isAdminUser: RedirectCondition = (user) => {
  return // true かどうか判定
};

type RedirectTo = string;

type RedirectRule = {
  from: RegExp;
  to: RedirectTo;
  condition: RedirectCondition;
};

// 登録済みユーザーのリダイレクトルール
const adminUserRules: RedirectRule[] = [
  // 管理画面リンク以下は管理者のみOK
  {
    from: new RegExp(`^admin/`),
    to: () => "/",
    condition: isAdminUser,
  },
  ...
];

const rules: RedirectRule[] = [
  ...adminUserRules,
];

export function useRedirectUser(
  user: User
): void {
  const router = useRouter();
  const { pathname, query } = router;
  const toHref = query.to as string;

  if (!user) {
    return;
  }

  for (const rule of rules) {
    if (rule.from.exec(pathname) && rule.condition(user)) {
      // ここに来たらリダイレクト
      return;
    }
  }
}

すべてのページでこの Hooks 処理が適用されたいので、「_app.tsx」で使います

const App: NextPage = () => {
  const user = useGetUser(); // ここで API を叩いて結果を得ている
  useRedirectUser(user);
  ...
}

すべてのリダイレクト処理が1箇所の hooks に集約されることで、この hooks だけをテストしておけば良くなります。テスタブルなリダイレクトコードを考えていくと、こうやってとある関数に集約されているほうがテストしやすいです。この URL ではこういう条件を満たしていないとリダイレクトする、という処理が一つにまとまっている感じです。

Next.js に GoogleAnalytics と GoogleTagManager を導入し計測する

Next.js でアプリケーションを作った際に、アクセス解析などやりたく GoogleAnalytics を導入しました。GoogleAnalytics をそのまま使うのではなく GoogleTagManager を経由すると何かと便利なので、Next.js → GoogleTagManager → GoogleAnalytics という流れで計測を行えるようにしました。

現状、インターネットにはエンジニア目線で GoogleAnalytics や GoogleTagManager について書かれた記事が少なく、これを書くことで何かの助けになると感じましたので、ここに記録しておきます。

ところで GoogleTagManager は長いので以下 GTM とします。また GoogleAnalytics も長いので以下 GA とします。

GoogleTagManager を設定する

アカウントとコンテナを用意する

https://tagmanager.google.com/

まずは GTM のアカウントを用意します。タグマネージャーアカウントという名称です。サービスと対応するのがタグマネージャーアカウントと思えば良いです。「ウェブサービスAのために、アカウントを1つ作る」という具合です。

用意したら、タグマネージャーアカウント内にコンテナというものを1つ作成しましょう。タグマネージャーアカウントは複数作成することができ、またタグマネージャーアカウント内には複数のコンテナを持つことができます。コンテナはサイトやモバイルアプリケーションごとに1つずつ作られるのが一般的とされています。

例えば自分が関わったプロダクトでは、プロダクトごとにアカウントを作っています。適当に置き換えて例を上げると「サービスA用のアカウント」「サービスB用のアカウント」「サービスC用のアカウント」といった具合です。それぞれのアカウントの中で、必ず一つ以上のコンテナがあります。コンテナは Web アプリ、Android アプリ、iOS アプリで分けていたりします。

ここではとりあえず1つのアカウントと、その中にコンテナが1つだけ用意されているという例で話を進めます。今回サンプルとして作った GTM では、テストと言う名前のアカウントを作り、その中に www.example.com というコンテナを用意しています。 f:id:funnelbit:20210601184234p:plain

環境を分ける

GoogleTagManager には「環境」という機能が用意されています。「開発用」「本番用」という環境を作ることで、それぞれの環境ごとに GoogleTagManager の設定を分けることができるものです。

support.google.com

この環境を作る前に、どのような開発環境があり得るのかを整理します。まず、手元環境(local とします)がありますよね。そして開発中の機能を社内向けのみにデプロイして確認する環境があるかも知れません(preview とします)。本番前に最終確認する社内向けもあるかも知れません(staging とします)。最後に本番環境(production とします)がありますね。

このうちの local ですが、用意すると常に手元環境から GA の確認ができて良さそうです。一方で、仮に開発メンバーが20人いた場合、20人が同じ GA を叩きまくることになるので、GA に仮の名前の計測値が入ったり、リアルタイム計測で本当に自分の開発中環境から値が送られているか確認したいけど、他の誰かの計測値が次々と飛んできてノイズになったりと、あまり嬉しくない結果になるかも知れません。

個人的なおすすめとしては、local 環境から常に GA(GTM を経由するので正確には GTM ですが)を叩く設定にはせず、本当に必要なときにのみ local 環境から GA を叩く設定にすると良いと思います。こうすると、GA に値を飛ばす機能に関連する部分を実装している人だけが開発中 GA に値を送ることが可能になるので、理想的な開発用 GA が用意されることになると思います。ただしこれが通用するのは少人数開発の話なので、チーム人数が多くなった場合は改めて管理を考える必要はあると思います。

ここでは staging、production 2つを用意する手順を紹介します。

Production の環境

いきなり Production 用環境の話からします。なぜかと言うと、Production 用環境は作る必要がないからです。 GTM の画面で、環境ページに行ってみてください。以下のような状態になっていると思います。

f:id:funnelbit:20210601183859p:plain

ここに行くと、何も作ってないのにデフォルト環境というものがあります。GTM ではデフォルトの環境が用意されており、それが今回の話で言う Production 用環境に該当します。よって Production 環境を新たに作る必要はありません。

Staging 環境

Staging 相当のものは用意されていないので作る必要があります。環境画面の「新規」ボタンを押して作成しましょう。

f:id:funnelbit:20210525184403p:plain

f:id:funnelbit:20210720131627j:plain この画面では「Staging に公開」を押して大丈夫です。Staging はガシガシ変更を公開していくスタイルで行こうと思います。

f:id:funnelbit:20210525184511p:plain このような状態になると思います。

環境ごとのスニペットの差異を確認する

今、2つの環境を用意しましたが、このそれぞれの環境は、どうやって GTM の呼び出し元(今回の場合は Next.js)から使い分けが行われるのでしょうか?それは環境ごとのコードスニペット(GTM を呼び出すために貼り付けるスクリプトタグ)の違いを見るとわかります。

まず、Production 用の環境を見てみましょう。 f:id:funnelbit:20210720131354j:plain 「デフォルト環境」の「Live(公開中)」の「操作」ボタンを押して「コードを取得」を押してください。

f:id:funnelbit:20210601184116p:plain

色々書いてありますね。では、同じように Staging 用も「コードを取得」して見てみましょう。

f:id:funnelbit:20210601184126p:plain

この2つのコードを比較すると、異なる部分があります。画像ではなくコードを貼ってわかりやすくしてみました。

Live 環境

<!-- Google Tag Manager -->
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl+ '&gtm_auth=-FNbDkDEkVnwbkvCdu8gYA&gtm_preview=env-1&gtm_cookies_win=x';f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-MJ28WWZ');</script>
<!-- End Google Tag Manager -->

Staging 環境

<!-- Google Tag Manager -->
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl+ '&gtm_auth=f-U7CczicK9t6Rkp4NP-HQ&gtm_preview=env-4&gtm_cookies_win=x';f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-MJ28WWZ');</script>
<!-- End Google Tag Manager -->

https://www.googletagmanager.com/gtm.js という url が書かれていますが、ここに付いているクエリパラメータ gtm_auth, gtm_preview の値が異なります。Live 環境は gtm_auth=-FNbDkDEkVnwbkvCdu8gYA&gtm_preview=env-1、Staging 環境では gtm_auth=f-U7CczicK9t6Rkp4NP-HQ&gtm_preview=env-4 となっています。この部分が各環境ごとに異なるため、フロントエンドはこの2つのクエリパラメータの値を変えさえすれば、それぞれの環境を切り分けることができるという仕組みになっていることがわかります(gtm_preview はプレビューのときのみ有効なものであると思いますが、まあ変えておいたら良いと思います)。

(小話)Latest 環境について

GTM の環境には「Latest」というものも存在します。GTM の変更を保存するとき、保存して特定の環境に公開するか、保存だけするかを選ぶことができます。このときに保存だけしたとき「Latest」環境だけ新しくなります。Staging で参照する GTM を常に最新の変更へ自動的に追従させたければ「Latest」を参照するという方針も取ることができます。ただし間違った変更も自動的に追従してしまうリスクはあります。

Next.js アプリケーションに GTM のコードを埋め込む

Next.js アプリケーション に GTM のコードをセットして、GTM と通信できるようにしましょう。先ほどのコードスニペットを Next.js アプリケーション にセットして、環境ごとに変わるコードスニペットの部分を切り替えることで、各環境ごとに動作する GTM 呼び出しを実現します。

まず、コードスニペットをどこに書くかです。これは ./pages/_document.tsx(Typescript ではない方は ./pages/_document.js) に書くのが良いでしょう。Next.js のカスタムドキュメントと呼ばれるものです。

nextjs.org

まずは GTM の Production 環境のコードスニペットをそのまま貼り付けてみましょう。とはいえそのままベタッと貼り付けるわけではなく、Next.js アプリケーション の書き方へコンバートする必要はあります。以下のようになりました。

import Document, { Head, Html, Main, NextScript } from "next/document";

class AppDocument extends Document {
  render(): JSX.Element {
    return (
      <Html>
        <Head>
          {/* Google Tag Manager */}
          <script
            dangerouslySetInnerHTML={{
              __html: `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
              new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
              j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
              'https://www.googletagmanager.com/gtm.js?id='+i+dl+ '&gtm_auth=-FNbDkDEkVnwbkvCdu8gYA&gtm_preview=env-1&gtm_cookies_win=x';f.parentNode.insertBefore(j,f);
              })(window,document,'script','dataLayer','GTM-MJ28WWZ');`,
            }}
          />
        </Head>
        <body>
          {/* Google Tag Manager (noscript) */}
          <noscript>
            <iframe
              src={
                "https://www.googletagmanager.com/ns.html?id=GTM-MJ28WWZ&gtm_auth=-FNbDkDEkVnwbkvCdu8gYA&gtm_preview=env-1&gtm_cookies_win=x"
              }
              height="0"
              width="0"
              style={{ display: "none", visibility: "hidden" }}
            ></iframe>
          </noscript>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default AppDocument;

試しに、この状態で GTM と通信できるのか確認してみましょう。GTM にはデバッグできるモードが存在するので、それを利用します。GTM のコンソール画面から「プレビュー」を押しましょう。

f:id:funnelbit:20210706190459p:plain

接続先 URL を聞かれるので、手元で Next.js アプリケーションを起動し、起動したアプリケーションの URL を入れましょう。 f:id:funnelbit:20210706190531p:plain

すると勝手に指定した URL のウィンドウが出てきて、以下のような画面になります。 f:id:funnelbit:20210706190352p:plain

左側にイベントが出ていて、DOM Ready や Window Loaded などがキャッチされており、実際にアプリケーションと接続されていることがわかります。 f:id:funnelbit:20210706190905p:plain

次に、環境ごとに接続先を変えるようにしてみましょう。まず、試したい環境を「公開」してください。試しに Staging にしてみましょう。GTM コンソール画面の「公開」ボタンを押して

f:id:funnelbit:20210706192905p:plain

「環境への公開」というところがあるので、そこが多分 Live となっているので f:id:funnelbit:20210706192746p:plain

Live の横にある編集ボタンを押して、Staging に変えてみましょう f:id:funnelbit:20210706192944p:plain

変えたら「公開」を押してください。

この状態で、コードを以下のように変更します。

import Document, { Head, Html, Main, NextScript } from "next/document";

class AppDocument extends Document {
  render(): JSX.Element {
    return (
      <Html>
        <Head>
          {/* Google Tag Manager */}
          <script
            dangerouslySetInnerHTML={{
              __html: `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
              new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
              j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
              'https://www.googletagmanager.com/gtm.js?id='+i+dl+ '&gtm_auth=f-U7CczicK9t6Rkp4NP-HQ&gtm_preview=env-4&gtm_cookies_win=x';f.parentNode.insertBefore(j,f);
              })(window,document,'script','dataLayer','GTM-MJ28WWZ');`,
            }}
          />
        </Head>
        <body>
          {/* Google Tag Manager (noscript) */}
          <noscript>
            <iframe
              src={
                "https://www.googletagmanager.com/ns.html?id=GTM-MJ28WWZ&gtm_auth=f-U7CczicK9t6Rkp4NP-HQ&gtm_preview=env-4&gtm_cookies_win=x"
              }
              height="0"
              width="0"
              style={{ display: "none", visibility: "hidden" }}
            ></iframe>
          </noscript>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default AppDocument;

この状態でもう一度プレビューして、コードがおかしくないか確認をしましょう。正しければつながるはずです。この時点では、まださっきのプレビューとの差がわかりにくいですね。実はこの時点では Staging 環境の動作は確認できていない(!?)のですが、とりあえずシンタックスが正しいか確認できたら良いのでこのまま進めます。

GTM で環境情報を取得する

それでは、この状態で環境を判別してみましょう。まず、環境情報を取得する設定を有効にします。GTM コンソール画面の変数から「組み込み変数」の「設定」を押しましょう。 f:id:funnelbit:20210713185915p:plain

すると Environment Name という組み込み変数名があると思うので、これにチェックを入れます。 f:id:funnelbit:20210713185956p:plain これによって、環境の名前が GTM で取得できるようになります。変数名の一覧に Environment Name が出ているはずです。 f:id:funnelbit:20210713190153p:plain

とりあえずこの変更を「公開」してみましょう。Staging で試したいので、Staging 環境で公開します。 f:id:funnelbit:20210713190305p:plain

公開したら、再びプレビューをしましょう。プレビューには Variables という項目があるので、そこで Environment Name が来ているか確認してください。

f:id:funnelbit:20210715141655p:plain

...来ていないように見えますね。実は Environment Name 変数をオンにするだけではこの画面では確認できません。実際に Environment Name を使うタグというものを書かなければ現れないのです。こういう暗黙感ある振る舞いがそこら中にあたりが GTM の難しいところです...。

(小話) 自力で html を書かずに js のみで実装する

今回は使っていませんが、自分で今回のような html を書かずとも以下のライブラリを使えば js でのみ GTM の導入ができます。 github.com

GTM でタグを作ってみる ~ 環境変数ごとに GA の ID を変更する

気を取り直してタグとやらを作ってみましょう。これは何かというと、ページを表示したら、環境ごとに GA の ID を変更し GA に接続するという機能を提供するものです。

ユーザー定義変数の作成

まず、ユーザー定義変数を作ってください(タグと言ってたのに!!)。変数画面に「ユーザー定義変数」というものがあるので f:id:funnelbit:20210715142433p:plain ここで「新規」を押し、そこから更に変数選択画面に行って「ルックアップテーブル」を選択します。そして下の画面のような状態にしてください。

f:id:funnelbit:20210715142712p:plain 読み方としては、環境情報名が Staging なら xxxxxx という文字列を返す、というものです。ほかに環境情報名がある場合はここに条件を追加していけます。値を返す Switch 文みたいなものと思ってもらえば良いと思います。本当はここに GA の ID が入りますが、今はまだ無いという状態なのでダミーを入れています。この状態で保存しましょう。

次に、再びユーザー定義変数を作ってください(またか!!)。Google アナリティクス設定というユーザー定義変数が必要です。 f:id:funnelbit:20210715142954p:plain

先ほど作った「GA ルックアップテーブル」を、Google アナリティクスの ID として読み込むためにこの変数が必要なのです。ほかにも様々な設定ができますが、今回は無視します。画像のような状態になったら保存しましょう。

トリガーの作成

そしてにトリガー画面からトリガーを作成します。ページビューを選んで、そのまま保存しましょう。 f:id:funnelbit:20210715143226p:plain ページが表示されたら何かをするというロジックを提供するものです。何をするのかの実装はタグというものに委ねられています。

タグの作成

タグ画面に行って、以下のようなタグを作ってください。 f:id:funnelbit:20210715143551p:plain トリガーにてページが表示されたら、ユーザー定義変数のGoogleアナリティクス設定を使い、GA にトラッキングを送るというタグがここに完成しました。

この状態でプレビューしてみましょう。 f:id:funnelbit:20210715144034p:plain f:id:funnelbit:20210715144049p:plain

GA タグが働いていることがわかり、また Environment Name も来ていることがわかります。Environment Name は 見慣れない値になっています。"Preview Environment 1 2021-07-06 024400" というものです。これはプレビュー専用の Environment Name であることを意味します。よって、GA ルックアップテーブルは "Staging" という文字列に合致しないので undefined を返しています。

今おそらくクライアントは Staging のコードスニペットになってますよね?実はこれは意味がないのです(わざわざやらせたのにすみません)。プレビュー中は、接続中クライアントのコードスニペットがどんな環境であれ関係なく、独自の環境に置き換えられるのです。クライアントのコードスニペットが Staging だろうがなんだろうが、強制的に別の環境にされてしまいます。環境を選ぶ権限は GTM 側にあり、そして環境を GTM 側で切り替えることもできます。ここで、プレビュー対象を Staging に変更してみましょう。この画面、よく見ると Previewing: というコーナーがあり、そこで env-4 を選んでみました。env-4は私の環境では Staging を表す Environment ID です。この数値を覚えていなくとも、Previewing: を押すとリストが出てくるので、Staging の Debug を押すと選択されます。 f:id:funnelbit:20210715144411p:plain

f:id:funnelbit:20210715144500p:plain

すると Environment Name に Staging が入っており、GA ルックアップテーブルにも値が入ってることが確認でき、設定できていることがわかります。 f:id:funnelbit:20210715144626p:plain

それでは、試しに GA を作って計測されるかどうか見てみましょうか。

GA を設定する

今回はユニバーサル アナリティクスを想定しているので、ユニバーサル アナリティクスで作ります。ユニバーサル アナリティクスの作成導線は少々巧妙に隠されており、プロパティの設定項目にある詳細オプションを表示を押すと出てきます。Staging のアナリティクスを試したいので Staging という名前にしました。 f:id:funnelbit:20210715152342p:plain

適当に作ったら、トラッキング ID を覚えておいてください。ユニバーサルアナリティクスで作ったなら UA-xxxxx となっているはずです。 f:id:funnelbit:20210715152447p:plain

この変更を Staging 環境にて「公開」してください。

GTM でトラッキング ID をセット

再び GTM にもどります。先ほどのトラッキング ID を GA ルックアップテーブルの Staging 枠に入れて保存しましょう。f:id:funnelbit:20210715152639p:plain

これで Staging 環境にてアクセスしたら GA に接続できるはずです!

GA で接続を確認する

GA をリアルタイムを開きます。 f:id:funnelbit:20210715152835p:plain

手元の Next.js アプリケーションは既に Staging 環境になっているはずなので、そのままアクセスしてみましょう。 f:id:funnelbit:20210715153113p:plain

リアルタイム計測が反応しました!ちゃんと接続できているようです!

本当に環境ごとに切り替わっているのか確認したい場合は、Next.js アプリケーションのコードスニペットを Live のものに変更しましょう。反応しなくなるはずです。

Next.js アプリケーションで環境ごとのコードスニペットを簡単に切り替える

環境ごとに GA を切り替えれることはわかりましたが、これをいちいちデプロイごとに手で書き換えるのは現実的ではないので、環境を切り替えれるように Next.js アプリケーションの環境変数 Environment Variables を使いましょう。

nextjs.org

とりあえず .env.local を作ってみましょうか。GTM のコードスニペットにある環境変数ごとに変化する部分だけを抜き出し、Next.js アプリケーション の環境変数に落とし込みます。

NEXT_PUBLIC_GTM_CONTAINER_ID="GTM-MJ28WWZ"
NEXT_PUBLIC_GTM_CONTAINER_ENV="&gtm_auth=f-U7CczicK9t6Rkp4NP-HQ&gtm_preview=env-4" # とりあえず Staging 試したいので Staging 固有の文字列を入れます

そして Next.js アプリケーション の _document.tsx のコードは環境変数を使うように変更します。

import Document, { Head, Html, Main, NextScript } from "next/document";

const gtmContainerId = process.env.NEXT_PUBLIC_GTM_CONTAINER_ID;
const gtmContainerEnv = process.env.NEXT_PUBLIC_GTM_CONTAINER_ENV;

class AppDocument extends Document {
  render(): JSX.Element {
    return (
      <Html>
        <Head>
          {/* Google Tag Manager */}
          <script
            dangerouslySetInnerHTML={{
              __html: `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
                       new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
                      j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
                      'https://www.googletagmanager.com/gtm.js?id='+i+dl${
                        gtmContainerEnv ? `+'${gtmContainerEnv}&gtm_cookies_win=x'` : ""
                      };f.parentNode.insertBefore(j,f);
                      })(window,document,'script','dataLayer','${gtmContainerId}');`,
            }}
          />
        </Head>
        <body>
          {/* Google Tag Manager (noscript) */}
          <noscript>
            <iframe
                src={`https://www.googletagmanager.com/ns.html?id=${gtmContainerId}${
                  gtmContainerEnv ?? ""
                }&gtm_cookies_win=x`}
              height="0"
              width="0"
              style={{ display: "none", visibility: "hidden" }}
            ></iframe>
          </noscript>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default AppDocument;

この状態で Next.js アプリケーションをリスタートすれば、再び GA のリアルタイムに計測が出てくるはずです。あとはこの環境変数をデプロイごとに変更すれば良いと言うことになります。ここはビルドする環境ごとに細かく違うと思うので割愛します。

これで環境ごとに切り替える Next.js アプリケーション、 GTM、GA それぞれの設定ができました。ほかにも環境を追加したければ、ここに書いた手順をたどればできるはずです。

本番環境 GA へ接続

本番環境 GA へ接続できるようにしましょう。本番用 GA の作成は Staging 用 GA を作ったときと同じなので割愛します。

Staging のときと同じような感じです。GA ルックアップ テーブル に Live だったら Live 用のトラッキング ID を返すようにします(説明のために GA 用意するのめんどくさくなってきたので xxxx としてますが、正しい GA のトラッキング ID を入れてください) f:id:funnelbit:20210715163930p:plain

公開しましょう。本番の環境なので Live 環境で公開するのを忘れないでください!

そして Next.js の .env.local の値を本番用に書き換え、Next.js アプリケーションを再起動してアクセスしてみましょう。本番用 GA のリアルタイム計測が変化するはずです。

Next.js アプリケーションで画面遷移したときにも計測するようにする

これが最後の手順になります。今の状態だと、画面遷移しても計測がなされません。現状反応するのはページをまるごと読み込みしたときだけです。Next.js は SPA であるので、ページを読み込んだ後は画面遷移しても今の設定だと何も反応しません。なので、ページ遷移しても計測するようにしましょう。

GTM のトリガには履歴の変更というものがあります。Next.js アプリケーションが、ブラウザの History API を操作し、ブラウザの戻る・進むボタンをハンドリングしていることは、既にご存知かと思います。この操作をフックするには、履歴の変更で行うことができます。試しに作成してみました。 f:id:funnelbit:20210715165841p:plain

これを GA のタグのトリガーとしてセットします。読み方としては「ページを新規読み込みした、または Next.js アプリケーションによる画面遷移が起こった」です。 f:id:funnelbit:20210715165933p:plain

さて、これで保存はしておいて GTM のプレビューを起動して挙動をみてみましょう。こちらの手元では画面遷移を行うコードを追加したので、遷移するボタンを押すと画面遷移できます。

f:id:funnelbit:20210715170437p:plain

画面遷移すると、GTM のコンソールの Summary 枠に History という項目が現れます。これが「履歴の変更」トリガーの効果です。一見するとこれで良さそうに見えます。 f:id:funnelbit:20210715170632p:plain

では、この状態でブラウザの戻るボタンを押してみるとどうなるでしょうか。

f:id:funnelbit:20210715170946p:plain 先ほど一つだけだった History が3つになっています。1つ多いです。つまり、戻るボタンを押すと、二重に GA に計測が飛んでいく事になります。このままでは正しい計測を行うことができません。

この問題に対して、以下の2つの方法があります。GTM のトリガー設定を工夫し乗り切るか、トリガーを諦め、自力で GTM にイベントを送るかの方法です。

選択肢1: GTM の履歴の変更トリガーの設定を工夫する方法

そもそも戻るボタンを押したときに何故 GTM に二重に History が発火されるのかというと、Next.js はブラウザの戻るが押されたときに出る popstate イベント発生後、 replaceState をしているため、GTM としては「履歴が二回操作されたから、二回 History イベントでしょ」ということでこうなっています。なのでこれをちょっと工夫します。

まず変数で History Source を有効にし、履歴の状態を変数として取得できるようにします。 f:id:funnelbit:20210715172331p:plain

次に、履歴の変更トリガーの設定を以下のように変更します。 f:id:funnelbit:20210720155936p:plain

履歴の変更トリガには正規表現で HistorySource を厳選できる機能があるので、ここで popstate は無視するようにします。これによって二重に GA 計測が呼ばれないようにはなります(replaceState は router.replace() を呼んだときに使われるので popstate を選択しています)。

この状態でプレビューを試してみると、History 自体は余分に出てしまいますが

f:id:funnelbit:20210715173145p:plain 片方は GA のタグが Fired になっておらず(GA が送られておらず)

f:id:funnelbit:20210715173141p:plain もう片方は GA のタグが Fired になり、片方だけ GA にイベントが送られていることがわかります。

無事に動きましたが、果たして popstate を除外するのがベストなのかどうかは不明です。またこれは Next.js のルーティング処理内部の仕様に左右されるところではあるので、今後挙動が変わったら計測が変になる懸念はあると思います。

選択肢2: 自力で GTM にイベントを送る方法

GTM の履歴の変更トリガーで GA に送るのをやめる選択です。Next.js の公式リポジトリには、この方法が書かれたファイルが存在します。

github.com

これは _app.tsx(_app.js) にて、履歴の変更を監視し、ページが読み込まれたら GTM にイベントを送るというものです。router にはページ遷移したら通知してくれる router.events.on('routeChangeComplete', () => {}) というものがあるので、これを使って履歴の変更をキャッチし、自力で GTM へイベントを送っています。単純に各ページの useEffect などで、ページが表示されたら一度だけ GMT にイベントを送るということもできますが、全部のページに送るコードを書いてられないので _app.tsx にだけ書いて送ろうという試みになります。

ここが難しいポイントですが、router.events.on('routeChangeComplete', () => {}) はブラウザによる初回読み込み時・再読込時には通知されないので、それらの場合だけは一度だけ動作する useEffect などを使ってカバーすることになります。ただし例外が2パターンあります。

1つ目は、クエリパラメータが付いているときです。このとき、ブラウザによる初回読み込み時・再読込時に router.events.on('routeChangeComplete', () => {}) が動いてしまいます。クエリパラメータがある時は特別扱いし router.events.on('routeChangeComplete', () => {}) に任せるようなコードが先ほどのNext.js の公式リポジトリには書かれています。

しかしこの方法にはまだ問題があります。2つ目の例外として、 Dynamic routing なページな場合もクエリパラメータ付き URL と同じく初回読み込み時・再読込時にも router.events.on('routeChangeComplete', () => {}) が動いてしまい、Next.js リポジトリの例通りにやると二重にイベントを送ってしまいます。これは issue も出ていますが、特に動かずに今に至っています。

github.com

Next.js のサンプルを参考に実装するのであれば、Dynamic routing のときもクエリパラメータありと同じく特別扱いするコードを追加で書くべきです。現在開いているページが Dynamic routing かどうかは、以下の方法でチェックすることができます。

funnelbit.hatenablog.com

これを利用し、_app.tsx(_app.js) にて以下のように書きましょう。

import { isDynamicRoute } from 'next/dist/next-server/lib/router/utils';
import { useEffectOnce } from 'react-use';
...

export default function App({ Component, pageProps }) {
  const router = useRouter();

  useEffectOnce(() => {
    if (!isDynamicRoute(router.pathname) && !router.asPath.includes('?')) {
      log();
    }
  });

  useEffect(() => {
    router.events.on('routeChangeComplete', log);
    return () => router.events.off('routeChangeComplete', log);
  }, [router.events]);

  return <Component {...pageProps} />
}

react-use には一度だけ動く useEffect であることをより明確にした文法である useEffectOnce があるので利用しています。 github.com これを利用せずに useEffect で依存変数を空配列にしてももちろん構いません。

次に、この routeChanged イベントを GTM がキャッチしたら動作するトリガーを作成します。トリガーのメニューから「新規」ボタンを押して「カスタム イベント」というトリガーを以下の画像のように作ってください。 f:id:funnelbit:20210720155207p:plain

次に、折角作りましたが GA タグのページビューと履歴の変更トリガーを外して、このカスタムイベントトリガーに付け替えます。 f:id:funnelbit:20210720155420p:plain

これで保存し、プレビューしてみましょう。 f:id:funnelbit:20210720155600p:plain

routeChanged イベントがあり、 GA タグが Fired になっていることがわかります。新規ページ読み込み時はもちろん、ブラウザの戻る・進むボタンでも正しく動きますし、二重に呼ばれているということもありません。

どちらが良いのか?

時間がないなら前者でシャッと作ってしまっても良いと思いますが、正直言ってこれは正しいのかわからず不気味です。後者を選択してもさほど工数は変わらないと思うので、できれば後者を選ぶと良いと思います。

終わりに

Next.js + GTM + GA は情報がかなり少なく、あっても二重に計測される問題をはらんでいる状態だったので、めっちゃ大変でしたが書いてみました。めっちゃ疲れました(特にスクショ用意するのが)。もっといい方法もあるかもしれないので、あればぜひ教えてほしいです。

ちなみにこの記事に出ている GTM とか Analytics の ID はアクセスしても既に削除されているので使えないです。

MAZDA3 FASTBACK 15S Touring(AT) 納車しました

こちらは納車時の軽い感想記事です。詳細なインプレッションはこちらをご覧ください
funnelbit.hatenablog.com


MAZDA3 FASTBACK 15S Touring 納車しました!!!


MAZDA3 FASTBACK にした理由

F56(ミニクーパー) を持っていた時代と違って、自分一人だけが満足できるクルマではなく、生活にある程度使えるクルマというところを考えなければなりませんでした。荷物がある程度は乗って、助手席にも必ず人がいる前提です。後席もある程度のスペースが有り、荷物を乗せたり人を乗せたりします。家計を考えてハイオクはだめでレギュラーガソリン、価格は全部合わせて300万以下にしたい。


はい、こういうと「ミニバンにせよ」と言われるのはわかっています。よくわかっているです。それでも自分は、どうしても走りを諦めたくなかった。僕はもともとハッチバックのクルマが好きなので、できればハッチバックがいいなあと思っていました。せっかく大金を出すのだから、持っていて誇らしげになれるというか、ある程度の趣味性があるクルマがほしかったのです。


SUV も最近沢山出ていて考えましたが、この価格帯のものだとプラットフォームをハッチバックと共有している物が多く、SUV として腰高になったとしても後席や荷室がそんなに広くはないので、SUV というスタイルが好きでないなら別に積極的に選ばなくて良いかなという思いになりました。


かなり色々考えましたが、結局の所少し大きめのハッチバックであればそれなりに便利に使えるんじゃないかと思って、ハッチバックを中心に選ぶことになりました。海外のクルマは予算オーバ、よって国産に絞って選んでいきました。

荷物が乗り、後席にも人を乗せることができる予算内のハッチバックは 2021/06/01 時点で、MAZDA3 以外にもいくつかありますが、MAZDA3 FASTBACK は

  • 素晴らしいデザイン
  • それなりの後席の居住性が有る
  • 荷物も結構載る
  • 運転した感じもよく所有感を満たせる
  • 値段がどう考えてもお買い得プライス

であったので MAZDA3 FASTBACK に決めました。

15S Touring にした理由

装備を考えると価格が安いっていうのはもちろんなのですが、そもそも1.5リッターエンジンの走行フィーリングが思っていたよりも良かったというのが大きいです。


僕は前にミニクーパーS(F56)に乗っていました。この車は馬力が192馬力あって、ターボも付いているので素晴らしい加速を楽しむことができました。今でもあのパワーが忘れられず、乗り換えるなら絶対にパワーのある車にすると思っていました。しかしミニクーパーのようにもりもりパワーのある車は新車で200万円代では存在しません。ですのでパワーをある程度妥協して車選びをすることになりました。


結局MAZDA3に落ち着いたのですが、エンジンが4種類あります。1.5リッターガソリンエンジン、1.8リッタディーゼルターボエンジン、2.0リッターガソリンエンジン。そして夢のエンジンとされるスカイアクティブX。


スカイアクティブXは価格がめちゃくちゃ高いので論外。ディーゼルはずっと前に試乗したのですが、GT的なゆったりした味付けが自分には合わなかったのと値段が高いのでやめました。そして1.5 か 2.0 で迷いました。


はじめは絶対に 1.5 を選ぶことは無いと思っていました。だって111馬力しか無いんですよ。めちゃくちゃ遅いじゃないですか。2.0 だと 156馬力あるので、どう考えてもこっちだろうと思っていました(今思えば予算に収まるかは不明ですが)。しかしながら実際に 1.5 に試乗してみるとエンジンの出来がとても良かったのです。


この 1.5 ですが、パワーは正直無いですが普段の道でそれを感じることはないです。パワー不足が現れるのは高速道路などで思い切り踏んで追い越し加速したときですが、パワー不足であるもののエンジンのフケが良くて踏んだら気持ちが良い。これにスポーツモードが加わって爽快感のあるフィールとなり、全くイライラしないので全然問題ないなと感じました。


またフロントの軽さが素晴らしく、ひょいひょい鼻先の向きを変えてコーナリングできてしまうフィールがとても気持ちよく、エンジンのフケの良さと相まってロードスターに乗って感じた楽しさと似たものを感じ(全く同じとは言えません!流石にこのあたりはロードスターにはかなわない)、普段使いでも楽しさが溢れているなと感じました。


本当は 2.0 も試乗して決めるべきですが、インプレを見てもそこまでパワーが有るわけではなく、普段使いで余裕があるエンジンという具合。もっとパワーがほしい、というインプレもちらほらあるので、パワーがほしいとおもって飛びついても「こんなものかあ」で終わりそうでした。比較対象が192馬力なので当たり前です。


常識的な速度域でパワーがすぐほしければ MT モードでギアを落とせばよいし、スポーツモードもある。どうせパワーがミニクーパーSよりも遥かに劣るエンジンに乗るのなら、ガンガン回して鼻先が軽いクルマを操ったほうが楽しめるほうが面白いのではないか。それに昔と違って一人で乗るわけではないので、パワーをもりもり使って飛ばすとかもないよなあと思って 1.5 に決めました。

ただまだ本格的に踏み込んでいません。現代のクルマは慣らし運転不要と言われていますが一応慣らしをしているからですね。回転を厳密に決めてるわけでは有りませんが3000回転ぐらいを上限にするようにしています。また慣らしが終わったら詳細なレビューをしようと思います。

ソウルレッドクリスタルメタリックにした理由

マツダの赤っていうのはほかのブランドにはない品のある赤色で、これはこの価格帯のクルマではマツダでしか体感できないだろうと思い決定しました。6万6千円しますが価値があるものだと思います。ただポリメタルグレーメタリックもかなりかっこいいので迷いました...こっちは追加料金かかりませんし。

つけたオプション

360°セーフティパッケージ

15S Touring につけれるメーカオプションはこれしか有りません。内訳としては駐車時などに上から見下ろすようにカメラ映像を確認できる「360°ビュー・モニター」とドライバーが居眠りしていないかシステムがカメラでチェックする「ドライバー・モニタリング」がついています。
正直このオプションはほぼ必須と言っていいと思います。ファストバックは後ろの見切りが非常に悪く、運転になれていても斜め後方が全然わかりません。バックカメラが標準でついていますがバックカメラだけでは心もとないです。多くの人がこう感じると思っており、ということはリセール時にもこれがついているといないでは結構変わると思うので、必ずつけることをおすすめします。

ドラレコ・ETC・レーダ探知機

すべて自分でつけました。取り付けはこのあたりを見て行いました。

www.youtube.com

www.youtube.com

diy-kuruma.com

car-support.jp

MAZDA3の難しいところは、車内ヒューズボックスにACC電源のヒューズが無いということです。

bbs.kakaku.com

エンジンルームにはありますが、エンジンルームから電源をとった後に車内に引き込む必要があるのでやってられません。よって様々なオーナがやっているように、車内の BCM 配線からエレクトロタップで割り込みして取得するという方法を取らざるを得ません。ディーラのメカの人にも聞きましたが、ここで正解とのことです。

この線かなり細く、一度割り込みが不十分で取り付け後しばらくしたら断線してしまい、その時は原因がわからずディーラのお世話になってしまいました(素人の施工ミスに対応してくださって助かった...)。この線が断線すると当然 ACC 電源につながっているドラレコなどの電源が入らなくなるほか

こういった警告が出てきます。

あと注意点ですが、リア上部にドラレコを付けるとノイズが発生し、AMラジオをまともに受信できなくなります。機種によってはそうはならないのかな?位置をリアウィンドウ下部にすれば行ける気がしますがAMラジオを僕は聞きませんし、AM はどうせ終わりを迎えるので

av.watch.impress.co.jp

このままで良いやと思って放置しています。場合によっては TV にも影響があるようなのですが、今のところ受信できています。

ドライブレコーダー(Yupitel SN-TW97c)

www.yupiteru.co.jp
夜間でもナンバーを記録できそうなSUPER NIGTHという機能を謳っている SN シリーズです。前後2カメラある製品で、やすかったのでこれにしました。2万円でした。

ETC(Panasonic CY-ET926D)

panasonic.jp
自分で ETC を付ける人は大体これではないでしょうか?安いし。セットアップはディーラに持ち込むことでもやってもらえますし、ネット販売店でもお願いできます(無理なショップもあります)。ディーラに持ち込む場合、車体に予め付けておく必要があって、僕は取り付けしたらすぐに使いたかったのでショップに依頼しました。本体にセットアップ料も入れて8000円ぐらいでした。

レーダー探知機(COMTEC ZERO 302V)

www.e-comtec.co.jp
貰い物なのでよくわかりませんw

納車してみて

MAZDA3 はデザインと内装以外は結構魅力がわかりにくいです。車好きからは「デザインは良いけどスペックが低い」「走りに刺激がない」と言われ、そうでない人からは「荷物が載らない」と言われるのではないかなと思います。僕も1車好きとして「スペックが低い」「走りに刺激がない」と思っていました。しかしこのクルマ、噛めば噛むほど味が出るスルメの如くしばらく乗り続けていると「なるほど!!」と思える魅力を持っています。マツダが何故他のメーカーのように刺激重視のマシンを作らないのかが、しばらく乗るとようやく理解できます。荷物も思ったより載りますよ!そしてデザインが素晴らしい....デザインの要素がここまで所有欲を満たしてくれるとは。


まあもともとバイクもパワーより操作性という人間なので、マツダの思想と相性が良かったということでしょうか。とりあえず細かいことは慣らしを終えてからまた書いていきます。MAZDA3オーナとして今後もよろしくお願いします。

Next.js で現在開いているページが Dynamic Route かどうか調べる

現在開いているページが Dynamic Route であるページが調べるにはどうすればよいでしょうか?それは単純にパスを調べるだけでわかります。

// pages/[name]/index.tsx

import { isDynamicRoute } from "next/dist/next-server/lib/router/utils";
  ...
    
const Index: NextPage = () => {
  const router = useRouter();
    if (isDynamicRoute(router.pathname)) {
      // true なら Dynamic Route
    }
    ...
}

isDynamicRoute(...) は Next.js で用意されている関数で、単純な正規表現マッチです。

github.com

Next.js の内部処理を見てみると、同じように Dynamic Route かどうかがチェックされているのがわかります。

github.com

MAZDA3 FASTBACK 15S Touring(AT) 試乗した

近頃 MAZDA3 の1.5リッターエンジンが気になりまして、試乗に行ってきました。


過去に MAZDA3 はディーゼルエンジンに試乗した経験がありましたが、MAZDA3 が発売してしばらく経って「実は1.5リッターがベストなのでは??」という記事が溢れ始め、これがかなり気になって試乗してみたという経緯です。

ひと目で「良い!」と思えるデザイン

久々に実物の MAZDA3 を間近で見ました。もうなんの文句もないデザインです。欧州車の高級グレード車と戦わせてもなんら遜色ない素晴らしいデザインです。よくこんなクルマ作ったなと思います。日本車の普通の価格帯のクルマでこんなものが生まれる日が来るとは思いも寄りませんでした。日本車には好きになる努力が必要なデザインも多々あるのですが、そういったものとはもう全く別次元のクオリティです。


ただし、デザインによって後方視界が犠牲になっています。後ろ見えません正直。バックの際はバックカメラ使う前提になると思います。おそらくはそれでも足りなくて、360度モニタのオプションを付けて購入することになるのではないかと見ています。

パワーがなくても苛つかない不思議なエンジン

まず気になるパワーですが、パワーはそんなに無いです。ただし「車体が重い割に進まないなあ...」と感じるほどではない、絶妙な位置に落ち着いています。


絶対的な馬力はないのでベタ踏みするとよくわかりますが、バン!と踏んだあとにズイーッと頑張って速度が上がっていく感じ。遅くは無いものの、まあこんなものか、と言った程度のパワーです。


ところが、不思議なことにパワーがなくても特に不満にならないのです。この原因ですが、エンジンのフィールと車体の良さにあります。


まずこの1.5リッターエンジン、パワーはないですが音が妙に良いのです。これはエンジン始動時にいきなりわかります。文字で伝えるのが非常に難しいのですが...スポーティー感、高揚感のある音で、加速時にもよく聞こえます。この良い音によって、パワーが無くても爽快な印象があります。


またエンジンの吹け上がりがかなり良いです。軽快に吹け上がります。本物のスポーツカーほどではないもののスポーティーな印象です。これによって「エンジンをいのままに操っている」印象がかなり強くなります。


そして車体の軽快感です。前はディーゼルエンジンということもあり、全重量が1.5リッターより 70kg 重い車体でした。ところが今回は -70kg となり、比較すると車体の軽快感がかなりあります。これは車体の前部分の軽さが 70kg 変わることになるために軽快感を感じやすくなっているのではと思います(ママチャリのかごに重い荷物を入れて走っているときと、そうでないときを考えるとわかりやすいのかなと思います)。


最後に着座位置です。かなり下目な着座位置になっています。これによって地面との距離が近くなるため、視覚的に加速感が強くなるのです。


これらの要素が組み合わさり、いのままに操れる車としてうまく作られているため、パワーがなくても「お?良いじゃないか」という印象を抱けるようになっています。ものすごくクルマづくりがうまいです。このクルマに試乗したとき、ND ロードスターを思い出しました。なんとなく全体的な印象が似ているんです。もちろん性能や楽しさはロードスターにはかないませんが、結構似てます。実際「ロードスターっぽい」というレビューもいくつかあるようです。また実際にMazdaとしては(全グレードに共通してだと思いますが)「4人乗りのロードスターを作ったつもりです」ということだそうです。


セールスの方に伺うと、エンジン自体はロードスターとベースは同じとのことです。

ブレーキ

かなり良いです。前試乗したときより良く感じました。前は踏んでもあまり効かずに、奥まで踏み込むとようやく効く、Mazdaの主張通りのブレーキになっていました。ただしちょっとやりすぎな感じもあったんですよね。


ところが今回は踏み込むと割とすぐ効き始め、奥まで踏めば踏むほどしっかり効くという作りになっていました。マイナーチェンジの恩恵か、ディーゼルエンジンとの重さの差か、あるいは両方かわかりませんが、とにかく違和感がないブレーキでした。

車体

先ほど軽快感がある車体と書きました。そのおかげか、コーナリングもすいっと入っていくイメージです。かなり自然に曲がれます。また MAZDA3 には G-ベクタリング コントロール プラス が搭載されており、この恩恵があるものと思います。正直ちょっと試乗したぐらいではそれが如何なるものか詳しくはわかりませんでしたが、つまり乗り手に意識せずに自然に曲がれるシステムがうまく働いているということなのかなと思いました。

総評

MAZDA3 FASTBACK 15S Touring は、エンジンのパワーはさほど無いものの様々な「走りを楽しくする要素」が高バランスで組み合わさっているので、 乗り手を不満にさせないクルマになっていることがよくわかりました。そしてこれが重要な点なのですが、1. 家族が問題なく乗れて、2. ロードスターにある程度近い楽しみを得ることができ、3. かつ欧州の高級車以上のデザインを得ることができる。このクルマの車体価格は230万程度なので、この価格で普段遣いできるクルマであるにも関わらず、運転が楽しく、特別感のある存在を手に入れることができるというのは、凄まじい価値だと感じました。

Apollo CLI の codegen から GraphQL Code Generator に移行する

Apollo CLIcodegen から GraphQL Code Generator に移行したくなり、移行しました。移行するにあたって、一度に全部変更すると破滅するので、以下の作戦で行いました。

  • GraphQL Code Generator を導入
  • apollo client:codegen と GraphQL Code Generator を共存させる
  • 徐々に GraphQL Code Generator で生成されたコードに置き換えていく
  • 最後に apollo client:codegen 記述を package.json から削除

といった具合で進めます。

GraphQL Code Generator を導入

こちらに導入方法をまとめてあるので良かったらどうぞ。導入だけであれば、特に既存の何かと衝突したりはせずすんなり入ると思います。 funnelbit.hatenablog.com

apollo client:codegen と GraphQL Code Generator を共存させる

多くの場合、package.json などにコード生成タスクを書いていて、そこで apollo client:codegen を動かしているプロダクトが多いと思います。これはそのままにしておいてください。また GraphQL Code Generator を導入したことで GraphQL Code Generator もコード生成処理が package.json に書かれていると思いますが、これもそのままにしてください。そして少し無茶な感じですが apollo client:codegen と GraphQL Code Generator のコード生成を同時に走らせても破滅しないようにします。

とはいえ、デフォルトの状態では駄目な可能性があります。GraphQL Code Generator でコード生成をした後に apollo-codegen のコード生成プロセスを実行してみてください。場合によってはエラーが出ます(もしでなければここは読み飛ばしてください)。

intro-graphql-codegen ● ⍟4  npm run dev:apollo # apollo client:codegen です
 
 > app@0.1.0 dev:apollo /app
 > apollo client:codegen --target=typescript --outputFlat --customScalarsPrefix=GraphQL --watch ./src/types/graphql.ts
 
 CLIError: Error in "Loading queries for Unnamed Project": Error: ️️There are multiple definitions for the `GetUser` operation. Please rename or remove all operations with the duplicated name before continuing.
     at Object.error (/app/node_modules/@oclif/errors/lib/index.js:26:15)
     at Generate.error (/app/node_modules/@oclif/command/lib/command.js:60:23)
     at OclifLoadingHandler.showError (/app/node_modules/apollo/lib/OclifLoadingHandler.js:28:22)
     at OclifLoadingHandler.handle (/app/node_modules/apollo/lib/OclifLoadingHandler.js:13:18) {
   oclif: { exit: 2 },
   code: undefined
 }
   ✖ Loading Apollo Project
     → Error initializing Apollo GraphQL project "Unnamed Project": Error: Error in "Loading queries for Unnamed Project": Error: ️️There are multiple definitions for the `GetUser`

GetUser が複数定義していると言われて怒られています。これは、GetUser の定義が graphql-codegen によって生成されており、そこと競合しているとみなされエラーとなっているためです。

// とあるコンポーネント内。gql が定義されている
 ...
 gql`
   query GetUser($id: ID!) {
     node(id: $id) {
       id
       ... on Entry {
         title
       }
     }
   }
 `;
 ...
//  GraphQL Code Generator によって作られた graphql.ts ファイル内
 ...
 export const GetUserDocument = gql` // ここで定義されているので、二重にあると認識されている
     query GetUser($id: ID!) {
   node(id: $id) {
     id
       ... on Entry {
         title
       }
   }
 }
     `;
     ...

graphql-codegen で作られた GetUserDocument は、各コンポーネントで import して利用すると便利なので、このままにしておきたいです。とはいえこのままでは apollo-codegen でコード生成できないので、何かしらの対策が必要になります。

そのための策としては、単純に apollo-codegen が graphql-codegen によって作られたファイルを見ないようにすると良いです。

// package.json
 ...
 "dev:apollo": "apollo client:codegen --target=typescript --includes='src/**/!(graphql).{ts,tsx}' --outputFlat --customScalarsPrefix=GraphQL --watch ./src/types/graphql.ts",
  ...

ポイントは --includes='src/**/!(graphql).{ts,tsx}' で、ここで graphql-codegen によって作られたコード src/generated/graphql.ts を含まないようにしています。この状態で apollo-codegen のコード生成を走らせるとうまくいきます。

徐々に GraphQL Code Generator で生成されたコードに置き換えていく

import や gql の宣言箇所、型などいくつか書き換える必要があります。以下に例を diff で示します。

  import { gql } from "@apollo/client";
 - import { CreatedWorksContent_createdWorks } from "types/graphql"; // 1
   import {
 -   createdWorkFragment,
 -   ListCreatedWork,
 - } from "./elements/ListItem"; // 2
 +   CreatedWorkContentFragment,
 +   CreatedWorkContentFragmentDoc,
 + } from "generated/graphql"; // 3
 + import { ListCreatedWork } from "./elements/ListItem";
  
 - export const createdWorksFragment = gql` // 4
 + gql` 
     fragment CreatedWorksContent on RegisteredViewer {
       createdWorks {
         id
         ...CreatedWorkContent
       }
     }
 -   ${createdWorkFragment}
 +   ${CreatedWorkContentFragmentDoc} // 5
   `;
   
   interface ListCreatedProps {
 -   createdWorks: CreatedWorksContent_createdWorks[] | null;
 +   createdWorks?: CreatedWorkContentFragment[] | null; // 6
   }
   
   export const ListCreated: React.FC<ListCreatedProps> = ({
...
  1. types/graphqlapollo codegen によって生成された定義が入っている。これは利用しないので import を削除する
  2. ここではわかりにくいですが、 ListItem コンポーネントの中でも apollo codegen で生成されたコードを GraphQLCodegen によって生成されたコードにおきかえています。ListItem は Fragment Collocation として props を要求しているので、結果として import が変わっている、ということになります。
  3. generated/graphql は graphql-codegen によって生成された定義が入っています。これからはこちらを利用するので import します。ここには 2 で削除した項目が(変数名は若干違うが中身は同じ)入っていることになります。
  4. graphql-codegen はここに相当する変数を export してくれているので、必要がなくなります。
  5. 4 によって export された変数を代わりにここで使います。
  6. apollo codegen は Fragment 内の各キーごとに型としていました。graphql-codegen は Fragment ごとの型が生成されます。これに則ると、このコンポーネントは自身で定義した Fragment の中の型を要求するのではなく、自信で定義した Fragment そのものの型を要求する形になります。

意外と様々な箇所を変えることになるので、最も下層のコンポーネントから手を入れて、そこから影響のあるコンポーネントも変えていく、というスタイルが良さそうです。