<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>QUARTETCOM TECH BLOG</title>
    <atom:link href="https://tech.quartetcom.co.jp/feed.xml" rel="self" type="application/rss+xml"/>
    <link>https://tech.quartetcom.co.jp/</link>
    <description>カルテットコミュニケーションズ開発部ブログ</description>
    <pubDate>Mon, 03 Aug 2026 18:56:23 +0900</pubDate>
    
    
    
      



      <item>
        <title>JavaScript の Temporal 入門 | QUARTETCOM TECH BLOG</title>
        
        
        <category>JavaScript</category>
        
        <link>https://tech.quartetcom.co.jp/2026/08/03/js-temporal/</link>
        <guid isPermaLink="true">https://tech.quartetcom.co.jp/2026/08/03/js-temporal/</guid>
        <description>
&lt;p&gt;JavaScript の新しい日付時刻 API の Temporal を知っていますか？&lt;/p&gt;

&lt;p&gt;記事執筆時点（2026/08）でのステータスは、プロポーザルが「Stage4」、Baseline が「Limited availability」となっています。MDN でも実験的な機能と紹介されており実用段階ではありませんが Google Chrome v144 や Node.js v26 で標準搭載されるなどランタイム環境のサポートは広がりつつあります。Safari で実装されればこの勢いはさらに加速するかもしれません。&lt;/p&gt;

&lt;p&gt;とはいえ Temporal のインターフェイスはシンプルで直感的とは思えず、お試しコードを書くところからつまづいてしまいました。リファレンスを見ながら考え込んでしまった自分に「最初はこういうコードから試してみるといいよ！」と説明するつもりで入門記事を書いてみます。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://tc39.es/proposal-temporal/docs/&quot;&gt;Temporal | tc39.es&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/tc39/proposal-temporal&quot;&gt;tc39/proposal-temporal | GitHub&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Temporal&quot;&gt;Temporal | MDN&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;table-of-contents&quot;&gt;目次&lt;/h1&gt;

&lt;h2 id=&quot;ざっくり読みたい方向け所要時間-5-分&quot;&gt;ざっくり読みたい方向け（所要時間 5 分）&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#plain-date-time-classes&quot;&gt;日付時刻の構成要素を表すクラス&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#exact-time-classes&quot;&gt;特定の瞬間を表すクラス&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#impression&quot;&gt;所感 〜関心の分離のトレードオフ〜&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;じっくり読みたい方向け所要時間-15-分&quot;&gt;じっくり読みたい方向け（所要時間 15 分）&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#overview&quot;&gt;1章） 概要を知る&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#temporal-namespace&quot;&gt;Temporal 名前空間&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#temporal-string-format&quot;&gt;Temporal の文字列表現&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#calendar&quot;&gt;暦&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#now&quot;&gt;現在時刻&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#plain-date-time-classes&quot;&gt;日付時刻の構成要素を表すクラス&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#exact-time-classes&quot;&gt;特定の瞬間を表すクラス&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#temporal-calculation&quot;&gt;2章） Temporal の日付時刻演算を知る&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#comparison&quot;&gt;比較&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#difference&quot;&gt;差分&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#addition&quot;&gt;加算&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#subtraction&quot;&gt;減算&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#update&quot;&gt;更新&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#rounding&quot;&gt;時間の丸め&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#playground&quot;&gt;Temporal お試し環境&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#impression&quot;&gt;所感 〜関心の分離のトレードオフ〜&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;overview&quot;&gt;1章） 概要を知る&lt;/h1&gt;

&lt;h2 id=&quot;temporal-namespace&quot;&gt;Temporal 名前空間&lt;/h2&gt;

&lt;p&gt;Temporal 名前空間にはいくつかのクラスがあります。&lt;br /&gt;
これらは、日付、時刻、日付時刻など用途に応じて呼び分けます。&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;クラス&lt;/th&gt;
      &lt;th&gt;用途&lt;/th&gt;
      &lt;th&gt;値の例&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Temporal.PlainDate&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;日付&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2026-08-01&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Temporal.PlainTime&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;時刻&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;10:00:00&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Temporal.PlainDateTime&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;日付時刻&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2026-08-01T09:00:00&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Temporal.PlainMonthDay&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;月日&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;08-01&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Temporal.ZonedDateTime&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;日付時刻（タイムゾーン付き）&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2026-08-01T09:00:00[Asia/Tokyo]&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Temporal.Instant&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;日付時刻（ナノ秒精度）&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1785542400000000000n&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Temporal.Duration&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;日付時刻（差分）&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;P3D&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;はじめに &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PlainDateTime&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ZonedDateTime&lt;/code&gt; を使ったコードを見てみましょう。どちらも日付時刻を扱うクラスで、タイムゾーンありなしの違いがあります。&lt;/p&gt;

&lt;p&gt;文字列またはオブジェクトを渡し、インスタンスを生成します。&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;t1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Temporal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;PlainDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;2026-08-01T09:00:00&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;t2&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Temporal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ZonedDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;2026-08-01T09:00:00[Asia/Tokyo]&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;t1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Temporal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;PlainDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;year&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2026&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;month&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;day&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;hour&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;9&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;minute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;second&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;t2&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Temporal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ZonedDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;timeZone&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Asia/Tokyo&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;year&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2026&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;month&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;day&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;hour&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;9&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;minute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;second&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;文字列またはプロパティで値を取り出します。&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;t1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 2026-08-01T09:00:00&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 2026-08-01T09:00:00+09:00[Asia/Tokyo]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;t1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;year&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 2026&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;month&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 8&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;day&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 1&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;hour&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 9&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;minute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 0&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;second&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 0&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;t2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;timeZoneId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Asia/Tokyo&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;year&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 2026&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;month&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 8&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;day&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 1&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;hour&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 9&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;minute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 0&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;second&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;temporal-string-format&quot;&gt;Temporal の文字列表現&lt;/h2&gt;

&lt;p&gt;Temporal の入出力は RFC 3339 をベースとした文字列を扱います。&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;08-01
2026-08-01
2026-08-01T09:00:00+09:00
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;タイムゾーンを持つ場合、その情報が追加されます。&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;2026-08-01T09:00:00+09:00[Asia/Tokyo]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;暦を指定した場合、カレンダー注釈が追加されます。&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;2026-08-01T09:00:00+09:00[Asia/Tokyo][u-ca=japanese]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;from()&lt;/code&gt; は文字列表現を入力で受け取ります。&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;t1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Temporal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;PlainDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;2026-08-01T09:00:00&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;t2&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Temporal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ZonedDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;2026-08-01T09:00:00+09:00[Asia/Tokyo]&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;t3&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Temporal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ZonedDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;2026-08-01T09:00:00+09:00[Asia/Tokyo][u-ca=japanese]&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;toString()&lt;/code&gt; は文字列表現を出力します。&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;t1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 2026-08-01T09:00:00&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 2026-08-01T09:00:00+09:00[Asia/Tokyo]&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 2026-08-01T09:00:00+09:00[Asia/Tokyo][u-ca=japanese]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;ECMAScript 拡張の ISO-8601 と RFC 3339&lt;/strong&gt;&lt;br /&gt;
&lt;a href=&quot;https://tc39.es/proposal-temporal/docs/ja/iso-string-ext.html&quot;&gt;https://tc39.es/proposal-temporal/docs/ja/iso-string-ext.html&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&quot;calendar&quot;&gt;暦&lt;/h2&gt;

&lt;p&gt;Temporal では ISO8601 以外の暦も扱うことができます。&lt;br /&gt;
いろいろに変化させられるのがおもしろいですね。&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Temporal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ZonedDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;2026-08-01T09:00:00[Asia/Tokyo]&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;withCalendar&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;iso8601&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// 西暦 2026年8月1日&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// {&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;//   era: undefined, eraYear: undefined,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;//   year: 2026, month: 8, day: 1,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// }&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;withCalendar&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;japanese&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// 日本の暦 令和8年8月&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// {&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;//   era: &apos;reiwa&apos;, eraYear: 8,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;//   year: 2026, month: 8, day: 1,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// }&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;withCalendar&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;hebrew&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// ヘブライ歴 5786年11月&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// {&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;//   era: undefined, eraYear: undefined,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;//   year: 5786, month: 11, day: 18,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;now&quot;&gt;現在時刻&lt;/h2&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Temporal.Now&lt;/code&gt; を使って現在時刻を取り出します。&lt;br /&gt;
Now は Temporal 名前空間のクラスではなくユーティリティオブジェクトです。&lt;/p&gt;

&lt;p&gt;このオブジェクトは他のクラスへの変換メソッドを持っています。&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;t1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Temporal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;plainDateTimeISO&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// to PlainDateTime&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;t2&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Temporal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;zonedDateTimeISO&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// to ZonedDateTime&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;ISO というサフィックスの通り、デフォルトで ISO8601 の暦が適用されます。&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;t1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;calendarId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// iso8601&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;calendarId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// iso8601&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;クラスに変換した後は文字列またはプロパティで値を取り出せます。&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;t1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;t1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;timeZoneId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;year&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;month&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;day&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;hour&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;minute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;second&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;plain-date-time-classes&quot;&gt;日付時刻の構成要素を表すクラス&lt;/h2&gt;

&lt;p&gt;カレンダーや時計の「2026年8月1日」「9時0分」は、見ている人それぞれのエリアでの時間を表すものです。Temporal のドキュメントでは壁時計時間（wall-clock time）と説明されています。新しい年が始まる「1月1日0時0分」のラベリングは世界共通ですが祝うタイミングは違いますよね。このように、タイミングよりも年月日が意味を持つケースがあります。&lt;/p&gt;

&lt;p&gt;Temporal には「年月日時分秒」といった日付時刻の構成要素を扱う専用クラスが存在します。これらはタイムゾーンを持たない素の値として &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Plain&lt;/code&gt; プレフィクスが付いています。&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;クラス&lt;/th&gt;
      &lt;th&gt;表す値&lt;/th&gt;
      &lt;th&gt;例&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Temporal.PlainDate&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;年月日&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2026-08-01&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Temporal.PlainTime&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;時分秒&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;10:00:00&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Temporal.PlainDateTime&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;年月日時分秒&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2026-08-01T09:00:00&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Temporal.PlainMonthDay&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;月日&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;08-01&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Plain プレフィクスのクラスは目的に沿った値だけを扱います。&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Temporal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;PlainDate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;2026-08-01&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// &apos;2026-08-01&apos;&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;year&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 2026&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;month&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 8&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;day&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Temporal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;PlainTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;09:01:02&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// &apos;09:01:02&apos;&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;hour&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 9&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;minute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 1&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;second&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Temporal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;PlainDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;2026-08-01T09:01:02&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// &apos;2026-08-01T09:01:02&apos;&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;year&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 2026&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;month&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 8&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;day&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 1&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;hour&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 9&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;minute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 1&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;second&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Temporal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;PlainMonthDay&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;08-01&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// &apos;08-01&apos;&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;month&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 8&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;day&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Plain プレフィクスのクラスにタイムゾーンを与えると &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ZonedDateTime&lt;/code&gt; に変換できます。&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// TZ=Africa/Abidjan として Node.js を実行&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Temporal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;PlainDate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;2026-08-01&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 2026-08-01T00:00:00+00:00[Africa/Abidjan]&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toZonedDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Temporal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;timeZoneId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 日本のタイムゾーンに変換（UTC との時差 9 時間）&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// 2026-08-01T00:00:00+09:00[Asia/Tokyo]&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toZonedDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Asia/Tokyo&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt; 

&lt;span class=&quot;c1&quot;&gt;// イギリスのタイムゾーンに変換（8 月はサマータイムで UTC との時差 1 時間）&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// 2026-08-01T00:00:00+01:00[Europe/London]&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toZonedDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Europe/London&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;exact-time-classes&quot;&gt;特定の瞬間を表すクラス&lt;/h2&gt;

&lt;p&gt;人類がはじめて月面に降り立ったのは 1969年7月21日2時56分15秒（UTC）です。その様子は世界中でリアルタイム中継され人類史上の価値ある瞬間となりました。このような場合は時分秒よりも、時間軸上のプロットである世界共通の「瞬間」に価値が置かれます。Temporal のドキュメントではカレンダーや場所によらない特定の時点での時刻（exact time）と説明されています。&lt;/p&gt;

&lt;p&gt;Temporal には瞬間を表現するクラスが存在します。&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;クラス&lt;/th&gt;
      &lt;th&gt;表す値&lt;/th&gt;
      &lt;th&gt;例&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Temporal.ZonedDateTime&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;瞬間&lt;br /&gt;タイムゾーンによる解釈&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2026-08-01T09:00:00+0900[Asia/Tokyo]&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Temporal.Instant&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;瞬間&lt;br /&gt;ナノ秒精度&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1785542400000000000n&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// 月面に降り立った瞬間&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Temporal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ZonedDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;1969-07-21T02:56:15Z[UTC]&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// コートジボワールの都市アビジャン: 深夜 2 時&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// 1969-07-21T02:56:15+00:00[Africa/Abidjan]&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;withTimeZone&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Africa/Abidjan&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// アームストロング船長の故郷オハイオ州: 夜 10 時&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// 1969-07-20T22:56:15-04:00[America/New_York]&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;withTimeZone&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;America/New_York&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 東京: 昼 11 時&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// 1969-07-21T11:56:15+09:00[Asia/Tokyo]&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;withTimeZone&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Asia/Tokyo&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;瞬間を表現する &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ZonedDateTime&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Now&lt;/code&gt; は Instant を取り出すメソッドを持っています。Instant は時間軸上のゼロ地点（エポックタイム）からの相対位置を示すプロパティを提供します。&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;t1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Temporal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;instant&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;epochMilliseconds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// ミリ秒: 1785542400000&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;epochNanoseconds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// ナノ秒: 1785542400000000000n&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;t2&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Temporal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ZonedDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(...).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toInstant&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;epochMilliseconds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;epochNanoseconds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;t3&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Temporal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;PlainDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(...).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toInstant&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// ERROR: PlainDateTime は瞬間を表現しないため Instant に変換できない&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;
&lt;p&gt;📖 &lt;a href=&quot;#table-of-contents&quot;&gt;目次に戻る&lt;/a&gt;&lt;/p&gt;

&lt;h1 id=&quot;temporal-calculation&quot;&gt;2章） Temporal の日付時刻演算を知る&lt;/h1&gt;

&lt;h2 id=&quot;comparison&quot;&gt;比較&lt;/h2&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;equals()&lt;/code&gt; で日付時刻が等しいかどうか判定できます。&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;t1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Temporal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;PlainDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;2026-05-01T09:00:00&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;t2&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Temporal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;PlainDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;2026-05-01T09:00:00&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;equals&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;t2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;t1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Temporal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ZonedDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;2026-05-01T00:00:00[Africa/Abidjan]&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;t2&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Temporal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ZonedDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;2026-05-01T09:00:00[Asia/Tokyo]&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;equals&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;t2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;compare()&lt;/code&gt; で大小の関係を数値化できます。&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// Temporal.&amp;lt;クラス名&amp;gt;.compare(t1, t2);&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// [-1] t2 が大きい&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// [0] t1 と t2 が等しい&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// [1] t1 が大きい&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;クラス名は比較対象のスコープを限定するため、日付のみ比較、日付時刻で比較、といった呼び分けができます。&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;t1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Temporal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;PlainDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;2026-08-01T09:01:01&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;t2&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Temporal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;PlainDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;2026-08-01T10:02:02&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// PlainDate: 日付で比較すると t1,t2 は等しい&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;Temporal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;PlainDate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;compare&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;t1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;t2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 0&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// PlainDateTime: 日付時刻で比較すると t2 が大きい&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;Temporal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;PlainDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;compare&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;t1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;t2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// -1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;compare&lt;/code&gt; を応用すると日付時刻のソートができます。&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;dates&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;Temporal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;PlainDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;2026-08-01T13:15:00&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;Temporal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;PlainDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;1995-08-01T23:00:00&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;Temporal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;PlainDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;2026-08-01T09:30:00&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;sort&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Temporal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;PlainDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;compare&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;dates&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;d&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;());&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// [&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;//   &apos;1995-08-01T23:00:00&apos;,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;//   &apos;2026-08-01T09:30:00&apos;,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;//   &apos;2026-08-01T13:15:00&apos;&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// ]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;difference&quot;&gt;差分&lt;/h2&gt;

&lt;p&gt;日付時刻の差分は専用クラスの Duration が担います。&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;クラス&lt;/th&gt;
      &lt;th&gt;表す値&lt;/th&gt;
      &lt;th&gt;値の例&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Temporal.Duration&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;日付時刻（差分）&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;P3D&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Duration は &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;since()&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;until()&lt;/code&gt; の結果として得られます。&lt;br /&gt;
文字列またはプロパティで値を取り出します。&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;past&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Temporal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;PlainDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;2026-08-01T00:00:00&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;future&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Temporal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;PlainDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;2026-08-02T10:45:00&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// since: 過去にさかのぼった差分&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;d1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;future&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;since&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;past&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;d1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// P1DT10H45M&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;d1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;days&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 1&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;d1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;hours&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 10&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;d1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;minutes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 45&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;d1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;seconds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// until: 未来に到達するまでの差分&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;d2&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;past&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;until&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;future&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;d2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// P1DT10H45M&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;d2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;days&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 1&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;d2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;hours&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 10&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;d2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;minutes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 45&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;d2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;seconds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;addition&quot;&gt;加算&lt;/h2&gt;

&lt;p&gt;N 日後、N 時間後、といった加算には &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;add()&lt;/code&gt; を使用します。&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Temporal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;PlainDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;2026-08-01T09:00:00&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 1 ヶ月後 / 2026-09-01T09:00:00&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;months&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 30 日後 / 2026-08-31T09:00:00&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;days&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;30&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 5 時間後 / 2026-08-01T14:00:00&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;hours&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;subtraction&quot;&gt;減算&lt;/h2&gt;

&lt;p&gt;N 日前、N 時間前、といった減算には &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;subtract()&lt;/code&gt; を使用します。&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Temporal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;PlainDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;2026-08-01T09:00:00&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 1 ヶ月前 / 2026-07-01T09:00:00&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;subtract&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;months&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 30 日前 / 2026-07-02T09:00:00&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;subtract&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;days&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;30&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 5 時間前 / 2026-08-01T04:00:00&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;subtract&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;hours&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;update&quot;&gt;更新&lt;/h2&gt;

&lt;p&gt;年月日時分秒の置き換えには &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;with()&lt;/code&gt; を使用します。&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Temporal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;PlainDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;2026-08-01T00:00:00&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 日付を更新 / 2026-09-02T00:00:00&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;year&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2026&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;month&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;9&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;day&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 時刻を更新 / 2026-08-01T12:13:14&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;hour&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;12&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;minute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;13&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;second&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;14&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;rounding&quot;&gt;時間の丸め&lt;/h2&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;round()&lt;/code&gt; を使うと時間の丸め処理ができます。ビルトインでできる計算の幅が広がりましたね。&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Temporal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;PlainTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;18:42:15&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// trunc（0 方向に切り捨て） / 18:30:00&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;round&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;smallestUnit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;minutes&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;roundingMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;trunc&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;roundingIncrement&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;30&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// expand（0 から外側に切り上げ） / 19:00:00&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;round&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;smallestUnit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;minutes&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;roundingMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;expand&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;roundingIncrement&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;30&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// halfCeil（四捨五入） / 18:30:00&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;round&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;smallestUnit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;minutes&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;roundingMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;halfCeil&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;roundingIncrement&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;30&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;blockquote&gt;
  &lt;p&gt;オプションの説明 - &lt;strong&gt;duration.round&lt;/strong&gt;&lt;br /&gt;
&lt;a href=&quot;https://tc39.es/proposal-temporal/docs/ja/duration.html#round&quot;&gt;https://tc39.es/proposal-temporal/docs/ja/duration.html#round&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&quot;playground&quot;&gt;Temporal お試し環境&lt;/h2&gt;

&lt;p&gt;この記事で紹介したコードは Node.js v26 で実行可能です。&lt;br /&gt;
暦指定など一部でエラーが出た場合は &lt;a href=&quot;https://github.com/tc39/proposal-temporal&quot;&gt;https://github.com/tc39/proposal-temporal&lt;/a&gt; の &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@js-temporal/polyfill&lt;/code&gt; をお試しください。&lt;/p&gt;

&lt;p&gt;すでに実装が始まっている Google Chrome でも、デベロッパーツールで &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Temporal&lt;/code&gt; とタイピングすればいろいろ遊んでみることができます。&lt;/p&gt;

&lt;hr /&gt;
&lt;p&gt;📖 &lt;a href=&quot;#table-of-contents&quot;&gt;目次に戻る&lt;/a&gt;&lt;/p&gt;

&lt;h1 id=&quot;impression&quot;&gt;所感 〜関心の分離のトレードオフ〜&lt;/h1&gt;

&lt;p&gt;Temporal では既存の Date オブジェクトの課題解決にあたり、日付時刻の扱いについて根本から見直しが入りました。&lt;/p&gt;

&lt;p&gt;名前空間には 7 つのクラスが並び、それぞれが概念を取り扱う専用道具として機能します。その道具のひとつを取り出せば、適度に抽象化され理路整然と並んだメソッドで日付時刻の演算ができます。このようなインターフェイスに、私たちが日常的に心がけている「関心の分離」を思い起こしました。&lt;/p&gt;

&lt;p&gt;現実世界にあてはめてみると、包丁棚に出刃包丁、菜切り包丁、中華包丁、柳刃包丁…とズラリと並んだような状態でしょうか。玄人向けの調理器具ですね。リアルな体験としてじゃがいもを調理しようと思った時、菜切り包丁では芽がくりぬけず、出刃包丁では皮がむけず、なんか違うと思いながら刺身用のめちゃ長い柳刃包丁を使ったことがあります。結局のところ欲しかったのはプロ並の切れ味はなくとも使いやすい万能包丁でした。&lt;/p&gt;

&lt;p&gt;包丁と Temporal はまったく違うものですが利用者としての体験は似ています。たくさんの専用道具を提示されると、どれを使えば良いのか分からず、最初に手にしたものが正解なのか分からず、迷いながら使いつづけます。日付時刻を扱うシーンは往々にして、関数の中だけの狭いスコープの演算と、フロントエンド外のレイヤーとのやり取りでタイムゾーンの解釈に気を配るくらいです。選択の余地がない Date オブジェクトのほうが手に取りやすい万能包丁に似ています。&lt;/p&gt;

&lt;p&gt;Temporal は関心や責務という意味において「シンプル」です。しかしながら身近でなじみがある「イージー」ではありません。値をナノ秒単位で扱うことができ、世界各国のタイムゾーンや暦に変換できる機能性の高さを実現しながら（おそらく世界中のフロントエンドエンジニア待望の）柔軟な文字列フォーマッターが提供されなかったことも、イージーを感じさせない要因です。&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://www.infoq.com/presentations/Simple-Made-Easy/&quot;&gt;Simple Made Easy&lt;/a&gt; | Rich Hickey&lt;/strong&gt;&lt;br /&gt;
シンプルさとは使いやすいことだと思っているのならそれは間違っています。&lt;br /&gt;
これらは全くもって違うものです。Easy と Simple は違います。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;私たちの開発体験でもシンプルを重要視する場面は数多くあります。ポリモーフィズムを適用したクラスの分離や、レイヤードアーキテクチャによる構造の分離、マイクロサービス化など、さまざまな粒度でシンプルを追求します。シンプルは保守の観点で大きなメリットをもたらしつつも、利用者の前に専用道具をずらりと並べて理解を強制します。手作業の更新を補助する開発者用コマンド、ファサードクラスの導入など、イージーの提供がないと後のメンテナーには辛いものになるかもしれません。&lt;/p&gt;

&lt;p&gt;Temporal も然り、どうにかイージーに寄せる方法としてラッパーなど作ると思考実験としておもしろそうですね。設計やコードリーディングの学びの機会にもなりそうです。&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cm&quot;&gt;/**
 * こういうインターフェイスはどうだろう
 * TemporalHarness() - now
 * TemporalHarness(new Date()) - legacy date
 * TemporalHarness(&apos;2026-08-01T00:00:00[Asia/Tokyo]&apos;) - RFC 9559
 * TemporalHarness(&apos;2026年08月01日&apos;, &apos;yyyy年MM月dd日&apos;) - string parser
 */&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;th&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;TemporalHarness&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;2026-08-01&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;withTimeZone&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Asia/Tokyo&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;withTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;09:00:00&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;addDays&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;th&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;MM/dd hh:mm&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 08/04 09:00&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;th&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;month&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 8&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;th&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;milliSeconds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 123456789...&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;th&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toLegacyDate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Date {}&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;th&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toDuration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;TemporalHarness&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;());&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// { days:-11,minutes:-12,seconds:-13... }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;読者のみなさんは Temporal をどのように受け止めましたか？&lt;/p&gt;

&lt;p&gt;この記事が Temporal にこれから入門するどなたかの参考になれば幸いです。&lt;/p&gt;

</description>
        <pubDate>Mon, 03 Aug 2026 00:00:00 +0900</pubDate>
      </item>
    
      



      <item>
        <title>ベストプラクティスは誰のためのものか - AWS Copilot CLIからAWS CDKに移行を終えて | QUARTETCOM TECH BLOG</title>
        
        
        <category>AWS</category>
        
        <category>CDK</category>
        
        <category>Copilot</category>
        
        <category>IaC</category>
        
        <link>https://tech.quartetcom.co.jp/2026/06/19/whose-best-practice/</link>
        <guid isPermaLink="true">https://tech.quartetcom.co.jp/2026/06/19/whose-best-practice/</guid>
        <description>
&lt;h2 id=&quot;はじめに&quot;&gt;はじめに&lt;/h2&gt;

&lt;p&gt;AWS Copilot CLIは、インフラ構成について深く考えずに済む、非常に強力なツールでした。&lt;/p&gt;

&lt;p&gt;先日公開された社内ブログ記事（&lt;a href=&quot;https://tech.quartetcom.co.jp/2026/05/01/sunset-aws-copilot-cli/&quot;&gt;宣言的記述とメンタルモデル ─ AWS Copilot CLI のサポート終了に寄せて&lt;/a&gt;）でも触れられていましたが、Copilotの最大の価値は&lt;strong&gt;「何がベストプラクティスかという判断を肩代わりしてくれる」&lt;/strong&gt;ことにありました。社内で広く使われていたのもこの明確な恩恵があったからです。&lt;/p&gt;

&lt;p&gt;今回のAWS CDKへの移行の直接のきっかけは、全社的に利用していたこのCopilotがサポート終了を迎えることになったためです。&lt;/p&gt;

&lt;p&gt;ただ、運用する中ですでにツールとのミスマッチを感じ始めていました。Copilotの想定から外れた要件が出るたびに追加のCloudFormation（Addon）で対応する必要があり、かえって構成が複雑化する場面が出てきていたのです。&lt;/p&gt;

&lt;p&gt;CDKへの移行は、サポート終了という避けられないイベントを機に、&lt;strong&gt;「今の自分たちの要件に合わせて、インフラ設計の主導権を握ろう」&lt;/strong&gt;と前向きに舵を切った結果です。&lt;/p&gt;

&lt;p&gt;本記事では、Copilotの「判断の肩代わり」がもたらした具体的な運用課題と、CDKへの移行によって私たちのインフラ管理がどう変わったのかを振り返ります。&lt;/p&gt;

&lt;h2 id=&quot;-目次&quot;&gt;📝 目次&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#1-判断の肩代わりがもたらした運用上の課題&quot;&gt;1. 「判断の肩代わり」がもたらした運用上の課題&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#11-構成の制約と記述パラダイムの断片化&quot;&gt;1.1. 構成の制約と、記述パラダイムの断片化&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#12-抽象化の裏にある挙動の把握&quot;&gt;1.2. 抽象化の裏にある挙動の把握&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#13-アプリとインフラの密結合インフラリポジトリの独立&quot;&gt;1.3. アプリとインフラの密結合（インフラリポジトリの独立）&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#2-aws-cdkに移行して変わったこと&quot;&gt;2. AWS CDKに移行して変わったこと&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#3-ベストプラクティスは誰のためのものか&quot;&gt;3. ベストプラクティスは誰のためのものか&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#4-おわりに&quot;&gt;4. おわりに&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;1-判断の肩代わりがもたらした運用上の課題&quot;&gt;1. 「判断の肩代わり」がもたらした運用上の課題&lt;/h2&gt;

&lt;p&gt;Copilotのmanifestでは、例えば &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;type: Load Balanced Web Service&lt;/code&gt; のような特定の型を指定するだけで、VPC・ALB・ECS・Auto Scalingなどをまとめて定義できました。人間のメンタルモデルに近い抽象度で簡潔に記述できることこそ、Copilot最大の強みでした。&lt;/p&gt;

&lt;p&gt;ただ、この「まとめて定義できる」ことの裏を返せば、&lt;strong&gt;「細かい部分はすべてCopilotが決定している」&lt;/strong&gt;ということです。実際に私たちが運用していく中で、壁となったのは以下のような点でした。&lt;/p&gt;

&lt;h3 id=&quot;11-構成の制約と記述パラダイムの断片化&quot;&gt;1.1. 構成の制約と、記述パラダイムの断片化&lt;/h3&gt;

&lt;p&gt;Copilotの用意する「型」は強力ですが、そこから一歩踏み込んで独自の最適化を行おうとした途端に、記述が複雑化するもどかしさがありました。&lt;/p&gt;

&lt;p&gt;manifestの標準機能では表現しきれないセキュリティグループの詳細設定やRoute53のドメイン設定などを「Addon（CloudFormation）」として別途記述して組み合わせる必要がありました。&lt;/p&gt;

&lt;p&gt;この&lt;strong&gt;manifestとCloudFormationという2つの異なる記述パラダイムを行き来するスイッチングコストやコードの断片化&lt;/strong&gt;は、運用規模が広がるにつれて小さくないストレスになっていきました。&lt;/p&gt;

&lt;h3 id=&quot;12-抽象化の裏にある挙動の把握&quot;&gt;1.2. 抽象化の裏にある挙動の把握&lt;/h3&gt;

&lt;p&gt;Copilotの高い抽象度は、&lt;strong&gt;manifestの設定変更が裏側でどのAWSリソースにどう影響するかをドキュメントから事前に読み取りにくくする&lt;/strong&gt;という側面を持っています。「この一行を変えたら何が変わるのか」をCopilotのドキュメントから把握しようとしても、manifest設定とCloudFormation出力の対応は網羅的に説明されていません。
結局はGitHub Issueを調べたり、実際にデプロイしてコンソールで確認する、というトライ＆エラーのサイクルに頼るしかありませんでした。&lt;/p&gt;

&lt;p&gt;最終的にCloudFormationのdiffで変更内容を確認するのが正しいアプローチでその点ではCDKも同じです。ただ、CDKはドキュメントが充実しており、設定の意味や出力を事前に把握できる場面がほとんどです。
特にL1コンストラクトはTypeScriptとCloudFormationリソースの対応が直接的なため、デプロイ前から変更結果を予測しやすく、トライ＆エラーのサイクルが大きく減りました。&lt;/p&gt;

&lt;h3 id=&quot;13-アプリとインフラの密結合インフラリポジトリの独立&quot;&gt;1.3. アプリとインフラの密結合（インフラリポジトリの独立）&lt;/h3&gt;

&lt;p&gt;現在の関わっているシステムでは、バックエンドはECSで、フロントエンドはS3+CloudFrontで独立してデプロイする構成を採用しています。
これはAWSが&lt;a href=&quot;https://docs.aws.amazon.com/prescriptive-guidance/latest/patterns/deploy-a-react-based-single-page-application-to-amazon-s3-and-cloudfront.html&quot;&gt;公式のPrescriptive Guidance&lt;/a&gt;でも推奨しているSPAのデプロイパターンです。
フロントエンドとバックエンドをデプロイ単位として分離することで、それぞれのリリースサイクルを独立させ、互いの変更に影響されずにデプロイできます。&lt;/p&gt;

&lt;p&gt;このアーキテクチャでは、インフラの管理対象がバックエンド（ECS）とフロントエンド（S3+CloudFront）の両方にまたがります。
そのため、アプリのコードとは完全に切り離された&lt;strong&gt;インフラ専用リポジトリで一元管理したい&lt;/strong&gt;という要件が自然に生まれました。&lt;/p&gt;

&lt;p&gt;AWS Copilotは、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;copilot/&lt;/code&gt; ディレクトリをアプリケーションリポジトリ内に配置し、アプリと同居させて管理することが基本の設計思想です。
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;copilot/&lt;/code&gt; をどのリポジトリに置くかはチームが選択できますが、どこに置いたとしても「インフラ定義がいずれかのアプリリポジトリ内に存在する」という状態は避けられません。
私たちが目指していた「すべてのアプリリポジトリから独立したインフラ専用リポジトリ」という構成は、Copilotの設計思想の外にありました。&lt;/p&gt;

&lt;h2 id=&quot;2-aws-cdkに移行して変わったこと&quot;&gt;2. AWS CDKに移行して変わったこと&lt;/h2&gt;

&lt;p&gt;CDKへの移行によって、記述のパラダイムが統一され、インフラ管理の見通しが大きく改善されました。具体的には、以下の4つの変化が挙げられます。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;自由な構成への再設計&lt;/strong&gt; — CDKには「L1〜L3コンストラクト」という抽象度の異なる概念があり、これらを単一のコード内で自由に組み合わせられます。特に効いたのが、L3コンストラクトである &lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;QueueProcessingFargateService&lt;/code&gt;&lt;/strong&gt; の導入です。SQSのメッセージ量に応じたオートスケーリングを含むワーカーの構成全体を、数行のコードで表現できるようになりました。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;プログラミング言語を活かしたインフラ記述&lt;/strong&gt; — CDKはTypeScriptをはじめ複数の言語をサポートしています。私たちはTypeScriptを選択しましたが、ifやループといった制御構文が使えることに加え、AWSリソースの生成ロジックそのものをTypeScriptの型システムで保護できることが大きな利点です。IDEの補完はCloudFormationのスキーマ定義でも一定程度得られますが、CDK+TypeScriptではリソースを生成するコードそのものが型安全になるため、複雑な構成も見通しよく管理できるようになりました。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;コードでの完全な完結&lt;/strong&gt; — Copilotでmanifestの表現力を超えるような設定が必要になると、CloudFormation（Addon）という別のパラダイムへの切り替えが発生し、その学習や記述のコストがかかっていました。CDKではL1コンストラクトを使えばCloudFormationと同等の表現力をCDKのコードのまま扱えるため、パラダイムを切り替えることなく、ほぼあらゆる設定を同じコードベース内で完結できます。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;インフラリポジトリの独立&lt;/strong&gt; — マルチリポジトリの運用に合わせて、インフラを専用の独立したリポジトリへ完全に分離し、一元管理できるようになりました。これにより、アプリケーションコードに一切ノイズを混ぜることなく、アプリとインフラのライフサイクルをきれいに分離できました。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;4つの変化に共通するのは、&lt;strong&gt;インフラの設計判断を「Copilotが想定する正解」ではなく「自分たちの要件」を起点に考えられるようになった&lt;/strong&gt;ことです。制約に沿った設計から、意図を直接コードに落とす設計へ——この転換が、日々の運用感覚を大きく変えました。&lt;/p&gt;

&lt;h2 id=&quot;3-ベストプラクティスは誰のためのものか&quot;&gt;3. ベストプラクティスは誰のためのものか&lt;/h2&gt;

&lt;p&gt;冒頭で紹介した社内記事にもあるように、Copilotの高い抽象度は「AWSを深く知らなくてもベストプラクティスな構成で動かせる」という素晴らしい価値を生み出していました。&lt;/p&gt;

&lt;p&gt;しかしこの移行を通じて改めて感じたのは、&lt;strong&gt;「AWSとしてのベストプラクティス（＝Copilotのデフォルト）」が、必ずしも「私たちにとってのベストプラクティス」とは限らない&lt;/strong&gt;ということです。&lt;/p&gt;

&lt;p&gt;私たちには私たちの要件があり、コスト感があり、技術スタックがあります。今回の移行でも、「どのリソースをどの粒度で管理するか」「ワーカーをどうスケールさせるか」といった問いをチームで議論し、Copilotのデフォルトとは異なる選択をした箇所が複数ありました。
インフラをアプリと完全に分離したリポジトリで管理したいという方針も、バックエンドとフロントエンドを独立したデプロイ単位として扱いたいという設計も、Copilotが想定する標準的な「正解」の外側にあったのです。&lt;/p&gt;

&lt;p&gt;「判断の肩代わり」は、「自分たちの要件に合わせた判断をする余地が狭まる」ということでもあります。AWSの知識を設計にしっかり活かしながら、インフラを自分たちでコントロールしていきたいチームには、間違いなくCDKがフィットするはずです。&lt;/p&gt;

&lt;h2 id=&quot;4-おわりに&quot;&gt;4. おわりに&lt;/h2&gt;

&lt;p&gt;Copilotのサポート終了は、単なるツールの移行作業ではなく、&lt;strong&gt;「何をツールに任せ、何を自分たちで判断するか」&lt;/strong&gt;を根本から問い直す貴重なきっかけになりました。&lt;/p&gt;

&lt;p&gt;Copilotが肩代わりしてくれていたインフラ設計の判断を、改めて自分たちで一つひとつ引き受けてみると、そこには、これまで諦めていた設計の選択肢が普通に取れる余地が広がっていました。&lt;/p&gt;

&lt;p&gt;万人に共通する「完璧なベストプラクティス」はありません。プロダクトの成長やチームの状況によって、正解は変わり続けます。その時々の要件ときちんと向き合って、自分たちに合った選択をしていく——そういう姿勢が、長い目で見たシステムの健全性につながるのではないかと考えています。&lt;/p&gt;

&lt;p&gt;今回のCDK移行を通して得た最大の収穫は、新しいインフラ構成そのもの以上に、&lt;strong&gt;「自分たちのベストプラクティスは自分たちで選べる」&lt;/strong&gt;という確かな感覚です。&lt;/p&gt;

&lt;p&gt;ツールやフレームワークの選択においても、同じことが言えるはずです。もし今、「世間的なベストプラクティス」と「自分たちの要件」の間に窮屈さを感じているなら、一度立ち止まって&lt;strong&gt;「これは誰のためのベストプラクティスなのか」&lt;/strong&gt;を問い直してみてはいかがでしょうか。&lt;/p&gt;

</description>
        <pubDate>Fri, 19 Jun 2026 00:00:00 +0900</pubDate>
      </item>
    
      



      <item>
        <title>サーバーサイド計測が必要になった背景 | QUARTETCOM TECH BLOG</title>
        
        
        <category>Cookie</category>
        
        <category>ITP</category>
        
        <category>SGTM</category>
        
        <link>https://tech.quartetcom.co.jp/2026/06/08/history-of-tracking-and-cooki/</link>
        <guid isPermaLink="true">https://tech.quartetcom.co.jp/2026/06/08/history-of-tracking-and-cooki/</guid>
        <description>
&lt;h1 id=&quot;はじめに&quot;&gt;はじめに&lt;/h1&gt;

&lt;p&gt;計測プラットフォーマーはユーザー計測をサーバーサイドに移行する動きを進めています。&lt;br /&gt;
計測ツール提供側（計測プラットフォーマー） / 利用側ともに負担の大きいサーバーサイド計測が求められている背景を整理します。&lt;/p&gt;

&lt;h2 id=&quot;1-クライアントサイド主体の計測&quot;&gt;1. クライアントサイド主体の計測&lt;/h2&gt;

&lt;p&gt;ユーザー計測ではブラウザに Cookie を保存してユーザーを識別します。Cookie は仕様で別ドメインに自動送信されません。&lt;br /&gt;
そのため、従来のユーザー計測では、Cookie に保存したユーザー識別子を計測サーバーへ送る手段としておもに 2 つの方法が使われています。&lt;/p&gt;

&lt;h3 id=&quot;img-ピクセルによる送信&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;img&amp;gt;&lt;/code&gt; ピクセルによる送信&lt;/h3&gt;

&lt;p&gt;1 × 1 px の透明画像タグをページに埋め込みブラウザが計測サーバーに画像をリクエストします。計測サーバーはレスポンスの &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Set-Cookie&lt;/code&gt; で Cookie を発行します。代表的な計測サーバーに Google 広告のコンバージョン計測やインプレッション計測する &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;doubleclick.net&lt;/code&gt; があります。&lt;/p&gt;

&lt;p&gt;具体的なコードは以下のようになります（正確性より分かりやすさを優先したサンプルです）。&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;img&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://ad.doubleclick.net/activity;src=XXXX;type=YYYY&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;width=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;1&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;height=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;1&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;javascript-による送信&quot;&gt;JavaScript による送信&lt;/h3&gt;

&lt;p&gt;JavaScript が Cookie の値を読み取り、クエリパラメーターに付与して計測サーバーに送信します。
代表的な計測サーバーに GA4 （ &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;google-analytics.com&lt;/code&gt; ）があります。&lt;/p&gt;

&lt;p&gt;擬似的なコードは以下のようになります（正確性より分かりやすさを優先したサンプルです）。&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// クライアントで発行した Cookie を document.cookie で取得&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;cid&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;cookie&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;_ga&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 取得した Cookie をクエリパラメーターに付与して計測サーバーに送信&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;send&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`https://www.google-analytics.com/g/collect?cid=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;cid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;amp;...`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;2-ブラウザのプライバシー制限&quot;&gt;2. ブラウザのプライバシー制限&lt;/h2&gt;

&lt;h3 id=&quot;21-サードパーティ-cookie-のブロック&quot;&gt;2.1 サードパーティ Cookie のブロック&lt;/h3&gt;

&lt;p&gt;Safari は ITP（ Intelligent Tracking Prevention ）を導入し機械学習でクロスサイトトラッキング能力があると判定したドメイン（ &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;classified domain&lt;/code&gt;&lt;sup id=&quot;fnref:classified_domain&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:classified_domain&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; ）のサードパーティ Cookie を段階的に制限しました。&lt;/p&gt;

&lt;p&gt;当初 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;google-analytics.com&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;doubleclick.net&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;facebook.com&lt;/code&gt; 等が &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;classified domain&lt;/code&gt; の対象となりました。その後、分類の有無に関わらず全サードパーティ Cookie をデフォルトでブロックしました。&lt;/p&gt;

&lt;p&gt;Firefox も ETP（Enhanced Tracking Protection）で同様にサードパーティ Cookie を全面的にブロックしています。&lt;/p&gt;

&lt;p&gt;これにより &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;img&amp;gt;&lt;/code&gt; ピクセルが依存するサードパーティ Cookie の発行・送信は Safari / Firefox で機能しなくなりました。&lt;/p&gt;

&lt;p&gt;一方、Chrome の対応は異なります。2020 年（ Chrome 80 ）に &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SameSite&lt;/code&gt; 属性が設定されていない Cookie のデフォルトを &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Lax&lt;/code&gt; に変更しました。これはサードパーティ Cookie の禁止ではなく、明示的な指定がなければ送信しないという変更です。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SameSite=None; Secure&lt;/code&gt; を指定すれば &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;img&amp;gt;&lt;/code&gt; ピクセルのレスポンスで発行されたサードパーティ Cookie が今も送受信されています（ &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;google-analytics.com&lt;/code&gt; や &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;doubleclick.net&lt;/code&gt; はこれを行っています）&lt;sup id=&quot;fnref:chrome&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:chrome&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;。&lt;/p&gt;

&lt;h3 id=&quot;22-javascript-発行-cookie-の有効期限制限&quot;&gt;2.2 JavaScript 発行 Cookie の有効期限制限&lt;/h3&gt;

&lt;p&gt;ITP は JavaScript（ &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document.cookie&lt;/code&gt; ）で発行した Cookie の有効期限を最大 7 日に制限しました。前述した GA4 の &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_ga&lt;/code&gt; Cookie はこの制限を受けるため、7 日を超えると再訪ユーザーが新規ユーザーとして誤認されて計測の精度が低下します。&lt;/p&gt;

&lt;p&gt;サーバー発行 Cookie は当初この制限を受けませんでしたが、自社サブドメインの DNS レコードで計測サーバーの IP アドレスを隠蔽する CNAME クローキング / IP アドレスクローキングという迂回策も ITP に検知され、サーバー発行 Cookie も 7 日制限の対象となりました。名前だけを自社ドメインに見せかける回避策も ITP に制限されるようになりました。&lt;/p&gt;

&lt;h2 id=&quot;3-サーバーサイド計測の必要性具体例&quot;&gt;3. サーバーサイド計測の必要性＆具体例&lt;/h2&gt;

&lt;p&gt;上述したとおりクライアントサイド主体の計測は段階的に制限されてきました。これらの制限を根本的に回避するには実際に自社ドメインのサーバーがリクエストを受けて Cookie を発行・管理する必要があります。通常自社サブドメイン（例: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;analytics.example.com&lt;/code&gt;）は &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;classified domain&lt;/code&gt; に分類されないため ITP の有効期限制限を受けません。&lt;/p&gt;

&lt;h3 id=&quot;例-google-server-side-tag-manager--sgtm-&quot;&gt;例 Google server-side Tag Manager （ SGTM ）&lt;/h3&gt;

&lt;p&gt;自社ドメインのサーバーに GTM のタグサーバーを設置しブラウザからのリクエストをいったん自社サーバーが受けてから Google の計測サーバーへ転送する構成です。自社サーバーが &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Set-Cookie&lt;/code&gt; で Cookie を発行するため ITP の有効期限制限を回避できます&lt;sup id=&quot;fnref:server-side-tag-manager&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:server-side-tag-manager&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;。名前だけを自社ドメインにするのではなく実際に自社サーバーでリクエストを処理する点が ITP 対策として機能する核心です。&lt;/p&gt;

&lt;h2 id=&quot;まとめ&quot;&gt;まとめ&lt;/h2&gt;

&lt;p&gt;ブラウザのプライバシー強化はクライアントサイド主体の計測（ &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;img&amp;gt;&lt;/code&gt; ピクセルと JavaScript による送信）を段階的に制限してきました。またその迂回策も都度制限されてきました。&lt;/p&gt;

&lt;p&gt;Google server-side Tag Manager へのシフトはこの流れへのソリューションといえます。
本記事がその背景理解の一助になれば幸いです。&lt;/p&gt;

&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:classified_domain&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;ITP の文脈での classified Domain とは、Safari が機械学習を用いてユーザーを追跡（トラッキング）していると判断したドメインのことです。 &lt;a href=&quot;#fnref:classified_domain&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:chrome&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;プライバシー重視の流れにある現在においては、サードパーティ Cookie はすべてのブラウザで制限されると考えがちです。実際は Safari / Firefox と異なり Chrome はオプトインを条件に今もサードパーティ Cookie を許容しています。 &lt;a href=&quot;#fnref:chrome&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:server-side-tag-manager&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;&lt;a href=&quot;https://developers.google.com/tag-platform/tag-manager/server-side/intro?hl=ja&quot;&gt;サーバーサイド タグ設定の概要&lt;/a&gt; &lt;a href=&quot;#fnref:server-side-tag-manager&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</description>
        <pubDate>Mon, 08 Jun 2026 00:00:00 +0900</pubDate>
      </item>
    
      



      <item>
        <title>reCAPTCHA v3の導入効果とスコアのしきい値の設定方法 | QUARTETCOM TECH BLOG</title>
        
        
        <category>reCAPTCHA</category>
        
        <category>スパム対策</category>
        
        <category>Bot対策</category>
        
        <link>https://tech.quartetcom.co.jp/2026/06/01/recaptcha-v3/</link>
        <guid isPermaLink="true">https://tech.quartetcom.co.jp/2026/06/01/recaptcha-v3/</guid>
        <description>
&lt;h1 id=&quot;はじめに&quot;&gt;はじめに&lt;/h1&gt;

&lt;p&gt;弊社の一部SaaSプロダクトではユーザー登録におけるスパム登録数が徐々に増加し、reCAPTCHA v3導入前の3ヶ月間ではユーザー登録の約89%がスパム登録という状況になっていました。&lt;br /&gt;
これによりデータを使った分析の精度低下やDBの圧迫などの問題が発生していました。&lt;/p&gt;

&lt;p&gt;そのため、対策としてまずは導入コストが低いハニーポットフィールド(ボット検知用のダミー入力欄)の設置を試してみましたが、効果が得られなかったためreCAPTCHA v3の導入に至りました。&lt;br /&gt;
これにより劇的な改善が見られたため導入前後での成果をこの記事にまとめます。&lt;/p&gt;

&lt;p&gt;本記事ではreCAPTCHA v3の実装方法ではなく、導入前後でのスパム登録数の変化やスコアのしきい値をどのように設定したかといった、&lt;strong&gt;効果測定・運用面&lt;/strong&gt;での学びを共有します。&lt;br /&gt;
実装方法については&lt;a href=&quot;https://docs.cloud.google.com/recaptcha/docs/implementation-workflow?hl=ja&quot;&gt;公式ドキュメント&lt;/a&gt;をご参照ください。&lt;/p&gt;

&lt;h1 id=&quot;導入前後の新規ユーザー登録におけるスパム登録率の変化&quot;&gt;導入前後の新規ユーザー登録におけるスパム登録率の変化&lt;/h1&gt;
&lt;p&gt;reCAPTCHA v3の導入前後でスパム登録率がどのように変化したかを登録者数は伏せ、割合(%)で見ていきます。&lt;/p&gt;

&lt;h2 id=&quot;月別スパム登録率の推移&quot;&gt;月別スパム登録率の推移&lt;/h2&gt;

&lt;p&gt;導入前後3ヶ月のスパム登録率の変化を以下のグラフにしました。&lt;br /&gt;
横軸は導入月の何ヶ月前後かを表していて、縦軸はスパム登録率を表しています。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/2026-06-01-recaptcha-v3/img_1.png&quot; alt=&quot;スパム登録率の推移(%)&quot; /&gt;&lt;/p&gt;

&lt;p&gt;導入前の3ヶ月では、 &lt;strong&gt;85.1%&lt;/strong&gt;, &lt;strong&gt;90.5%&lt;/strong&gt;, &lt;strong&gt;90.1%&lt;/strong&gt; と高止まりしておりスパム登録がユーザー登録の約89%という状況でしたが、 導入後はスパム登録率は &lt;strong&gt;0%&lt;/strong&gt; となりその後もスパム登録は確認されていません。&lt;br /&gt;
導入後の副次的な効果として、スパム登録を試みるリクエスト数そのものが減少していました。
そちらの推移もグラフで見ていきます。&lt;/p&gt;

&lt;h2 id=&quot;スパム登録リクエスト数の推移&quot;&gt;スパム登録リクエスト数の推移&lt;/h2&gt;

&lt;p&gt;導入前の3ヶ月のスパム登録リクエスト数の平均を100%として各月のスパム登録リクエスト数を相対値でグラフに表しています。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/2026-06-01-recaptcha-v3/img_2.png&quot; alt=&quot;スパム登録リクエスト数の推移（導入前3ヶ月の平均を100%とした相対値）&quot; /&gt;&lt;/p&gt;

&lt;p&gt;導入前後3ヶ月の合計でスパム登録リクエスト数を相対的に比較すると、導入後は約90%の削減ができました。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/2026-06-01-recaptcha-v3/img_3.png&quot; alt=&quot;スパム登録リクエスト数の削減（導入前後3ヶ月間合計比較）&quot; /&gt;&lt;/p&gt;

&lt;h1 id=&quot;スコアのしきい値の設定方法&quot;&gt;スコアのしきい値の設定方法&lt;/h1&gt;
&lt;p&gt;reCAPTCHA v3では、ユーザーの行動を分析して人間らしさを数値化した&lt;strong&gt;スコア&lt;/strong&gt;と、基準値となる&lt;strong&gt;しきい値&lt;/strong&gt;をもとにスパム登録をブロックするかどうかを判断します。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;スコア：&lt;strong&gt;0.0~1.0&lt;/strong&gt;で表し、より人間らしい行動ほどスコアが高くなります。&lt;/li&gt;
  &lt;li&gt;しきい値：どのスコアまでを受け入れるかの基準になる数値です。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;このしきい値をいくつに設定するかによって、 &lt;strong&gt;スパム登録をブロックできるか&lt;/strong&gt; 、&lt;strong&gt;正規登録ユーザーを誤ってブロックしてしまうか&lt;/strong&gt; が変わってくるため以下の順で段階的に設定しました。&lt;/p&gt;

&lt;h2 id=&quot;1-公式ドキュメントを参考に初期値を設定&quot;&gt;1. 公式ドキュメントを参考に初期値を設定&lt;/h2&gt;

&lt;p&gt;まず&lt;a href=&quot;https://docs.cloud.google.com/recaptcha/docs/account-defender?hl=ja#standard-risk-score-thresholds&quot;&gt;公式ドキュメントのスコアしきい値ガイド&lt;/a&gt;を参考に初期のしきい値を&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0.5&lt;/code&gt;に設定しました。&lt;/p&gt;

&lt;p&gt;設定したしきい値を下回るスコアのリクエストでも、この段階ではブロックせずにSlack通知のみを行いました。&lt;/p&gt;

&lt;h2 id=&quot;2-slack通知で初期しきい値を検証&quot;&gt;2. Slack通知で初期しきい値を検証&lt;/h2&gt;

&lt;p&gt;初期値として高いしきい値を設定すると正規ユーザーを誤ってブロックするリスクがあったため、最初の1ヶ月間は通知ベースで以下を確認しました。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Slack通知される(しきい値を下回る)ユーザー登録が実際にスパム登録かどうか&lt;/li&gt;
  &lt;li&gt;DB内に登録されたユーザーにSlack通知されていないスパム登録があるかどうか&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;3-しきい値の設定&quot;&gt;3. しきい値の設定&lt;/h2&gt;

&lt;p&gt;観測データをもとに以下の2点を確認し、しきい値を設定しました。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;スパムと確認されたリクエストが全てしきい値を下回っているか&lt;/li&gt;
  &lt;li&gt;正規ユーザー登録のスコアがしきい値を上回っているか&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;両方を満たすしきい値を設定しました。&lt;br /&gt;
その結果、&lt;strong&gt;正規ユーザーのブロック&lt;/strong&gt;や&lt;strong&gt;スパムユーザーの登録&lt;/strong&gt;はない状態で現在も維持できています。&lt;/p&gt;

&lt;h1 id=&quot;まとめ&quot;&gt;まとめ&lt;/h1&gt;

&lt;p&gt;reCAPTCHA v3の導入によって、約89%だったスパム登録率を0%にまで抑えることができました。&lt;br /&gt;
さらに、スパム登録リクエスト数そのものも導入前と比較すると約90%削減という副次的な効果もありました。&lt;/p&gt;

&lt;p&gt;今回のreCAPTCHA v3導入で効果測定としきい値の設定の観点で以下が重要だと感じました。&lt;/p&gt;

&lt;h3 id=&quot;効果測定の観点&quot;&gt;効果測定の観点&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;リクエスト数の抑止効果の測定もできるように、スパム登録だけでなくリクエスト数も測定する。&lt;/li&gt;
  &lt;li&gt;ボットの手法の変化に対応するために、導入後も継続的にモニタリングする。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;しきい値の設定の観点&quot;&gt;しきい値の設定の観点&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;安全にしきい値の設定をするために、最初はスパム登録をブロックせずに通知のみで検証をする。&lt;/li&gt;
  &lt;li&gt;正規ユーザーのブロックやスパムユーザーの登録防止のために、しきい値は公式ドキュメントと検証したスコアを参考に設定する。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;スパム登録にお悩みの方々の参考になれば幸いです。&lt;/p&gt;

</description>
        <pubDate>Mon, 01 Jun 2026 00:00:00 +0900</pubDate>
      </item>
    
      



      <item>
        <title>互換性のないオブジェクトを低コストにコンバートする | QUARTETCOM TECH BLOG</title>
        
        
        <category>カノニカルデータモデル</category>
        
        <link>https://tech.quartetcom.co.jp/2026/05/07/convert-incompatible-objects-with-low-cost/</link>
        <guid isPermaLink="true">https://tech.quartetcom.co.jp/2026/05/07/convert-incompatible-objects-with-low-cost/</guid>
        <description>
&lt;h2 id=&quot;はじめに&quot;&gt;はじめに&lt;/h2&gt;

&lt;p&gt;弊社では多様な広告媒体を扱います。&lt;br /&gt;
その中で &lt;strong&gt;「Aという媒体のオブジェクトを、Bの媒体へ移動したい」ようなケース&lt;/strong&gt; があります。&lt;/p&gt;

&lt;p&gt;素直に考えると、「AオブジェクトをBオブジェクトへ変換する Mapper」を実装すればよさそうです。&lt;br /&gt;
しかし、この方法は媒体が増えるほど大きな負債になります。&lt;/p&gt;

&lt;h3 id=&quot;問題点-対応する媒体が増えた時にコードが膨れ上がる&quot;&gt;問題点: 対応する媒体が増えた時にコードが膨れ上がる&lt;/h3&gt;

&lt;p&gt;対応する媒体が &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;A&lt;/code&gt; と &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;B&lt;/code&gt; だけであれば、必要な Mapper は以下の2つです。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;A → B へコンバートする Mapper&lt;/li&gt;
  &lt;li&gt;B → A へコンバートする Mapper&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/2026-05-07-convert-incompatible-objects-with-low-cost/1.png&quot; width=&quot;400&quot; alt=&quot;コンバート図&quot; /&gt;&lt;/p&gt;

&lt;p&gt;現状の実装であれば Mapper は2つで済みます。&lt;br /&gt;
しかし、新たに C、D に対応する必要が出てきた場合、状況が変わります。&lt;/p&gt;

&lt;p&gt;この場合だと、以下のMapperを実装することになります。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;A → B へコンバートする Mapper&lt;/li&gt;
  &lt;li&gt;A → C へコンバートする Mapper&lt;/li&gt;
  &lt;li&gt;A → D へコンバートする Mapper&lt;/li&gt;
  &lt;li&gt;B → A へコンバートする Mapper&lt;/li&gt;
  &lt;li&gt;B → C へコンバートする Mapper&lt;/li&gt;
  &lt;li&gt;B → D へコンバートする Mapper&lt;/li&gt;
  &lt;li&gt;C → A へコンバートする Mapper&lt;/li&gt;
  &lt;li&gt;C → B へコンバートする Mapper&lt;/li&gt;
  &lt;li&gt;C → D へコンバートする Mapper&lt;/li&gt;
  &lt;li&gt;D → A へコンバートする Mapper&lt;/li&gt;
  &lt;li&gt;D → B へコンバートする Mapper&lt;/li&gt;
  &lt;li&gt;D → C へコンバートする Mapper&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mapper 数が膨れ上がっていることがわかります。&lt;/p&gt;

&lt;p&gt;さらに、「Aオブジェクト」の仕様変更が入った場合も影響範囲も広いです。&lt;/p&gt;

&lt;p&gt;まとめると、以下の点で負債になります。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Mapper の数が増え続ける&lt;/li&gt;
  &lt;li&gt;媒体ごとの差を補完する処理が各 Mapper に散らばる&lt;/li&gt;
  &lt;li&gt;媒体都合の仕様変更時の影響範囲が広くなる&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;素直に実装するとコードが膨れ上がりそうです。&lt;/p&gt;

&lt;h3 id=&quot;解決策-mapper-を抽象オブジェクトに依存させ汎用的に扱えるようにする&quot;&gt;解決策: Mapper を抽象オブジェクトに依存させ、汎用的に扱えるようにする&lt;/h3&gt;

&lt;p&gt;現状の問題は、媒体同士が直接依存しているため、変更の影響が広がる構造になっています。&lt;/p&gt;

&lt;p&gt;これを解決するために、独自に定義した抽象オブジェクトを実装します。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;抽象オブジェクト本体&lt;/li&gt;
  &lt;li&gt;Aを抽象へコンバートするMapper&lt;/li&gt;
  &lt;li&gt;Bを抽象へコンバートするMapper&lt;/li&gt;
  &lt;li&gt;Cを抽象へコンバートするMapper&lt;/li&gt;
  &lt;li&gt;Dを抽象へコンバートするMapper&lt;/li&gt;
  &lt;li&gt;抽象をAへコンバートするMapper&lt;/li&gt;
  &lt;li&gt;抽象をBへコンバートするMapper&lt;/li&gt;
  &lt;li&gt;抽象をCへコンバートするMapper&lt;/li&gt;
  &lt;li&gt;抽象をDへコンバートするMapper&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;特徴は「全媒体に共通の中間表現(抽象)を持つ」、「具象同士を直接依存させず、抽象を経由してコンバートする」ことです。&lt;/p&gt;

&lt;p&gt;図にすると以下です。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/2026-05-07-convert-incompatible-objects-with-low-cost/2.png&quot; width=&quot;400&quot; alt=&quot;コンバート図&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;数値で比較した場合&quot;&gt;数値で比較した場合&lt;/h2&gt;

&lt;p&gt;以下は具体的な数値に落とし込んだ表です。&lt;/p&gt;

&lt;h3 id=&quot;ビフォー&quot;&gt;ビフォー&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;媒体数&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;必要なMapper数&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;仕様変更時に影響するMapper数&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;2&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;2&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;2&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;3&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;6&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;4&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;4&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;12&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;6&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;5&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;20&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;8&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h3 id=&quot;アフター&quot;&gt;アフター&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;媒体数&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;抽象でコンバートした際のMapper数&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;仕様変更時に影響するMapper数&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;2&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;4&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;2&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;3&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;6&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;2&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;4&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;8&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;2&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;5&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;10&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;2&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&quot;この設計のメリット--デメリット&quot;&gt;この設計のメリット &amp;amp; デメリット&lt;/h2&gt;

&lt;h3 id=&quot;メリット&quot;&gt;メリット&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;媒体が 3 つを超えるあたりから Mapper 数の優位が逆転し、媒体が増えるほど差が広がる&lt;/li&gt;
  &lt;li&gt;媒体の仕様変更時の影響を受ける数が少ない&lt;/li&gt;
  &lt;li&gt;抽象オブジェクトの仕様を読めば、コンバートできるプロパティを把握できる&lt;/li&gt;
  &lt;li&gt;特定の媒体オブジェクトを抽象オブジェクトに変換さえすれば、他の媒体へコンバートしやすい&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;デメリット&quot;&gt;デメリット&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;媒体数が少ない場合「2〜3」の場合は過剰な設計になりやすい&lt;/li&gt;
  &lt;li&gt;抽象オブジェクトを管理するコストが掛かる&lt;/li&gt;
  &lt;li&gt;変換先媒体に存在しない項目は扱わない仕様にする必要がある&lt;/li&gt;
  &lt;li&gt;抽象モデルの設計を誤ると、逆に複雑性が中央に集中する&lt;/li&gt;
  &lt;li&gt;抽象モデルに存在しない情報は失われる（情報ロスが発生する）&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;実装例&quot;&gt;実装例&lt;/h2&gt;

&lt;p&gt;以下はコンバート例です。
ニュアンスが伝わるように最小限の構成にしています。&lt;/p&gt;

&lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;declare&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;strict_types&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;cd&quot;&gt;/**
 * 共通フォーマット
 */&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AbstractAd&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;__construct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$title&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;cd&quot;&gt;/**
 * A 媒体
 */&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AVendorAd&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;__construct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$headline&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;cd&quot;&gt;/**
 * B 媒体
 */&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BVendorAd&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;__construct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$adTitle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;cd&quot;&gt;/**
 * A → 抽象 Mapper
 */&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aVendorAdToAbstractAd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;AVendorAd&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$ad&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AbstractAd&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AbstractAd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$ad&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;headline&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;cd&quot;&gt;/**
 * 抽象 → B Mapper
 */&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;abstractAdToBVendorAd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;AbstractAd&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$ad&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;BVendorAd&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BVendorAd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;adTitle&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$ad&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;cd&quot;&gt;/**
 * A → B へコンバートする例
 */&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$aVendorAd&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AVendorAd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;headline&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;春のセール開催中&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// コンバート対象の A のオブジェクト&lt;/span&gt;

&lt;span class=&quot;nv&quot;&gt;$abstractAd&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;aVendorAdToAbstractAd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$aVendorAd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 一度抽象化する&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$bVendorAd&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;abstractAdToBVendorAd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$abstractAd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 抽象 -&amp;gt; B へ&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 拡張例：抽象化してしまえば、今後増える媒体へコンバートが容易になる&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$cVendorAd&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;abstractAdToCVendorAd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$abstractAd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 抽象 -&amp;gt; C へ&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$dVendorAd&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;abstractAdToDVendorAd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$abstractAd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 抽象 -&amp;gt; D へ&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;まとめ&quot;&gt;まとめ&lt;/h2&gt;

&lt;p&gt;「A→B」「B→C」みたいに直接変換するのをやめて、&lt;br /&gt;
一度「共通フォーマット」に変換してから別の媒体に変換しよう、という設計でした。&lt;/p&gt;

&lt;p&gt;これにより、媒体同士が直接依存しなくなり、変更が他の媒体に波及しにくくなります。&lt;/p&gt;

&lt;p&gt;記事を書きながら知りましたが、&lt;br /&gt;
この設計は「Canonical Data Model パターン」と言うようです。&lt;/p&gt;

&lt;p&gt;正規化したモデルを使ってやりとりしようというデザインパターンのようで、今回の記事とやっていることは大体同じようです。&lt;/p&gt;

&lt;h2 id=&quot;感想&quot;&gt;感想&lt;/h2&gt;

&lt;p&gt;実際に担当しているプロダクトに、このデザインパターンを導入しました。&lt;/p&gt;

&lt;p&gt;導入当初は、「YAGNI ではないか？」と思うこともありました。&lt;br /&gt;
しかし、媒体追加や仕様変更が発生した際に、既存コードへの影響を最小限に抑えられる構造を事前に用意しておくことは、長期的に見ると重要だと感じています。&lt;/p&gt;

&lt;p&gt;特に、媒体数が増え続けるようなシステムでは、&lt;br /&gt;
「今の実装コスト」よりも「将来の変更コスト」を下げる価値の方が大きくなると、
数年間、リスティング広告というドメインに触れていると思うことが多いです。&lt;/p&gt;

&lt;p&gt;それに、&lt;strong&gt;極端な話ですが、広告媒体のモデルとは全く関係なくても、&lt;br /&gt;
抽象モデル(正規化したモデル)に変換さえすれば、他媒体のオブジェクトへコンバートできます。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;生成 AI との相性が良さそうだと思いませんか？&lt;/p&gt;

</description>
        <pubDate>Thu, 07 May 2026 00:00:00 +0900</pubDate>
      </item>
    
      



      <item>
        <title>宣言的記述とメンタルモデル ── AWS Copilot CLI のサポート終了に寄せて | QUARTETCOM TECH BLOG</title>
        
        
        <category>AWS</category>
        
        <category>Copilot</category>
        
        <category>CDK</category>
        
        <category>CloudFormation</category>
        
        <link>https://tech.quartetcom.co.jp/2026/05/01/sunset-aws-copilot-cli/</link>
        <guid isPermaLink="true">https://tech.quartetcom.co.jp/2026/05/01/sunset-aws-copilot-cli/</guid>
        <description>
&lt;h1 id=&quot;きっかけ&quot;&gt;きっかけ&lt;/h1&gt;

&lt;p&gt;2026年3月、AWS は Copilot CLI のサポート終了を発表しました。2026年6月12日をもって、新機能の追加もセキュリティアップデートも止まります。&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://aws.amazon.com/jp/blogs/containers/announcing-the-end-of-support-for-the-aws-copilot-cli/&quot;&gt;Announcing the end-of-support for the AWS Copilot CLI | Containers&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;私が Copilot に見出していた価値は、単純に「便利なツール」ということではありませんでした。それが終わるという知らせを受けて、改めてその価値の正体を言語化してみたくなりました。&lt;/p&gt;

&lt;h1 id=&quot;宣言的記述と手続き的記述&quot;&gt;宣言的記述と手続き的記述&lt;/h1&gt;

&lt;p&gt;まず前提となる概念を整理しておきます。&lt;/p&gt;

&lt;p&gt;プログラムの記述スタイルは大きく二つに分類できます。&lt;strong&gt;宣言的（Declarative）&lt;/strong&gt;と&lt;strong&gt;手続き的（Procedural）&lt;/strong&gt;です。&lt;/p&gt;

&lt;p&gt;同じ「偶数だけ取り出して2倍にする」処理を TypeScript で書き比べてみます。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;手続き的&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[];&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;numbers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;length&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;numbers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;numbers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;宣言的&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;numbers&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;n&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;n&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;n&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;n&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;手続き的な記述は「インデックスを進めて、条件を確認して、配列に追加して」という&lt;strong&gt;手順&lt;/strong&gt;を書きます。宣言的な記述は「偶数を選んで、2倍にする」という&lt;strong&gt;意図&lt;/strong&gt;を書きます。&lt;/p&gt;

&lt;p&gt;ただし、これは二値ではなくグラデーションだという点に注意が必要です。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;filter().map()&lt;/code&gt; にも順序はあります。「完全に宣言的」というのはほぼ理想概念であり、現実のコードはどこかにグラデーションとして位置しています。本質的な違いは「順序があるかどうか」ではなく、「制御フローを自分で記述しているかどうか」にあると考えています。&lt;/p&gt;

&lt;p&gt;また、宣言的記述は関数型言語の専売特許ではありません。SQL は宣言的の典型例ですが、関数型言語ではありません。TypeScript でも Python でも、書き手の選択次第で宣言的にも手続き的にも記述することができます。&lt;/p&gt;

&lt;h1 id=&quot;cloudformationcopilotcdk-を並べてみる&quot;&gt;CloudFormation・Copilot・CDK を並べてみる&lt;/h1&gt;

&lt;p&gt;この宣言的／手続き的という軸と、もう一つの軸である&lt;strong&gt;抽象度&lt;/strong&gt;を組み合わせると、AWS の IaC ツール群の立ち位置が見えてきます。&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;ツール&lt;/th&gt;
      &lt;th&gt;記述スタイル&lt;/th&gt;
      &lt;th&gt;抽象度&lt;/th&gt;
      &lt;th&gt;メンタルモデルとの距離&lt;/th&gt;
      &lt;th&gt;ベストプラクティスの規定&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;AWS CloudFormation&lt;/td&gt;
      &lt;td&gt;宣言的&lt;/td&gt;
      &lt;td&gt;低&lt;/td&gt;
      &lt;td&gt;遠い（AWSリソース視点）&lt;/td&gt;
      &lt;td&gt;なし（自分で判断）&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;AWS Copilot CLI&lt;/td&gt;
      &lt;td&gt;宣言的&lt;/td&gt;
      &lt;td&gt;高&lt;/td&gt;
      &lt;td&gt;近い（サービス視点）&lt;/td&gt;
      &lt;td&gt;強い（AWS が規定）&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;AWS CDK&lt;/td&gt;
      &lt;td&gt;宣言的〜手続き的&lt;/td&gt;
      &lt;td&gt;自由&lt;/td&gt;
      &lt;td&gt;設計次第&lt;/td&gt;
      &lt;td&gt;弱い（開発者の規律次第）&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&quot;aws-cloudformation&quot;&gt;AWS CloudFormation&lt;/h2&gt;

&lt;p&gt;CloudFormation は宣言的です。「あるべき状態」を YAML または JSON で記述し、AWS がその状態に収束させます。冪等性があり、差分管理もできます。&lt;/p&gt;

&lt;p&gt;一方で抽象度は低いです。「ロードバランサー付きの Web サービスを動かしたい」という意図を表現するのに、VPC・サブネット・セキュリティグループ・ECS クラスター・タスク定義・ALB・ターゲットグループ・AutoScaling……と、数百行の記述が必要になります。&lt;/p&gt;

&lt;p&gt;つまり CloudFormation では、「自分が何をしたいか」を記述するのではなく「AWS が何を必要とするか」を記述しているのです。宣言的ではありますが、人間のメンタルモデルからは遠いと言えます。&lt;/p&gt;

&lt;h2 id=&quot;aws-copilot-cli&quot;&gt;AWS Copilot CLI&lt;/h2&gt;

&lt;p&gt;Copilot のマニフェストはこうです。&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;my-web-service&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Load Balanced Web Service&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Dockerfile&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;80&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;cpu&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;256&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;memory&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;512&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;type: Load Balanced Web Service&lt;/code&gt; の一行が、VPC・ALB・ECS クラスター・Fargate・AutoScaling をまとめて意味します。これは人間が「ロードバランサー付きの Web サービス」と考えるときの&lt;strong&gt;メンタルモデルそのもの&lt;/strong&gt;です。&lt;/p&gt;

&lt;p&gt;最終的には CloudFormation に変換されて実行されます。しかしその変換を意識させません。「変換される先が何か」よりも、「書いている人間が何を考えながら書けるか」の方が重要だと考えています。&lt;/p&gt;

&lt;h2 id=&quot;aws-cdk&quot;&gt;AWS CDK&lt;/h2&gt;

&lt;p&gt;CDK は抽象度の自由度が高いです。L3 Constructs を使えば Copilot に近い高抽象度の記述も可能です。最大の利点は、インフラをプログラムとして扱うことで、複雑な要件を論理的に整理し、再利用可能なコンポーネントとして管理できる点にあります。&lt;/p&gt;

&lt;p&gt;一方で、プログラミング言語の性質上、リソース間の「依存関係」を明示的に記述するスタイルが基本となります。「VPC を定義し、それを Cluster に参照させ、Service を紐付ける」といった、リソース同士の配線を人間がコントロールする必要があります。これは「記述の簡潔さ」よりも、&lt;strong&gt;「システム全体の構造を詳細に把握・制御すること」&lt;/strong&gt;に重きを置いた設計思想と言えます。&lt;/p&gt;

&lt;p&gt;そして CDK もまた最終的には CloudFormation に変換されます。&lt;/p&gt;

&lt;h1 id=&quot;三者に共通する事実とそれでも違うこと&quot;&gt;三者に共通する事実と、それでも違うこと&lt;/h1&gt;

&lt;p&gt;CloudFormation・Copilot・CDK、いずれも最終的には CloudFormation のテンプレートとして実行されます。その意味では「同じものを作っている」とも言えます。&lt;/p&gt;

&lt;p&gt;しかし、&lt;strong&gt;書いている人間が何を考えながら書くか&lt;/strong&gt;はまったく異なります。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;CloudFormation:&lt;/strong&gt; AWS の「リソースの定義」そのものを記述する（確実な基盤）&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;AWS CDK:&lt;/strong&gt; インフラの「構造と関係性」をプログラマブルに構築する（柔軟な設計）&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;AWS Copilot:&lt;/strong&gt; 特定のパターンに基づき「サービスの意図」を表明する（高速なデリバリー）&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;変換される先が同じでも、書き手の思考コストは同じではありません。&lt;/p&gt;

&lt;h1 id=&quot;copilot-が肩代わりしていたもの&quot;&gt;Copilot が肩代わりしていたもの&lt;/h1&gt;

&lt;p&gt;ここが核心だと考えています。&lt;/p&gt;

&lt;p&gt;Copilot が「考えなくて済む」ようにしてくれていたのは、記述量や抽象度の話だけではありませんでした。&lt;strong&gt;「何がベストプラクティスか」という判断そのものを肩代わりしてくれていたのです。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;type: Load Balanced Web Service&lt;/code&gt; と書いた瞬間に、VPC の構成・セキュリティグループの切り方・ヘルスチェックの設定・AutoScaling の組み方という AWS の知見が暗黙的に注入されていました。ドキュメントを読んで自分で判断する必要がなかったのです。&lt;/p&gt;

&lt;p&gt;CDK に移ると、この「判断の肩代わり」が消えます。L3 Constructs はある程度それを引き継ごうとしていますが、Copilot ほど強い意見は持っていません。そして強い意見を持たないということは、悪いプラクティスを選ぶ自由も同時に与えられるということです。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;宣言的記述の本質的な価値は、単に「コードを短くすること」ではない。本来はプラットフォームが担うべき「最適解への到達手順」をツールに委ねることで、人間が「構成の細部」ではなく「本来の目的」に集中できる環境を作ることにある。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Copilot はまさにその判断を AWS 側に留保することで、開発者を本来集中すべき問題に向かわせようとしていました。&lt;/p&gt;

&lt;p&gt;この性質は、&lt;a href=&quot;https://tech.quartetcom.co.jp/2025/06/27/rebuild-team/&quot;&gt;職能別チームからプロダクトチームへと移行した&lt;/a&gt;私たちの組織にとって、親和性の高いものでした。各メンバーがより幅広いスキルセットを求められる体制において、AWS に精通していないメンバーであっても Copilot が実装指針をベストプラクティスとともに示してくれる点は、実際の運用上の助けになっていたと認識しています。&lt;/p&gt;

&lt;p&gt;ただし、この高い抽象度には「表現力の制約」という側面もありました。Copilot がカバーしていないリソースが必要になった際、結局は CloudFormation での直接記述が求められるなど、利便性と柔軟性の境界線が課題となる場面もありました。&lt;/p&gt;

&lt;h1 id=&quot;おわりに&quot;&gt;おわりに&lt;/h1&gt;

&lt;p&gt;Copilot のサポート終了を受けて、後継として ECS Express Mode や CDK L3 Constructs への移行が案内されています。&lt;/p&gt;

&lt;p&gt;移行にあたって整理しておきたいのは、Copilot が担っていた 「ベストプラクティスの規定者」としての役割のうち、何が引き継がれていて、何は私たち自身の解像度を上げて対応しなければならないのか、という点です。&lt;/p&gt;

&lt;p&gt;その視点を持ちながら、これからのインフラ記述のあり方を考えていきたいと思います。&lt;/p&gt;

</description>
        <pubDate>Fri, 01 May 2026 00:00:00 +0900</pubDate>
      </item>
    
      



      <item>
        <title>Symfonyの.envファイルをうっかり git プッシュから守る方法 | QUARTETCOM TECH BLOG</title>
        
        
        <category>Symfony</category>
        
        <link>https://tech.quartetcom.co.jp/2026/03/16/protect-symfony-credential/</link>
        <guid isPermaLink="true">https://tech.quartetcom.co.jp/2026/03/16/protect-symfony-credential/</guid>
        <description>
&lt;p&gt;Symfony でアプリ開発をしていると &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.env.local&lt;/code&gt; にクレデンシャルをハードコードすることがありますが、センシティブな情報をプレーンテキストで扱うことに若干の怖さを感じています。かんたんにリネームもファイルコピーもできますし GitHub リポジトリに間違えてプッシュしちゃったら怖いですよね。&lt;/p&gt;

&lt;p&gt;そんなうっかり操作のヒヤリハットを何らかの仕組みで防止できないか調べてみました。&lt;/p&gt;

&lt;h2 id=&quot;はじめに&quot;&gt;はじめに&lt;/h2&gt;

&lt;p&gt;この記事の「クレデンシャル」とは Symfony アプリ内でクローズドに利用する接続情報やトークンのことを示しています。パラメータを経由してサービスクラスに注入しサードパーティベンダーの認証などに使用するイメージです。&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# .env.local&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;APY_KEY=1234abcd&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;SLACK_TOKEN=xoxb-000000000000-EXAMPLE-TOKEN&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# config/services.yaml&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;parameters&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;apiKey&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;%env(APY_KEY)%&apos;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;slackToken&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;%env(SLACK_TOKEN)%&apos;&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;App\Command\SomeService&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;arguments&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;$apiKey&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;%apiKey%&apos;&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;$slackToken&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;%slackToken%&apos;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;クレデンシャルは API キー / OAuth 認証トークン / デベロッパートークンなどさまざまな種類があり、ハードコード先も &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.env.*&lt;/code&gt; だけでなく &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.key&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.yaml&lt;/code&gt; などバリエーションがあります。広範囲に散らばっているクレデンシャルを守るには、いくつかの方法を組み合わせて多層防御するのがベターと言えそうです。&lt;/p&gt;

&lt;h2 id=&quot;envlocal-を-git-で追跡しないgitignore&quot;&gt;.env.local を git で追跡しない：.gitignore&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;https://git-scm.com/docs/gitignore&quot;&gt;gitignore&lt;/a&gt; は基本的でシンプルな方法です。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.gitignore&lt;/code&gt; に書いたパターンにマッチすると git で追跡されず、ステージもコミットもされません。&lt;a href=&quot;https://symfony.com/doc/current/setup/symfony_cli.html&quot;&gt;Symfony CLI&lt;/a&gt; でプロジェクトを作成するとプロジェクトに最適な &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.gitignore&lt;/code&gt; が作成されます。&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# .gitignore&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;###&amp;gt; symfony/framework-bundle ###&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;/.env.local&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;/.env.local.php&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;/.env.*.local&lt;/span&gt;
&lt;span class=&quot;nn&quot;&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;けれどもプロジェクトは常に Symfony CLI で構築されるとは限りません。別プロジェクトからソースコードを流用したり、ゼロベースからディレクトリを構成することもあるでしょう。&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/github/gitignore&quot;&gt;https://github.com/github/gitignore&lt;/a&gt; のようなテンプレートを使って &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.gitignore&lt;/code&gt; を作成した場合 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.env.*&lt;/code&gt; はその対象に含まれていないかもしれません。記事執筆時点の Symfony 最新バージョンは &lt;a href=&quot;https://symfony.com/blog/symfony-8-0-6-released&quot;&gt;v8.0.6&lt;/a&gt; ですがテンプレートは v4 以降のリリースに追随していないように見えます。&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/github/gitignore/blob/b4105e73e493bb7a20b5d7ea35efd5780ca44938/Symfony.gitignore&quot;&gt;gitignore
/Symfony.gitignore&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;グローバル設定の &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.config/git/ignore&lt;/code&gt; に &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.env.*&lt;/code&gt; と書いてあるから安全！という思い込みもうっかりミスを誘発します。新しいデバイスをセットアップした時、このディレクトリの同期作業をうっかり忘れてしまうかもしれないからです。&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.gitignore&lt;/code&gt; による追跡防止は &lt;strong&gt;開発の基本であって過信しないこと&lt;/strong&gt; というのが私の考えです。&lt;/p&gt;

&lt;h2 id=&quot;envlocal-をコミットしない1password-local-env-files&quot;&gt;.env.local をコミットしない：1Password Local .env files&lt;/h2&gt;

&lt;p&gt;1Password は有償のパスワードマネージャーです。&lt;br /&gt;
2025/10 に「Local .env files」というベータ版機能がリリースされました。この機能は &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.env&lt;/code&gt; ファイルを安全な場所に退避し、プロセスからの読み取りに開発者の許可を必要とします。&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://1password.com/blog/1password-environments-env-files-public-beta&quot;&gt;Introducing new .env file support in 1Password environments | 1Password&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;1Password アプリで「環境」を追加しプロジェクトを判別する任意の名前を付与します。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/2026-03-protect-symfony-credential/image-7.png&quot; width=&quot;800&quot; /&gt;&lt;/p&gt;

&lt;p&gt;追加した環境に &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.env.local&lt;/code&gt; をインポートします。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/2026-03-protect-symfony-credential/image-1.png&quot; width=&quot;800&quot; /&gt;&lt;/p&gt;

&lt;p&gt;プロジェクトの既存の &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.env.local&lt;/code&gt; を削除し、替わりに 1Password のファイルを保存（マウント）します。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/2026-03-protect-symfony-credential/image-3.png&quot; width=&quot;800&quot; /&gt;
&lt;img src=&quot;/assets/img/2026-03-protect-symfony-credential/image-6.png&quot; width=&quot;800&quot; /&gt;&lt;/p&gt;

&lt;p&gt;マウントした &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.env.local&lt;/code&gt; はファイルの実体ではなく UNIX の &lt;a href=&quot;https://ja.wikipedia.org/wiki/%E5%90%8D%E5%89%8D%E4%BB%98%E3%81%8D%E3%83%91%E3%82%A4%E3%83%97&quot;&gt;名前付きパイプ&lt;/a&gt; です。その内容は 1Password によって保護されているため、プロセスが読み取ろうとすると認証ダイアログが表示されます。許可すると 1Password アプリのロックがかかるまでアクセスが可能になります。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/2026-03-protect-symfony-credential/image-4.png&quot; width=&quot;800&quot; /&gt;
&lt;img src=&quot;/assets/img/2026-03-protect-symfony-credential/image-5.png&quot; width=&quot;800&quot; /&gt;&lt;/p&gt;

&lt;p&gt;この仕組みは「うっかりプッシュ」の観点で良い副作用をもたらします。&lt;strong&gt;ファイルの実体ではないため git でコミットできない&lt;/strong&gt; のです。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.gitignore&lt;/code&gt; にうっかり書き忘れても、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.env.locall&lt;/code&gt; とファイル名をタイポしても、コミット時点でブロックされます。&lt;/p&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;git add .env.local
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; error: .env.local: can only add regular files, symbolic links or git-directories
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; fatal: updating files failed
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;そして &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.env.local&lt;/code&gt; を一元管理できるメリットもあります。自宅とオフィスで PC を使い分けているような場合、どのデバイスでも 1Password からマウントしておけば「自宅の PC で変更した内容をオフィスの PC に反映し忘れた」なんてことがなくなります。&lt;/p&gt;

&lt;p&gt;ただし落とし穴もあります。一時的なバックアップのために &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cp .env.local .env.local.bak&lt;/code&gt; を実行すると &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.env.local.bak&lt;/code&gt; はファイルの実体として作成され、うっかりコミットが可能になります。&lt;/p&gt;

&lt;h2 id=&quot;envlocal-を作らないsymfonys-secrets-management-system&quot;&gt;.env.local を作らない：Symfony’s secrets management system&lt;/h2&gt;

&lt;p&gt;そもそもクレデンシャルを &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.env.local&lt;/code&gt; にハードコードしない手段についても検討の余地があります。Symfony 公式で紹介されている「Symfony’s secrets management system」はそのひとつです。&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://symfony.com/doc/current/configuration/secrets.html&quot;&gt;How to Keep Sensitive Information Secret | Symfony docs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;この方法は PHP 7.2+ でバンドルされているモジュール &lt;a href=&quot;https://www.php.net/manual/ja/book.sodium.php&quot;&gt;sodium&lt;/a&gt; を使ってクレデンシャルを暗号化し &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;config/secrets/*&lt;/code&gt; に格納します。&lt;/p&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# モジュールが含まれているか調べる&lt;/span&gt;
php &lt;span class=&quot;nt&quot;&gt;-m&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;grep &lt;/span&gt;sodium
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; sodium
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;クレデンシャルの登録や削除はコマンドで行います。&lt;/p&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# 暗号化・復号化のためのキーペアを作成（初回のみ）&lt;/span&gt;
php bin/console secrets:generate-keys

&lt;span class=&quot;c&quot;&gt;# クレデンシャルを登録&lt;/span&gt;
php bin/console secrets:set API_KEY
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;プロダクション環境用にはプレフィクス &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;APP_RUNTIME_ENV=prod&lt;/code&gt; を付与してコマンドを実行します。&lt;/p&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# 暗号化・復号化のためのキーペアを作成（初回のみ）&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;APP_RUNTIME_ENV&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;prod php bin/console secrets:generate-keys

&lt;span class=&quot;c&quot;&gt;# クレデンシャルを登録&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;APP_RUNTIME_ENV&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;prod php bin/console secrets:set API_KEY
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;一部のクレデンシャルをローカル開発用にカスタマイズする時は &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--local&lt;/code&gt; オプションを使用します。&lt;/p&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;bin/console secrets:set API_KEY &lt;span class=&quot;nt&quot;&gt;--local&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dev&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;prod&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;local&lt;/code&gt; それぞれの環境用に生成されたファイル群は以下のように格納されます。&lt;/p&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;config/secrets
├── dev
│   ├── dev.API_KEY.b58958.php &lt;span class=&quot;c&quot;&gt;# 個別のクレデンシャル&lt;/span&gt;
│   ├── dev.decrypt.private.php &lt;span class=&quot;c&quot;&gt;# 復号化の秘密鍵&lt;/span&gt;
│   ├── dev.encrypt.public.php &lt;span class=&quot;c&quot;&gt;# 暗号化の公開鍵&lt;/span&gt;
│   ├── dev.list.php &lt;span class=&quot;c&quot;&gt;# クレデンシャルのリスト&lt;/span&gt;
└── prod
    ├── prod.API_KEY.b58958.php &lt;span class=&quot;c&quot;&gt;# 個別のクレデンシャル&lt;/span&gt;
    ├── prod.decrypt.private.php &lt;span class=&quot;c&quot;&gt;# 復号化の秘密鍵&lt;/span&gt;
    ├── prod.encrypt.public.php &lt;span class=&quot;c&quot;&gt;# 暗号化の公開鍵&lt;/span&gt;
    └── prod.list.php &lt;span class=&quot;c&quot;&gt;# クレデンシャルのリスト&lt;/span&gt;

.env.dev.local &lt;span class=&quot;c&quot;&gt;# ローカル開発用にカスタマイズした値&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;これらのファイルのうち、プロダクション環境の秘密鍵 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;prod.decrypt.private.php&lt;/code&gt; だけはコミットしてはいけません。秘密鍵を直接サーバーに置くか、サーバーの環境変数 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SYMFONY_DECRYPTION_SECRET&lt;/code&gt; に登録します。&lt;/p&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# (A) サーバーに直接配置&lt;/span&gt;
config/secrets/prod/prod.decrypt.private.php

&lt;span class=&quot;c&quot;&gt;# (B) 環境変数に登録&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;export &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;SYMFONY_DECRYPTION_SECRET&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;={&lt;/span&gt;Base64 エンコードした鍵&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;登録したクレデンシャルはコマンドでリストアップできます。&lt;/p&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# --reveal オプションで値の表示ができる&lt;/span&gt;
php bin/console secrets:list &lt;span class=&quot;nt&quot;&gt;--reveal&lt;/span&gt;

&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;------------&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;----------&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-------------&lt;/span&gt; 
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;  Secret       Value      Local Value  
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;------------&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;----------&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-------------&lt;/span&gt; 
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;  API_KEY      &lt;span class=&quot;s2&quot;&gt;&quot;123abc&quot;&lt;/span&gt;   &lt;span class=&quot;s2&quot;&gt;&quot;111aaa&quot;&lt;/span&gt;    
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;  DB_PASSWORD  &lt;span class=&quot;s2&quot;&gt;&quot;123def&quot;&lt;/span&gt;   &lt;span class=&quot;s2&quot;&gt;&quot;222bbb&quot;&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;------------&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;----------&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-------------&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;この仕組みは &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.env.*&lt;/code&gt; を使用せず &lt;strong&gt;「うっかりプッシュ」対象を「プッシュしても良い情報」&lt;/strong&gt; に置き換えます。ただしローカル開発用にカスタマイズした &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.env.dev.local&lt;/code&gt; は依然としてうっかりコミットの可能性が残ります。&lt;/p&gt;

&lt;h2 id=&quot;プッシュをブロックgithub-push-protection&quot;&gt;プッシュをブロック：GitHub Push protection&lt;/h2&gt;

&lt;p&gt;クレデンシャルの格納先は &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.env.*&lt;/code&gt; とは限りません。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.key&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.yaml&lt;/code&gt; にハードコードした内容を無害なファイルパスとして &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.env.local&lt;/code&gt; に渡すことがあります。GitHub の Push protection はそのようなケースで有効です。&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://docs.github.com/ja/code-security/how-tos/secure-your-secrets/prevent-future-leaks/push-protection-for-users&quot;&gt;ユーザーのプッシュ保護 | GitHub ドキュメント&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Organization に属さない個人アカウント配下でパブリックなリポジトリを作成したところ、デフォルトで Push protection が有効になっていました。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/2026-03-protect-symfony-credential/image-8.png&quot; width=&quot;800&quot; /&gt;&lt;/p&gt;

&lt;p&gt;試しに AWS アカウントなどダミーのクレデンシャルを書いたファイルをプッシュすると Push protection によってデータ転送がブロックされました。&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# config/keys/sample.key&lt;/span&gt;

&lt;span class=&quot;s&quot;&gt;DUMMY_GITHUB_TOKEN=ghp_dummy12341234nopqrstuvwxyz....&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;DUMMY_AWS_KEY=AKIAQA2IFJE3MPUDUMMY&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;DUMMY_AWS_SECRET=gsDummyyGtzLXigRMVcDHeuS7Neg6vd....&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# 主要な部分のみ抜粋&lt;/span&gt;
git push origin main

Enumerating objects: 8, &lt;span class=&quot;k&quot;&gt;done&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;.&lt;/span&gt;
Counting objects: 100% &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;8/8&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;, &lt;span class=&quot;k&quot;&gt;done&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;.&lt;/span&gt;
remote: Resolving deltas: 100% &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;2/2&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;, completed with 2 &lt;span class=&quot;nb&quot;&gt;local &lt;/span&gt;objects.
remote: error: GH013: Repository rule violations found &lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;refs/heads/main.
remote: 
remote: - GITHUB PUSH PROTECTION
remote:   —————————————————————————————————————————
remote:     Resolve the following violations before pushing again
remote:     - Push cannot contain secrets
remote:     
remote:       —— Amazon AWS Access Key ID ——————————————————————————
remote:        locations:
remote:          - commit: 0a000111222...
remote:            path: config/keys/sample.key:4
remote:     
remote:       —— Amazon AWS Secret Access Key ——————————————————————
remote:        locations:
remote:          - commit: 0a000111222...
remote:            path: config/keys/sample.key:5
remote: 
 &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;remote rejected] main -&amp;gt; main &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;push declined due to repository rule violations&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# ブロックしてくれた ☺️&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;ダミーの RSA 秘密鍵も試したところ、こちらは Push protection が反応せずプッシュが完了しました。&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# config/keys/sample.key（実在しないクレデンシャル）&lt;/span&gt;

&lt;span class=&quot;s&quot;&gt;-----BEGIN RSA PRIVATE KEY-----&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;MIIEowIBAAKCAQEArandomrandomrandomrandomrandomrandomrandomrandom&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;....&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;-----END RSA PRIVATE KEY-----&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;git push origin main
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; Total 0 &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;delta 0&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;, reused 0 &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;delta 0&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;, pack-reused 0 &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;from 0&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; + 1234c7c...4567ccc main -&amp;gt; main

&lt;span class=&quot;c&quot;&gt;# プッシュ完了...🙄&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;GitHub ドキュメントの &lt;a href=&quot;https://docs.github.com/ja/code-security/reference/secret-security/supported-secret-scanning-patterns#supported-secrets&quot;&gt;Supported secrets&lt;/a&gt; を読むと Push protection は単純なパターンマッチングのようなチェックではないことが分かります。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;トークン提供元のプロバイダ（AWS / Google など）によるチェックと、非プロバイダ（Generic）に分かれる&lt;/li&gt;
  &lt;li&gt;プロバイダにクレデンシャルを送信してトークンの有効性をチェックすることがある&lt;/li&gt;
  &lt;li&gt;トークンやプロバイダによってチェック内容の網羅度合いが異なる&lt;/li&gt;
  &lt;li&gt;個人アカウントのパブリックリポジトリに適用されるチェックと GitHub Team / Enterprise の GitHub Secret Protection で適用されるチェックに違いがある&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;偽物のクレデンシャルとほんの数回のプッシュ確認では、ブロック可能なパターンを帰納的に洗い出すことはできませんでした。&lt;/p&gt;

&lt;p&gt;しかしながら、あらゆるクレデンシャルを対象に GitHub がスキャンしてくれる機能はとても強力です。ブロックが成功した AWS アクセスキーについてはファイルから削除するだけではダメで、コミットログも完全に消し去らないとプッシュができませんでした。&lt;/p&gt;

&lt;p&gt;GitHub Push protection は &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.env.local&lt;/code&gt; の &lt;strong&gt;外に漏れ出てしまった情報の監視役&lt;/strong&gt; として有効に機能してくれそうです。&lt;/p&gt;

&lt;h2 id=&quot;まとめ&quot;&gt;まとめ&lt;/h2&gt;

&lt;p&gt;Symfony や GitHub など、それぞれがうっかりプッシュの防止策を提供してくれています。どれもすべてのクレデンシャルやユースケースを網羅できる仕組みではありませんが、組み合わせれば高い効果が期待できそうです。&lt;/p&gt;

&lt;p&gt;これさえやっておけば大丈夫！と慢心せず、ローカルでもクラウドでも電子的に保存したデータは常に漏洩リスクを孕んでいることを念頭に開発していきたいと思っています。&lt;/p&gt;

</description>
        <pubDate>Mon, 16 Mar 2026 00:00:00 +0900</pubDate>
      </item>
    
      



      <item>
        <title>Claude Code 入門: 導入から基本操作・コスト管理まで | QUARTETCOM TECH BLOG</title>
        
        
        <category>ClaudeCode</category>
        
        <link>https://tech.quartetcom.co.jp/2026/02/24/claude-code-guide/</link>
        <guid isPermaLink="true">https://tech.quartetcom.co.jp/2026/02/24/claude-code-guide/</guid>
        <description>
&lt;p&gt;開発部では最近、開発効率を向上させる施策の一環として、Anthropic 社の CLI 型 AI エージェント「&lt;strong&gt;Claude Code&lt;/strong&gt;」を導入しました。&lt;/p&gt;

&lt;p&gt;本記事は、これから Claude Code を触ってみたいエンジニアや、AI ツールによる開発効率化に関心のある方に向けた実践ガイドです。&lt;/p&gt;

&lt;p&gt;今回は、「基本概念」から、コストを抑える運用まで共有します。&lt;/p&gt;

&lt;h2 id=&quot;1-まず押さえたい基本概念-セッションとコンテキスト&quot;&gt;1. まず押さえたい基本概念: 「セッション」と「コンテキスト」&lt;/h2&gt;

&lt;p&gt;Claude Code を使いこなす上で、以下の 2 つの概念を理解しておくとスムーズです。&lt;/p&gt;

&lt;h3 id=&quot;セッション作業のまとまり&quot;&gt;セッション（作業のまとまり）&lt;/h3&gt;

&lt;p&gt;Claude Code は、起動してから終了するまでの一連のやり取りを「セッション」として管理します。&lt;/p&gt;

&lt;p&gt;最も重要な仕様は、&lt;strong&gt;セッションは「起動ディレクトリ（フォルダ）」に紐付いて管理される&lt;/strong&gt;という点です。&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[プロジェクトAのディレクトリ] ⇔ [セッションA（Aの記憶）]
        ｜
    (ディレクトリ移動)
        ↓
[プロジェクトBのディレクトリ] ⇔ [セッションB（Bの記憶）]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;つまり、ディレクトリを移動して&lt;strong&gt;新しく &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;claude&lt;/code&gt; を起動すれば、別のセッションとして扱われます&lt;/strong&gt;（前のディレクトリのコンテキストを引きずりません）。&lt;/p&gt;

&lt;p&gt;履歴は保存されているため、完全に消失するわけではありません。&lt;/p&gt;

&lt;p&gt;後述の &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/resume&lt;/code&gt; コマンドを使えば、過去のセッション一覧から再開したい作業を選んで元の状態を復元できます。&lt;/p&gt;

&lt;h3 id=&quot;コンテキストai-の短期記憶&quot;&gt;コンテキスト（AI の短期記憶）&lt;/h3&gt;

&lt;p&gt;AI が一度に覚えられる情報量のことです。
会話が長くなると、この「コンテキスト」が溜まっていきます。&lt;/p&gt;

&lt;p&gt;コンテキストが溜まると、AI が処理すべき情報量が増えるため、結果として利用料（コスト）も上がってしまいます。&lt;/p&gt;

&lt;h2 id=&quot;2-初期設定-init-と-claudemd&quot;&gt;2. 初期設定: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/init&lt;/code&gt; と &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt;&lt;/h2&gt;

&lt;h3 id=&quot;プロジェクトの記憶を作る-init&quot;&gt;プロジェクトの「記憶」を作る &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/init&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;初めて Claude Code を使うディレクトリでは、最初に以下のコマンドを実行します。&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&amp;gt; /init
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;これを実行すると &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt; というファイルが生成されます。
ここにはビルドコマンドやコーディング規約などを記述します。
Claude Code は起動時にこのファイルを読み込み、プロジェクトのルールを理解します。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;チーム開発での運用ルール&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;共有設定（Git 管理する）&lt;/strong&gt;: プロジェクトルートの &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt; は&lt;strong&gt;Git にコミット&lt;/strong&gt;します。これにより、チーム全員が同じルール（ビルド手順や規約）を共有でき、通常は他のメンバーが &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/init&lt;/code&gt; を実行する必要がなくなります（既に設定ファイルが存在するため）。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;要件定義ファイルなどの扱い&quot;&gt;要件定義ファイルなどの扱い&lt;/h3&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt; はあくまで「ルールブック」です。機能ごとの仕様書などは &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docs/spec.md&lt;/code&gt; などのファイルに分け、指示出しの際に &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@docs/spec.md&lt;/code&gt; のように参照させます。&lt;/p&gt;

&lt;h2 id=&quot;3-うまく伝わらない時は-コンテキスト管理のコツ&quot;&gt;3. うまく伝わらない時は？ コンテキスト管理のコツ&lt;/h2&gt;

&lt;p&gt;使っていると、「最近、指示の意図を汲んでくれなくなったな……」と感じることがあります。
これはコンテキストが溜まりすぎて、AI が混乱しているサインかもしれません。&lt;/p&gt;

&lt;h3 id=&quot;基本は-clear-でリセット&quot;&gt;基本は &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/clear&lt;/code&gt; でリセット&lt;/h3&gt;

&lt;p&gt;タスクの区切りや、話が噛み合わなくなったら、以下のコマンドを打ちます。&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&amp;gt; /clear
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;これにより、これまでのコンテキストが消去され、まっさらな状態で再スタートできます。
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt; の内容は保持されます。&lt;/p&gt;

&lt;h3 id=&quot;コンテキストを残したいなら-compact&quot;&gt;コンテキストを残したいなら &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/compact&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;「今の文脈は維持したいけど、少し記憶を整理したい」という場合は &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/compact&lt;/code&gt; が有効です。&lt;/p&gt;

&lt;p&gt;これは&lt;strong&gt;会話の要約のみを残し、個別のやり取りの詳細な履歴を削除する&lt;/strong&gt;ことで、コンテキストを圧縮します。&lt;/p&gt;

&lt;h2 id=&quot;4-利用状況の確認とモデルの使い分け&quot;&gt;4. 利用状況の確認とモデルの使い分け&lt;/h2&gt;

&lt;p&gt;Claude Code の利用状況や制限は、以下のコマンドで確認できます。
契約形態（サブスクリプションまたは API 課金）によって使い分けます。&lt;/p&gt;

&lt;h3 id=&quot;利用状況の確認&quot;&gt;利用状況の確認&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;サブスクリプションプランの場合&lt;/strong&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/usage&lt;/code&gt; コマンド
    &lt;ul&gt;
      &lt;li&gt;Claude Pro などのサブスクリプションプランで利用している場合、ここから&lt;strong&gt;レートリミット（残り使用回数）&lt;/strong&gt;やプランの使用状況を確認できます。&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;API キーを利用（従量課金）の場合&lt;/strong&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/cost&lt;/code&gt; コマンド
    &lt;ul&gt;
      &lt;li&gt;組織で API キーを利用（従量課金）している場合、現在のセッション費用を確認できます。&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;モデルの使い分け&quot;&gt;モデルの使い分け&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Sonnet 系モデル&lt;/strong&gt;: 日常的なコーディングや修正。速度とコストのバランス型です。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Opus 系モデル&lt;/strong&gt;: 複雑な設計や難解なバグ調査。高精度で、深い推論を必要とする作業向けです。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;※モデル性能はアップデートにより変更される場合があります。&lt;/p&gt;

&lt;h2 id=&quot;5-急な割り込み対応セッション管理&quot;&gt;5. 急な割り込み対応（セッション管理）&lt;/h2&gt;

&lt;p&gt;開発中に緊急対応が入った場合も、セッション機能を使えば安全に中断・再開できます。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;中断&lt;/strong&gt;: 作業中のセッションに &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/rename [タスク名]&lt;/code&gt; で名前を付けて保存します。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;再開&lt;/strong&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/resume&lt;/code&gt; で過去のセッションを選択し、元の状態（コンテキスト含む）から再開できます。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;並行作業&lt;/strong&gt;: Claude Code のセッションはディレクトリに紐付いています。そのため、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git worktree&lt;/code&gt; でディレクトリを分け、セッションを分離して並行作業できます。片方で「バグ調査」をさせつつ、もう片方で「新機能実装」を進めるなど、互いのコンテキストを混ぜずに並行作業が行えます。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;まとめ&quot;&gt;まとめ&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;基本&lt;/strong&gt;: セッションはディレクトリに紐付くため、場所を変えて起動すれば記憶も変わる。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;設定&lt;/strong&gt;: チームルールはルートの &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt;（Git 管理）&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;維持&lt;/strong&gt;: 話が噛み合わなくなったら &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/clear&lt;/code&gt;。コスト節約にもなる。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;運用&lt;/strong&gt;: モデルの使い分けと &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/resume&lt;/code&gt; で柔軟に対応。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ぜひ、皆さんの開発フローにも取り入れてみてください。&lt;/p&gt;

</description>
        <pubDate>Tue, 24 Feb 2026 00:00:00 +0900</pubDate>
      </item>
    
      



      <item>
        <title>GitHubのマージ（Merge Commit/Squash Merging/Rebase Merging）の違い | QUARTETCOM TECH BLOG</title>
        
        
        <link>https://tech.quartetcom.co.jp/2025/12/05/github-pull-requests-merge/</link>
        <guid isPermaLink="true">https://tech.quartetcom.co.jp/2025/12/05/github-pull-requests-merge/</guid>
        <description>
&lt;p&gt;GitHub でプルリクエストをマージする時、マージボタンのプルダウンに「Squash and merge」「Rebase and merge」ってありますよね。これらの違いをよく知らなかったので手元で試してみました。&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://docs.github.com/ja/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/about-pull-request-merges&quot;&gt;プルリクエストのマージについて | GitHub ドキュメント&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;動作確認用に使ったプルリクエストはタイトルを &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PR-A&lt;/code&gt; に、コミットメッセージを &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;A1 {コミット時刻}&lt;/code&gt; と統一しています。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/2025-12-05/pull-request.png&quot; width=&quot;600&quot; /&gt;&lt;/p&gt;

&lt;h1 id=&quot;1-create-a-merge-commit&quot;&gt;1) Create a merge commit&lt;/h1&gt;

&lt;p&gt;プルダウンを展開せずにマージボタンを押したデフォルトの動作です。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/2025-12-05/merge-type-default.png&quot; width=&quot;600&quot; /&gt;&lt;/p&gt;

&lt;p&gt;同じブランチから切った複数ブランチをマージすると、時系列でコミットが積み上がります。&lt;/p&gt;

&lt;p&gt;メインブランチに見立てた &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;merge&lt;/code&gt; にブランチ &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;merge-A&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;merge-B&lt;/code&gt; をマージすると次の図のようになります。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/2025-12-05/merge-tree.png&quot; width=&quot;700&quot; alt=&quot;デフォルトマージの図&quot; /&gt;&lt;/p&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;git log merge-A
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; 44b8478 A2 21:25
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; fba0ad2 A1 21:23

git log merge-B
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; 6bfb8c4 B2 21:26
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; 74b8961 B1 21:24

git log merge
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; 881e805 Merge pull request &lt;span class=&quot;c&quot;&gt;#18 from ringtail003/merge-B&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; d2bef64 Merge pull request &lt;span class=&quot;c&quot;&gt;#17 from ringtail003/merge-A&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; 6bfb8c4 B2 21:26 &amp;lt;&lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; プルリクエスト&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;2&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;のコミット
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; 44b8478 A2 21:25 &amp;lt;&lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; プルリクエスト&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;1&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;のコミット
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; 74b8961 B1 21:24 &amp;lt;&lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; プルリクエスト&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;2&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;のコミット
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; fba0ad2 A1 21:23 &amp;lt;&lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; プルリクエスト&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;1&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;のコミット
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/ringtail003/blog-github-pull-requests-merge/commits/merge/&quot;&gt;GitHub でサンプルを見る&lt;/a&gt;&lt;br /&gt;
&lt;img src=&quot;/assets/img/2025-12-05/merge-commit.png&quot; width=&quot;800&quot; /&gt;&lt;/p&gt;

&lt;h1 id=&quot;2-squash-and-merge&quot;&gt;2) Squash and merge&lt;/h1&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/2025-12-05/merge-type-squash.png&quot; width=&quot;600&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Squash はプルリクエストのコミット群をひとつにまとめた新しいコミットを作成します。マージ先のブランチでは、プルリクエストとコミットが 1 対 1 になります。&lt;/p&gt;

&lt;p&gt;メインブランチに見立てた &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;squash&lt;/code&gt; にブランチ &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;squash-A&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;squash-B&lt;/code&gt; をマージすると次の図のようになります。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/2025-12-05/squash-tree.png&quot; width=&quot;700&quot; alt=&quot;squashの図&quot; /&gt;&lt;/p&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;git log squash-A
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; 36001a5 A2 21:33 &amp;lt;&lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; マージ元のコミット&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;2個目&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; 709eb44 A1 21:30 &amp;lt;&lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; マージ元のコミット&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;1個目&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;

git log squash-B
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; 2d087cb B2 21:34
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; aea1a38 B1 21:32

git log squash
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; 7d65cc7 PR-B &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;c&quot;&gt;#20)&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; 6d63afc PR-A &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;c&quot;&gt;#19) &amp;lt;== マージ先のコミット（1個にまとまる）&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/ringtail003/blog-github-pull-requests-merge/commits/squash/&quot;&gt;GitHub でサンプルを見る&lt;/a&gt;&lt;br /&gt;
&lt;img src=&quot;/assets/img/2025-12-05/squash-commit.png&quot; width=&quot;800&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;squash-and-merge-の-author&quot;&gt;Squash and merge の Author&lt;/h2&gt;

&lt;p&gt;いくつかプルリクエストを作って試したところ、プルリクエストに一番最初にコミットした人がマージコミットの Author として採用されるようです。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/2025-12-05/squash-author.png&quot; width=&quot;600&quot; /&gt;&lt;/p&gt;

&lt;p&gt;プルリクエストに 2 番目以降にコミットした人は「共作者」という扱いになります。&lt;/p&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;git log &lt;span class=&quot;nt&quot;&gt;--pretty&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;full

&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; commit 1234abc
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; Author: user1 &amp;lt;user1@example.com&amp;gt;
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; Commit: GitHub &amp;lt;noreply@github.com&amp;gt;
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;  PR-A &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;c&quot;&gt;#100)&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; 
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;  &lt;span class=&quot;k&quot;&gt;*&lt;/span&gt; A1
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;  &lt;span class=&quot;k&quot;&gt;*&lt;/span&gt; A2
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;  &lt;span class=&quot;nt&quot;&gt;---------&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;  Co-authored-by: user2 &amp;lt;user2@example.com&amp;gt; &amp;lt;&lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; 共作者
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href=&quot;https://docs.github.com/ja/pull-requests/committing-changes-to-your-project/creating-and-editing-commits/creating-a-commit-with-multiple-authors&quot;&gt;複数の作者を持つコミットを作成する | GitHub ドキュメント&lt;/a&gt;&lt;/p&gt;

&lt;h1 id=&quot;3-rebase-and-merge&quot;&gt;3) Rebase and merge&lt;/h1&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/2025-12-05/merge-type-rebase.png&quot; width=&quot;600&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Rebase はプルリクエストをマージ先ブランチでリベースしてからコミットを積みます。そのためコミットにそれぞれ新しいハッシュが付与されます。&lt;/p&gt;

&lt;p&gt;メインブランチに見立てた &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rebase&lt;/code&gt; にブランチ &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rebase-A&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rebase-B&lt;/code&gt; をマージすると次の図のようになります。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/2025-12-05/rebase-tree.png&quot; width=&quot;700&quot; alt=&quot;rebaseの図&quot; /&gt;&lt;/p&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;git log rebase-A
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; 7258210 A2 21:39
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; 7fcedd0 A1 21:37 &amp;lt;&lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; マージ元のコミット

git log rebase-B
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; 6ecf78f B2 21:40
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; 5bd574d B1 21:38

git log rebase
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; 3acd2d8 B2 21:40
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; f4e83f7 B1 21:38
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; e733a59 A2 21:39
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; def1981 A1 21:37 &amp;lt;&lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; マージ先のコミット&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;ハッシュが変わる&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/ringtail003/blog-github-pull-requests-merge/commits/rebase/&quot;&gt;GitHub でサンプルを見る&lt;/a&gt;&lt;br /&gt;
&lt;img src=&quot;/assets/img/2025-12-05/rebase-commit.png&quot; width=&quot;800&quot; /&gt;&lt;/p&gt;

&lt;h1 id=&quot;マージメッセージ&quot;&gt;マージメッセージ&lt;/h1&gt;

&lt;p&gt;マージコミットのメッセージは Settings ページで変更できます。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/2025-12-05/message-setting.png&quot; width=&quot;700&quot; /&gt;&lt;/p&gt;

&lt;p&gt;選択肢がたくさんあるので表にまとめました。動作確認用に使ったプルリクエストはタイトルを &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PR-A {メッセージの種類}&lt;/code&gt; と統一しています。&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;マージの動作&lt;/th&gt;
      &lt;th&gt;メッセージの種類&lt;/th&gt;
      &lt;th&gt;Commits ページの表示&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Merge&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Default message&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;img src=&quot;/assets/img/2025-12-05/message-merge-default.png&quot; width=&quot;500&quot; /&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Merge&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Pull request title&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;img src=&quot;/assets/img/2025-12-05/message-merge-pr-title.png&quot; width=&quot;500&quot; /&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Merge&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Pull request title and description&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;img src=&quot;/assets/img/2025-12-05/message-merge-pr-title-and-desc.png&quot; width=&quot;500&quot; /&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Squash&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Default message&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;img src=&quot;/assets/img/2025-12-05/message-squash-default.png&quot; width=&quot;500&quot; /&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Squash&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Pull request title&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;img src=&quot;/assets/img/2025-12-05/message-squash-pr-title.png&quot; width=&quot;500&quot; /&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Squash&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Pull request title and commit details&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;img src=&quot;/assets/img/2025-12-05/message-squash-pr-title-and-detail.png&quot; width=&quot;500&quot; /&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Squash&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Pull request title and description&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;img src=&quot;/assets/img/2025-12-05/message-squash-pr-title-and-desc.png&quot; width=&quot;500&quot; /&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/ringtail003/blog-github-pull-requests-merge/commits/message-merge&quot;&gt;GitHub でサンプルを見る | Merge&lt;/a&gt;&lt;br /&gt;
&lt;a href=&quot;https://github.com/ringtail003/blog-github-pull-requests-merge/commits/message-squash&quot;&gt;GitHub でサンプルを見る | Squash&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;一見すると同じに見えるマージメッセージですが、マージコミットのボディ部分が異なります。&lt;/p&gt;

&lt;h2 id=&quot;default-message&quot;&gt;Default message&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/2025-12-05/message-squash-default-2.png&quot; width=&quot;700&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;pull-request-title&quot;&gt;Pull request title&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/2025-12-05/message-squash-pr-title-2.png&quot; width=&quot;700&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;pull-request-title-and-commit-details&quot;&gt;Pull request title and commit details&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/2025-12-05/message-squash-pr-title-and-detail-2.png&quot; width=&quot;700&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;pull-request-title-and-description&quot;&gt;Pull request title and description&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/2025-12-05/message-squash-pr-title-and-desc-2.png&quot; width=&quot;700&quot; /&gt;&lt;/p&gt;

&lt;h1 id=&quot;考察&quot;&gt;考察&lt;/h1&gt;

&lt;p&gt;マージについて調べようと思ったきっかけは、どのプルリクエストをどの順番でマージしたのかサマリを見たいと思ったことでした。&lt;/p&gt;

&lt;p&gt;この用途だと、プルリクエストごとにマージコミットが作成される「Squash and merge」が良さそうだということになります。ですが 1 コミットに集約されることで、本来のコミットごとの作業履歴が見られなくなる、単一のコミットでリバートできなくなる、などデメリットもあるようです。たとえば release ブランチにマージする時だけ「Squash and merge」を適用できたら嬉しいのですが記事執筆時点（2025/12）ではブランチごとのコントロールはできないようでした。&lt;/p&gt;

&lt;p&gt;せっせとキャプチャをとってまとめましたが結論はデフォルトのまま現状維持、いずれまた検討する時がきたらこのブログを見に来ます。&lt;/p&gt;

</description>
        <pubDate>Fri, 05 Dec 2025 00:00:00 +0900</pubDate>
      </item>
    
      



      <item>
        <title>暗黙知が支える技術の本質 | QUARTETCOM TECH BLOG</title>
        
        
        <category>チーム</category>
        
        <link>https://tech.quartetcom.co.jp/2025/10/10/the-essence-of-technology/</link>
        <guid isPermaLink="true">https://tech.quartetcom.co.jp/2025/10/10/the-essence-of-technology/</guid>
        <description>
&lt;p&gt;ソフトウェア開発の現場では、属人化を避けるために「なんでもドキュメント化しよう」という文化がよく見られます。&lt;br /&gt;
ナレッジを共有し、誰がいなくなっても困らないチームを作るという考え方自体は、合理的で正しいものだと思います。&lt;/p&gt;

&lt;p&gt;しかし、「なんでもドキュメント化しよう」という文化は、捉え方次第では技術の習得という観点で思わぬ弊害を生むことがあります。&lt;/p&gt;

&lt;h2 id=&quot;属人化とドキュメントのジレンマ&quot;&gt;属人化とドキュメントのジレンマ&lt;/h2&gt;
&lt;p&gt;例えば、あるプロジェクトの環境構築の手順をまとめたドキュメントがあるとします。&lt;br /&gt;
そのドキュメントの内容は、何のために、どこまで詳細に書くべきでしょうか。&lt;br /&gt;
その線引きは、チームの方針やメンバーに求めるスキルレベルなどによって大きく異なります。&lt;/p&gt;

&lt;p&gt;手順を細かく書けば、誰でも手順通りに作業でき、効率的に環境構築を進められます。&lt;br /&gt;
もし途中で問題が発生したとしても、そういったケースのトラブルシューティングの手順も記載しておけば問題ないでしょう。&lt;/p&gt;

&lt;p&gt;しかし、そのようなドキュメントに完全に頼って環境構築を終えたメンバーは、「自走して環境構築ができた」と言えるでしょうか。&lt;br /&gt;
多くの場合、それは「再現できた」にすぎず、「理解して環境構築できた」とは異なります。&lt;/p&gt;

&lt;p&gt;ドキュメントが過度に充実してマニュアルのようになると、「なぜそうするのか」を考える機会が失われます。&lt;br /&gt;
チーム全体がそれを当然のように良しとし、盲目的にドキュメント化を推進していると、将来的にメンバーの技術習得が課題になったり、評価と自己認識のズレや、学習意欲の低下といった問題が生じるかもしれません。&lt;/p&gt;

&lt;h2 id=&quot;技術の習得には思考の身体化が必要&quot;&gt;技術の習得には、思考の身体化が必要&lt;/h2&gt;
&lt;p&gt;ドキュメントは形式知を伝えるための手段としてとても有効ですが、それだけでは技術を習得することはできません。&lt;/p&gt;

&lt;p&gt;技術を習得するということは、単に知識を得ることではありません。&lt;br /&gt;
知識や経験をうまく繋げ、状況に応じて適切に判断するための思考を身体化することが必要です。&lt;/p&gt;

&lt;p&gt;開発者としてある程度経験を積むと、「この実装はなぜ気持ち悪いのか」「なぜ今はこの構成のほうが良いのか」といった、言語化しづらい感覚的な知識（暗黙知）を得て、それに基づいた判断ができるようになります。&lt;/p&gt;

&lt;p&gt;この暗黙知は、どれだけ充実したドキュメントを読んでも得ることはできません。&lt;br /&gt;
仮に「なぜそうするのか」まで書かれていても、それを形式的に知るだけでは、思考の再現には至りません。&lt;/p&gt;

&lt;p&gt;暗黙知についての有名な例えである「自転車の乗り方」（自転車の乗り方は実際に自転車に乗ることでしか知ることができず、どのように操作すれば良いかを言語化して説明することが難しい）と同じように、技術もまた、手と頭を動かし、試行錯誤するプロセスを通じてしか身につきません。&lt;br /&gt;
うまくいったり、うまくいかない経験のなかで生まれる感覚が、技術者としての「地力」となっていきます。&lt;/p&gt;

&lt;h2 id=&quot;暗黙知は文化として残す&quot;&gt;暗黙知は、文化として残す&lt;/h2&gt;
&lt;p&gt;「属人化を避けよう」というスローガンの背景には、「個人への依存を減らそう」という意図があると思います。&lt;br /&gt;
しかし、開発をうまく進めるための本質的な知識の多くは、個人の経験からでしか獲得できない暗黙知にあります。&lt;/p&gt;

&lt;p&gt;本当に強いチームとは、単に手順が共有されているチームではなく、暗黙知を文化として共有できるチームです。&lt;br /&gt;
ドキュメントは形式知を必要な分だけ残すことに留め、設計思想、デバッグの勘所、コードの美学といった暗黙知は、ペアプロ・コードレビュー・日常的な対話などを通して受け継いでいくことが大切です。&lt;/p&gt;

&lt;p&gt;一緒に手を動かし、考え方や価値観を自然に共有できる時間を増やすことが、結果的にチームを強くします。&lt;/p&gt;

&lt;h2 id=&quot;終わりに&quot;&gt;終わりに&lt;/h2&gt;
&lt;p&gt;「できる」や「できるようになった」という感覚や手応えがあると、プロセスを楽しめたり、より創造的になれたりします。&lt;/p&gt;

&lt;p&gt;今回の記事では、ドキュメント化の具体的な方法までは踏み込めませんでしたが、今後は「強いチームをつくるためのドキュメントとは何か」を意識して考えていきたいと思います！&lt;/p&gt;

</description>
        <pubDate>Fri, 10 Oct 2025 00:00:00 +0900</pubDate>
      </item>
    
      



      <item>
        <title>独自のドメインモデルを捨てて、外部のドメインモデルに従う提案 | QUARTETCOM TECH BLOG</title>
        
        
        <category>逆DDD</category>
        
        <link>https://tech.quartetcom.co.jp/2025/10/01/discard-own-domain-model-follow-external-domain-model/</link>
        <guid isPermaLink="true">https://tech.quartetcom.co.jp/2025/10/01/discard-own-domain-model-follow-external-domain-model/</guid>
        <description>
&lt;h2 id=&quot;はじめに&quot;&gt;はじめに&lt;/h2&gt;

&lt;p&gt;担当しているプロダクトには &lt;strong&gt;「広告を入稿する」機能&lt;/strong&gt; があり、これは社内のユーザ部門の業務効率化が主な目的です。&lt;/p&gt;

&lt;p&gt;今までのフローとして、お客様に「広告設定確認書」という書類を提出して、&lt;br /&gt;
その「広告設定確認書内の広告設定」を元に「当該の広告媒体へ入稿を行う」業務フローがありました。&lt;/p&gt;

&lt;p&gt;このフローをシステム化することで、業務効率を上げていました。
システムの内部の処理は、 &lt;strong&gt;「独自ドメインモデル(広告設定確認書内の広告設定)」を定義し、それを「外部ドメインモデル（媒体API・SDK）」に変換して入稿 API を叩く、という二段構えのフローを採用&lt;/strong&gt; していました。&lt;/p&gt;

&lt;p&gt;このプロダクトを運用していくうちに、運用においても開発においても課題が見えてきました。&lt;/p&gt;

&lt;h2 id=&quot;プロダクトの課題&quot;&gt;プロダクトの課題&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;「独自ドメインモデル(広告設定確認書内の広告設定)」&lt;/strong&gt; を &lt;strong&gt;「外部ドメインモデル (媒体API・SDK)」&lt;/strong&gt; に変換する際に、スキーマの不一致によってデータの互換性が取れず、API エラーが起きやすい&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;「独自ドメインモデル」、「外部ドメインモデル」と理解するモデルが2つあり、認知負荷が高い&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;データ互換性維持のための差分対応が属人化し、特定の人しか直しづらい状態になっている&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;まとめると&lt;strong&gt;「認知負荷が高い」と「データ互換性維持が困難」、「独自ドメインモデルに属人性がある」という課題&lt;/strong&gt; がありました。&lt;br /&gt;
また、広告媒体 API はアップデートされる機会が多く、このような問題に陥るケースが多くありました。&lt;/p&gt;

&lt;p&gt;そのことから、現状の設計を根本から見直して、課題を解決する方法を考えました。&lt;/p&gt;

&lt;h2 id=&quot;実装した設計&quot;&gt;実装した設計&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/2025-10-01-discard-own-domain-model-follow-external-domain-model/architect.png&quot; width=&quot;800&quot; alt=&quot;外部ドメインモデルをシステムに浸透させる設計&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/2025-10-01-discard-own-domain-model-follow-external-domain-model/driven-pattern.png&quot; width=&quot;800&quot; alt=&quot;「DDD」と「逆DDD」の差&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;コンセプト&quot;&gt;コンセプト&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;「外部ドメインモデル駆動」&lt;/strong&gt; とする設計です。&lt;br /&gt;
&lt;strong&gt;外部ドメインモデルの変化をシステムがしっかりと影響を受けて、システムが変化する&lt;/strong&gt; ことを狙っています。&lt;/p&gt;

&lt;p&gt;つまり外部のドメインモデルに主体性があります。&lt;/p&gt;

&lt;p&gt;API 更新頻度が高いため、ACL 層を維持して独自ドメインモデルを管理するよりも、外部ドメインモデルに直接追従する方が、全体のコストを下げて安定運用できると判断して、
ACL 層を外す判断をしました。&lt;/p&gt;

&lt;p&gt;これは一般的な設計の方針から外れる大胆な判断ですが、&lt;strong&gt;「API に追従することが最優先の領域」&lt;/strong&gt; では合理的だと考えました。&lt;/p&gt;

&lt;p&gt;その他のコンセプトを箇条書きすると以下となります。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;特定のオブジェクトのスキーマ(今回で言うと入稿 API を実行する際に必要なオブジェクト)を外部ドメインモデルに寄せている&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;SDK のスキーマをミラー(写像)した Interface&lt;/strong&gt; を定義している
    &lt;ul&gt;
      &lt;li&gt;その Interface を持つオブジェクトを&lt;strong&gt;各層&lt;/strong&gt;に実装する
        &lt;ul&gt;
          &lt;li&gt;&lt;strong&gt;Infrastructure 層&lt;/strong&gt;
            &lt;ul&gt;
              &lt;li&gt;API や SDK とやり取りする層&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
          &lt;li&gt;&lt;strong&gt;Presentation 層&lt;/strong&gt;
            &lt;ul&gt;
              &lt;li&gt;フロントエンドの関心を持つ層
                &lt;ul&gt;
                  &lt;li&gt;バリデーションの定義を持つ&lt;/li&gt;
                  &lt;li&gt;API スキーマとして表に出す層&lt;/li&gt;
                &lt;/ul&gt;
              &lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
          &lt;li&gt;&lt;strong&gt;Entity 層&lt;/strong&gt;
            &lt;ul&gt;
              &lt;li&gt;DB に保存する関心を持つ層&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;いずれの層は同じインターフェース(スキーマ)が実装されているので「オブジェクト to オブジェクト」のような変換が容易。
        &lt;ul&gt;
          &lt;li&gt;フロントエンドで来たものを Entity 層に変換したり、Infrastructure 層に変換して API の実行ができたりと、レイヤーの意味を考えながら実装ができる&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;本来あるはずの Anti-Corruption Layer (ACL) 層 を外して、外部ドメインモデルのスキーマを自システムに影響を反映させる&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;また「SDK のスキーマをミラー(写像)した Interface」は以下のように「getter」を定義して、
スキーマの互換性を表現しています。&lt;/p&gt;

&lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CampaignInterface&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;getName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;?string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// SDK スキーマと同じフィールド&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;気をつけていること&quot;&gt;気をつけていること&lt;/h4&gt;

&lt;ul&gt;
  &lt;li&gt;SDK のオブジェクトを「我々のコード」に注入せず、&lt;strong&gt;SDK のスキーマをミラー(写像)した Interface&lt;/strong&gt; に依存している
    &lt;ul&gt;
      &lt;li&gt;SDK のオブジェクトは基本的に使いづらかったり、実装が汚れがちであるため、Infrastructure 層だけ SDK のオブジェクトを扱うようにして、実装の汚れを閉じ込めている&lt;/li&gt;
      &lt;li&gt;いわば「依存関係逆転の原則(DIP)」を取り入れて、「SDK -&amp;gt; Interface -&amp;gt; Domain」の関係性を作っています。&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;この設計を取り入れるメリット&quot;&gt;この設計を取り入れるメリット&lt;/h4&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;API 更新への追従が容易になる&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;API の更新内容を見て、変更があれば「我々のシステムの外部ドメインモデルを変更する」、という一対一の関係ができる&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;属人化を排除できる&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;モデルの説明を媒体ドキュメントに委譲できる。&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;データの互換性の維持が容易になる&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;抽象層(独自ドメインモデル)を介さず、媒体スキーマに準拠しているため、適合性が高くできる&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;この設計を取り入れるデメリット&quot;&gt;この設計を取り入れるデメリット&lt;/h4&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;「外部ドメインモデル（媒体API・SDK）」の理解が必要&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;ただし、これは広告運用プロダクトを扱う以上、避けられない前提知識でもある&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;「外部ドメインモデル（媒体API・SDK）」が廃止した時、プロダクトの改修範囲が大きくなる&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;外部ドメインモデルをそのままコピーするのでファイル数が膨大になる&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;SDK 側の仕様変更に強く依存するため「ドメイン固有の最適化」がやりにくい&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;例：「API のマイクロバジェット値(マイクロ単位の通貨値)」をシステムでは「円」として保存しておきたいなど&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;元々存在した「独自ドメインモデル(広告設定確認書内の広告設定)」を使わなくなったため、ユーザ部門のコントロール性が下がった。（ただし、今回の設計変更は、ユーザ部門の負うこととなるこの「デメリット」を許容してもらうことをまず合意したうえでとりかかった）&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;ユビキタス言語もロストしているため、業務の影響は多少なりとも出ている&lt;/li&gt;
      &lt;li&gt;正規化したと考えても良いが、業務をシステム都合で変えてしまっているのは変わりがない&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;まとめ&quot;&gt;まとめ&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;抽象化は一見便利ですが、常に媒体 API に追従しなければならないシステムにおいては、むしろ負債&lt;/strong&gt; になりがちです。&lt;/p&gt;

&lt;p&gt;一方で、抽象化することで 1 つのモデルで他媒体の関心を共通化できるメリットもあるので、&lt;br /&gt;
どの選択をしてもリスクとリターンがあると、総括して感じました。&lt;/p&gt;

&lt;p&gt;今回の設計は独自のドメインモデルを捨てて外部ドメインモデルに従う、いわば&lt;strong&gt;「逆DDD」&lt;/strong&gt;ですが、&lt;br /&gt;
認知負荷の低減・属人化の排除・最新 API への追従を考えると、やはりメリットが多いと感じます。&lt;/p&gt;

&lt;p&gt;そのことから&lt;strong&gt;「むしろ API にしっかりと追従する必要があるシステムには 逆DDD が良い」&lt;/strong&gt;という提案をしました。&lt;/p&gt;

&lt;p&gt;実際に、この設計を今のプロダクトに適応している最中ですが、
API の追従がしやすかったり、スキーマに互換性があるため、API を叩きやすかったりと効果的だと感じています。&lt;/p&gt;

&lt;p&gt;とはいえ、知れ渡っている理論を逆行する設計なので、心理的にも思い切りが必要です。&lt;br /&gt;
そのため &lt;strong&gt;「逆DDD」を採用する時は、しっかりと「そのシステムで何をしたいのか？」を明確化して判断する&lt;/strong&gt; のをオススメします。&lt;/p&gt;

&lt;h2 id=&quot;所感&quot;&gt;所感&lt;/h2&gt;

&lt;p&gt;自分でモデリングする機会が減りました。&lt;br /&gt;
面白いところだと思いますが、少し残念です。&lt;/p&gt;

&lt;p&gt;ひたすら外部ドメインモデルのリファレンスを読んで、&lt;br /&gt;
インターフェースやクラスを書いているので、作業感がとてもあります。&lt;br /&gt;
(かなり膨大なので疲れます)&lt;/p&gt;

&lt;p&gt;それでもシステムは安定してきているので、作っているシステムにとって良い設計だろうと判断しています。&lt;/p&gt;

</description>
        <pubDate>Wed, 01 Oct 2025 00:00:00 +0900</pubDate>
      </item>
    
      



      <item>
        <title>Symfonyのretry_failed optionsを使って、APIリクエスト失敗時に再試行するようにする | QUARTETCOM TECH BLOG</title>
        
        
        <category>PHP</category>
        
        <category>Symfony</category>
        
        <category>ExponentialBackoff</category>
        
        <category>API</category>
        
        <link>https://tech.quartetcom.co.jp/2025/09/25/symfony-retry-failed-option/</link>
        <guid isPermaLink="true">https://tech.quartetcom.co.jp/2025/09/25/symfony-retry-failed-option/</guid>
        <description>
&lt;p&gt;Symfonyのretry_failed optionsを活用すると、APIリクエストが失敗した際に指数バックオフアルゴリズムによる再試行が簡単に実装できます。&lt;/p&gt;

&lt;p&gt;本記事では、その設定方法と検証結果を紹介します :saluting_face:&lt;/p&gt;

&lt;h3 id=&quot;指数バックオフアルゴリズム-exponential-backoff-algorithmとは&quot;&gt;指数バックオフアルゴリズム (Exponential Backoff Algorithm)とは&lt;/h3&gt;
&lt;p&gt;ネットワークやAPIリトライなどで使われる「待ち時間を指数的に増やして再試行する仕組み」です。&lt;/p&gt;

&lt;p&gt;例えば初回 1秒 → 2秒 → 4秒 → 8秒 … のように、待機時間を指数関数的に増加させてリトライ処理をします。&lt;/p&gt;

&lt;p&gt;過去記事「リトライ処理時の指数バックオフアルゴリズムを AWS Step Functions で実装」も参考になるので読んでみてください。&lt;br /&gt;
&lt;a href=&quot;https://tech.quartetcom.co.jp/2024/10/29/exponential-backoff-step-functions/&quot;&gt;https://tech.quartetcom.co.jp/2024/10/29/exponential-backoff-step-functions/&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;symfonyのretry_failed-optionsの設定方法&quot;&gt;Symfonyのretry_failed optionsの設定方法&lt;/h2&gt;
&lt;p&gt;config/packages/framework.yamlを以下のように設定します。&lt;br /&gt;
※Symfony 5.2以降で利用可能です。&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;s&quot;&gt;//  config/packages/framework.yaml&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;framework&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;http_client&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;default_options&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;retry_failed&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;na&quot;&gt;enabled&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;
                &lt;span class=&quot;na&quot;&gt;max_retries&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# 最大リトライ回数&lt;/span&gt;
                &lt;span class=&quot;na&quot;&gt;delay&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1000&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;# 初期遅延 (ミリ秒) → 1s&lt;/span&gt;
                &lt;span class=&quot;na&quot;&gt;multiplier&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;2&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 指数の倍率（2 なら 1s,2s,4s,8s...）&lt;/span&gt;
                &lt;span class=&quot;na&quot;&gt;max_delay&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;64000&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# 最大待機（ミリ秒）→ Memorystoreの推奨に合わせ64s上限など&lt;/span&gt;
                &lt;span class=&quot;na&quot;&gt;jitter&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0.1&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;# ジッター（0.0〜1.0、ここでは10%の揺らし）。同じタイミングで大量のリトライが発生しないようにランダムなばらつきをもたせる&lt;/span&gt;
                &lt;span class=&quot;na&quot;&gt;http_codes&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;423&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;425&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;429&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;500&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;502&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;503&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;504&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;507&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;510&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# リトライ対象のHTTPコード&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;特定API使用時のみリトライする場合は、scoped_clientsを使います。&lt;/p&gt;
&lt;div class=&quot;language-diff highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;framework:
&lt;/span&gt;    http_client:
&lt;span class=&quot;gd&quot;&gt;-        default_options:
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+        scoped_clients:
+            api_time.client:
+               base_uri: &apos;https://127.0.0.1:8000/api/time&apos;
&lt;/span&gt;                    retry_failed:
                        enabled: true
                        max_retries: 5
                        ...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;//src/Controller/RequestController.php&lt;/span&gt;

&lt;span class=&quot;mf&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Symfony\Contracts\HttpClient\HttpClientInterface&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;RequestController&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AbstractController&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;#[Route(&apos;/request&apos;, name: &apos;app_request&apos;)]&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;index&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;#[Autowire(service: &apos;api_time.client&apos;)]&lt;/span&gt;
        &lt;span class=&quot;kt&quot;&gt;HttpClientInterface&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$httpClient&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Response&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;//APIにアクセスする&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;$response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$httpClient&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;GET&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;https://127.0.0.1:8000/api/time&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getStatusCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;request/error.html.twig&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
                &lt;span class=&quot;s1&quot;&gt;&apos;message&apos;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;API request failed with status code: &apos;&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getStatusCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;]);&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;nv&quot;&gt;$content&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toArray&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;request/index.html.twig&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
            &lt;span class=&quot;s1&quot;&gt;&apos;time&apos;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$content&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;time&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;]);&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;参考: &lt;a href=&quot;https://symfony.com/doc/current/reference/configuration/framework.html#http-client&quot;&gt;https://symfony.com/doc/current/reference/configuration/framework.html#http-client&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;本当に指数関数的にリトライしているか検証&quot;&gt;本当に指数関数的にリトライしているか検証&lt;/h2&gt;
&lt;p&gt;以下の手順で検証を行いました。&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;&lt;a href=&quot;#1-http-429エラー2リクエスト10秒を意図的に発生させるapiを作成&quot;&gt;HTTP 429エラー(2リクエスト/10秒)を意図的に発生させるAPIを作成&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#2-1で作成したapiを使用するクライアントにsymfonyのretry_failedオプションを設定&quot;&gt;1で作成したAPIにリクエストをするクライアントにSymfonyのretry_failedオプションを設定&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#3-リトライ処理が実際に行われているか確認&quot;&gt;リトライ処理が実際に行われているか確認&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;1-http-429エラー2リクエスト10秒を意図的に発生させるapiを作成&quot;&gt;1. HTTP 429エラー(2リクエスト/10秒)を意図的に発生させるAPIを作成&lt;/h3&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;composer create-project symfony/skeleton:&lt;span class=&quot;s2&quot;&gt;&quot;7.3.x&quot;&lt;/span&gt; rate-limitter-example
&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;rate-limitter-example/
composer require webapp
composer require symfony/rate-limiter
bin/console make:controlle ApiTimeController
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;// src/Controller/ShowTimeController.php

&lt;span class=&quot;cp&quot;&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;namespace&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;App\Controller&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Symfony\Bundle\FrameworkBundle\Controller\AbstractController&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Symfony\Component\HttpFoundation\Request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Symfony\Component\HttpFoundation\Response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Symfony\Component\RateLimiter\RateLimiterFactoryInterface&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Symfony\Component\Routing\Attribute\Route&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ApiTimeController&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AbstractController&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;#[Route(&apos;/api/time&apos;, name: &apos;app_api_time&apos;)]&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;index&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;Request&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;RateLimiterFactoryInterface&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$apiTimeLimiter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Response&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;$limiter&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$apiTimeLimiter&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getClientIp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;());&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;// consume(1)の1は消費するトークン数&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$limiter&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;consume&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;isAccepted&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;TooManyRequestsHttpException&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Too Many Requests&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([&lt;/span&gt;
            &lt;span class=&quot;s1&quot;&gt;&apos;time&apos;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;DateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;Y-m-d H:i:s&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;]);&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;    
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;s&quot;&gt;// config/packages/rate_limiter.yaml&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;framework&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;rate_limiter&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;api_time&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;c1&quot;&gt;# use &apos;sliding_window&apos; if you prefer that policy&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;policy&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;fixed_window&apos;&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;limit&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;2&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;interval&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;second&apos;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;2-1で作成したapiを使用するクライアントにsymfonyのretry_failedオプションを設定&quot;&gt;2. 1で作成したAPIを使用するクライアントにSymfonyのretry_failedオプションを設定&lt;/h2&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;composer create-project symfony/skeleton:&lt;span class=&quot;s2&quot;&gt;&quot;7.3.x&quot;&lt;/span&gt; clear-late-limiter
&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;clear-late-limiter/
composer require webapp
bin/console make:controlle RequestController
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;namespace&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;App\Controller&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Symfony\Bundle\FrameworkBundle\Controller\AbstractController&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Symfony\Component\HttpFoundation\Response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Symfony\Component\Routing\Attribute\Route&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Symfony\Contracts\HttpClient\HttpClientInterface&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;RequestController&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AbstractController&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;#[Route(&apos;/request&apos;, name: &apos;app_request&apos;)]&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;index&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;kt&quot;&gt;HttpClientInterface&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$httpClient&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Response&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;//APIにアクセスする&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;$response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$httpClient&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;GET&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;https://127.0.0.1:8000/api/time&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getStatusCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;request/error.html.twig&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
                &lt;span class=&quot;s1&quot;&gt;&apos;message&apos;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;API request failed with status code: &apos;&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getStatusCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;]);&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;nv&quot;&gt;$content&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toArray&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;request/index.html.twig&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
            &lt;span class=&quot;s1&quot;&gt;&apos;time&apos;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$content&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;time&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;]);&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;s&quot;&gt;//  config/packages/framework.yaml&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;framework&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;http_client&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;default_options&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;retry_failed&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;na&quot;&gt;enabled&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;
                &lt;span class=&quot;na&quot;&gt;max_retries&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# 最大リトライ回数&lt;/span&gt;
                &lt;span class=&quot;na&quot;&gt;delay&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1000&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;# 初期遅延 (ミリ秒) → 1s&lt;/span&gt;
                &lt;span class=&quot;na&quot;&gt;multiplier&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;2&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 指数の倍率（2 なら 1s,2s,4s,8s...）&lt;/span&gt;
                &lt;span class=&quot;na&quot;&gt;max_delay&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;64000&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# 最大待機（ミリ秒）→ Memorystoreの推奨に合わせ64s上限など&lt;/span&gt;
                &lt;span class=&quot;na&quot;&gt;jitter&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0.0&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;# ジッター（0.0〜1.0、ここでは10%の揺らし）。同じタイミングで大量のリトライが発生しないようにランダムなばらつきをもたせる&lt;/span&gt;
                &lt;span class=&quot;na&quot;&gt;http_codes&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;423&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;425&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;429&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;500&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;502&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;503&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;504&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;507&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;510&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# リトライ対象のHTTPコード&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;上記の設定で指数関数的にリトライするように設定します。 今回は検証結果がわかりやすいように、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jitter: 0.0&lt;/code&gt;とします。&lt;/p&gt;

&lt;h3 id=&quot;3-リトライ処理が実際に行われているか確認&quot;&gt;3. リトライ処理が実際に行われているか確認&lt;/h3&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;symfony server:start&lt;/code&gt;で各サーバを立てます。以下&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://127.0.0.1:8000&quot;&gt;https://127.0.0.1:8000&lt;/a&gt; API側&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://127.0.0.1:8001&quot;&gt;https://127.0.0.1:8001&lt;/a&gt; クライアント側&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;とします。&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://127.0.0.1:8001/request&quot;&gt;https://127.0.0.1:8001/request&lt;/a&gt; に短時間に連続してアクセスしてみます。&lt;/p&gt;

&lt;p&gt;その後、クライアント側のProfiler &amp;gt; HTTP Client &amp;gt; Responseを確認すると以下のことがわかります。&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;retry_count&quot; =&amp;gt; 4&lt;/code&gt;より、4回リトライし、5回目で成功している&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;retries&quot;&lt;/code&gt;より、各リトライが1秒後 → 2秒後 → 4秒後 → 8秒後と指数関数的に試行されている&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;▼
  &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;▼
  &lt;span class=&quot;s2&quot;&gt;&quot;info&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;▼
    ...
    &lt;span class=&quot;s2&quot;&gt;&quot;debug&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&quot;
      ...
      * Request completely sent off
      &amp;lt; HTTP/2 200 
      &amp;lt; cache-control: no-cache, private
      &amp;lt; content-type: application/json
      &amp;lt; date: Mon, 22 Sep 2025 01:37:56 GMT
      ...
  ]
  &quot;&lt;/span&gt;retry_count&lt;span class=&quot;s2&quot;&gt;&quot; =&amp;gt; 4
  &quot;&lt;/span&gt;response_headers&lt;span class=&quot;s2&quot;&gt;&quot; =&amp;gt; [▶]
  &quot;&lt;/span&gt;response_json&lt;span class=&quot;s2&quot;&gt;&quot; =&amp;gt; [▶]
  &quot;&lt;/span&gt;retries&lt;span class=&quot;s2&quot;&gt;&quot; =&amp;gt; [▼
    [▼
      ...
      &quot;&lt;/span&gt;debug&lt;span class=&quot;s2&quot;&gt;&quot; =&amp;gt; &quot;&quot;&quot;&lt;/span&gt;
        ...
        &lt;span class=&quot;k&quot;&gt;*&lt;/span&gt; Request completely sent off
        &amp;lt; HTTP/2 429 
        &amp;lt; cache-control: no-cache, private
        &amp;lt; content-type: text/html&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;charset&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;UTF-8
        &amp;lt; &lt;span class=&quot;nb&quot;&gt;date&lt;/span&gt;: Mon, 22 Sep 2025 01:37:41 GMT
        ...
    &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;▼
      ...
      &lt;span class=&quot;s2&quot;&gt;&quot;debug&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&quot;
        ...
        * Request completely sent off
        &amp;lt; HTTP/2 429 
        &amp;lt; cache-control: no-cache, private
        &amp;lt; content-type: text/html; charset=UTF-8
        &amp;lt; date: Mon, 22 Sep 2025 01:37:42 GMT
        ...
    ]
    [▼
      ...
      &quot;&lt;/span&gt;debug&lt;span class=&quot;s2&quot;&gt;&quot; =&amp;gt; &quot;&quot;&quot;&lt;/span&gt;
        ...        
        &lt;span class=&quot;k&quot;&gt;*&lt;/span&gt; Request completely sent off
        &amp;lt; HTTP/2 429 
        &amp;lt; cache-control: no-cache, private
        &amp;lt; content-type: text/html&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;charset&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;UTF-8
        &amp;lt; &lt;span class=&quot;nb&quot;&gt;date&lt;/span&gt;: Mon, 22 Sep 2025 01:37:44 GMT
        ...
    &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;▼
      ...
      &lt;span class=&quot;s2&quot;&gt;&quot;debug&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&quot;
        ...
        * Request completely sent off
        &amp;lt; HTTP/2 429 
        &amp;lt; cache-control: no-cache, private
        &amp;lt; content-type: text/html; charset=UTF-8
        &amp;lt; date: Mon, 22 Sep 2025 01:37:48 GMT
        ...
    ]
  ]
]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;まとめ&quot;&gt;まとめ&lt;/h2&gt;
&lt;p&gt;今回の検証では、2リクエスト/10秒という制限のあるAPIに対して、delay: 1000(=1秒)・multiplier: 2に設定したところ、リトライが頻繁に発生し、設定した最大リトライ回数（max_retries）に達してしまうケースがありました。&lt;/p&gt;

&lt;p&gt;このため、API側のレート制限に合わせて、delayやmultiplier、max_retriesなどのパラメータを適切に調整することが重要です :warning:&lt;/p&gt;

&lt;p&gt;Symfonyのretry_failed optionsを活用すれば、指数バックオフアルゴリズムによるリトライ機能を簡単に設定できます！ぜひ皆さんのプロジェクトでも取り入れてみてください :heart_hands:&lt;/p&gt;

</description>
        <pubDate>Thu, 25 Sep 2025 00:00:00 +0900</pubDate>
      </item>
    
      



      <item>
        <title>PHPでGraphQLサーバー構築入門：Symfony＋API Platformを活用した実践 | QUARTETCOM TECH BLOG</title>
        
        
        <category>PHP</category>
        
        <category>Symfony</category>
        
        <category>GraphQL</category>
        
        <category>API</category>
        
        <link>https://tech.quartetcom.co.jp/2025/08/15/symfony-api-platform-graphql/</link>
        <guid isPermaLink="true">https://tech.quartetcom.co.jp/2025/08/15/symfony-api-platform-graphql/</guid>
        <description>
&lt;p&gt;近年のWebアプリケーションでは、画面構成やUXの多様化にともない&lt;strong&gt;「より柔軟で効率的なデータ取得」&lt;/strong&gt;の重要性が高まっています。&lt;/p&gt;

&lt;p&gt;特にフロントエンドでは、&lt;strong&gt;不要なデータ取得&lt;/strong&gt;や&lt;strong&gt;複数回のAPIリクエスト&lt;/strong&gt;が開発体験やパフォーマンスに影響を与えるケースも増えてきました。&lt;/p&gt;

&lt;p&gt;よく使われている&lt;strong&gt;REST API&lt;/strong&gt;では、エンドポイントごとに決まったレスポンスが返ってくるため、
フロント側の要件に合わせて&lt;strong&gt;「不要なデータを受け取る」&lt;/strong&gt;や&lt;strong&gt;「複数のエンドポイントを呼び分ける」&lt;/strong&gt; といった非効率が発生しがちです。&lt;/p&gt;

&lt;p&gt;これらの課題を解決する手段として注目されているのが&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GraphQL&lt;/code&gt;です。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GraphQL&lt;/strong&gt;を使えば、クライアントが必要なデータだけを明確に指定して取得できるため、 通信の無駄が減り開発効率やパフォーマンスの向上につながります。&lt;/p&gt;

&lt;p&gt;本記事では、&lt;strong&gt;Symfony と API Platform&lt;/strong&gt;を使って、&lt;strong&gt;GraphQL API&lt;/strong&gt;を簡単に作成する方法を紹介します。&lt;/p&gt;

&lt;h2 id=&quot;️-今回作成するもの&quot;&gt;✅️ 今回作成するもの&lt;/h2&gt;

&lt;p&gt;今回は「書籍（Book）」と「レビュー（Review）」のデータを扱う&lt;strong&gt;GraphQL API&lt;/strong&gt;を作成します。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;書籍一覧の取得&lt;/li&gt;
  &lt;li&gt;各書籍に紐づくレビューの平均スコアの取得&lt;/li&gt;
  &lt;li&gt;React + Apollo Clientを使ったフロントエンドでのデータ取得&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;フロント以外のコードはGitHubに公開していますので、ぜひ参考にしてください。&lt;/p&gt;

&lt;p&gt;🔗&lt;a href=&quot;https://github.com/mako5656/symfony-api-platform-graphql&quot;&gt;GitHub：symfony-api-platform-graphql&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;-目次&quot;&gt;📝 目次&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#1-symfonyプロジェクトのセットアップ&quot;&gt;1. Symfonyプロジェクトのセットアップ&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#2-api-platformの導入&quot;&gt;2. API Platformの導入&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#3-データベース接続postgresql&quot;&gt;3. データベース接続（PostgreSQL）&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#4-book--reviewエンティティの作成とマイグレーション&quot;&gt;4. Book / Reviewエンティティの作成とマイグレーション&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#5-データ登録-rest-api&quot;&gt;5. データ登録 (REST API)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#6-graphqlの有効化&quot;&gt;6. GraphQLの有効化&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#7-graphqlで書籍一覧を取得&quot;&gt;7. GraphQLで書籍一覧を取得&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#8-カスタムフィールドレビューの平均スコアを追加&quot;&gt;8. カスタムフィールド（レビューの平均スコア）を追加&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#9-フロント側からデータを取得する&quot;&gt;9. フロント側からデータを取得する&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#10-まとめ&quot;&gt;10. まとめ&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;1-symfonyプロジェクトのセットアップ&quot;&gt;1. Symfonyプロジェクトのセットアップ&lt;/h2&gt;

&lt;p&gt;使用バージョン：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;PHP:&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;v8.2.28&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Symfony:&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;v7.3.1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;まずは新規プロジェクトを作成します。&lt;/p&gt;

&lt;div class=&quot;language-zsh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;symfony new symfony-api-platform-graphql
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;2-api-platformの導入&quot;&gt;2. API Platformの導入&lt;/h2&gt;

&lt;p&gt;API Platformは、PHP製のWeb APIフレームワークです。 
導入するだけで自動的にRESTとGraphQLのAPIエンドポイントが作成されます。&lt;/p&gt;

&lt;div class=&quot;language-zsh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;composer require api
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;これだけで&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/api&lt;/code&gt;にアクセスすると、&lt;strong&gt;Swagger UIでREST APIのドキュメント&lt;/strong&gt;が確認できます。&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;💡 API Platform では、REST API と GraphQL API の両方を併用できます。
エンティティを定義するだけで、自動的に REST のエンドポイントが生成され、Swagger UI から確認や操作も可能です。
本記事では GraphQL をメインに扱いますが、動作確認も兼ねてデータ登録には REST API を使って進めていきます。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/2025-08-15/01.png&quot; width=&quot;800&quot; alt=&quot;APIドキュメント-データ0&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;3-データベース接続postgresql&quot;&gt;3. データベース接続（PostgreSQL）&lt;/h2&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.env&lt;/code&gt;ファイルを編集し、PostgreSQLに接続できるように設定します。&lt;/p&gt;

&lt;div class=&quot;language-diff highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;gd&quot;&gt;- DATABASE_URL=&quot;postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&amp;amp;charset=utf8&quot;
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+ DATABASE_URL=&quot;postgresql://app:!@127.0.0.1:5432/symfony-api-platform-graphql?serverVersion=16&amp;amp;charset=utf8&quot;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;ユーザー名やパスワードは自分の環境に合わせて変更してください。&lt;/li&gt;
  &lt;li&gt;事前にPostgreSQLをインストールしておく必要があります。&lt;/li&gt;
  &lt;li&gt;PostgreSQL の代わりに SQLite や MySQL を使っても大丈夫です。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;その後、以下のコマンドでデータベースを作成します。&lt;/p&gt;

&lt;div class=&quot;language-zsh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;php bin/console doctrine:database:create
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;4-book--reviewエンティティの作成とマイグレーション&quot;&gt;4. Book / Reviewエンティティの作成とマイグレーション&lt;/h2&gt;

&lt;p&gt;まずは開発用に&lt;strong&gt;MakerBundle&lt;/strong&gt;を追加します。&lt;/p&gt;

&lt;div class=&quot;language-zsh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;composer require symfony/maker-bundle &lt;span class=&quot;nt&quot;&gt;--dev&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;次に、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;書籍（Book）&lt;/code&gt;と&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;レビュー（Review）&lt;/code&gt;のエンティティを作成し、マイグレーションを実行します。&lt;/p&gt;

&lt;div class=&quot;language-zsh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;php bin/console make:entity
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;php bin/console make:migration 
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;php bin/console doctrine:migrations:migrate
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;👇️️ エンティティ定義のコード&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/mako5656/symfony-api-platform-graphql/blob/master/src/Entity/Book.php&quot;&gt;Book.php&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/mako5656/symfony-api-platform-graphql/blob/master/src/Entity/Review.php&quot;&gt;Review.php&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;反映後、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/api&lt;/code&gt;にアクセスすると、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Book&lt;/code&gt;と&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Review&lt;/code&gt;のエンドポイントが自動生成されていることが確認できます。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/2025-08-15/02.png&quot; width=&quot;800&quot; alt=&quot;REST APIドキュメント-エンティティ反映&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/2025-08-15/03.png&quot; width=&quot;800&quot; alt=&quot;REST APIドキュメント-スキーマ&quot; /&gt;&lt;/p&gt;

&lt;p&gt;次に、実際にBookとReviewのデータをAPI経由で登録してみます。&lt;/p&gt;

&lt;h2 id=&quot;5-データ登録-rest-api&quot;&gt;5. データ登録 (REST API)&lt;/h2&gt;

&lt;p&gt;生成された&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/api&lt;/code&gt;エンドポイントに対して、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Postman&lt;/code&gt;などを使ってデータを登録します。&lt;/p&gt;

&lt;p&gt;Bookデータを登録するために、以下のように&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/api/books&lt;/code&gt;に&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;POST&lt;/code&gt;リクエストを送信します。&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;isbn&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;978-4-7741-8411-1&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;title&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;ドメイン駆動設計入門&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;description&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;DDDの基礎を学べる書籍です。&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;author&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;山田太郎&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;publicationDate&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;2024-05-01T00:00:00+09:00&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Reviewデータの登録も同様に&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/api/reviews&lt;/code&gt;に&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;POST&lt;/code&gt;リクエストを行います。&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;rating&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;body&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;とても分かりやすい内容でDDDが理解できました！&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;author&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;読者A&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;publicationDate&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;2024-05-10T00:00:00+09:00&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;book&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;/api/books/1&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/2025-08-15/04.png&quot; width=&quot;800&quot; alt=&quot;Postmanでのデータ登録&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/2025-08-15/05.png&quot; width=&quot;800&quot; alt=&quot;PostgreSQLでBookデータ確認&quot; /&gt;&lt;/p&gt;

&lt;p&gt;データベースにデータが入っていることが確認できます。&lt;/p&gt;

&lt;p&gt;サンプルデータとして&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Book&lt;/code&gt;と&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Review&lt;/code&gt;データをいくつか登録しておきます。&lt;/p&gt;

&lt;p&gt;サンプルデータは&lt;a href=&quot;https://github.com/mako5656/symfony-api-platform-graphql/blob/master/README.md&quot;&gt;README.md&lt;/a&gt;に記載しておきます。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/2025-08-15/06.png&quot; width=&quot;800&quot; alt=&quot;PostgreSQLでBookとReviewのデータ確認&quot; /&gt;&lt;/p&gt;

&lt;p&gt;REST APIが問題なく動作することを確認できたので、 ここからは本題であるGraphQLの利用方法を見ていきます。&lt;/p&gt;

&lt;h2 id=&quot;6-graphqlの有効化&quot;&gt;6. GraphQLの有効化&lt;/h2&gt;

&lt;p&gt;GraphQLを有効化します。&lt;/p&gt;

&lt;div class=&quot;language-zsh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;composer require api-platform/graphql
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;これで&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/graphql&lt;/code&gt;にアクセスすれば、&lt;strong&gt;GraphiQL（GraphQL IDE）&lt;/strong&gt;が利用可能になります。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/2025-08-15/07.png&quot; width=&quot;800&quot; alt=&quot;GraphQL IDE&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;7-graphqlで書籍一覧を取得&quot;&gt;7. GraphQLで書籍一覧を取得&lt;/h2&gt;

&lt;p&gt;API Platformでは、GraphQLエンドポイント（&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/graphql&lt;/code&gt;）が自動で用意されており、ブラウザから&lt;strong&gt;GraphiQL&lt;/strong&gt;を使ってクエリを試すことができます。&lt;/p&gt;

&lt;p&gt;まずは、シンプルに書籍の一覧を取得してみます。&lt;/p&gt;

&lt;div class=&quot;language-graphql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;books&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;edges&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;node&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;author&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;isbn&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;publicationDate&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;このように、GraphQLでは&lt;strong&gt;必要なフィールドだけを指定して取得できる&lt;/strong&gt;のが大きなメリットです。&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;edges &amp;gt; node&lt;/code&gt;という構造は、&lt;a href=&quot;https://relay.dev/&quot;&gt;Relay仕様&lt;/a&gt;に沿ったページネーション形式です。&lt;/p&gt;

&lt;p&gt;👇️️ 結果画面はこんな感じ&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/2025-08-15/08.png&quot; width=&quot;800&quot; alt=&quot;GraphQLのクエリ実行&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;8-カスタムフィールドレビューの平均スコアを追加&quot;&gt;8. カスタムフィールド（レビューの平均スコア）を追加&lt;/h2&gt;

&lt;p&gt;クライアントでレビューを集計するのは手間なので、バックエンドで平均スコアを計算して返すようにします。&lt;/p&gt;

&lt;h3 id=&quot;-bookエンティティに集計ロジックを追加&quot;&gt;📌 Bookエンティティに集計ロジックを追加&lt;/h3&gt;

&lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;#[Groups([&apos;book:read&apos;])]&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;getAverageRating&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;?float&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;is_iterable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;reviews&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;reviews&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;nv&quot;&gt;$sum&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;$count&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;foreach&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;reviews&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$review&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;$sum&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$review&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rating&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;$count&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$count&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;round&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$sum&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;レビューが存在する場合だけ平均を計算し、小数第2位で丸めて返すというシンプルな実装です。&lt;/p&gt;

&lt;h3 id=&quot;-graphqlでフィールドを有効化する&quot;&gt;📌 GraphQLでフィールドを有効化する&lt;/h3&gt;

&lt;p&gt;この&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;averageRating&lt;/code&gt;をGraphQLのレスポンスに含めるためには、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ApiResource&lt;/code&gt;にグループ指定を追加します。&lt;/p&gt;

&lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;#[ApiResource(normalizationContext: [&apos;groups&apos; =&amp;gt; [&apos;book:read&apos;]])]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;これでGraphQL側でも以下のようなクエリで取得可能になります！&lt;/p&gt;

&lt;div class=&quot;language-graphql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;books&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;edges&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;node&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;averageRating&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;👇️️ 結果画面はこんな感じ&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/2025-08-15/09.png&quot; width=&quot;800&quot; alt=&quot;平均スコア追加後のGraphQLのクエリ実行&quot; /&gt;&lt;/p&gt;

&lt;p&gt;バックエンドで集計処理を済ませておけば、フロント側は&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;averageRating&lt;/code&gt;をクエリに追加するだけで、すぐに表示できます。&lt;/p&gt;

&lt;p&gt;REST APIでも集約済みのデータを1リクエストで取得することは可能ですが、 事前に専用のエンドポイントを用意する必要があり画面ごとのデータ要件に応じて設計が増えていきがちです。&lt;/p&gt;

&lt;p&gt;その点GraphQLでは、クライアントが欲しい形で柔軟にクエリを定義できるため、バックエンド側は汎用的なスキーマを用意するだけで済みます。&lt;/p&gt;

&lt;p&gt;その結果、&lt;strong&gt;クライアントは欲しいデータを、欲しい形で取得&lt;/strong&gt;できるようになり、
バックエンド側も汎用的なスキーマ設計だけで済むため、&lt;strong&gt;API設計の手間を大きく減らす&lt;/strong&gt;ことができます。&lt;/p&gt;

&lt;p&gt;GraphQL のこうした柔軟さは、複雑な画面構成や多様なデータ要件を持つアプリ開発において、特に効果を発揮します。&lt;/p&gt;

&lt;h2 id=&quot;9-フロント側からデータを取得する&quot;&gt;9. フロント側からデータを取得する&lt;/h2&gt;

&lt;p&gt;GraphQLの大きな利点のひとつが、「&lt;strong&gt;フロントエンドから必要なデータだけを効率的に取得できる&lt;/strong&gt;」点です。&lt;/p&gt;

&lt;p&gt;ここでは&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;React + Apollo Client&lt;/code&gt;を使って、書籍のタイトルと平均スコアを表示するシンプルなUIを作ってみます。&lt;/p&gt;

&lt;h3 id=&quot;graphqlクエリの定義&quot;&gt;GraphQLクエリの定義&lt;/h3&gt;

&lt;div class=&quot;language-tsx highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;GET_BOOKS&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;gql&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`
  query {
    books {
      edges {
        node {
          id
          title
          averageRating
        }
      }
    }
  }
`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;コンポーネント側の実装&quot;&gt;コンポーネント側の実装&lt;/h3&gt;

&lt;div class=&quot;language-tsx highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;gql&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useQuery&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;@apollo/client&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;BookList&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;loading&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useQuery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;GET_BOOKS&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;loading&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;読み込み中...&lt;span class=&quot;p&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;エラー: &lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;;&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;ul&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;books&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;edges&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(({&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;node&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;any&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;li&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;node&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;node&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt; - 平均スコア: &lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;node&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;averageRating&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;評価なし&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;li&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;ul&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;BookList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;このように、GraphQLクエリを使ってフロント側で必要なフィールドだけを柔軟に取得できます。&lt;/p&gt;

&lt;p&gt;バックエンド側で&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Groups&lt;/code&gt;属性によってフィールド公開範囲を調整できるのも、API Platform の便利な点です。&lt;/p&gt;

&lt;h2 id=&quot;10-まとめ&quot;&gt;10. まとめ&lt;/h2&gt;

&lt;p&gt;今回の構成（Symfony + API Platform + GraphQL）を振り返ってみると、こんな強みがありました。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;✅ &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;自動生成&lt;/code&gt;：エンティティ定義だけでREST/GraphQL APIを即構築可能&lt;/li&gt;
  &lt;li&gt;✅ &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;柔軟性&lt;/code&gt;：Groups属性で公開フィールドを制御したり、フロントエンドから必要なデータを自由に取得できる&lt;/li&gt;
  &lt;li&gt;✅ &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;効率性&lt;/code&gt;：GraphQL により、必要な情報だけを最小限で取得できるため、通信や処理の無駄がない&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GraphQLが初めての方でも、API Platformを使えば簡単に構築できます。&lt;/p&gt;

&lt;p&gt;特に、&lt;strong&gt;データの粒度を調整したい場面&lt;/strong&gt;や&lt;strong&gt;複雑な画面構成を持つSPA開発&lt;/strong&gt;においては、GraphQLのメリットを最大限に活かせます。&lt;/p&gt;

&lt;p&gt;今回は実装中心の紹介でしたが、GraphQLはUIの多様化に柔軟に対応できる強力な選択肢です。&lt;/p&gt;

&lt;p&gt;ぜひ一度、試してみてください！&lt;/p&gt;

&lt;h2 id=&quot;️-参考リンク&quot;&gt;📚️ 参考リンク&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://api-platform.com/docs/&quot;&gt;API Platform 公式ドキュメント&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://zenn.dev/ttskch/articles/89a4a6420313bb&quot;&gt;Zenn 記事：Symfony + API Platform で GraphQL API を実装&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
        <pubDate>Fri, 15 Aug 2025 00:00:00 +0900</pubDate>
      </item>
    
      



      <item>
        <title>Angularリアクティブフォームの型安全な使い方 ─ 罠を避ける実践ポイント | QUARTETCOM TECH BLOG</title>
        
        
        <category>Angular</category>
        
        <category>TypeScript</category>
        
        <category>Form</category>
        
        <link>https://tech.quartetcom.co.jp/2025/08/01/angular-form/</link>
        <guid isPermaLink="true">https://tech.quartetcom.co.jp/2025/08/01/angular-form/</guid>
        <description>
&lt;h1 id=&quot;はじめに&quot;&gt;はじめに&lt;/h1&gt;
&lt;p&gt;今年に入りチーム編成が変わり、バックエンドエンジニアからフロントエンドやインフラも触ることになりました。
詳しくは&lt;a href=&quot;https://tech.quartetcom.co.jp/2025/06/27/rebuild-team/&quot;&gt;こちら&lt;/a&gt;をご覧ください。&lt;/p&gt;

&lt;p&gt;その中でAngular v18環境でのフォーム実装に関わる機会があり、特にリアクティブフォームの「型の付け方」や「構成パターン」で混乱があったので、
本記事ではTyped Forms(型付きフォーム)におけるフォームオブジェクトの生成、型推論、および &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nonNullable&lt;/code&gt; の扱いなどを体系的に整理し、より型が安全なフォーム実装に必要な観点をまとめます。&lt;/p&gt;

&lt;h1 id=&quot;angularのフォーム構成の方法&quot;&gt;Angularのフォーム構成の方法&lt;/h1&gt;
&lt;p&gt;Angularにはフォームを作成するための2つの主要な構成方法があります。&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://angular.jp/guide/forms#choosing-an-approach&quot;&gt;公式ドキュメント&lt;/a&gt;を見ていただければわかりますがデータフローや入力フィールド数の規模感の違いなどがあり、 フォームの構造やロジックの定義箇所、型推論の取り扱いが選定時に特に重要になると感じました。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;リアクティブフォーム：フォーム構造・ロジックをTypeScript側で定義する。&lt;/li&gt;
  &lt;li&gt;テンプレート駆動フォーム：フォーム構造・ロジックをHTMLテンプレート側に記述する。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;本記事では、Typed Forms(型付きフォーム)を活かせるリアクティブフォームに焦点を当てて解説します。&lt;/p&gt;

&lt;h1 id=&quot;リアクティブフォームの構成要素&quot;&gt;リアクティブフォームの構成要素&lt;/h1&gt;
&lt;p&gt;フォームの実装をする上で良いとされる構成パターンはよく見かけますがなぜいいのかがわからない状態だったので、
以下の3つの観点に分けて順に理解していくとわかりやすいと感じたので構成方法や型指定方法の違いによって起きうる危険性も含めて解説していきます。&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;a href=&quot;#フォームオブジェクトの生成方法&quot;&gt;フォームオブジェクトの作成方法&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#フォームの型指定方法&quot;&gt;フォームの型の指定方法&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#オプション設定&quot;&gt;オプション設定&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;フォームオブジェクトの生成方法&quot;&gt;フォームオブジェクトの生成方法&lt;/h2&gt;
&lt;p&gt;オブジェクトの作成には、主に以下の方法があります。&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;フォームの基盤となるクラスを&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;new&lt;/code&gt;(インスタンス化)して作成する方法(以下、「ローレベルAPI」)&lt;/li&gt;
  &lt;li&gt;Angularが提供するユーティリティサービスのFormBuilderを使用して作成する方法&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;以下にそれぞれのサンプルコードを示し違いを説明します。&lt;/p&gt;

&lt;p&gt;他にも深いところで違いがあるんだとは思いますが、個人的にはDIでのテストのしやすさや可読性の点からFormBuilderの方が好みでした。&lt;/p&gt;

&lt;h3 id=&quot;ローレベルapiのサンプルコード&quot;&gt;ローレベルAPIのサンプルコード&lt;/h3&gt;
&lt;p&gt;フォームの基盤クラスである&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormGroup&lt;/code&gt;や&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormControl&lt;/code&gt;を明示的にインスタンス化してフォームを作成します。&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;UserFormComponent&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;userForm&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormGroup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormControl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;formbuilderのサンプルコード&quot;&gt;FormBuilderのサンプルコード&lt;/h3&gt;
&lt;p&gt;FormBuilderを使用してフォームを作成します。&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;UserFormComponent&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;constructor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;fb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormBuilder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;userForm&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;group&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;control&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;フォームの型指定方法&quot;&gt;フォームの型指定方法&lt;/h2&gt;
&lt;p&gt;上記で作成したフォームに型を指定する方法として、v14以降に導入されたTyped Forms(型付きフォーム)を使用することでより型安全なフォームの実装が可能になります。&lt;/p&gt;

&lt;p&gt;Typed Forms(型付きフォーム)を使っていて最も混乱したのが、&lt;strong&gt;開発者が指定した型と型推論の違い&lt;/strong&gt;でした。
型パラメータやNonNullableFormBuilderを使うことでより開発者が明示的に扱う型を宣言することができ、より型安全なフォームの実装が可能になります。ですが、フォームのFormControlが&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;null&lt;/code&gt;を許容するかどうかによって意図しない型推論が行われることがあるので注意が必要です。&lt;/p&gt;

&lt;p&gt;以下に分けて解説します。&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;nullを許容しない場合&lt;/li&gt;
  &lt;li&gt;nullを意図して許容する場合&lt;/li&gt;
  &lt;li&gt;nullを意図せず許容してしまっている場合&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;nullを許容しない場合&quot;&gt;nullを許容しない場合&lt;/h3&gt;
&lt;p&gt;nullを許容しない場合は、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NonNullableFormBuilder&lt;/code&gt;と型パラメータ&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;T&amp;gt;&lt;/code&gt;使うことでシンプルにより型安全なフォームを作成できます。&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;userForm&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;group&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;control&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;constructor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;fb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;NonNullableFormBuilder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;FormGroup&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;   
    &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormControl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;明示した通り&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;string&amp;gt;&lt;/code&gt;の型パラメータを指定することで、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;null&lt;/code&gt;を許容しない型(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormControl&amp;lt;string&amp;gt;&lt;/code&gt;)として推論されます。&lt;/p&gt;

&lt;h3 id=&quot;nullを許容する場合&quot;&gt;nullを許容する場合&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;null&lt;/code&gt;を許容する場合は、以下のようにFormBuilderと型パラメータ&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;T&amp;gt;&lt;/code&gt;を使ってフォームを作成できます。&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;userForm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormGroup&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormControl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;group&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;control&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;constructor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;fb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormBuilder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;FormGroup&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormControl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;これは記述通りの型推論になっており、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormControl&amp;lt;string | null&amp;gt;&lt;/code&gt;のように&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;null&lt;/code&gt;を許容する型として推論されます。&lt;/p&gt;

&lt;h3 id=&quot;nullを意図せず許容してしまっている場合&quot;&gt;nullを意図せず許容してしまっている場合&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;null&lt;/code&gt;を意図せず許容してしまっている場合は、以下のようにFormBuilderで明示的に&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;string&amp;gt;&lt;/code&gt;の型パラメータを与えているように見えて、内部的なFormControlの仕様によって&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;null&lt;/code&gt;が許容されてしまいます。&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;userForm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormGroup&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormControl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;group&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;control&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;constructor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;fb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormBuilder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;FormGroup&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormControl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fb.control&amp;lt;string&amp;gt;&lt;/code&gt;で型パラメータを&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;string&amp;gt;&lt;/code&gt;で与えているので&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;null&lt;/code&gt;は許容されていないように見えますが、Angulerの&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormControl&lt;/code&gt;の仕様によって、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;null&lt;/code&gt;が許容されてしまいます。&lt;/p&gt;

&lt;p&gt;以下はFormControlの該当する&lt;a href=&quot;https://github.com/angular/angular/blob/5b25d93f27b259098ed968a8a355e9c16867b37b/goldens/public-api/forms/index.api.md?plain=1#L310&quot;&gt;ソースコード&lt;/a&gt;です。&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;control&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;formState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormControlState&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;validatorOrOpts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ValidatorFn&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ValidatorFn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormControlOptions&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;asyncValidator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;AsyncValidatorFn&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;AsyncValidatorFn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormControl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;control&amp;lt;T&amp;gt;&lt;/code&gt;で&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;T&amp;gt;&lt;/code&gt;を型パラメータとして与えても返り値は&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;T | null&amp;gt;&lt;/code&gt;になっていることがわかります。
上記のように型パラメータだけでは&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;null&lt;/code&gt;が許容されている状態となりコンパイルエラーやバグの原因になります。&lt;/p&gt;

&lt;p&gt;これを回避するためにオプション設定を理解しておくことが重要です。&lt;/p&gt;

&lt;h2 id=&quot;オプション設定&quot;&gt;オプション設定&lt;/h2&gt;
&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormControl()&lt;/code&gt; や &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fb.control()&lt;/code&gt; では、第2引数として構成オプションを渡すことができます。
nullを許容しないようにするためには、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nonNullable: true&lt;/code&gt;オプションを指定します。&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;#nullを意図せず許容してしまっている場合&quot;&gt;nullを意図せず許容してしまっている場合&lt;/a&gt;にオプションを追記すると以下のようになります。&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;userForm&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;group&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormControl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;control&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;nonNullable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}),&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;constructor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;fb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormBuilder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;userForm&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;group&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;control&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;nonNullable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;FormGroup&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;   
    &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormControl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; 
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nonNullable: true&lt;/code&gt;を指定することで、返り値の&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormControl&amp;lt;T|null&amp;gt;&lt;/code&gt;の型が&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormControl&amp;lt;T&amp;gt;&lt;/code&gt;に変わり、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;null&lt;/code&gt;を許容しないことで型安全性を高めることができます。&lt;/p&gt;

&lt;p&gt;以下は&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nunNullable: true&lt;/code&gt;を設定した場合の&lt;a href=&quot;https://github.com/angular/angular/blob/5b25d93f27b259098ed968a8a355e9c16867b37b/goldens/public-api/forms/index.api.md?plain=1#L304&quot;&gt;ソースコード&lt;/a&gt;です。
先ほどのnonNullableのオプション設定のないソースコードと比べると、返り値が&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormControl&amp;lt;T|null&amp;gt;&lt;/code&gt;ではなく&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormControl&amp;lt;T&amp;gt;&lt;/code&gt;になっていることがわかります。&lt;/p&gt;
&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;control&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;formState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormControlState&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;opts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormControlOptions&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;nonNullable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormControl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;オプションを使うことで&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;null&lt;/code&gt;を許容しないようにすることはできますが、より型安全なフォームを実装したい場合は&lt;a href=&quot;#nullを許容しない場合&quot;&gt;nullを許容しない場合&lt;/a&gt;でフォーム作成をすることで型安全性を高めることができます。&lt;/p&gt;

&lt;h1 id=&quot;まとめ&quot;&gt;まとめ&lt;/h1&gt;
&lt;p&gt;リアクティブフォームは柔軟性が高いため設計を曖昧にしたまま進めると、型の不一致や型推論のnullによるバグが発生しやすくなる印象を受けました。
そのため最初にフォームオブジェクト+型パラメータ+オプション設定を実装の意図に合わせて組むことが、型安全性とテスト容易性を高めるために重要だと感じました。&lt;/p&gt;

&lt;p&gt;本記事では触れませんでしたが、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;interface&lt;/code&gt; で値の型を明示したり、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormBuilder.nonNullable.group()&lt;/code&gt; を使ったりすることでも型の安全性を担保できます。
またリアクティブフォームに触れる中で、RxJSによる非同期処理やストリームの考え方(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Observable&lt;/code&gt;の&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pipe()&lt;/code&gt;を使ったオペレーター連結など)も非常に新鮮に感じたのでまた紹介できればと思います。&lt;/p&gt;

&lt;h1 id=&quot;おまけ&quot;&gt;おまけ&lt;/h1&gt;
&lt;h2 id=&quot;v13以前での型の戻り値&quot;&gt;v13以前での型の戻り値&lt;/h2&gt;
&lt;p&gt;ブログを書く中でわかったことですがv13以前では&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormGroup&lt;/code&gt;や&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormControl&lt;/code&gt;に対して型パラメータ&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;T&amp;gt;&lt;/code&gt;を与えることができず、戻り値は&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormGroup&amp;lt;any&amp;gt;&lt;/code&gt;と返ってきていたようで完全に型安全にしたい場合は、&lt;a href=&quot;https://github.com/Quramy/ngx-typed-forms&quot;&gt;ngx-typed-forms&lt;/a&gt;などのライブラリを使う必要があったようです。
この問題はv14でTypedFormsが公式に導入されたことで解消され、今は&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormGroup&amp;lt;T&amp;gt;&lt;/code&gt;や&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormControl&amp;lt;T&amp;gt;&lt;/code&gt;による型安全なフォームが標準機能として扱えるようになっています。
詳しくは&lt;a href=&quot;https://github.com/angular/angular/discussions/44513&quot;&gt;こちら&lt;/a&gt;を参照してください。&lt;/p&gt;

</description>
        <pubDate>Fri, 01 Aug 2025 00:00:00 +0900</pubDate>
      </item>
    
      



      <item>
        <title>プロダクトチーム制への移行とユーザー部門との連携強化に取り組みました！ | QUARTETCOM TECH BLOG</title>
        
        
        <category>チーム</category>
        
        <category>組織</category>
        
        <link>https://tech.quartetcom.co.jp/2025/06/27/rebuild-team/</link>
        <guid isPermaLink="true">https://tech.quartetcom.co.jp/2025/06/27/rebuild-team/</guid>
        <description>
&lt;p&gt;今回は、システム開発部が現在取り組んだ「チーム体制の見直し」と「他部署との連携強化」についてご紹介します。&lt;/p&gt;

&lt;h1 id=&quot;前提&quot;&gt;前提&lt;/h1&gt;

&lt;p&gt;私たちは大きさの異なるプロダクトや担当業務を合計で約20ほどを開発・運用しています。そのほとんどがいわゆる Web+RDBMS で構成されるアプリケーションですが一部は Node.JS on AWS Lambda だったり Google Apps Script だったりします。&lt;/p&gt;

&lt;h1 id=&quot;旧体制における課題職能別チームの状況&quot;&gt;旧体制における課題：職能別チームの状況&lt;/h1&gt;

&lt;p&gt;これまでのシステム開発部は、PHP、UI、インフラといった&lt;strong&gt;職能別の専門チーム&lt;/strong&gt;で構成されていました。各チームは、それぞれの専門分野を担当する数名のメンバーで構成されていました。この体制には、いくつかの課題がありました。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/2025-06-27/skill-teams.png&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;課題例1-チーム間での業務量のばらつき&quot;&gt;課題例1: チーム間での業務量のばらつき&lt;/h2&gt;

&lt;p&gt;チームによって担当するタスクの質と量にばらつきが見られました。これにより、メンバー個々の&lt;strong&gt;価値提供の機会に偏り&lt;/strong&gt;が生じ、必ずしも健全とは言えない状況でした。&lt;/p&gt;

&lt;h2 id=&quot;課題例2-プロダクトオーナーシップの不明確さ&quot;&gt;課題例2: プロダクトオーナーシップの不明確さ&lt;/h2&gt;

&lt;p&gt;特定のプロダクトに対し、複数の職能チームのメンバーがその都度対応する形となっていました。そのため、各プロダクトを主体的に担当するメンバーやチームが明確ではなく、結果として&lt;strong&gt;責任の所在が曖昧になる傾向&lt;/strong&gt;が見られました。&lt;/p&gt;

&lt;h1 id=&quot;新体制プロダクトチーム制への移行&quot;&gt;新体制：プロダクトチーム制への移行&lt;/h1&gt;

&lt;p&gt;これらの課題に対応するため、私たちは組織体制を「&lt;strong&gt;プロダクトチーム制&lt;/strong&gt;」へと変更しました。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/2025-06-27/product-teams.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;プロダクトチーム&lt;/strong&gt;にはリーダーが置かれ、会社の役職上のマネージャがその職責を担います。（ちなみに各チーム名はシステム開発部の業務で欠かせない Linux のイメージキャラクタであるペンギンの種別から着想を得たものです）&lt;br /&gt;
各プロダクトチームには、担当するプロダクトや業務範囲が設定されています。&lt;br /&gt;
さらに、各プロダクトや業務範囲に対し、&lt;strong&gt;一人の PdM（プロダクトマネージャ） と一人以上のメンバー&lt;/strong&gt;を明確に割り当てます。&lt;/p&gt;

&lt;p&gt;新体制への移行により、各メンバーの&lt;strong&gt;権限と責任がより明確に&lt;/strong&gt;なりました。これにより、各プロダクトにおけるメンバーの&lt;strong&gt;オーナーシップ意識の醸成&lt;/strong&gt;にも寄与すると考えています。一方で、従来の職能チーム体制では求められなかった、より幅広いスキルセットが各メンバーに求められるようになっています。これは、メンバー一人ひとりが専門分野を超えて成長していくことを前提としており、個人のスキル向上とキャリア形成の一助となると考えています。&lt;/p&gt;

&lt;h2 id=&quot;新体制への過程部門内での検討&quot;&gt;新体制への過程：部門内での検討&lt;/h2&gt;

&lt;p&gt;今回のプロダクトチーム制への移行は、慎重かつ綿密な検討を重ねて行われました。私たちは、どのようなチーム構成が適切か、また各プロダクトをどのチームが担当すべきかについて、適宜さまざまな座組で複数回にわたり検討を行いました。&lt;br /&gt;
最終段階ではメンバー全員による複数回の協議を実施し、結論を出すための最終協議はリモートワークメンバーも含め、全員が出社して対面で意見を交換しました。忌憚のない、お互いを尊重するコミュニケーションを通じて、結論を導き出しました。
検討過程でのコミュニケーションは、「変更されたチーム体制」という成果物以上に、メンバーの相互理解を深め、関係を醸成するという価値を提供してくれるものでした。
この変革は、部長である私から部のメンバーに提起したものですが、各メンバーの建設的で主体的な姿勢があったことで実現できたものと認識しています。大変感謝しています！&lt;/p&gt;

&lt;h1 id=&quot;ユーザー部門との連携強化システム開発における共創&quot;&gt;ユーザー部門との連携強化：システム開発における共創&lt;/h1&gt;

&lt;p&gt;私たちが開発するプロダクトやタスクの大部分は、他部署の業務効率化やサポートを目的としています。そこで、ユーザー部門の従業員にもより明確なオーナーシップを持ってもらえるよう、新たな連携の枠組みを構築し始めています。&lt;br /&gt;
その端緒として、私からシステム開発におけるユーザー部門の従業員の関与の重要性を共有し、&lt;strong&gt;各プロダクトに一人の PO（プロダクトオーナー） の設定&lt;/strong&gt;を依頼しました。&lt;br /&gt;
システム開発部の PdM とユーザー部門の PO が一般的に呼称される役割に限定せず、それぞれの役割を適切に協議し設定することで、プロダクトがユーザーの課題を的確に解決し、さらに効果的に利用されることを目指します。&lt;/p&gt;

&lt;h1 id=&quot;今後の展望&quot;&gt;今後の展望&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;プロダクトチーム制への移行&lt;/strong&gt;と&lt;strong&gt;ユーザー部門との連携強化&lt;/strong&gt;は、システム開発部がより柔軟で、かつ責任を持った組織へと発展するための重要な一歩となります。この変革を通じて、私たちは今後も事業が高品質なサービスを提供するためのシステムを支えていきます！&lt;/p&gt;

&lt;hr /&gt;

</description>
        <pubDate>Fri, 27 Jun 2025 00:00:00 +0900</pubDate>
      </item>
    
  </channel>
</rss>
