Techtouch Developers Blog テックタッチ株式会社の開発チームによるテックブログです 2024-03-11T09:00:00+09:00 techtouch Hatena::Blog hatenablog://blog/26006613575164298 v5 で何が変わる? 非同期状態管理ライブラリ TanStack Query の新機能と最適化 hatenablog://entry/6801883189088597090 2024-03-11T09:00:00+09:00 2024-03-11T09:00:00+09:00 この記事では 2023 年 10 月に正式リリースされた TanStack Query の v5 へのアップデート内容を紹介します! また、その中でも Optimistic Update (楽観的更新)にまつわる内容を深堀りしていこうと思います🧐 <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tocomi/20240307/20240307102859.png" alt="v5 &#x3067;&#x4F55;&#x304C;&#x5909;&#x308F;&#x308B;&#xFF1F; &#x975E;&#x540C;&#x671F;&#x72B6;&#x614B;&#x7BA1;&#x7406;&#x30E9;&#x30A4;&#x30D6;&#x30E9;&#x30EA; TanStack Query &#x306E;&#x65B0;&#x6A5F;&#x80FD;&#x3068;&#x6700;&#x9069;&#x5316;" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <ul class="table-of-contents"> <li><a href="#はじめに">はじめに</a></li> <li><a href="#三行まとめ">三行まとめ</a></li> <li><a href="#TanStack-Query-とは">TanStack Query とは?</a></li> <li><a href="#v5-アップデート内容の紹介">v5 アップデート内容の紹介</a><ul> <li><a href="#hooks-のインターフェースの統一">hooks のインターフェースの統一</a></li> <li><a href="#Suspense-の正式サポート">Suspense の正式サポート</a><ul> <li><a href="#v4">v4</a></li> <li><a href="#v5">v5</a></li> </ul> </li> <li><a href="#Optimistic-Update-をシンプルに">Optimistic Update をシンプルに</a></li> </ul> </li> <li><a href="#そもそも-Optimistic-Update-とは">そもそも Optimistic Update とは?</a><ul> <li><a href="#デモアプリ">デモアプリ</a></li> </ul> </li> <li><a href="#Optimistic-Update-の実装例">Optimistic Update の実装例</a><ul> <li><a href="#v4-1">v4</a></li> <li><a href="#v5-1">v5</a></li> </ul> </li> <li><a href="#まとめ">まとめ</a></li> <li><a href="#参考資料">参考資料</a></li> </ul> <h2 id="はじめに">はじめに</h2> <p>こんにちは!テックタッチでフロントエンドエンジニアをしている tsune です。野球好きの自分は、この時期になるとプロ野球開幕が待ち遠しくなってきます⚾ 開幕戦のチケットも確保したので、当日は会社のメンバーと一緒に神宮球場に行く予定です🥳</p> <p>この記事では 2023 年 10 月に正式リリースされた <a href="https://tanstack.com/query/latest">TanStack Query</a> の v5 へのアップデート内容を紹介します! また、その中でも Optimistic Update (楽観的更新)にまつわる内容を深堀りしていこうと思います🧐</p> <h2 id="三行まとめ">三行まとめ</h2> <ul> <li>hooks のインターフェースが統一された!</li> <li>Suspense の型レベルでのサポートが追加された!</li> <li>Optimistic Update がよりシンプルに実装できるようになった!</li> </ul> <h2 id="TanStack-Query-とは">TanStack Query とは?</h2> <p>TanStack Query (旧 React Query) とは、Web 関連の OSS を開発している <a href="https://tanstack.com/">TanStack</a> から提供されているライブラリの一つです。</p> <p>名前に Query と付くこともあってかデータフェッチ用のライブラリと思われることがあるようですが、実際は TanStack Query 自体がデータフェッチをすることはなく、非同期で取得したデータの状態管理を効率化するのがこのライブラリの目的です。</p> <p>テックタッチでは、API や chrome storage などの非同期処理で取得したデータの管理にこのライブラリを利用しています。使用バージョンはまだ v4 なので、頃合いを見て v5 にアップデートしたいと思っています💪</p> <h2 id="v5-アップデート内容の紹介">v5 アップデート内容の紹介</h2> <p>アップデート内容から、個人的に気になった内容をかいつまんで紹介します。詳細なアップデート内容については<a href="https://tanstack.com/blog/announcing-tanstack-query-v5">公式アナウンス</a>をご確認ください!</p> <h3 id="hooks-のインターフェースの統一">hooks のインターフェースの統一</h3> <p>useQuery や useMutation 等 hooks の引数が object に統一されました。</p> <p>v4 以前では複数の書き方が可能でしたが、これによって多くの<a href="https://typescriptbook.jp/reference/functions/overload-functions">オーバーロード関数</a>が存在していて、メンテナンスコストを増大させていたそうです。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synComment">// 引数は queryKey, queryFn, options の 3 つ</span> <span class="synComment">// v4: ✅ OK</span> <span class="synComment">// v5: 🚨 NG</span> <span class="synType">const</span> useTodo <span class="synStatement">=</span> useQuery<span class="synStatement">(</span> <span class="synIdentifier">[</span><span class="synConstant">'todo'</span><span class="synIdentifier">]</span><span class="synStatement">,</span> <span class="synStatement">()</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synComment">// 何かしらのデータフェッチ処理</span> <span class="synIdentifier">}</span><span class="synStatement">,</span> <span class="synIdentifier">{</span> staleTime: <span class="synConstant">30000</span><span class="synStatement">,</span> <span class="synIdentifier">}</span> <span class="synStatement">)</span> <span class="synComment">// 引数は queryKey, options の 2 つ</span> <span class="synComment">// v4: ✅ OK</span> <span class="synComment">// v5: 🚨 NG</span> <span class="synType">const</span> useTodo <span class="synStatement">=</span> useQuery<span class="synStatement">(</span><span class="synIdentifier">[</span><span class="synConstant">'todo'</span><span class="synIdentifier">]</span><span class="synStatement">,</span> <span class="synIdentifier">{</span> queryFn: <span class="synStatement">()</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synComment">// 何かしらのデータフェッチ処理</span> <span class="synIdentifier">}</span><span class="synStatement">,</span> staleTime: <span class="synConstant">30000</span><span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">)</span> <span class="synComment">// 引数は options のみ</span> <span class="synComment">// v4: ✅ OK</span> <span class="synComment">// v5: ✅ OK</span> <span class="synType">const</span> useTodo <span class="synStatement">=</span> useQuery<span class="synStatement">(</span><span class="synIdentifier">{</span> queryKey: <span class="synIdentifier">[</span><span class="synConstant">'todo'</span><span class="synIdentifier">]</span><span class="synStatement">,</span> queryFn: <span class="synStatement">()</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synComment">// 何かしらのデータフェッチ処理</span> <span class="synIdentifier">}</span><span class="synStatement">,</span> staleTime: <span class="synConstant">30000</span><span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">)</span> </pre> <p>TypeScript 4.7 のアップデートによってインターフェースの統一が可能になり、これらのオーバーロード関数を削除することが出来たとのこと🎉 この Breaking Change に関しては <a href="https://tanstack.com/query/v5/docs/framework/react/guides/migrating-to-v5#codemod">codemod</a> が提供されていました。</p> <p>また、テックタッチでは<a href="https://tanstack.com/query/v4/docs/eslint/prefer-query-object-syntax">公式の ESLint ルール</a>を利用することで、インターフェースの統一を先んじて実施しています。</p> <h3 id="Suspense-の正式サポート">Suspense の正式サポート</h3> <p>v4 でも Suspense はサポートされていましたが、型レベルで Suspense の恩恵を受けられるようになりました🎉</p> <h4 id="v4">v4</h4> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synComment">// options で suspense を有効化出来る</span> <span class="synType">const</span> useTodo <span class="synStatement">=</span> useQuery<span class="synStatement">(</span><span class="synIdentifier">{</span> queryKey: <span class="synIdentifier">[</span><span class="synConstant">'todo'</span><span class="synIdentifier">]</span><span class="synStatement">,</span> queryFn: <span class="synStatement">()</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synComment">// 何かしらのデータフェッチ処理</span> <span class="synIdentifier">}</span><span class="synStatement">,</span> <span class="synIdentifier">{</span> suspense: <span class="synConstant">true</span> <span class="synIdentifier">}</span> <span class="synIdentifier">}</span><span class="synStatement">)</span> <span class="synComment">// Suspense はサポートされるが、</span> <span class="synComment">// data の型は undefined も取るのでコンポーネント側でチェックが必要😭</span> <span class="synType">const</span> SomehingComponent <span class="synStatement">=</span> <span class="synStatement">()</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synType">const</span> <span class="synIdentifier">{</span> data <span class="synIdentifier">}</span> <span class="synStatement">=</span> useTodo<span class="synStatement">()</span> <span class="synComment">// ^ Data | undefined</span> <span class="synStatement">if</span> <span class="synStatement">(</span><span class="synConstant">!</span>data<span class="synStatement">)</span> <span class="synIdentifier">{</span> <span class="synStatement">&lt;</span>p<span class="synStatement">&gt;</span>loading...<span class="synStatement">&lt;</span>/p<span class="synStatement">&gt;</span> <span class="synIdentifier">}</span> <span class="synStatement">return</span> <span class="synStatement">(</span> ... <span class="synStatement">)</span> <span class="synIdentifier">}</span> </pre> <h4 id="v5">v5</h4> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synComment">// Suspense 専用の hook が登場</span> <span class="synType">const</span> useTodo <span class="synStatement">=</span> useSuspenseQuery<span class="synStatement">(</span><span class="synIdentifier">{</span> queryKey: <span class="synIdentifier">[</span><span class="synConstant">'todo'</span><span class="synIdentifier">]</span><span class="synStatement">,</span> queryFn: <span class="synStatement">()</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synComment">// 何かしらのデータフェッチ処理</span> <span class="synIdentifier">}</span><span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">)</span> <span class="synComment">// data の型から undefined が消えるのでコンポーネント側でのチェックが不要🥳</span> <span class="synType">const</span> SomehingComponent <span class="synStatement">=</span> <span class="synStatement">()</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synType">const</span> <span class="synIdentifier">{</span> data <span class="synIdentifier">}</span> <span class="synStatement">=</span> useTodo<span class="synStatement">()</span> <span class="synComment">// ^ Data</span> <span class="synStatement">return</span> <span class="synStatement">(</span> ... <span class="synStatement">)</span> <span class="synIdentifier">}</span> </pre> <h3 id="Optimistic-Update-をシンプルに">Optimistic Update をシンプルに</h3> <p>useMutation から variables という変数が返されるようになり、これを使うことで Optimistic Update のロジックをシンプルに実装できるようになります🎉</p> <p>これについては次のセクションで詳しく見ていきます!</p> <h2 id="そもそも-Optimistic-Update-とは">そもそも Optimistic Update とは?</h2> <p>Optimistic Update とは、非同期処理(主に API 呼び出し)が成功することを前提に、操作を即座に UI に反映する手法です。API のレスポンスを待たないことにより、ユーザーへのフィードバック速度を高めることが出来ます。</p> <p>よく例に挙げられるのは X や Instagram のいいねボタンで、いいねボタンを押したときのハートのインタラクションは、API のレスポンスを待たずに実行されています。</p> <h3 id="デモアプリ">デモアプリ</h3> <p>Optimistic Update を使う場合と使わない場合でユーザーの体験がどのように変わるのか、以下にデモを作成しました。ラジオボタンを変更するとその更新のための API リクエストが飛び、1 秒後にレスポンスが返ってくるという設定です。成功時と失敗時の違いをわかりやすくするためにランダムで失敗するようにしています。</p> <p>実際の API リクエストは基本的に成功することを踏まえると、Optimistic Update を利用したほうがユーザーの操作が即座に UI に反映されて、違和感がないことがわかるかと思います。</p> <p>(わかりやすいように次のリクエストが成功するか失敗するかをあらかじめ表示しています。)</p> <p><iframe src="https://stackblitz.com/edit/tsq-v5-optimistic-update?embed=1&amp;file=src%2FApp.tsx&amp;view=preview" height="400" width="745"></iframe><cite class="hatena-citation"><a href="https://stackblitz.com/edit/tsq-v5-optimistic-update?embed=1&file=src%2FApp.tsx&view=preview">stackblitz.com</a></cite></p> <h2 id="Optimistic-Update-の実装例">Optimistic Update の実装例</h2> <p>上記のサンプルを実装する際に、v4 以前と v5 でどのような変化があるか見てみます。</p> <p>直接関わるロジック部分を抜粋して載せています。</p> <h3 id="v4-1">v4</h3> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> Component <span class="synStatement">=</span> <span class="synStatement">()</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synComment">// 更新中の値を保持するための state</span> <span class="synType">const</span> <span class="synIdentifier">[</span>newValue<span class="synStatement">,</span> setNewValue<span class="synIdentifier">]</span> <span class="synStatement">=</span> useState<span class="synStatement">(</span><span class="synConstant">''</span><span class="synStatement">)</span> <span class="synComment">// API の実行後に、成否にかかわらず更新中の値をリセットする</span> <span class="synType">const</span> onSettled <span class="synStatement">=</span> <span class="synStatement">()</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> setNewValue<span class="synStatement">(</span><span class="synConstant">''</span><span class="synStatement">)</span> <span class="synIdentifier">}</span> <span class="synComment">// useMutation をラップしている hook</span> <span class="synType">const</span> <span class="synIdentifier">{</span> mutate: changeRadio<span class="synStatement">,</span> isError <span class="synIdentifier">}</span> <span class="synStatement">=</span> useChangeRadio<span class="synStatement">(</span><span class="synIdentifier">{</span> onSettled<span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">)</span> <span class="synComment">// useQuery をラップしている hook</span> <span class="synType">const</span> <span class="synIdentifier">{</span> data: valueFromAPi <span class="synIdentifier">}</span> <span class="synStatement">=</span> useRadio<span class="synStatement">()</span> <span class="synComment">// 実際に画面に表示する値</span> <span class="synComment">// エラーが発生したか更新中の値がない場合は API から取得した値を表示</span> <span class="synType">const</span> displayedValue <span class="synStatement">=</span> isError <span class="synConstant">||</span> <span class="synConstant">!</span>newValue ? valueFromApi : newValue <span class="synStatement">return</span> <span class="synStatement">(</span> <span class="synComment">// render component</span> <span class="synStatement">)</span> <span class="synIdentifier">}</span> </pre> <h3 id="v5-1">v5</h3> <p>v5 では、更新対象の値として useMutation から variables を受け取れるようになりました。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> Component <span class="synStatement">=</span> <span class="synStatement">()</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synComment">// useMutation をラップしている hook</span> <span class="synComment">// v5 から追加された variables が更新中の値を保持している</span> <span class="synType">const</span> <span class="synIdentifier">{</span> mutate: changeRadio<span class="synStatement">,</span> variables<span class="synStatement">,</span> isError <span class="synIdentifier">}</span> <span class="synStatement">=</span> useChangeRadio<span class="synStatement">(</span><span class="synIdentifier">{}</span><span class="synStatement">)</span> <span class="synComment">// useQuery をラップしている hook</span> <span class="synType">const</span> <span class="synIdentifier">{</span> data: valueFromApi <span class="synIdentifier">}</span> <span class="synStatement">=</span> useRadio<span class="synStatement">()</span> <span class="synComment">// 実際に画面に表示する値</span> <span class="synComment">// エラーが発生したか更新中の値がない場合は API から取得した値を表示</span> <span class="synType">const</span> displayedValue <span class="synStatement">=</span> <span class="synStatement">(</span>isError <span class="synConstant">||</span> <span class="synConstant">!</span>variables<span class="synStatement">)</span> ? valueFromApi : variables <span class="synStatement">return</span> <span class="synStatement">(</span> <span class="synComment">// render component</span> <span class="synStatement">)</span> <span class="synIdentifier">}</span> </pre> <p>v4 と比較してみると、v5 では更新中の値の state や手続き的なコードが消えて、ロジックがスッキリしたことが見て取れます!</p> <h2 id="まとめ">まとめ</h2> <p>以上、TanStack Query v5 の新機能とその中でも Optimistic Update に関わる変更を詳しく見ていきました。</p> <p>今回は詳しく取り上げませんでしたが、<a href="https://tkdodo.eu/blog/breaking-react-querys-api-on-purpose">useQuery からの callback 削除とそれについての開発者の見解</a>も面白い内容だったので、ぜひご一読ください👀</p> <p>僕からは以上!</p> <h2 id="参考資料">参考資料</h2> <ul> <li><a href="https://medium.com/@kyledeguzmanx/what-are-optimistic-updates-483662c3e171">What Are Optimistic Updates?. How Optimistic Updates May Improve…</a></li> <li><a href="https://tanstack.com/query/v5/docs/framework/react/guides/optimistic-updates">Optimistic Updates</a></li> <li><a href="https://tanstack.com/query/v4/docs/eslint/prefer-query-object-syntax">Prefer object syntax for useQuery</a></li> </ul> tocomi デザインプロセスに生成 AI を取り入れる実験 ~プロンプト全公開~ hatenablog://entry/6801883189088073805 2024-03-05T09:00:00+09:00 2024-03-06T10:29:15+09:00 生成AIを業務にどう取り入れるか、デザインチームで試行錯誤した記録です。 <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/keita03030303/20240304/20240304112507.png" alt="デザインプロセスに生成 AI を取り入れる実験 ~プロンプト全公開~" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image" /></p> <p>デザイナーの keita です。 年始にベンチプレスの MAX 測定をしたら 105kg でした。今年の目標は 120kg です。</p> <p>この記事では、生成 AI をデザインプロセスに取り入れるためにチームで試行錯誤した内容を紹介します。ぜひ、最後までお付き合いください。</p> <ul class="table-of-contents"> <li><a href="#きっかけ">きっかけ</a></li> <li><a href="#はじめに">はじめに</a></li> <li><a href="#AI-を使ってみる">AI を使ってみる</a><ul> <li><a href="#インタビューデータからペルソナを作成">インタビューデータからペルソナを作成</a><ul> <li><a href="#まずは-1-人ずつ整理">まずは 1 人ずつ整理</a></li> <li><a href="#1-人のペルソナに統合">1 人のペルソナに統合</a></li> </ul> </li> <li><a href="#ペルソナの課題抽出">ペルソナの課題抽出</a></li> <li><a href="#リサーチを-AI-で代替してみた結果">リサーチを AI で代替してみた結果</a></li> </ul> </li> <li><a href="#AI-が作成したペルソナを使ってみる">AI が作成したペルソナを使ってみる</a><ul> <li><a href="#ペルソナにヘルプセンターの記事を評価させてみる">ペルソナにヘルプセンターの記事を評価させてみる</a></li> <li><a href="#結果">結果</a></li> </ul> </li> <li><a href="#最後に">最後に</a></li> <li><a href="#あとがき">あとがき</a><ul> <li><a href="#ボツ案">ボツ案</a><ul> <li><a href="#文字起こしデータから記事を作成">文字起こしデータから記事を作成</a></li> <li><a href="#出力された記事のトーンを変更">出力された記事のトーンを変更</a></li> <li><a href="#出力された記事">出力された記事</a></li> </ul> </li> </ul> </li> </ul> <h2 id="きっかけ">きっかけ</h2> <p>「ChatGPT をはじめとした AI ツールが便利なのはわかったけど、自分たちの業務をより効率化するためにデザインプロセスで代替できるところないかな?」というデザインチーム内での会話から今回の取り組みがスタートしました。</p> <h2 id="はじめに">はじめに</h2> <p>まずはじめに、デザインプロセスにおいて、どこにコストがかかるのかを話し合いました。</p> <p>UX の 5 段階モデルの戦略〜表層に照らし合わせると「戦略のリサーチ部分が 1 番コストかかるかもね」という結論になりました。</p> <figure class="figure-image figure-image-fotolife mceNonEditable" title="引用:KA法とは?インタビューなどの定性調査で得たデータをまとめる手法を紹介"> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/keita03030303/20240304/20240304111723.jpg" width="1024" height="693" loading="lazy" title="" class="hatena-fotolife" itemprop="image" /></p> <figcaption class="mceEditable">引用:<a href="https://brik.co.jp/tips/4850" target="_blank" rel="noopener">ギャレットの5段階モデルの図解</a></figcaption> </figure> <p>インタビューをして、その内容を文字起こし。文字起こしデータから KA 法で価値を抽出し、KJ法で整理。その結果をもとにペルソナとカスタマージャーニーマップを作成。</p> <blockquote> <p>KA法とは、定性調査で明らかにした顧客の声や行動・体験などの「質的データ」を分析・モデリングし、本質的なニーズやユーザーのインサイトを明らかにしていく手法です。 UXデザインやUXリサーチの現場でよく活用されており、考案者の浅田 和実氏のイニシャルから命名されました。</p> <p>引用:<a href="https://www.unprinted.design/articles/ka-method/#:~:text=KA%E6%B3%95%E3%81%A8%E3%81%AF%E3%80%81%E5%AE%9A%E6%80%A7,%E3%81%8B%E3%82%89%E5%91%BD%E5%90%8D%E3%81%95%E3%82%8C%E3%81%BE%E3%81%97%E3%81%9F%E3%80%82" target="_blank" rel="noopener">KA法とは?インタビューなどの定性調査で得たデータをまとめる手法を紹介</a></p> </blockquote> <p>半年ほど前に7名分(各 1 時間)のインタビューデータをもとに、上記のプロセスをデザインチーム 3 名で実施しました。KA 法で価値を抽出するだけでも時間がかかりましたが、インタビュー対象者 7 名 * デザイナーチーム 3 名ということで大量の KA カードができてしまい、それの擦り合わせと整理にもとても時間がかかりました。</p> <h2 id="AI-を使ってみる">AI を使ってみる</h2> <p>上記のプロセスを順番に AI で実施してみました。</p> <p>使用するインタビューデータは、実際にペルソナ、カスタマージャーニーマップまで作成したときと同じデータを使いました。そうすることで、自分たちが作成したものと AI が作成したものの差分が明確になると考えたからです。</p> <h3 id="インタビューデータからペルソナを作成">インタビューデータからペルソナを作成</h3> <h4 id="まずは-1-人ずつ整理">まずは 1 人ずつ整理</h4> <pre><code>#お願い 以下の #文章 からペルソナを構築する要素を抽出してください。 #目的 #文章 はとあるインタビューを文字起こししたものです。<br />そこから抽出した要素を使って、ペルソナを作ろうとしています。<br />そのために過不足のないよう、要素を抽出する必要があります。 #ルール 抽出するペルソナはインタビュイーのみ。 要素は以下の ##箇条書きのルール で出力すること。 ##箇条書きのルール - 以下の項目で書き出してください -- (1)名前 -- (2)性別 -- (3)年齢 -- (4)職業 -- (5)業務内容 -- (6)このペルソナにキャッチコピーをつけるとしたら -- (7)ライフゴール(長期的に達成したいゴール) -- (8)エンドゴール(ライフゴールを達成するために具体的に努力していること) -- (9)キーファインディングス(ライフゴール、エンドゴールを導くために使った気づきやペルソナのインサイト) - (5),(6),(7),(8),(9)は複数項目書き出してください - (9)は最低10個書き出してください - (1)は架空のものを設定してください #文章 #出力 </code></pre> <p></p> <h4 id="1-人のペルソナに統合">1 人のペルソナに統合</h4> <pre><code>#お願い 以下の #ペルソナ(1) #ペルソナ(2) #ペルソナ(3) #ペルソナ(4) #ペルソナ(5) #ペルソナ(6) #ペルソナ(7)を統合して、1人のペルソナに仕立ててください。 #ルール ペルソナの要素に加えて、そのペルソナがどんな課題を抱えているかも抽出してください。 #ペルソナ(1) #ペルソナ(2) #ペルソナ(3) #ペルソナ(4) #ペルソナ(5) #ペルソナ(6) #ペルソナ(7) </code></pre> <h3 id="ペルソナの課題抽出">ペルソナの課題抽出</h3> <pre><code>#お願い 以下の #インタビュー から、インタビュイーが抱えている課題を分析してください。<br />分析の際は #ルール に則ってください。 #ルール - #出力 は箇条書きにしてください - 最低10個以上出してください - 課題の重要度によって、以下の3つのレベルに振り分けてください。 -- (最重要) -- (重要) -- (やや重要) #インタビュー #出力 </code></pre> <h3 id="リサーチを-AI-で代替してみた結果">リサーチを AI で代替してみた結果</h3> <p>実際のペルソナや課題は掲載できませんが、自分たちで作成したものと比較してまったく同じものができたかというと、答えは NO です。AI が作成したものは、基本的には我々のプロダクトのペルソナからズレてはいないのですが、感情の細かい部分というか、いい意味で人によって捉え方が変わる部分というか、そういう要素が省かれてしまい、一般的なものによってしまうなという印象をもちました。</p> <p>しかし、実際には何日もかかる業務を数時間で終えることができたので、これくらいの精度のものができるのであれば業務で利用するのはアリだなと思いました。</p> <h2 id="AI-が作成したペルソナを使ってみる">AI が作成したペルソナを使ってみる</h2> <p>ペルソナを作っただけでは、AI によってデザインプロセスの一部を代替できたというには少し物足りない気がしたので、もう一歩先まで踏み込んでみました。</p> <h3 id="ペルソナにヘルプセンターの記事を評価させてみる">ペルソナにヘルプセンターの記事を評価させてみる</h3> <p>別件で、数ヶ月前からヘルプセンターの再設計プロジェクトを進めていました。 サイト自体の再設計は完了したんですが、まだ新しい記事フォーマットでリライトできていない記事があったので、リライト前後でどちらがペルソナにとって適した記事かを評価してもらうことにしました。</p> <pre><code>#お願い 添付のPDFは #テックタッチ というDAP SaaSの「イベント分析」という機能についてのヘルプ記事です。 「イベントでできること_after.pdf」と「イベントでできること_before.pdf」の二つの記事を #ペルソナ と #ペルソナの課題 をもとにどちらがペルソナに適しているかチェックし結果を日本語で出力してください。<br />その結果に至った観点も出力してください。 #テックタッチ &lt;https://techtouch.jp/&gt; #ペルソナ #ペルソナの課題 #出力 </code></pre> <h3 id="結果">結果</h3> <pre><code>"イベントでできること_before.pdf"および"After.pdf"の両ドキュメントの内容を検討した結果、ペルソナにとって"イベントでできること_after.pdf"がより適していると判断しました。 考慮点 業務効率化の必要性:ペルソナはマニュアル作成やデータ分析、RPAの導入など、業務の効率化を重視しています。<br />"after"ドキュメントには、イベントのデータ集計開始タイミング、条件変更の方法、イベントの複製や計測終了機能などが記載されており、これらの機能は効率的なデータ管理と作業の簡素化に寄与します。 データの可視化と分析の重要性:ペルソナはデータ収集と分析を業務改善の鍵と見なしています。<br />"after"ドキュメントは、より詳細なイベント分析機能を提供し、インタラクション数やユーザー数などの指標を明確にしています。<br />これはデータ駆動型の意思決定に有用です。 ユーザーインタフェースの改善:ペルソナはUIの問題点の解決を重視しています。<br />"after"ドキュメントでは、イベントの条件設定や変更の手順がより明確にされており、ユーザーフレンドリーなインターフェースの改善が見受けられます。 顧客対応の効率化:ペルソナは顧客対応の効率と質の向上を目指しています。<br />"after"ドキュメントに示されている機能は、顧客の行動をより詳細に分析し、迅速な対応を可能にします。 以上の点から、"イベントでできること_after.pdf"の方が、ペルソナのニーズや業務目標に合致していると結論付けられます。 </code></pre> <p>リライト後の記事の方がペルソナに適しているという評価が出力されました。<br />リライトということもあり、足りない内容が補足されていたり文章構造としても正しく書けていたということが評価されたのでは?という点は否めませんが、ただ文章がペルソナにとって適しているという結果は得られたので、実験は成功とします。<br />誰かにレビューをお願いする前に一度 AI レビューを通しておくことで、コストは削減できる手応えはありました。</p> <p>今回はペルソナに適しているかどうかを評価してもらいましたが、文章のレビューであれば校正・校閲についての観点もプロンプトに盛り込んでもいいかなと、これを書きながら思いました。</p> <h2 id="最後に">最後に</h2> <p>今回、デザインチームで AI を使ってデザインプロセスを効率化しようと実験してみましたが、AI の進化の速度は我々の想像をはるかに超えてくるので、UX の 5 段階モデルすべてが AI によって代替される未来も近いのかもしれません。ペルソナや課題をもとに AI が Figma のデザインシステムを使って UI 作成しユーザビリティテストで評価。これができたらすごいですね(もうできていたらすいません🙇‍♂️)</p> <p>AI を業務で利活用できるようデザインプロセスに取り込んでいきながら、人間のデザイナーにしかできないことは何かを少し考えておこうと思います。</p> <h2 id="あとがき">あとがき</h2> <p>本当は、この記事のネタをまとめるチームミーティングを録画、文字起こしして記事の骨子を AI に考えさせ、結果を自分が書いた文章のように出力。それをそのまま記事にして「どうでした?実はこれ AI が書いた記事なんですよ」で締めくくりたかったんですが、そんなうまくはいきませんね。</p> <p>私の芥川賞へ道のりはなかなか長そうです。</p> <h3 id="ボツ案">ボツ案</h3> <h4 id="文字起こしデータから記事を作成">文字起こしデータから記事を作成</h4> <pre><code>添付したファイルはデザイナーの3人が、AIを活用した記事を書くMTGを文字起こししたデータです。<br />なお、文字起こしデータは完璧ではない可能性があるので、適宜補正しながら読み込むようにしてください。 あなたは一流の編集者として、この会話から記事の構成を考え、#出力形式 にならって5000文字以上の日本語で記事を出力してください。 #出力形式 - 目次 - 見出し - テキスト - 見出し - テキスト - 見出し - テキスト #出力結果 </code></pre> <h4 id="出力された記事のトーンを変更">出力された記事のトーンを変更</h4> <pre><code>#参考記事 を参照し、同じ著者が書いたように修正し出力してください。 #参考記事 </code></pre> <h4 id="出力された記事">出力された記事</h4> <pre><code>### 目次 1. 序章:AIと私たちの日常 2. ペルソナとは?AIの力で深掘り 3. 記事作成の冒険:AIを使った新たな試み 4. ユーザビリティ評価:ペルソナを軸に見る世界 5. 協働の未来:AIとデザイナーの共演 6. 結び:AIと共に進むデザインの旅路 ### 序章:AIと私たちの日常 遥か昔、AIはSF映画の中の話でした。<br /></code><code>でも今、AIは私たちの日常に溶け込み、生活を豊かにしてくれています。<br />朝起きて最初に目にするニュースフィードも、AIのおかげ。<br />そんなAIが、今やデザインプロセスの変革をもたらしています。 ### ペルソナとは?AIの力で深掘り 「ペルソナ」ってよく聞くけど、実はみんながみんな、ペルソナのことをしっかり理解してるわけじゃないんですよね。<br />ここでAIの出番です。<br />AIを使って、ユーザーのデータからリアルなペルソナを作り出すことができるんです。<br />このペルソナが、私たちのデザインをより具体的に、よりリアルにしてくれます。 ### 記事作成の冒険:AIを使った新たな試み 記事を書くって、実は結構大変。<br />でもAIがいれば、ペルソナに基づいてコンテンツを生成したり、スタイルを調整したりすることができます。<br />想像してみてください、あなたが記事の一部始終をAIと共に創り出す様子を。<br />これがまさに、私たちが今取り組んでいる新たな試みです。 ### ユーザビリティ評価:ペルソナを軸に見る世界 作った記事が実際にユーザーにとって使いやすいかどうか、これ重要ですよね。<br />AIはこのユーザビリティ評価のプロセスにも役立っています。<br />ペルソナを基に、記事が本当にターゲットに刺さるものになっているかをチェックするわけです。 ### 協働の未来:AIとデザイナーの共演 AIの登場で、デザイナーの仕事がなくなるんじゃないかって心配されることもあるけど、実は全くの杞憂。<br />AIは私たちの仕事を奪うんじゃなくて、よりクリエイティブな仕事に集中できるようサポートしてくれるんです。<br />AIとデザイナー、この最強タッグで、未来のデザインを創り上げていきます。 ### 結び:AIと共に進むデザインの旅路 私たちのデザインの旅路は、AIと共によって形成されています。<br />AIと人間の協働の未来は、共に創造的な作業を推進し、デザインプロセスを革新することで、新しい価値を生み出していくことです。<br />AIの技術進化によって、デザイナーはより創造性を発揮しやすい環境が整い、人間とAIがそれぞれの強みを活かしながら共に働くことが可能になります。<br />この協働によって、未来のデザインプロセスはより効率的かつ革新的なものになると期待されています。 </code></pre> <p></p> keita03030303 セキュリティインシデントから会社を守る!~小さく始めるCSIRT設計事例~ hatenablog://entry/6801883189077980004 2024-02-01T09:00:00+09:00 2024-02-01T09:00:04+09:00 テックタッチにおいて CSIRT を設立した経緯について、立ち上げまでの準備や設計について記述します。 <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20240129/20240129125734.png" alt="" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <ul class="table-of-contents"> <li><a href="#はじめに">はじめに</a></li> <li><a href="#CSIRT-とは">CSIRT とは</a></li> <li><a href="#CSIRT-を立ち上げるモチベーション">CSIRT を立ち上げるモチベーション</a><ul> <li><a href="#セキュリティインシデントは避けられない">セキュリティインシデントは避けられない</a></li> <li><a href="#意思決定者の訓練機会の不足">意思決定者の訓練機会の不足</a></li> <li><a href="#世界的なインシデントレスポンスの重要性の高まり">世界的なインシデントレスポンスの重要性の高まり</a></li> </ul> </li> <li><a href="#CSIRT-作りの準備">CSIRT 作りの準備</a><ul> <li><a href="#教科書に学ぶ">教科書に学ぶ</a></li> <li><a href="#他社に学ぶ">他社に学ぶ</a></li> </ul> </li> <li><a href="#テックタッチにおける-CSIRT-の設計">テックタッチにおける CSIRT の設計</a><ul> <li><a href="#スモールスタートするための責任境界">スモールスタートするための責任境界</a></li> <li><a href="#ちゃんと機能するハンドリングマニュアル">ちゃんと機能する!ハンドリングマニュアル</a></li> <li><a href="#窓口の明確化">窓口の明確化</a></li> <li><a href="#継続的なスペシャリティの維持">継続的なスペシャリティの維持</a></li> </ul> </li> <li><a href="#さいごに">さいごに</a></li> <li><a href="#参考">参考</a></li> </ul> <h2 id="はじめに">はじめに</h2> <p>こんにちは。SRE 兼 CSIRT の izzii(<a href="https://twitter.com/ahneahneahne">𝕏)</a>です。<a href="https://flatt.tech/news/231225_flatt-security-mini-ctf-4">Flatt Security mini CTF #4</a> に参加して入賞景品のTシャツをゲットできたのが最近のプチ自慢です。</p> <p>さて本日の記事は、テックタッチにおいて私含む現場のメンバー(izzii, kacchan, ue)が Computer Security Incident Response Team (CSIRT) を設立した経緯について、立ち上げまでの準備や設計について記述します。CSIRT 立ち上げに関心を持つ方、セキュリティインシデント対応に不安を感じている方が、<a href="https://www.jpcert.or.jp/csirt_material/">CSIRTマテリアル</a>や有償の書籍といった教科書と併せて、具体例として参考にしていただけると幸いです。</p> <h2 id="CSIRT-とは">CSIRT とは</h2> <p><a href="https://www.nca.gr.jp/">日本シーサート協議会</a>から定義を引用します。</p> <blockquote><p>シーサート (CSIRT: Computer Security Incident Response Team) とは、コンピュータセキュリティにかかるインシデントに対処するための組織の総称です。インシデント関連情報、脆弱性情報、攻撃予兆情報を常に収集、分析し、対応方針や手順の策定などの活動をします。</p></blockquote> <p>CSIRT はコンピュータセキュリティのインシデントに関して学び、対応するため専門チームだと定義されています。実際にはより多くの機能を備えた組織を CSIRT と称している場合もあり、会社によって役割にバラツキはあります。 イメージしやすいようにより具体的にセキュリティインシデントを例示すると、</p> <ul> <li>PC 端末などの紛失</li> <li>利用しているシステムからの情報流出</li> <li>電子メールの誤送信</li> </ul> <p>などです。</p> <h2 id="CSIRT-を立ち上げるモチベーション">CSIRT を立ち上げるモチベーション</h2> <p>CSIRT を設立しようと考えたきっかけは、他社のインシデント対応の失敗を目の当たりにしたことです。インシデント対応の不十分さが企業の信頼を大きく揺るがしうることを学び、対策の必要性を痛感しました。同様の失敗でテックタッチの信用が失われるのは絶対に避けたい、というが根底のモチベーションです。その他に CSIRT が組織にとって必要だと考える理由は以下です。</p> <h3 id="セキュリティインシデントは避けられない">セキュリティインシデントは避けられない</h3> <p>小さなインシデントを含めれば、人や IT 資産が存在すれば必ず起こります。規模の大小に関わらずどの企業でも、SNS やニュースで話題になるレベルのインシデントが起こり得ます。</p> <h3 id="意思決定者の訓練機会の不足">意思決定者の訓練機会の不足</h3> <p>インシデントは災害と一緒で、訓練をしなければ最善の行動を取ることは難しいはずです。その上でサイバー空間の脅威は常に変化します。専門チームによって意思決定者をサポートする必要があります。</p> <h3 id="世界的なインシデントレスポンスの重要性の高まり">世界的なインシデントレスポンスの重要性の高まり</h3> <p>例えば GDPR では個人情報の漏洩の検知から報告まで 72 時間という制限を設けています(<a href="https://www.soumu.go.jp/johotsusintokei/whitepaper/ja/h30/html/nd112240.html">サイバーセキュリティに関する国際動向</a>)。日本でも 3 日から 5 日以内の告知が要求されています( <a href="https://www.ppc.go.jp/news/kaiseihou_feature/roueitouhoukoku_gimuka/">漏えい等報告・本人への通知の義務化について</a>)。さらに台湾では上場企業がサイバー侵害を受けた場合、30 分以内に Taiwan Stock Exchange (TWSE) と Financial Services Commission (FSC) に報告する義務を課し始めました(<a href="https://euroview.ecct.com.tw/category-inside.php?id=1064">Cybersecurity in Taiwan</a>)。今後も世界中で特にインシデントレスポンスのスピードの水準は高まっていくものと思われます。</p> <h2 id="CSIRT-作りの準備">CSIRT 作りの準備</h2> <h3 id="教科書に学ぶ">教科書に学ぶ</h3> <p>上記の通り CSIRT によるインシデントレスポンス機能が会社にとって必要だと想像はしていましたが、改めて自分達の仮説を客観的に評価したいと考え、調査しました。我々が目を通した中では、<a href="https://www.ipa.go.jp/security/economics/checktool.html">サイバーセキュリティ経営可視化ツール</a>と<a href="https://isog-j.org/output/2023/Textbook_soc-csirt_v3.html">セキュリティ対応組織の教科書</a>を用いるのが、定番かもしれませんがオススメです。まず<a href="https://www.ipa.go.jp/security/economics/checktool.html">サイバーセキュリティ経営可視化ツール</a>を用いることで、客観的に自分達の会社に不足しているセキュリティ機能をレーダーチャートの形であぶり出すことができます。</p> <p><figure class="figure-image figure-image-fotolife" title="[サイバーセキュリティ経営可視化ツール](https://www.ipa.go.jp/security/economics/checktool.html)に掲載されている出力のサンプルを引用しました。"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20240126/20240126194515.png" alt="" width="1200" height="677" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption><a href="https://www.ipa.go.jp/security/economics/checktool.html">サイバーセキュリティ経営可視化ツール</a>に掲載されている出力のサンプルを引用しました。</figcaption></figure></p> <p>さらに<a href="https://isog-j.org/output/2023/Textbook_soc-csirt_v3.html">セキュリティ対応組織の教科書</a>では、会社が持つべきセキュリティ機能が分類されているだけでなく、それらの優先度も記載されており、インシデント対応を整理することが今のテックタッチにとって重要であることを客観的に把握することができました。</p> <p><figure class="figure-image figure-image-fotolife" title="[セキュリティ対応組織の教科書](https://isog-j.org/output/2023/Textbook_soc-csirt_v3.html)に掲載されている機能分類の図を引用しました。破線で囲まれたものは対応の優先度が高い基本機能です。"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20240126/20240126192604.png" alt="" width="1124" height="804" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption><a href="https://isog-j.org/output/2023/Textbook_soc-csirt_v3.html">セキュリティ対応組織の教科書</a>に掲載されている機能分類の図を引用しました。破線で囲まれたものは対応の優先度が高い基本機能です。</figcaption></figure></p> <p>こうした専門家による資料は経営層に対する説明資料にも活用することができました。</p> <h3 id="他社に学ぶ">他社に学ぶ</h3> <p>教科書とリアルの違いを学ぶために、 エンタープライズ SaaS の大先輩である Cybozu の CSIRT 組織 <a href="https://www.cybozu.com/jp/productsecurity/management/cysirt.html">Cy-SIRT</a> にお声がけし、オンライン MTG の時間をとって頂いた上で、実際の CSIRT 運用並びにセキュリティ組織の成り立ちなどをご教示いただけました。併せて僕らの仮説をぶつけて、答え合わせをすることもできました。この場で日本シーサート協議会や SaaS セキュリティの会といったコミュニティを紹介していただき、生の現場の声にたくさん触れることのできる機会をいただけました。</p> <h2 id="テックタッチにおける-CSIRT-の設計">テックタッチにおける CSIRT の設計</h2> <h3 id="スモールスタートするための責任境界">スモールスタートするための責任境界</h3> <p>まずは責任境界を明確にしました。設計のゴールを定義することと、外部から見た際に一貫性のある役割が期待できる組織にしたかったことが理由です。テックタッチの CSIRT の役割は、主体的な実行者ではなく、最適な行動を促すコンサルタントと位置付けました。</p> <p><figure class="figure-image figure-image-fotolife" title="セキュリティインシデント時における責任境界"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20240126/20240126200544.png" alt="" width="1200" height="745" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>セキュリティインシデント時における責任境界</figcaption></figure></p> <p>インシデント事象は個別で見るとハイコンテクストであり、セキュリティに関する知識だけでは対応できません。横断的に人員を集めれば役割を全部盛りできるかもしれませんが、モチベーションの高いメンバーだけの組織としてスタートしたかったのです。現状はこれで十分機能しています。</p> <p>注意ですが、予防的措置についてはテックタッチの CSIRT で役割を持っていません。別の組織が主体となっています(所属メンバーは結構被ってるんですけどね)。予防的措置を実施する主体との責任境界の設計も重要だと思います。</p> <p><figure class="figure-image figure-image-fotolife" title="予防的措置に関する責任境界"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20240126/20240126192848.png" alt="" width="1200" height="481" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>予防的措置に関する責任境界</figcaption></figure></p> <h3 id="ちゃんと機能するハンドリングマニュアル">ちゃんと機能する!ハンドリングマニュアル</h3> <p>絶対に回るマニュアルを作成するために</p> <ul> <li>基本インシデント対応マニュアル</li> <li>応用インシデント対応マニュアル</li> </ul> <p>の 2 種類を作成しました。先に「応用インシデント対応マニュアル」の説明をしますが、こちらには、我々の考える、テックタッチで今後起こる可能性の高いインシデント 10 選を、PR チームと机上訓練を行った上で清書した資料になります。なぜ PR チームかというと、セキュリティインシデントが重篤化した際に、一番難易度が高いのは情報発信だと考えたからです。お互い認識を合わせることで、インシデントの正しい沈静化をシミュレートしました。発生するインシデントはパレートの法則に従い、大半は同様のインシデントです。よってこのマニュアルによってカバーされる想定です。セキュリティインシデントをテーマにしたボードゲームからネタを仕入れたりしました。</p> <p>もう 1 つの「基本インシデント対応マニュアル」は、<a href="https://www.jpcert.or.jp/csirt_material/operation_phase.html">CSIRTマテリアル 運用フェーズ</a>に掲載されている資料や<a href="https://www.meti.go.jp/press/2022/03/20230308006/20230308006.html">サイバー攻撃被害に係る情報の共有・公表ガイダンス</a>から集めた情報をテックタッチに合う形にまとめた資料になります。目的は「応用インシデント対応マニュアル」では漏れてしまうインシデントの際に指針が必要だと考えたからです。こうした資料は長大であり、どうしても IT スタートアップ企業という特殊な条件ではそのまま利用できないため、テックタッチで実際に利用できる形にカスタマイズしました。</p> <h3 id="窓口の明確化">窓口の明確化</h3> <p>最後にセキュリティインシデント報告の窓口を設計しました。Slack チャネルです。混乱を避けるために、インシデントだと思えば投稿して良いという方針にしました。セキュリティインシデントでない場合は他チームに回すだけになるのですが。セキュリティインシデントかわからない場合に報告を躊躇してほしくないためです。例えば社員や会社が SNS で炎上したような場合は判断が難しいでしょう。</p> <h3 id="継続的なスペシャリティの維持">継続的なスペシャリティの維持</h3> <p>これはまだ回りきっていません!(なのにこんな記事書いちゃってすみません)<a href="https://www.nca.gr.jp/">日本シーサート協議会</a>などのコミュニティに入り込んでいくことで生の情報収集サイクルを加速させていきます。<a href="https://www.nca.gr.jp/">日本シーサート協議会</a>に参加するためには加盟組織による推薦が必要なのですが、こちらは Cy-SIRT にお願いさせていただきました。本当に何から何まで感謝です。</p> <h2 id="さいごに">さいごに</h2> <p>CSIRT の立ち上げのモチベーション、準備、設計について解説しました。本来ならば、その効果についても書きたいところですが、機密に触れるのでやめます笑 この文章が、誰かの参考資料となれば幸いです。諸々をセキュリティベンダーに任せることも可能だったかもしれませんが、やはり自分たちで組織を一から設計するのは楽しかったです。急成長する組織において、セキュリティは改善を続けなければ持続不可能だと思っているので、これからも改善を続けたいと思います。</p> <p>改めて Cybozu の Cy-SIRT の皆様には心から感謝の意を表明します。我々も Cy-SIRT のように業界の発展に寄与できるように精進します。最後にささやかなお礼も込めて Cy-SIRT(= Cybozu のセキュリティ室)の自己紹介記事をシェアします。</p> <p><a href="https://note.com/security_cybozu/n/n205795917f1f">https://note.com/security_cybozu/n/n205795917f1f</a><cite class="hatena-citation"><a href="https://note.com/security_cybozu/n/n205795917f1f">note.com</a></cite></p> <h2 id="参考">参考</h2> <p><a href="https://www.jpcert.or.jp/csirt_material/">CSIRTマテリアル</a><br/> <a href="https://www.nca.gr.jp/">日本シーサート協議会</a><br/> <a href="https://www.soumu.go.jp/johotsusintokei/whitepaper/ja/h30/html/nd112240.html">サイバーセキュリティに関する国際動向</a><br/> <a href="https://www.ppc.go.jp/news/kaiseihou_feature/roueitouhoukoku_gimuka/">漏えい等報告・本人への通知の義務化について</a><br/> <a href="https://euroview.ecct.com.tw/category-inside.php?id=1064">Cybersecurity in Taiwan</a><br/> <a href="https://www.ipa.go.jp/security/economics/checktool.html">サイバーセキュリティ経営可視化ツール</a><br/> <a href="https://isog-j.org/output/2023/Textbook_soc-csirt_v3.html">セキュリティ対応組織の教科書</a><br/> <a href="https://www.cybozu.com/jp/productsecurity/management/cysirt.html">Cy-SIRT</a><br/> <a href="https://www.meti.go.jp/press/2022/03/20230308006/20230308006.html">サイバー攻撃被害に係る情報の共有・公表ガイダンス</a></p> izzii 心理的安全性の高い職場はどのように作られているか? hatenablog://entry/6801883189067121503 2024-01-22T08:00:00+09:00 2024-01-22T08:00:15+09:00 テックタッチは 2023 年 8 月にドクタートラスト社実施の「ストレスチェック」で 957 社中 2 位を獲得しました。 この記事では心理的安全性が高い職場の内側はどのようになっているのか、そしてどのように作られているのかを具体的に解説します。 現在の職場で心理的安全性をどのように作れば良いか悩まれている人や、テックタッチの職場をもっと知りたい人の参考になれば幸いです。 <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yhoriuchi/20231219/20231219135931.png" alt="" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>テックタッチのエンジニアリングマネージャー堀内です。<br/> テックタッチに入社して以来、職場の心理的安全性が非常に高いと感じています。<br/> 私は数社を渡り歩いてエンジニアリングマネージャーを 10 年以上経験していますが、このような組織に出会ったことがありません。<br/> この記事では心理的安全性が高い職場の内側はどのようになっているのか、そしてどのように作られているのかを具体的に解説します。<br/> 現在の職場で心理的安全性をどのように作れば良いか悩まれている人や、テックタッチの職場をもっと知りたい人の参考になれば幸いです。</p> <h3 id="テックタッチのストレスの低さはトップクラス">テックタッチのストレスの低さはトップクラス</h3> <p>テックタッチは 2023 年 8 月にドクタートラスト社実施の「ストレスチェック」で <strong><a href="https://techtouch.jp/news/20230823stresscheck/" target="_blank">957 社(約 21 万名)中 2 位</a></strong>を獲得しました。<br/> 職場環境指数で偏差値 91.6 と評価されています。<br/> <figure class="figure-image figure-image-fotolife" title="職場環境優良法人として957社中 2位を獲得!"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yhoriuchi/20231215/20231215201917.png" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>職場環境優良法人として957社中 2位を獲得!</figcaption></figure></p> <p>このような結果を出せたのは、テックタッチという組織の心理的安全性が高く、ストレスの低い環境が整っているからだと自負しています。<br/> 自画自賛になってしまいますが、心理的安全性の高い職場がどのようなものか、テックタッチの組織文化について紹介させていただきます。</p> <h3 id="ストレスが低い理由は組織成長と心理的安全性の高さ">ストレスが低い理由は組織成長と心理的安全性の高さ</h3> <p>テックタッチのストレスが低いのは、みんなが自分らしく振る舞い、組織として成果が出せているためです。<br/> おかげさまでテックタッチは順調に成長を続けており、国内外ユーザー数が 300 万人を突破しました。<br/> デジタルアダプションツールの国内シェア No.1 で、お客様に価値をしっかり届けているとともに、組織としての成長を実感できる状況になっています。<br/> <figure class="figure-image figure-image-fotolife" title="ユーザー数 300万突破"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yhoriuchi/20231215/20231215202902.png" width="647" height="343" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>(参考)<a href="https://techtouch.jp/news/202309133million/">https://techtouch.jp/news/202309133million/</a></figcaption></figure></p> <p>組織が成長することは大事ですが、その成長を支える社員がつらい思いをしていては長続きしません。<br/> テックタッチは以下の 3 つのバリューを掲げており、<strong>楽しく自分らしく振る舞える文化</strong>ができあがっています。</p> <ul> <li><strong>挑み続けろ 援護があるから </strong><br/> 自らが中心となり困難なゴールに挑もう、まわりのメンバーは可能性を信じサポートしよう</li> <li><strong>Deep Thinking </strong><br/> 深く考え抜くことで、本質をみきわめ、他にはないユニークな価値を生み出そう</li> <li><strong>いつでもごきげん </strong><br/> チームとして最高のアウトプットが出せるように、お互いに尊敬・尊重しあい、いつでもごきげんでいよう</li> </ul> <p>このバリューこそ、ストレスを低く抑えながら成果を出せている秘訣であり、心理的安全性の高い職場になっている理由だと考えています。</p> <h2 id="心理的安全性とは自分らしくいられること"><strong>心理的安全性とは「自分らしくいられる」こと</strong></h2> <p>心理的安全性とは何でしょうか。<br/> 心理的安全性の生みの親であるエイミー・C・エドモンドソン氏は著書「<a href="https://www.amazon.co.jp/dp/4862762883" target="_blank">恐れのない組織 ―「心理的安全性」が学習・イノベーション・成長をもたらす</a> 」で以下のように説明しています。</p> <blockquote><p>みんなが気兼ねなく意見を述べることができ、自分らしくいられる文化</p></blockquote> <p>エドモンドソン氏の研究によると、優秀なチームほどミスをすすんで報告しているそうです。<br/> ミスを報告しても安全と思えるからこそ、自分らしくいられるのだと思います。<br/> そして心理的安全性が重要な理由は次の 3 つです。</p> <ol> <li>チームメンバーが自分の貢献の重要性を理解し、報復を恐れず発言できるため、エンゲージメントとやる気が向上する</li> <li>メンバーは意見や懸念を安心して口にできるため、より良い意思決定につながる</li> <li>メンバーがお互いの失敗を共有し、そこから学ぶことができる</li> </ol> <p>テックタッチは「自分らしくいられる文化」を大事にしているため、率直な意見を述べることが当たり前になっています。<br/> この文化があるからこそ、挑戦と組織としての学習が生まれ成果につながる、というサイクルがうまく回っていると思います。</p> <h2 id="テックタッチの心理的安全性を支える文化"><strong>テックタッチの心理的安全性を支える文化</strong></h2> <p>テックタッチが心理的安全性を作り、維持できている理由を知るには、コミュニケーションと人の観点からテックタッチの文化を知るのが近道です。<br/> テックタッチの特徴は以下の通りです。</p> <ol> <li>カルチャーを大事にした採用プロセス</li> <li>入社直後、自チームと関係のない人と 1on1 を必ずやる</li> <li>相手を信頼し、お互いを尊重し合う行動が定着している</li> <li>コミュニケーションはほぼすべてオープン</li> <li>フラットなコミュニケーションが可能</li> </ol> <p>順番に見ていきましょう。</p> <ol> <li><p><strong>カルチャーを大事にした採用プロセス</strong><br/> 心理的安全性を生み出すカルチャーは作るだけでなく、維持する工夫も重要です。<br/> すでにあるカルチャーを守るにはテックタッチ目線だけでなく、候補者目線でもカルチャーフィットするかを確認してもらうことが必要です。<br/> なぜなら、入社してから思っていたのと違うと感じてしまうと、パフォーマンスが発揮できないばかりか、カルチャーに悪影響を及ぼす可能性があるからです。<br/> テックタッチでは採用プロセスの中に「カルチャーフィット面接」と「採用ミートアップ」の 2 つを用意しています。<br/> カルチャーフィット面接は文字通りテックタッチのカルチャーにフィットするかを確認する「面接」です。<br/> 面接官と候補者という関係性から、フランクにコミュニケーションをとることが難しい側面があるため、テックタッチ目線での確認がメインになると考えています。<br/> 候補者目線でもカルチャーフィットを確認してもらう場が<strong>採用ミートアップ</strong>です。<br/> これは<strong>選考要素のないカジュアルなコミュニケーションの場</strong>として設定しています。<br/> 選考に関わらない社員と候補者とのコミュニケーションが目的で、一緒に働く仲間がどのような人かを候補者からの目線で感じ取ってもらうことができます。</p></li> <li><p><strong>入社直後、自チームと関係のない人と 1on1 を必ずやる</strong><br/> テックタッチはオンボーディングが非常に手厚く、入社時に 3 週間近くの時間をかけています。<br/> そして、入社直後からカルチャーを体感してもらえるように、自チーム以外の 15 人と 1on1 することをオンボーディングに取り入れていることも特徴の 1 つです。<br/> 1on1 の相手は VP だったり、他チームのマネージャーだったり、社歴の長いメンバーだったりとさまざまです。<br/> この 1on1 を通じて、社内にどういった人がいるのかを知ることができ、コミュニケーションを通じてテックタッチの文化を知ることができるようになっています。<br/> また、一度でも会話しておけば、今後の仕事で絡むことがあったとしてもコミュニケーションが非常にやりやすくなるメリットもあります。</p></li> <li><p><strong>相手を信頼し、お互いを尊重し合う行動が定着している</strong><br/> 心理的安全性は相手を信頼し、お互いを尊重しあった行動が伴うことで醸成されます。<br/> この行動を示す事例の 1 つが打ち合わせの予定確保です。<br/> テックタッチでは打ち合わせの事前相談が不要で、スケジュール変更も直接連絡は不要です。<br/> もちろん、事前相談しておいたほうがやりやすいことはありますが、必ずしもそうではないと思います。<br/> 特に他部署の人とのコミュニケーションを目的とした 1on1 などは事前相談不要で開催しています。<br/> 基本的にスケジュールは「カレンダーに登録したら、カレンダーの通知機能で通知すれば OK」というスタイルです。<br/> 登録されたスケジュールで都合が悪ければ、勝手に変更してカレンダーの変更通知をしておけば問題ありません。<br/> この行動は相手が受け入れてくれると信頼しているからこそできる行動だと思います。<br/> また、打ち合わせ当日も「今日はよろしくお願いします」から始まるため、テックタッチメンバーの人の良さを感じ取れます。<br/> このようにお互いを信頼していることが伝わる雰囲気があるからこそ、安心して自分の意見を発言できる文化が保たれています。</p></li> <li><p><strong>コミュニケーションはほぼすべてオープン</strong><br/> ほぼすべてのコミュニケーションやドキュメントはオープンです。<br/> 「ほぼすべて」というのは、人事に関する情報や、機密情報はクローズドで適切に管理されているためです。<br/> 私が入社して驚いたのは、オンラインの会議がオープンに行われているため、興味のある会議があれば誰でもリスナー参加が許容されていることです。<br/> これにより「知っておいたほうが良い」と思える場合、会議に呼ばれていなかったとしてもリスナーとして参加することで情報を集めることができます。<br/> 最近では非同期コミュニケーションをもっと有効活用する動きも活発になってきており、ドキュメントや会議の録画を誰でも見れるようにオープンにしています。</p></li> <li><p><strong>フラットなコミュニケーションが可能</strong><br/> テックタッチは役割を重視しており、役割に上下関係はありません。<br/> 役割に応じた責任を果たすため、自律的に行動することが求められています。<br/> そして、自律性を発揮するために年齢や役職は関係ないことを皆が理解しています。<br/> 年齢や役職を気にせず意見を言い合えるため、職場内でのコミュニケーションは活発で、自由に何でも相談できる関係性と環境が整っているのです。<br/> また、テックタッチではお互いをニックネームで呼び合う文化があります。<br/> 全員がニックネームで呼び合うことで、職場全体に親密性が生まれ、フラットでフランクな関係性を築くことができています。<br/> テックタッチのバリューの 1 つである「いつでもごきげん」が体現されていると実感できる文化と環境があるのです。</p></li> </ol> <p>以上が心理的安全性を高く保ち、ストレスを低く抑えているテックタッチの職場となります。</p> <h2 id="最後に"><strong>最後に</strong></h2> <p>テックタッチという職場が心理的安全性が高くストレスの低い環境になっているか、どのように作られているかイメージしてもらえたでしょうか。<br/> テックタッチは人とコミュニケーションを大事にしています。<br/> 興味を持っていただけるなら、ぜひ一度カジュアル面談でお話しましょう。<br/> いつでもごきげんな仲間がお待ちしています!</p> yhoriuchi テストプロセスが自走するチーム体制をめざして QA が取り組んでいること hatenablog://entry/6801883189072213741 2024-01-11T09:00:00+09:00 2024-01-11T09:00:01+09:00 テックタッチのQAチームにおけるテストプロセス改善の取り組みを紹介します。 <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/cobalt_catal/20240104/20240104112625.png" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"><ul class="table-of-contents"> <li><a href="#はじめに">はじめに</a></li> <li><a href="#前提情報">前提情報</a><ul> <li><a href="#プロダクトチームの体制">プロダクトチームの体制</a></li> <li><a href="#Four-Keys-の-Elite-を目指して">Four Keys の Elite を目指して</a></li> </ul> </li> <li><a href="#品質保証の課題">品質保証の課題</a><ul> <li><a href="#1-テストの重複">1. テストの重複</a></li> <li><a href="#2-刻々と変化するチーム体制">2. 刻々と変化するチーム体制</a></li> <li><a href="#3-属人化したテストケース管理">3. 属人化したテストケース管理</a></li> </ul> </li> <li><a href="#改善策テストプロセスの変更とテストケース管理ツールの導入">改善策:テストプロセスの変更とテストケース管理ツールの導入</a><ul> <li><a href="#1-テストプロセスの改善Test-It-Yourself">1. テストプロセスの改善〜Test It Yourself〜</a></li> <li><a href="#2-テストマネジメントツールの導入">2. テストマネジメントツールの導入</a></li> </ul> </li> <li><a href="#おわりに">おわりに</a></li> </ul> <h2 id="はじめに">はじめに</h2> <p>こんにちは、テックタッチで QA PM (Quality Assurance Project Manager)をしている shutty です。先日はテストエンジニア向けの合宿型ワークショップ <a href="https://wacate.jp/workshops/2023winter/">WACATE2023 冬</a>に初めて参加してきました。実行委員をはじめとして参加者全員の熱量を全身に浴びてきました。<br/> この記事では最近テックタッチの開発チームで行なっているテストプロセスの改善について紹介します。</p> <h2 id="前提情報">前提情報</h2> <h3 id="プロダクトチームの体制">プロダクトチームの体制</h3> <p>弊社では、効果的なチーム運営を目指すために <a href="https://www.amazon.co.jp/dp/4820729632">Team Topologies</a> を参考にしています。</p> <p><figure class="figure-image figure-image-fotolife" title="テックタッチの開発体制"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/cobalt_catal/20240105/20240105160934.jpg" width="960" height="540" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>テックタッチの開発体制</figcaption></figure> <a href="https://speakerdeck.com/techtouch/product-team?slide=28">&#x30C6;&#x30C3;&#x30AF;&#x30BF;&#x30C3;&#x30C1;&#x4F1A;&#x793E;&#x7D39;&#x4ECB;&#x8CC7;&#x6599;&#xFF08;&#x30D7;&#x30ED;&#x30C0;&#x30AF;&#x30C8;&#x30C1;&#x30FC;&#x30E0;&#x5411;&#x3051;&#xFF09; / We are hiring !! - Speaker Deck</a></p> <p>スクラムガイドでは、開発業務に必要なメンバーを『開発者』と表現しており、もちろん QA も『開発者』ですが、 本記事で扱う 『開発者』は、フロントエンドやバックエンドのエンジニアを指します。</p> <h3 id="Four-Keys-の-Elite-を目指して">Four Keys の Elite を目指して</h3> <p>プロダクトチームは Four Keys の Elite を目指し、リードタイムの短縮に取り組んでいます。その施策の一つとして、E2E テストの自動化を部分的に適用していく施策を進めています。しかし、まだ安定性や既存のテストケースとの整合性の問題から、全面的な運用には至っていません。</p> <h2 id="品質保証の課題">品質保証の課題</h2> <p><figure class="figure-image figure-image-fotolife" title="開発プロセスと課題の全体像"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/cobalt_catal/20240104/20240104103134.png" width="1200" height="239" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>開発プロセスと課題の全体像</figcaption></figure></p> <h3 id="1-テストの重複">1. テストの重複</h3> <p>開発者はテストカバレッジを上げられるように単体テストを作成します。 一方で、QA はユーザーストーリーと仕様書を基に E2E テストを行ってきた結果、重複するテストが生まれ、E2E のテストケースが増大しました。これにより手動でのリグレッションテストが膨れ上がり、工数が大幅に増えてしまいました。 現状では、チーム全体で修正範囲を共有し、都度テストスコープを調整・確認する方法をとっています。</p> <h3 id="2-刻々と変化するチーム体制">2. 刻々と変化するチーム体制</h3> <p>テックタッチではスクラム開発を採用し、事業の成長に伴って変化するチーム体制に対応しながら、品質保証プロセスを広範囲に適応する必要があります。<br/> 現状、私たちはインプロセス QA を実践しており、<a href="https://jstqb.jp/syllabus.html#syllabus_foundation">JSTQB</a> の定義にあるような『テスト計画からテスト完了』までのテストプロセスをカバーしています。テックタッチのスクラム開発はインクリメンタルな開発アプローチであり、テストプロセスのうち『テスト分析・テスト設計・テスト実装・テスト実行』に多くのリソースを割いています。<br/> テックタッチの組織設計上、QA は <a href="https://www.amazon.co.jp/dp/4820729632">Team Topologies</a> でいうイネーブリング・チームです。<br/> QA チームの立ち上げから現在までで、スクラムチームのインプロセス QA としてテストプロセスを最適化することに集中してきましたが、事業とチームが成長してきた今こそ、<a href="https://www.slideshare.net/YasuharuNishi/quality-management-funnel-3d-how-to-organize-qarelated-roles-and-specialties">QM ファンネル</a>における QA コーチとしてバリューを発揮できるようになっていきたい考えです。<br/> プロダクトチームにおける QA の役割を QM ファンネルを参考にして再設計し、開発チームの拡大に備えた柔軟な組織設計を目指すことで、開発のボトルネックを回避したいと考えています。</p> <h3 id="3-属人化したテストケース管理">3. 属人化したテストケース管理</h3> <p>現在、テストケースはドメインごとにスプレッドシートで管理されており、合計で約 60 のシートがあります。<br/> スプレッドシートはドメインごとにファイル横断しているため、テストケースの検索性が低い状態です。そのため、テストケース作成者以外がテストケースを追跡しようとすると、時間がかかります。この属人化したテスト実装のプロセスを改善する必要があります。<br/> また、このファイル横断した仕組みではテスト実行時のモニタリングが難しく、テスト効率の低下に気づきづらい状態です。 さらに、テストケースは使い捨てになりやすい傾向があります。</p> <h2 id="改善策テストプロセスの変更とテストケース管理ツールの導入">改善策:テストプロセスの変更とテストケース管理ツールの導入</h2> <h3 id="1-テストプロセスの改善Test-It-Yourself">1. テストプロセスの改善〜Test It Yourself〜</h3> <p><strong>導入</strong><br/> 「刻々と変化するチーム体制」にイネーブリング・チームとして適応するために、段階的に QA コーチへの進化を目指していきます。<br/> 我々QA チームは、この活動を『Test It Yourself』(以降、TIY として記載)と名付けました。これにより、開発プロセスに新しいテストのアプローチを取り入れ、チーム全体で品質を作り込む文化を育てようとしています。</p> <p><strong>TIY とは</strong><br/> 主なポイントは、テストプロセス(テスト計画、テスト分析、テスト設計、テスト実装、テスト実行)を開発者が主体的に実施することです。 これまでインプロセス QA としてテストプロセスを担当していた QA は、開発者と並走しながらテストプロセスをレビューします。<br/> TIY を導入することで、QA の役割は今よりもイネーブリングにスクラムチームをサポートする方向にシフトしていきます。</p> <p><strong>QA として何をしたか</strong><br/> 現在、TIY をスクラムチームに適応できるよう推進中です。<br/> 例えば、開発者が TIY に慣れるよう、「隣のチームも手順に従ってテスト実行できるように」をコンセプトにドキュメントを作ったり、「なぜこの変更が必要なのか」やどんな良いことがあるのかを丁寧に説明しています。<br/> また、誰もが簡単にテストができるように、テストプロセスを改善することも大切です。開発者の視点でテストプロセスの改善案などが出たり、よりテスト技法の理解を深めようとするアプローチがあります。このようなアプローチに応えるため、QA が持っている暗黙的なナレッジを言語化することで、再現性のあるテストプロセスに成長する予感がしています。</p> <p><strong>期待する効果</strong><br/> TIY による期待効果は 2 つあります。<br/> 1 つめは、開発者が最終的なプロダクトの品質を作り込むため、「いつどこで、どうやってテストするか?」を考えながら実装することで、新たな気づきを得ることができることです。<br/> 2 つめは、どのテストレベルでテストを自動化するか明確にできることです。例えば、単体テストに近い領域でテストが自動化ができた場合、E2E テストを自動化しようとしていたテストを削減させることができ、結果としてもっと効率の良い開発ができるようになります。</p> <h3 id="2-テストマネジメントツールの導入">2. テストマネジメントツールの導入</h3> <p>テックタッチでは、テストマネジメントを強化し効率化するために新しい<a href="https://qase.io/">テストマネジメントツールの「Qase」</a>を導入しました。 このツールにはいくつかのメリットがあり、それによってテストプロセスが大きく改善されます。</p> <p><strong>Qase 導入のメリット</strong></p> <ul> <li>Qase を使うことで、すべてのテストケースを一か所で管理できるようになります。 これにより、情報の散逸を防ぎ、テスト活動の一貫性を保つことが可能となります。</li> <li>JIRA と連携ができるため、チーム内の異なるタスク間でのスムーズな情報共有が実現します。</li> <li>CircleCI と連携可能で、自動テストの結果を直接ツールに取り込むことができ、効率的なフィードバックループを構築できます。</li> <li>Qase では各テストがどのテストレベル(Qase では “Layer”と表現されています)で実行されているかを管理できるため、テストの重複を避け、必要なテストに集中できます。</li> <li>CSV でテストケースをインポートできる機能があり、既存のテストケースを効率的に移行できます。</li> <li>テスト手順をテストケース間で共有できる機能があり、テストケースが再利用しやすい。</li> <li>自動テストがどの範囲で行われているのかが見える化できます。</li> </ul> <p>ただ、Qase ではバージョン管理ができないため工夫する必要があります。</p> <p><strong>今後の活用</strong><br/> このツールの利用によって、E2E テストを効率的に実施すべき領域を特定しやすくなります。これは、開発者と QA のチーム両方にとって大きな利点となるでしょう。 どのテストケースが存在するかが明確になることで、より迅速で効果的なテストとフィードバックが可能となり、これまで別々で管理していたため重複したテストケースを抑制し、結果としてプロダクトの品質向上に貢献することを期待しています。</p> <h2 id="おわりに">おわりに</h2> <p>最後に、テストプロセスが自走するチーム体制を目指して取り組んでいることを振り返ります。</p> <ol> <li>テストの重複を避けるため、単体テストと E2E テストのテストスコープを調整し、手動リグレッションテストの工数を削減しました。</li> <li>刻々と変化するチーム体制に対応するため、テストプロセスを最適化し、QA チームの役割を再設計しました。</li> <li>属人性の低いテストケース管理のために、新しいテストマネジメントツール「Qase」を導入。これにより、テストケースの管理と共有が効率化され、テスト活動の一貫性と品質向上に貢献しました。</li> </ol> <p>テックタッチでは、テストプロセスが自走するチーム体制を築くために、更なる成長を支える人材を募集しています。とくに SET(Software Engineer in Test)という役割に興味をお持ちの方は、ぜひご応募ください!一緒に品質を追求し、テストプロセスの改善に取り組みましょう。</p> <p>興味を持っていただけたら、ぜひ弊社の採用ページをご覧ください!</p> cobalt_catal UIの一貫性と開発生産性を高める!UI実装ガイドライン制定と浸透のプロセス hatenablog://entry/6801883189066140082 2024-01-09T09:00:00+09:00 2024-01-09T09:00:01+09:00 UI の一貫性と開発生産性を高めるために、テックタッチの UI チームがどのような取り組みをして、どのような効果があったかを紹介します! <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20231212/20231212122600.png" alt="" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>こんにちは。テックタッチの shoko です。<br/> 最近モスのフィッシュバーガーにハマっていて、3日に1回食べています🐟</p> <p>さて、本日は、UI の一貫性と開発生産性を高めるために、テックタッチの UI チームがどのような取り組みをしているかをお話したいと思います。</p> <ul class="table-of-contents"> <li><a href="#UI-チームとは">UI チームとは</a></li> <li><a href="#テックタッチが抱える-UI-の課題">テックタッチが抱える UI の課題</a></li> <li><a href="#UI-実装ガイドライン制定と浸透のプロセス">UI 実装ガイドライン制定と浸透のプロセス</a><ul> <li><a href="#1-メンバーから課題を収集">1. メンバーから課題を収集</a></li> <li><a href="#2-課題議論の集約">2. 課題・議論の集約</a></li> <li><a href="#3-UI-チームでガイドラインを制定">3. UI チームでガイドラインを制定</a></li> <li><a href="#4-チーム全体へのガイドライン浸透">4. チーム全体へのガイドライン浸透</a></li> </ul> </li> <li><a href="#ガイドライン制定の効果">ガイドライン制定の効果</a></li> <li><a href="#おわりに">おわりに</a></li> </ul> <h2 id="UI-チームとは">UI チームとは</h2> <p>テックタッチでは、複数のスクラムチームでプロダクト開発を進めています。主要な機能の開発は順調に進んでいるものの、UI 改善のようなタスクに時間を割くのが難しいという状況に直面していました。</p> <p>そこで、開発の優先度では後回しになりがちなものの、プロダクトの品質を維持するためには欠かせない改善活動を進めるために、いくつかのサブチームが設立されました。</p> <p>サブチームの中でわたしは UI チームに所属し、「より使いやすい UI を最大最速でユーザーに届ける」というミッションのもと活動しています。<br/> メンバー構成は自分を含めたフロントエンドエンジニア3人とデザイナー1人、そして時折テックリード1人です。</p> <h2 id="テックタッチが抱える-UI-の課題">テックタッチが抱える UI の課題</h2> <p>テックタッチでは、プロダクトや組織が拡大するにつれて、次のような UI の課題が生まれていました。</p> <ul> <li>一貫性の低下 <ul> <li>同じ意味合いを持つコンポーネントの見た目や挙動が、画面によって異なるところがある</li> </ul> </li> <li>開発生産性の低下 <ul> <li>似たようなコンポーネントが複数あり、実装時にどれを使うべきか迷う</li> <li>共通コンポーネントを拡張すべきところを、スタイルの上書きで対応しており、メンテナンスがしづらくなっている</li> </ul> </li> </ul> <p>そこで、UI 実装時に守ってほしいことや気をつけてほしいことをガイドラインとして定め、これをチーム全体に浸透させることで、上記の課題を改善しようとしています。この活動のプロセスを次の章でご紹介します。</p> <h2 id="UI-実装ガイドライン制定と浸透のプロセス">UI 実装ガイドライン制定と浸透のプロセス</h2> <h3 id="1-メンバーから課題を収集">1. メンバーから課題を収集</h3> <p>最初から網羅的なガイドラインを作ろうとすると、膨大な時間がかかることは自明です。<br/> そこで、私たちはボトムアップ的なアプローチを採用し、まずは UI 実装の途中で見つけた課題の改善に力を注ぐことに決めました。</p> <p>フロントエンドチームの各メンバーから課題を継続して収集するために、<a href="https://slack.com/intl/ja-jp/" target="_blank">Slack</a> や <a href="https://github.co.jp/" target="_blank">GitHub</a> でUIチームのグループを作成し、メンションを使って簡単に相談できる体制を整えました。</p> <h3 id="2-課題議論の集約">2. 課題・議論の集約</h3> <p>UI チームに寄せられた相談内容は、ケーススタディとして <a href="https://www.notion.so/ja-jp" target="_blank">Notion</a> に集約しています。<br/> 時には Slack 等でそのまま議論が進むこともありますが、その場合でも議論の内容を必ず Notion に記録しています。</p> <p><figure class="figure-image figure-image-fotolife" title="Notion に集約しているUI改善ケーススタディ"> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20231227/20231227142120.png" alt="Notion&#x306B;&#x3042;&#x308B;UI&#x6539;&#x5584;&#x30B1;&#x30FC;&#x30B9;&#x30B9;&#x30BF;&#x30C7;&#x30A3;&#x30AE;&#x30E3;&#x30E9;&#x30EA;&#x30FC;&#x96C6;&#x306E;&#x30B9;&#x30AF;&#x30EA;&#x30FC;&#x30F3;&#x30B7;&#x30E7;&#x30C3;&#x30C8;&#x3002;Button&#x3001;Chip&#x3001;Dialog&#x3001;IconButton&#x3068;&#x3044;&#x3063;&#x305F;&#x30B3;&#x30F3;&#x30DD;&#x30FC;&#x30CD;&#x30F3;&#x30C8;&#x3054;&#x3068;&#x306B;&#x30AB;&#x30C6;&#x30B4;&#x30EA;&#x308F;&#x3051;&#x304C;&#x3055;&#x308C;&#x3066;&#x3044;&#x308B;&#x3002;&#x4EE5;&#x4E0B;&#x306E;&#x3088;&#x3046;&#x306A;Dialog&#x306E;&#x8AB2;&#x984C;&#x304C;&#x8868;&#x793A;&#x3055;&#x308C;&#x3066;&#x3044;&#x308B;&#x3002;&#x300C;&#x30AA;&#x30FC;&#x30D0;&#x30FC;&#x30EC;&#x30A4;&#x30AF;&#x30EA;&#x30C3;&#x30AF;&#x3067;&#x30C0;&#x30A4;&#x30A2;&#x30ED;&#x30B0;&#x3092;&#x9589;&#x3058;&#x308B;&#x3079;&#x304D;&#x304B;&#xFF1F;&#x300D;&#x300C;Dialog Content&#x5185;&#x3067;outline&#x304C;&#x898B;&#x5207;&#x308C;&#x308B;&#x554F;&#x984C;&#x300D;&#x300C;&#x30C0;&#x30A4;&#x30A2;&#x30ED;&#x30B0;&#x306E;&#x591A;&#x91CD;&#x8868;&#x793A;&#x306B;&#x3064;&#x3044;&#x3066;&#x300D;&#x300C;Dialog&#x3068;SettingBox&#x306E;&#x4F7F;&#x3044;&#x5206;&#x3051;&#x65B9;&#x300D;&#x300C;Dialog&#x306E;&#x30B9;&#x30AF;&#x30ED;&#x30FC;&#x30EB;&#x306B;&#x3064;&#x3044;&#x3066;&#x300D;" width="1200" height="802" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> <figcaption>Notion に集約しているUI改善ケーススタディ</figcaption></figure></p> <p>寄せられた課題とその議論を集約することで、以下のようなメリットが得られると感じています。</p> <ul> <li>課題の優先順位や進捗状況が管理しやすくなる</li> <li>結論が出るまでの過程がクリアになり、透明性が向上する</li> <li>過去にあがった課題を全体的に把握できる</li> </ul> <h3 id="3-UI-チームでガイドラインを制定">3. UI チームでガイドラインを制定</h3> <p>週1回の UI チーム定例で、各課題について対応方針を話し合います。</p> <p>方針決めにあたっては、他社の秀逸なデザインシステムを非常に参考にしています。</p> <p><strong>UI チームの推しデザインシステム</strong></p> <ul> <li><a href="https://smarthr.design/" target="_blank">SmartHR Design System</a></li> <li><a href="https://spindle.ameba.design/" target="_blank">Spindle(Ameba)</a></li> </ul> <p>話し合いの結果、方針がまとまり結論が出たものをガイドライン化しています。</p> <h3 id="4-チーム全体へのガイドライン浸透">4. チーム全体へのガイドライン浸透</h3> <p>ガイドラインを定めた後の課題は、それをチーム全体に浸透させることです。</p> <p>最初はケーススタディを見てもらうようにアナウンスしていましたが、すべてに目を通してもらうことは、なかなか難しく…<br/> そこで、わたしたち UI チームは、ガイドラインが自然とみんなの目に入る方法を探す旅に出ました🧳</p> <p><strong>JSDoc コメントにガイドラインを書く</strong></p> <p>色々と模索した結果、「<a href="https://jsdoc.app/" target="_blank">JSDoc</a> コメントにガイドラインを書く」方法を採用することに!</p> <p>JSDoc コメントは、関数や変数の宣言の直前に <code>/** ◯◯ */</code> の形式でコメントを書く記法です。</p> <pre class="code jsx" data-lang="jsx" data-unlink>/** * ## 太字を適用する方法(`&lt;strong&gt;`,`&lt;em&gt;`,`&lt;b&gt;`タグの扱い) * - `&lt;b&gt;` , `&lt;em&gt;` は使わない * - [`&lt;b&gt;`は代わりに`&lt;strong&gt;`を使用するべき](https://developer.mozilla.org/ja/docs/Web/HTML/Element/b) */ const Typography = (props) =&gt; { // ... }</pre> <p><a href="https://code.visualstudio.com/" target="_blank">Visual Studio Code</a> などの JSDoc 対応エディターを利用すると、コンポーネントにホバーするだけでコメントに書いた内容がツールチップとして表示されます。</p> <p><figure class="figure-image figure-image-fotolife" title="Visual Studio Code で Typography コンポーネントにホバーした時のスクリーンショット"> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20231212/20231212143628.png" alt="Visual Studio Code&#x3067;Typography&#x30B3;&#x30F3;&#x30DD;&#x30FC;&#x30CD;&#x30F3;&#x30C8;&#x306B;&#x30DB;&#x30D0;&#x30FC;&#x3057;&#x305F;&#x6642;&#x306E;&#x30B9;&#x30AF;&#x30EA;&#x30FC;&#x30F3;&#x30B7;&#x30E7;&#x30C3;&#x30C8;&#x3002;&#x592A;&#x5B57;&#x3092;&#x9069;&#x7528;&#x3059;&#x308B;&#x65B9;&#x6CD5;&#x306A;&#x3069;&#x3001;Typography&#x5B9F;&#x88C5;&#x6642;&#x306B;&#x6C17;&#x3092;&#x3064;&#x3051;&#x305F;&#x3044;&#x30DD;&#x30A4;&#x30F3;&#x30C8;&#x304C;&#x8A18;&#x8F09;&#x3055;&#x308C;&#x3066;&#x3044;&#x308B;" width="796" height="426" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> <figcaption>Visual Studio Code で Typography コンポーネントにホバーした時のスクリーンショット</figcaption></figure></p> <p>これにより、ガイドラインとコンポーネントが密接に関連するため、必要なときにすぐ確認でき、内容も理解しやすくなります。</p> <p>さらに、この JSDoc コメントは <a href="https://storybook.js.org/" target="_blank">Storybook</a> の Docs にも反映されます。<br/> テックタッチでは UI コンポーネントを Storybook で管理しているため、これも非常に有用なポイントでした。</p> <p>コード上だけでなく Storybook でもガイドラインを確認できることで、どのようなガイドラインが定められているのかを全体的に知りたい時や、デザイナーなどの非エンジニアが確認することも容易になります。</p> <p><figure class="figure-image figure-image-fotolife" title="Storybook で管理している Typography コンポーネントのドキュメントページ"> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20231212/20231212150215.png" alt="&#x30C6;&#x30C3;&#x30AF;&#x30BF;&#x30C3;&#x30C1;&#x306E;Storybook&#x306E;Typography&#x30DA;&#x30FC;&#x30B8;&#x306E;&#x30B9;&#x30AF;&#x30EA;&#x30FC;&#x30F3;&#x30B7;&#x30E7;&#x30C3;&#x30C8;&#x3002;Typography&#x3092;&#x5B9F;&#x88C5;&#x3059;&#x308B;&#x3068;&#x304D;&#x306B;&#x6C17;&#x3092;&#x3064;&#x3051;&#x305F;&#x3044;&#x30AC;&#x30A4;&#x30C9;&#x30E9;&#x30A4;&#x30F3;&#x3084;&#x30B3;&#x30F3;&#x30DD;&#x30FC;&#x30CD;&#x30F3;&#x30C8;&#x306E;&#x30D7;&#x30EC;&#x30D3;&#x30E5;&#x30FC;&#x304C;&#x8868;&#x793A;&#x3055;&#x308C;&#x3066;&#x3044;&#x308B;&#x3002;" width="1200" height="824" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> <figcaption>Storybook で管理している Typography コンポーネントのドキュメントページ</figcaption></figure></p> <p>参考:<a href="https://storybook.js.org/docs/react/api/doc-block-description" target="_blank">Doc blocks - discription | Storybook</a></p> <h2 id="ガイドライン制定の効果">ガイドライン制定の効果</h2> <p>運用が開始されてから約3ヶ月で、35個のケーススタディが集まりました。<br/> そのうち議論が深まり結論が出た9個をガイドライン化し、JSDoc コメントに移植しています。</p> <p>フロントエンドチームのメンバーからは好評の声が届いています🎉</p> <p><figure class="figure-image figure-image-fotolife" title="Slack でもらったメンバーからのコメント"> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20231212/20231212144007.png" alt="&#x30D5;&#x30ED;&#x30F3;&#x30C8;&#x30A8;&#x30F3;&#x30C9;&#x30E1;&#x30F3;&#x30D0;&#x30FC;&#x306E;tsune&#x3055;&#x3093;&#x304B;&#x3089;Slack&#x4E0A;&#x3067;&#x3082;&#x3089;&#x3063;&#x305F;&#x30B3;&#x30E1;&#x30F3;&#x30C8;&#x306E;&#x30B9;&#x30AF;&#x30EA;&#x30FC;&#x30F3;&#x30B7;&#x30E7;&#x30C3;&#x30C8;&#x3002;&#x300C;&#x30B3;&#x30F3;&#x30DD;&#x30FC;&#x30CD;&#x30F3;&#x30C8;&#x306E; docs &#x3088;&#x304D;&#x3067;&#x3059;&#x306D; @ui-frontend&#x300D;&#x3068;&#x30B3;&#x30E1;&#x30F3;&#x30C8;&#x304C;&#x3042;&#x308A;&#x3001;&#x4ED6;&#x306E;&#x30E1;&#x30F3;&#x30D0;&#x30FC;&#x304B;&#x3089;&#x30CF;&#x30FC;&#x30C8;&#x30DE;&#x30FC;&#x30AF;&#x3001;&#x3046;&#x308C;C&#x306A;&#x3069;&#x306E;&#x30EA;&#x30A2;&#x30AF;&#x30B7;&#x30E7;&#x30F3;&#x30B9;&#x30BF;&#x30F3;&#x30D7;&#x304C;&#x3064;&#x3051;&#x3089;&#x308C;&#x3066;&#x3044;&#x308B;&#x3002;" width="868" height="490" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> <figcaption>Slack でもらったメンバーからのコメント</figcaption></figure></p> <p>定性的な効果としては、以下のようなメリットを実感しています。</p> <ul> <li>ガイドラインに従って実装することで、プロダクト全体で UI の一貫性を保てる</li> <li>UI 実装時の迷いが減り、開発生産性が向上する</li> </ul> <p>定量的な効果を測定する方法は正直まだ模索中のため、具体的な方法を確立できたら、また記事にしたいと思います。</p> <h2 id="おわりに">おわりに</h2> <p>本記事では、わたしたちが取り組むUI実装ガイドライン制定とチームに浸透させるためのプロセス、ガイドライン制定の効果を紹介しました。</p> <p>今回の取り組みの中でわたしが特に重要だと感じたポイントは</p> <ul> <li>まずは情報を一元化すべし</li> <li>ガイドラインは定めて終わりではなく、チームに浸透させるための工夫をすべし</li> </ul> <p>の2点です!</p> <p>わたしたちと同じように UI の一貫性・開発生産性に悩んでいるチームに、本記事が参考になれば嬉しいです。</p> <p>最後までお読みいただき、ありがとうございました⛄️</p> shoco55 えぇっ、Nx Cloud を知らない!?――後編:「実際の導入の仕方とハマりどころ」―― hatenablog://entry/820878482967221996 2023-12-26T09:00:00+09:00 2023-12-26T11:31:14+09:00 Nx Cloud を実際に導入する時のやり方とハマりどころについて書いてみます <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20231225/20231225163647.png" alt="&#x8A18;&#x4E8B;&#x306E;&#x30BF;&#x30A4;&#x30C8;&#x30EB;&#x753B;&#x50CF;&#x3002;&#x8A18;&#x4E8B;&#x306E;&#x30BF;&#x30A4;&#x30C8;&#x30EB;&#x3068;&#x3001;&#x7B46;&#x8005;&#x3067;&#x3042;&#x308B;canalun&#x306E;&#x30A2;&#x30A4;&#x30B3;&#x30F3;&#x304C;&#x306E;&#x3063;&#x3066;&#x3044;&#x308B;&#x3002;" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>おはようございます、テックタッチの canalun(<a href="https://twitter.com/i_am_canalun">@i_am_canalun</a>) と言います👶</p> <p>DOM 大好きエンジニア(<a href="https://zenn.dev/canalun">DOMDOMタイムス</a>、みんなも読んでみてね)ですが、DOM 以外のこともやっている毎日です。<br/> というわけで、今日は<strong> Nx Cloud を実際に導入する時のやり方とハマりどころ</strong>について書いてみます✌</p> <p>なお、この記事は、Nx Cloud について前後編2回にわたり解説するシリーズの後編にあたります。<br/> <strong>「そもそも Nx Cloud ってなに?」という方は、ぜひ👇前編の記事をお読み下さい</strong>!Nx Could の <strong>CI の自動分散並列実行</strong>と <strong>CI 結果のキャッシング</strong>について説明をしています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.techtouch.jp%2Fentry%2Ffundamentals_of_nx_cloud" title="えぇっ、Nx Cloud を知らない!?――前編:「CIで分散並列実行とキャッシュ利用ができるだって!?」―― - Techtouch Developers Blog" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tech.techtouch.jp/entry/fundamentals_of_nx_cloud">tech.techtouch.jp</a></cite></p> <p>では早速行きましょう!<br/> 目次を見てオモロそうなところだけ読むもよし、とりあえず上から読んでみるもよしです。自由にやってね!!</p> <ul class="table-of-contents"> <li><a href="#前回のおさらいちょっとだけ">前回のおさらい(ちょっとだけ)</a></li> <li><a href="#えっ3ステップで導入できるの">えっ、3ステップで導入できるの!?!?!?</a><ul> <li><a href="#1-tokenの設定">1. tokenの設定</a></li> <li><a href="#2-nx-cloudのインストール">2. nx-cloudのインストール</a></li> <li><a href="#3-CIフローの書き換え">3. CIフローの書き換え</a></li> </ul> </li> <li><a href="#具体的なハマりポイント">具体的なハマりポイント</a></li> <li><a href="#今後に期待したいところ">今後に期待したいところ</a><ul> <li><a href="#タスクごとの平均所要時間を分散時に考慮してほしい">タスクごとの平均所要時間を分散時に考慮してほしい</a></li> <li><a href="#すべてのタスクの割当が済んだあとは自タスクを消化しきった-agent-を順次閉じてほしい">すべてのタスクの割当が済んだあとは、自タスクを消化しきった agent を順次閉じてほしい</a></li> </ul> </li> <li><a href="#えんもたけなわ">えんもたけなわ</a></li> </ul> <h2 id="前回のおさらいちょっとだけ">前回のおさらい(ちょっとだけ)</h2> <p>一応前回のおさらいをしてみましょう。<br/> めちゃくちゃまとめると、要はこういうことでした📖</p> <ul> <li>Nx というモノレポフレームワークがある</li> <li>Nx を使っている環境では、Nx Cloud を使うことで CI/CD を大幅に高速化できる</li> <li>テックタッチでは Nx Cloud のおかげで <strong>CI の実行時間が 40% 削減された</strong> <ul> <li>高速化のためのメジャーな打ち手はだいたい実施済みのところから 40% 減った</li> <li><strong>直近の1ヶ月でも 200 時間強の削減効果</strong>を出しています……!</li> </ul> </li> <li>Nx Cloudによる高速化は、主に<strong>コマンドの自動分散並列実行</strong>と<strong>コマンド実行結果のキャッシュ</strong>によって実現される</li> </ul> <p>まあ細かい話はぜひ<a href="https://tech.techtouch.jp/entry/fundamentals_of_nx_cloud">前編</a>を読んで頂こうということでね、ここからはさっそく後編の話を始めていきますよ〜!</p> <h2 id="えっ3ステップで導入できるの">えっ、3ステップで導入できるの!?!?!?</h2> <p>めちゃくちゃ頼りになるエヌクラですが、「どうせ導入するの大変なんでしょ〜」という声が聞こえてきます。</p> <p>「ワーワー!」</p> <p>「どうせ導入するの大変なんだろ〜!!!!」</p> <p>「時間返せ!!」</p> <p>「そうだそうだ〜〜!!」</p> <p>「わーわー!!!」</p> <p>群衆の声が聞こえてきますね。</p> <p><strong>ところがどっこい!!!実は簡単です!!!</strong></p> <p>Nx Cloud の訴求ポイントの1つに「いま使っている Nx コマンドをほとんど変えずに使えます!」ということがあります。これは実際その通りで、<strong>Nx Cloud を使うための設定を少し追記するだけですぐに利用を開始できます</strong>。</p> <p><strong>細かい疑問が浮かんだり、動かなかったりしたら公式ドキュメントをみていただくのは前提</strong>として、ここではどのくらい簡単かを示しておく意味で導入手順をかいつまんで紹介します。</p> <h3 id="1-tokenの設定">1. tokenの設定</h3> <p>Nx のサーバーを使わせてもらおうということで token の設定が必要になります。</p> <p>まず token を Nx Cloud のコンソールに<a href="https://nx.app/">公式ページ</a>からログインして色々やって取得してね👶</p> <p>そのあと <a href="https://nx.dev/ci/recipes/security/access-tokens#setting-access-tokens">Nx 公式ドキュメントにあるように</a> token を設定します。<br/> 私が設定したときは 16 系だったので下記のようにしました。<code>cacheableOperations</code>のところは適当です。キャッシュを使いたいターゲットを各自でちゃんと入れてね。</p> <pre class="code" data-lang="" data-unlink>&#34;tasksRunnerOptions&#34;: { &#34;default&#34;: { &#34;runner&#34;: &#34;nx-cloud&#34;, &#34;options&#34;: { &#34;accessToken&#34;: &#34;SOMETOKEN&#34;, &#34;cacheableOperations&#34;: [ &#34;build&#34;, &#34;test&#34; ], } } }</pre> <p>token を直接書くのは……という人は<a href="https://nx.dev/ci/recipes/security/access-tokens#using"><code>nx-cloud.env</code>ファイル</a>を使うと良いかと思います👶<br/> (テックタッチでも<code>nx-cloud.env</code>を使っています)</p> <h3 id="2-nx-cloudのインストール">2. nx-cloudのインストール</h3> <p>token が設定できたら<code>yarn add @nrwl/nx-cloud</code>で nx-cloud をいれます。<br/> 自分がやったときとは公式ドキュメントの記述が変わっており、<strong><a href="https://nx.dev/ci/features/remote-cache#setup-remote-caching-with-nx-cloud">いまは<code>npx nx connect</code>というコマンドを一回叩けばそれで終わりなようです</a></strong>。<br/> どちらでもいいのかなという気はしますが、おとなしく<code>npx nx connect</code>したほうがよさそうですね👀</p> <p>実はここまでやると既に、Nx を介したコマンド実行においてキャッシュが効くようになっているはずです。<br/> 公式ドキュメントの<a href="https://nx.dev/ci/features/remote-cache">リモートキャッシュの導入手順</a>の手順が済んでいるわけですからね。</p> <h3 id="3-CIフローの書き換え">3. CIフローの書き換え</h3> <p>キャッシュが効くようになったので自動分散並列実行(<a href="https://nx.dev/ci/features/distribute-task-execution#distribute-task-execution-dte">Distribute Task Execution: DTE</a>)も有効化しましょう。<br/> Nx Cloud の DTE は CI プラットフォーム上に agent を立ち上げて行われます。<br/> そういった <strong>DTE 体制が毎回の CI で構築されて活用されるように CI フローを書き換える必要があります</strong>。</p> <p>書き換えは公式から出されている <a href="https://nx.dev/ci/recipes/set-up">CI プラットフォームごとのテンプレート</a>に従えばよいです。<br/> ここにも一応、2023/12/23 日時点での CircleCI 向けテンプレートを転載しておいてみます。</p> <pre class="code" data-lang="" data-unlink>version: 2.1 orbs: nx: nrwl/nx@1.5.1 jobs: main: docker: - image: cimg/node:lts-browsers steps: - checkout - run: npm ci - nx/set-shas # Tell Nx Cloud to use DTE and stop agents when the build tasks are done - run: npx nx-cloud start-ci-run --stop-agents-after=build # Send logs to Nx Cloud for any CLI command - run: npx nx-cloud record -- npx nx format:check # Lint, test and build on agent jobs everything affected by a change - run: npx nx affected --base=$NX_BASE --head=$NX_HEAD -t lint,test,build --parallel=2 --configuration=ci agent: docker: - image: cimg/node:lts-browsers parameters: ordinal: type: integer steps: - checkout - run: npm ci # Wait for instructions from Nx Cloud - run: command: npx nx-cloud start-agent no_output_timeout: 60m workflows: build: jobs: - agent: matrix: parameters: ordinal: [1, 2, 3] - main</pre> <p>なんとなくポイントかなと思うのは下記です。</p> <ul> <li><p>Nx Cloud に接続して(<code>nx-cloud start-ci-run</code>)、agent を立ち上げて(<code>start-agent</code>)、CI コマンドを実行するという流れを<strong>1つの job でやるわけではない</strong>。1つは接続と CI コマンド実行、1つは agent の立ち上げというように2つの job に分け、それらを並置して実行する</p></li> <li><p><code>nx-cloud start-agent</code>では、<code>parameters</code>で指定した数だけ agent が立ち上がる</p></li> </ul> <p><figure class="figure-image figure-image-fotolife" title="parameters では CI 上での agent の表示名を文字列で指定できます。配列の要素数だけ agent が立ち上がります"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20231222/20231222194542.png" alt="CI&#x306E;&#x8A2D;&#x5B9A;&#x30D5;&#x30A1;&#x30A4;&#x30EB;&#x306B;&#x3066;parameters&#x306E;&#x76F4;&#x4E0B;&#x306B;names&#x3068;&#x3044;&#x3046;&#x30D7;&#x30ED;&#x30D1;&#x30C6;&#x30A3;&#x3092;&#x4F5C;&#x308A;&#x3001;&#x3044;&#x308D;&#x3044;&#x308D;&#x306A;&#x9854;&#x6587;&#x5B57;&#x306E;&#x6587;&#x5B57;&#x5217;&#x3067;&#x69CB;&#x6210;&#x3055;&#x308C;&#x305F;&#x914D;&#x5217;&#x3092;&#x5024;&#x3068;&#x3057;&#x3066;&#x8A2D;&#x5B9A;&#x3057;&#x3066;&#x3044;&#x308B;" width="1200" height="956" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>parameters では CI 上での agent の表示名を文字列で指定できます</br>配列の要素数だけ agent が立ち上がります</figcaption></figure></p> <p>というわけで、ここで紹介した3ステップで動くようにはなるはずです!<br/> ただ現実はそう甘くなく、テックタッチで Nx Cloud を導入した時は何箇所かハマりどころがありました。次からはそこを見ていってみます👀</p> <h2 id="具体的なハマりポイント">具体的なハマりポイント</h2> <p>めちゃ個人的にですが、Nx は本当にできることが多いし「こんなことまで!?」という驚き(e.g. <a href="https://nx.dev/nx-api/eslint/generators/workspace-rule">eslint カスタムルールのテンプレートとテストをコマンド1つで展開できる</a>)があって最高なのですが、いかんせんドキュメントが少し分かりづらいんですよね。</p> <p>というわけで、実際どこにハマってどう解決したかを未来のNx Cloudユーザーに向けて記しておきます。</p> <ul> <li><p>先ほども書きましたが「<code>nx-cloud start-ci-run</code>のあとにタスク実行する」job と、「<code>start-agent</code>を実行する」job は、<strong>並置して実行する必要があります</strong></p></li> <li><p>Nx Cloud に任せた job が終わっても agent が動き続けてしまう場合は、最後に<code>nx-cloud stop-all-agent</code>なるコマンドを実行してみましょう。これは agent 群を停止するためのコマンドです: <a href="https://nx.dev/ci/reference/nx-cloud-cli#npx-nxcloud-stopallagents">https://nx.dev/ci/reference/nx-cloud-cli#npx-nxcloud-stopallagents</a></p></li> <li><p>Nx Cloud をガチで活用しだすと、<strong>いくつかの Nx コマンドを Nx Cloud にまとめて渡したい</strong>時が来ると思います。<br/> このとき<strong><code>stop-agents-on-failure</code>というオプションに注意して下さい</strong>。これは「正常終了しなかったコマンドが1つでも発生したら、その時点ですべてのコマンドの実行を止めて agent も全て停止する」というものです。これは<strong>デフォルトで<code>true</code>になっています</strong>。<br/> したがって、例えば <strong>lint が失敗しても test は最後までやってほしい、という人はこれを<code>false</code>に設定しなおすのが良さそうです</strong>👶 なお、このオプションに関する公式の説明は<a href="https://nx.dev/ci/reference/nx-cloud-cli#stopagentsonfailure">ここ</a>にあります。</p></li> </ul> <h2 id="今後に期待したいところ">今後に期待したいところ</h2> <p>さて最後に少し「こんなこともできたらいいねえ」という点を挙げてみます。<br/> 前後編を通して Nx Cloud アゲ感が強かったので、ちょっとバランスを取っておこうかなという思いからです。心を鬼にして書きます👹</p> <h3 id="タスクごとの平均所要時間を分散時に考慮してほしい">タスクごとの平均所要時間を分散時に考慮してほしい</h3> <p>1台の agent へ1回に割り当てるタスクの数(<code>parallel</code>)を2以上にしておいたとき、Nx Cloud は各タスクの平均実行時間は考慮しないのか、時間のかかるタスクいくつかを1つの agent にアサインしてしまうことがあります😭エーン</p> <h3 id="すべてのタスクの割当が済んだあとは自タスクを消化しきった-agent-を順次閉じてほしい">すべてのタスクの割当が済んだあとは、自タスクを消化しきった agent を順次閉じてほしい</h3> <p>現在の自動分散実行において、agent は順次閉じられるのではなく、すべてのタスクの実行が終了したとき(<strong>すべてのタスクの割当が済んだときではない</strong>)に一斉に閉じられます。つまり agent のいずれかがタスクを処理している限り、他 agent もみんな CI 上で稼働し続けます😭シクシク<br/> したがって仕事が終わっている agent についても、CI プラットフォーム(e.g. CircleCI)のクレジットがかかってしまいます。</p> <p>なおこれは余談ですが、これについて1点目として挙げた「割り当てるときにタスクの所要時間を考えない問題」とあわせて考えますと、タスクごとの平均所要時間にばらつきがある状況で<code>parallel</code>(1台の agent へ1回に割り当てるタスクの数)を2以上にした場合、仕事が終わった agent が一番遅い agent と最後まで一緒に動き続ける可能性が大きくなり、CI コストの無駄が発生しやすくなるわけです。この点からすると、<code>parallel</code>は小さい方がよさそうに思えます。</p> <p>一方で<code>parallel</code>(1台の agent へ1回に割り当てるタスクの数)は大きければ大きい方がやはり望ましそうです。というのも、テックタッチの環境で何度か試したところ、Nx Cloud が各 agent にタスクを割り当てる際には「割り当てる行為そのもの」にも時間がそれなりにかかっていそうでした(本当に他の環境でもそうかは分からないのでみんなも確かめてみよう!)<br/> 実際、この割り当てそのもののコストは、<strong>細かなタスクの集合であるような Nx コマンド(e.g. lint)で顕著に現れます</strong>。1つ1つのプロジェクトの lint には時間がかからないはずなのに、プロジェクト数が多いことで、割当コストを含めると長い時間がかかってしまうわけです。</p> <p>このように考えると、当たり前なのですが<strong>一番いいのはタスクごとの平均所要時間にばらつきがない状況を実現して、一度にそれなりの数をとっていくようにする(</strong><code>parallel</code><strong>を大きくする)</strong>ことだと、やはり結論づけられると思います。</p> <h1 id="えんもたけなわ">えんもたけなわ</h1> <p>かのマハトマ・ガンジーは「速度を上げるばかりが、人生ではない」と言ったそうですが、CI は速度です。「CI は速度を上げるばかりである」です。</p> <p>というわけで Nx Cloud、みなさんも検討してみて下さいネ👶</p> <p>周りで「使ってるよ!」という話をあまり聞かないので、もし使っていてかつ気が向いたらcanalun(<a href="https://twitter.com/i_am_canalun">@i_am_canalun</a>)まで教えてくださいナ🥷(色々情報交換させてください😭)</p> canalundayo テックイベントの開催で食べ物、飲み物、音楽、道案内にこだわってみたよ🌞 hatenablog://entry/6801883189064704334 2023-12-12T09:00:00+09:00 2023-12-15T09:40:00+09:00 この記事では、食べ物や音楽にこだわってイベントを開催してみたら、皆けっこう楽しんでくれていたよという話をします。 要するにイベントやるときのちょっとしたTips集です! <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20231212/20231212110119.png" alt="&#x8A18;&#x4E8B;&#x306E;&#x30BF;&#x30A4;&#x30C8;&#x30EB;&#x753B;&#x50CF;&#x3002;&#x8A18;&#x4E8B;&#x306E;&#x30BF;&#x30A4;&#x30C8;&#x30EB;&#x3068;&#x3001;&#x7B46;&#x8005;&#x3067;&#x3042;&#x308B;canalun&#x306E;&#x30A2;&#x30A4;&#x30B3;&#x30F3;&#x304C;&#x306E;&#x3063;&#x3066;&#x3044;&#x308B;&#x3002;" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><strong>この記事は「<a href="https://qiita.com/advent-calendar/2023/meguro-lt">めぐろLT Advent Calendar 2023</a>」の7日目の記事であります!!</strong></p> <p>そこのあなた!<br/> こだわっていますか!?<br/> 普段からこだわっている方、イベント開催でもこだわってみませんか?<br/> そんなに普段からこだわりがない方、イベント開催ではこだわってみませんか?</p> <p>この記事では、食べ物や音楽にこだわってイベントを開催してみたら、皆けっこう楽しんでくれていたよというハナシをします。<br/> <strong>要するにイベントやるときのちょっとしたTips集</strong>っぽい感じです👶</p> <p>イベントをやったことがある方も、ない方も、<br/> これからやってみようと思っている方も、一生やらない気がする方も、<br/> ぜひ読んでいってみて下さいネ!</p> <p>この記事はテックタッチのDOM大好きフロントエンドエンジニアcanalun(twitter: <a href="https://twitter.com/i_am_canalun">@i_am_canalun</a>)の提供でお送りいたします。<br/> (いまだにtwitterって言ってる人いますよね、私がそうです🌞)</p> <ul class="table-of-contents"> <li><a href="#忙しい人のためのまとめ">忙しい人のためのまとめ</a></li> <li><a href="#なんのイベントをやったのかな">なんのイベントをやったのかな?</a></li> <li><a href="#さてさて何を工夫したのよ">さてさて、何を工夫したのよ??</a><ul> <li><a href="#食べ物">食べ物</a></li> <li><a href="#飲み物">飲み物</a></li> <li><a href="#音楽">音楽</a></li> <li><a href="#道案内">道案内</a></li> </ul> </li> <li><a href="#まとめ">まとめ</a></li> </ul> <h2 id="忙しい人のためのまとめ">忙しい人のためのまとめ</h2> <p>本当は全部読んでいってほしいのですが……あなたが忙しいと仰るのならしょうがないでしょう。<br/> <a href="https://techplay.jp/event/925333">11月末に主催した「めぐろLT」というイベント</a>でテックタッチがこだわったポイントを下記にまとめておきましょう👶</p> <ul> <li>食べ物 <ul> <li><strong>受付で食べ物と飲み物を受け取ってもらい</strong>、LTセッション中に自由に食べ飲みしながら楽しんでもらうようにした</li> <li><strong>まい泉さんのミニバーガー</strong>がすごくおいしいし、イベント運営にあたり<strong>とても取り回しがよかった</strong></li> <li>食べ物の選択肢を広げるために、<strong>プラントベースフード(主要原材料に動物性食材を使わない食べ物)も用意した</strong>。スターバックスさんは表記が分かりやすくてとても助かった</li> <li>懇親会はおやつに特化した</li> </ul> </li> <li>飲み物 <ul> <li>ソフトドリンク含め、<strong>アルコール度数を低めから高めまで分散させた</strong></li> <li>ちょっと<strong>おしゃれだったり珍しかったりするお酒</strong>を色々入れてみたら見栄えがよくて楽しかった</li> </ul> </li> <li>音楽 <ul> <li><strong>誰も話さない時間はおしゃれな喫茶店のような音楽を流した</strong>。始まるまでの時間やLTセッションの入れ替わり時に<strong>会場がシーンとせず</strong>、なんだか雰囲気が良いまま進められた</li> </ul> </li> <li>会場までの道案内 <ul> <li><strong><a href="https://techtouch.notion.site/LT-9-3d92e9d9254647e9922e48f74f252853">会場までの道案内ページ</a>を作った</strong>。これはとてもわかりやすいと好評だった</li> </ul> </li> </ul> <p>さてここでまとめを読んでしまったあなた。<br/> ここで満足してタブを閉じるもよし、<br/> <a href="https://tech.techtouch.jp/">このブログの他の記事を読みにいく</a>もよし、<br/> <a href="https://qiita.com/advent-calendar/2023/meguro-lt">アドベントカレンダーの他の記事を読みにいく</a>もよし。<br/> 自由にお過ごし頂いてよいわけですが、<strong>ここからがたのしい本文ですから</strong>是非読んでいってみて下さい!!!</p> <h2 id="なんのイベントをやったのかな">なんのイベントをやったのかな?</h2> <p>みなさんは「めぐろLT」をご存知ですか??<br/> <strong>「目黒100km圏内を対象に、主にWeb系のプロダクト開発事業をしている複数社共同でゆるめに開催しているLTコミュニティ」</strong>です!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fmeguro-lt.connpass.com%2F" title="めぐろLT" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://meguro-lt.connpass.com/">meguro-lt.connpass.com</a></cite></p> <p>ラクスルさんが運営をしており、これまで月次で色々な会社さんがイベントを開催してきました。<br/> テックタッチではその9回目を11月末に主催させて頂きました✌</p> <p><strong>テーマは「Webブラウザのおもしろいハナシ!!」</strong>ということで、めぐろLTにしては珍しくけっこう狭めの範囲設定。<br/> どうしようけっこうテーマ絞っちゃったけど人来るかな……😭という心配も束の間。<br/> <strong>なんと予約は現地枠の上限を超え、オンライン枠も増設が必要になりました!</strong><br/> (現地枠に入れなかった方ごめんなさい……🙇いつかテックタッチもがんばって大きな会場でイベントを開きます!!)</p> <p><figure class="figure-image figure-image-fotolife" title="みてください、この盛り上がりを……!なんとここには会場の4分の1しか映っていません!!"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20231208/20231208121345.png" alt="LT&#x30BB;&#x30C3;&#x30B7;&#x30E7;&#x30F3;&#x4E2D;&#x306E;&#x69D8;&#x5B50;&#x3002;&#x30B9;&#x30D4;&#x30FC;&#x30AB;&#x30FC;&#x304C;&#x30B9;&#x30E9;&#x30A4;&#x30C9;&#x3092;&#x6295;&#x5F71;&#x3057;&#x3066;&#x8A71;&#x3057;&#x3066;&#x3044;&#x308B;&#x306E;&#x3092;15&#x4EBA;&#x307B;&#x3069;&#x304C;&#x5404;&#x3005;&#x306E;&#x30C6;&#x30FC;&#x30D6;&#x30EB;&#x306B;&#x7740;&#x5E2D;&#x3057;&#x3066;&#x8074;&#x3044;&#x3066;&#x3044;&#x308B;&#x3002;" width="1200" height="1144" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>みてください、この盛り上がりを……!なんとここには会場の4分の1しか映っていません!!</figcaption></figure></p> <p>今回は5つのLTセッションが行われましたが、いずれもすごく内容が濃かったです。<br/> poteboy(<a href="https://twitter.com/_poteboy_">@_poteboy_</a>)さん(ゲストスピーカーとしてお呼びさせて頂きました!)のCSSパフォーマンス測定、<br/> はと(<a href="https://twitter.com/kthatoto">@kthatoto</a>)さんのウィンドウ連結型アニメーション、<br/> KotaFujishiroさんのマイナンバーカード読み取り、<br/> ばんぶー(<a href="https://twitter.com/bamboooo_music">@bamboooo_music</a>)さんのStylusトーク、どれも本当に面白かったです!<br/> 自分もDOMラッパーをテーマにお話させて頂きましたが、面白かったと言ってくれる方がいて嬉しかったです😊</p> <p>おわったあとの懇親会も想像を超えて本当に大盛り上がりで大成功だったと言わざるをえません!!<br/> <strong>足を運んで頂いた方々、そして何よりご登壇くださった4名の方々ほんとうにありがとうございました!</strong></p> <p><figure class="figure-image figure-image-fotolife" title="あまりウケなかった小ボケ"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20231207/20231207023938.png" alt="&#x3042;&#x307E;&#x308A;&#x30A6;&#x30B1;&#x306A;&#x304B;&#x3063;&#x305F;&#x3068;&#x3055;&#x308C;&#x308B;&#x5C0F;&#x30DC;&#x30B1;&#x306E;&#x30B9;&#x30E9;&#x30A4;&#x30C9;&#x3002;DOM&#x30E9;&#x30C3;&#x30D1;&#x30FC;&#xFF1F;&#xFF1F;&#x3068;&#x3044;&#x3046;&#x30BF;&#x30A4;&#x30C8;&#x30EB;&#x3067;&#x3001;wikimedia&#x304B;&#x3089;&#x53D6;&#x3063;&#x3066;&#x304D;&#x305F;DOMDOM&#x30D0;&#x30FC;&#x30AC;&#x30FC;&#x306E;&#x5199;&#x771F;&#x3068;&#x3044;&#x3089;&#x3059;&#x3068;&#x3084;&#x306E;&#x30E9;&#x30C3;&#x30D1;&#x30FC;&#x306E;&#x30A4;&#x30E9;&#x30B9;&#x30C8;&#x304C;&#x63B2;&#x8F09;&#x3055;&#x308C;&#x3066;&#x3044;&#x308B;" width="917" height="512" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>自分のLTであまりウケなかった小ボケをここに掲載します</figcaption></figure></p> <h2 id="さてさて何を工夫したのよ">さてさて、何を工夫したのよ??</h2> <p>さあさあ、<strong>ここからがお待ちかねのこだわりおひろめタイムです</strong>。</p> <p>今回のイベントを開催するにあたってメンバーでキックオフをした時、<br/> <strong>「せっかくやるからにはできる範囲で好きに工夫して、楽しくイベントを運営してみようよ!」</strong>という声があったのです👶<br/> いやそれマジでそうじゃない!?と、思いました。<br/> 開催側が楽しんでいる方がおもしろいイベントになるんじゃない!?と、思いました!<br/> そんな考えから、今回は色々な部分でこだわったイベントにしてみようという話になったのです。</p> <p>なんだかめちゃくちゃハードルを上げてしまいましたが、できる範囲での工夫ということで「へぇ〜そんなやり方もあるんだねえ」くらいの気持ちで<strong>ゆるく眺めていってみて下さい!</strong></p> <h3 id="食べ物">食べ物</h3> <p>今回はのべ1時間のLTセッションの後に1時間半の懇親会がある、そんなタイムテーブルでした。<br/> オーソドックスなやり方としては、あらかじめ注文しておいたケータリングやピザなどを懇親会で並べて、みんなでワイワイするというものがあります。<br/> テックタッチでも、大好評を博した<a href="https://3rdpartyjs.connpass.com/event/289558/">3rdparty.js</a>(なんか急に自社の過去イベントの周知してくるヤツ)ではそのやり方で楽しく宴をすることができました😊</p> <p>ただし、今回は敢えてこだわっていこうというわけです!!<br/> なんと<strong>受付で食べ物と飲み物を受け取ってもらい、自由に食べ飲みしながらLTセッションを楽しんでもらう</strong>という形式にしました!</p> <p>これは下記に挙げられるような、「懇親会中に食事をする」というオーソドックスなやり方の悩みどころを解決するためのアイデアでした。</p> <ul> <li><strong>人と話しながら食べるのって、マナーとかなんだかんだ色々気を遣っちゃいがち</strong> <ul> <li>モグモグしている間はしゃべらないという子供の頃からの掟とは裏腹に、目の前の相手と会話しないといけない</li> <li>なんなら初対面同士だったりするとちょっと緊張するし</li> </ul> </li> <li><strong>ちょうどいい量を食べるのが難しい</strong> <ul> <li>なんか気づいたらあまり食べられずにお腹が空いたまま終わってしまう(帰りにカップ麺とか買いがち)</li> <li>逆に、余ったりなんだりしてるのを見て、気を遣って多めに食べちゃう</li> </ul> </li> <li><strong>TBTT(ザ・ベスト・食べ頃・タイム)を逃す</strong> <ul> <li>あったかいのが美味しいはずなのに冷めちゃう(ピザとかね😭)</li> <li>お寿司がちょっと乾いちゃう</li> </ul> </li> </ul> <p><strong>もちろん食事をしながらワイワイするのも楽しいですからね!</strong>私だって、なんだかんだそれも好きです✌<br/> ただ今回は工夫してみようというわけで、<strong>受付で食べ物と飲み物を受け取ってもらい、LTセッションを自由に食べ飲みしながら楽しんでもらう</strong>という形式でやってみました。<br/> これは、参加者の方全員分の椅子とテーブルが用意できたということもあって実現した作戦でもあります。</p> <p>具体的には受付時点で下記のものをお渡ししました。</p> <ul> <li>食べ物: まい泉さんのミニバーガー2つ、もしくは、プラントベースのピザトースト1つ</li> <li>飲み物: お酒もしくはソフトドリンク1本、と、お水もしくはお茶1本</li> </ul> <p>まず、<a href="https://mai-sen.com/delica/category/sandwich/burger">まい泉さんのミニバーガー</a>は<strong>本当においしい</strong>です。これはおいしい。<br/> そして、配るのも簡単。<strong>手のひらより少し大きいくらいのサイズで個包装だからサクッと手渡しできます</strong>。<br/> で、この<strong>サイズ感が楽しい</strong>んですよね。「かわいい!!!」と受付でもらったときに盛り上がって下さる方もいました。<br/> ……<br/> ……<br/> <strong>……なんですかこの素晴らしすぎる食べ物は!!!!😭アリガトウゴザイマス</strong></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fmai-sen.com%2Fdelica%2Fcategory%2Fsandwich%2Fburger%2F" title="ミニバーガー | 販売店 | とんかつ まい泉 | MAISEN" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://mai-sen.com/delica/category/sandwich/burger/">mai-sen.com</a></cite></p> <p>また、今回は食べ物の選択肢を広げておこうということで<strong>プラントベースフード</strong>も用意させて頂きました🌵</p> <blockquote><p>プラントベースとは、動物由来の原材料を配合せず、植物由来の原材料を使用した食品全般のことを言います<br/> <a href="https://www.kagome.co.jp/products/brand/plantbased/">&#x30AB;&#x30B4;&#x30E1;&#xFF5C;&#x30D7;&#x30E9;&#x30F3;&#x30C8;&#x30D9;&#x30FC;&#x30B9;</a>より</p></blockquote> <p>動物由来のものを好まない/食べられないような方でも安心して食べられるように<strong>明確な表記のあるもの</strong>を探したところ、スターバックスさんが該当商品に店頭で「プラントベース」の表示をしてくださっていました!<br/> 例えば下記のリンク先にあるような食べ物が売っています。<br/> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.starbucks.co.jp%2Fpress_release%2Fpr2022-4669.php" title="スターバックス フードの新しい選択肢が登場食べて美味しい、カラダもココロもやさしい気分に満たされる「プラントベース」フードメニューを拡充!6月1日(水)より販売開始|スターバックス コーヒー ジャパン" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://www.starbucks.co.jp/press_release/pr2022-4669.php">www.starbucks.co.jp</a></cite></p> <p>今回はこちらの「セミドライトマトのピザトースト」を選ばせて頂きました🍅<br/> おいしそうだろ〜??</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fmenu.starbucks.co.jp%2F4524785552517" title="セミドライトマトのピザトースト|サンドイッチ|スターバックス コーヒー ジャパン" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://menu.starbucks.co.jp/4524785552517">menu.starbucks.co.jp</a></cite></p> <p>プラントベースフードが店頭で入手できるのはとても嬉しいです。<br/> これは安心だということで購入し、受付でお渡しする食べ物の選択肢に加えてみました。</p> <p>さて、つまりはこういうことです。<br/> <strong>私たちは好きな飲み物を片手に、好きな食べ物を頬張りながら、すごく良い内容のLTを聞く!!!!</strong><br/> これ以上あるのだろうか??と思ってしまいますね。うんうん。<br/> (好きな食べ物や飲み物がなかった方、すみません😭こんどまた来てみてくださいね)</p> <p>そして懇親会はおやつに特化しました。<br/> おやつは単に「自分の趣味で色々用意するのが楽しかったよ」ということがすべてですのでスキップします。<br/> ちょっとチャレンジングなところとしては、<strong>個包装のよっちゃん</strong>も置かせて頂きました。匂いがすごいんですよね。<br/> でもウェットティッシュと一緒に置いておいたところ無くなっていたので、私もよっちゃんも報われました。 ありがとう、食べてくれた人……😭</p> <h3 id="飲み物">飲み物</h3> <p>さあ、お次は飲み物です。</p> <p>こちらは<strong>アルコール度数を正規分布的に分散させる</strong>ことを意識しました。<br/> 具体的には0%(ソフトドリンク)、3%、5%、7%のグループで3%と5%が一番多いという感じです。</p> <p>3%だとサントリーさんの「ほろよい」が王道ですよね。<br/> 「レモみかん」「白いサワー」「フルーツサングリア」「もも」などなど、色々なフレーバーでご用意しました。<br/> (ほろよいで「フレーバー」って言う人珍しくない?)</p> <p>5%および7%の部ではビールをメインに据えて、ハイボールやサワーを織り交ぜる戦略をとりました。<br/> ビールは<strong>クラフトビール</strong>を予算の範囲内で積極的に散りばめていきます。</p> <p>手に入りやすい、しかしそこまで万人が見慣れていないビールとしては<strong>「エチゴビール」</strong>さんの缶でしょうか。<br/> <strong>パステルグリーンにシロクマがかわいい「のんびりふんわり白ビール」</strong>。<br/> <strong>パステルブルーに躍動感のあるドラゴン「FLYING IPA」</strong>。<br/> リンク先に、<strong>アガるデザイン</strong>の缶がたくさん掲載されているので見に行ってみましょう👀<br/> アガってけ!!<br/> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fechigobeer.com%2Fproducts.php" title="商品一覧-エチゴビール株式会社" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://echigobeer.com/products.php">echigobeer.com</a></cite></p> <p>他にはサントリーさんの「TOKYO CRAFT」。<br/> 「めぐろLT」なのに「オリオンビール」や「サッポロ生ビール黒ラベル」も置いちゃいました。 めぐろを起点に日本列島を縦断しちゃうんですねえ。</p> <p><strong>色々な見た目の飲み物を集めたのでなんとなく見栄えもよくなって楽しかった</strong>です✌<br/> <figure class="figure-image figure-image-fotolife" title="色々あって楽しい🍺"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20231208/20231208112240.png" alt="&#x30C6;&#x30FC;&#x30D6;&#x30EB;&#x306E;&#x4E0A;&#x306B;&#x7F36;&#x306E;&#x304A;&#x9152;&#x304C;&#x8272;&#x3005;&#x3068;&#x4E26;&#x3079;&#x3089;&#x308C;&#x3066;&#x3044;&#x308B;&#x69D8;&#x5B50;&#x3002;&#x7DD1;&#x3084;&#x8D64;&#x3001;&#x9280;&#x3001;&#x30D4;&#x30F3;&#x30AF;&#x306A;&#x3069;&#x8272;&#x304C;&#x69D8;&#x3005;&#x306B;&#x306A;&#x3063;&#x3066;&#x3044;&#x308B;&#x3002;" width="1200" height="565" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>色々あって楽しい🍺</figcaption></figure></p> <p>なお自分は当日準備中に「クラフトビール屋さんみたいに缶積み上げちゃいますか!?」とかメンバーに言ってみたのですが、相手が極めて冷静に「いや、倒れたりしたら、ほら、大変だしテンション下がるし……」という意見を陳述してきて我に返りました。<br/> アルコール摂取前でしたが、すでに雰囲気にのまれてしまっていましたね。うんうん。</p> <p>この飲み物バリエーション作戦も大成功でして、来場者の方の中には<strong>「え〜なにこれこんな色々あるの〜」と楽しそうな方がいらっしゃいました😊</strong><br/> 自分はそれを見ながら「おいおいまだイベント始まってないのにそんな楽しんじゃってていいの!?(もちろんいい)」と嬉しかったです。</p> <h3 id="音楽">音楽</h3> <p>さて、次は音楽。<br/> 「音楽!?」と思ったそこのあなた(別に思ってませんかね)、これが意外とけっこう重要です。</p> <p>イベントを開催したり、イベントに行ってみたりすると分かるかと思いますが、 <strong>無音の時間がびみょ〜にストレスになることがあります</strong>。<br/> 例えば、始まるまでに会場でみんなが座って待っている時間やLTスピーカーの交代タイミングなどなど。<br/> <strong>「こんなに人がいるのになんか静かだな!?」という時間がイベントにはつきものです。</strong></p> <p>そしてそんな時間はなんだか気まずかったり、何より<strong>せっかく上がってきた会場のボルテージが下がってしまったりします</strong>。<br/> これは本当にもったいないです。LTセッションでせっかく温まった場の空気も、音をたててしぼんでいきます(会場は静かなのにね、ってやかましいわ!)。<br/> 気にしすぎだよと言われれればそこまでですが、自分はけっこう気になるタイプです🐤</p> <p>さて、そこで音楽です。<br/> 始まるまで会場でみんなが座って待っている時間。<br/> LTスピーカーの交代タイミング。<br/> そんな<strong>「手持ちぶさたになる時間」におしゃれな音楽を流してみます</strong>。<br/> すると、あら不思議。<strong>もう完全におしゃれ喫茶店です。</strong><br/> (※どんな雰囲気になるかは音楽次第です。ハウスみたいな曲でノリノリになるのも一興)</p> <p>これは相当よかったですよ、みんなも音楽を流してみてね!!</p> <h3 id="道案内">道案内</h3> <p>最後に道案内です。イベントで意外と問題になりがちなのが、会場までの行き方なんですよね。</p> <p>今回テックタッチで使わせて頂いた会場は「Global Business Hub Tokyo」というすごくキレイでおしゃれなスペースです👶<br/> 大手町駅直結でめちゃくちゃアクセスもいいというハンパない場所です。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.gbh-tokyo.or.jp%2F" title="グローバルビジネスハブ東京 | 大手町から世界につながるコラボレーションオフィス" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://www.gbh-tokyo.or.jp/">www.gbh-tokyo.or.jp</a></cite></p> <p>ただ、大手町駅のビルはどれも、とても充実しているがゆえに広くてちょっと複雑です。<br/> ビル自体が駅直結ですぐにたどり着けたと思ったら、<br/> <strong>「えっなんか地下街がある……?」</strong><br/> <strong>「??違うビルについちゃった☆」</strong><br/> <strong>「エスカレーターがない!?!?ってか階段もない!?」</strong><br/> ……こんなことになってしまいがちです😭<br/> しまいには<strong>「ど、どうやって行くんだ〜!?うわあああああああああ」</strong>となるのが関の山。</p> <p>そこで、今回は<strong>道案内ページをconnpassに掲載</strong>しておくこととしました。<br/> 下記に実際のページを載せますので、ぜひ見てみてほしいです。けっこう分かりやすいんじゃないですか!?</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftechtouch.notion.site%2FLT-9-3d92e9d9254647e9922e48f74f252853" title="めぐろLT #9 会場案内 | Built with Notion" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://techtouch.notion.site/LT-9-3d92e9d9254647e9922e48f74f252853">techtouch.notion.site</a></cite></p> <p>このようなページは他社さんのイベントでも作られているのをよく見ます。<br/> 道案内ページがあると本当に会場に行く手間が1/50倍くらいになっている気がします。<br/> みんなも道案内を作ってみてね!!</p> <h2 id="まとめ">まとめ</h2> <p>さて、しつこいようですが最後にもう一度ポイントを掲載しておきます。<br/> 要点ゴリ押し型ブログ記事です。</p> <ul> <li>食べ物 <ul> <li><strong>受付で食べ物と飲み物を受け取ってもらい</strong>、LTセッション中に自由に食べ飲みしながら楽しんでもらうようにした</li> <li><strong>まい泉さんのミニバーガー</strong>がすごくおいしいし、イベント運営にあたり<strong>とても取り回しがよかった</strong></li> <li>食べ物の選択肢を広げるために、<strong>プラントベースフード(主要原材料に動物性食材を使わない食べ物)も用意した</strong>。スターバックスさんは表記が分かりやすくてとても助かった</li> <li>懇親会はおやつに特化した</li> </ul> </li> <li>飲み物 <ul> <li>ソフトドリンク含め、<strong>アルコール度数を低めから高めまで分散させた</strong></li> <li>ちょっと<strong>おしゃれだったり珍しかったりするお酒</strong>を色々入れてみたら見栄えがよくて楽しかった</li> </ul> </li> <li>音楽 <ul> <li><strong>誰も話さない時間はおしゃれな喫茶店のような音楽を流した</strong>。始まるまでの時間やLTセッションの入れ替わり時に<strong>会場がシーンとせず</strong>、なんだか雰囲気が良いまま進められた</li> </ul> </li> <li>会場までの道案内 <ul> <li><strong><a href="https://techtouch.notion.site/LT-9-3d92e9d9254647e9922e48f74f252853">会場までの道案内ページ</a>を作った</strong>。これはとてもわかりやすくて良かった</li> </ul> </li> </ul> <p>テックタッチでイベントを主催したのはこれが2回目でしたが、これからも色々なイベントができたらなと思っています!<br/> これまでは「<a href="https://3rdpartyjs.connpass.com/event/289558/">3rdpartyスクリプト</a>」や「<a href="https://meguro-lt.connpass.com/event/295779/">ブラウザのおもしろい話</a>」という<strong>ちょっとニッチなフロントエンドトピック</strong>で大盛りあがりでしたが、今後の展開にもぜひご期待下さい👶</p> <p>ではでは、引き続き<a href="https://qiita.com/advent-calendar/2023/meguro-lt">めぐろLT Advent Calendar 2023</a>をお楽しみ下さい!<br/> お読みくださりありがとうございました〜👋</p> techtouch リアルタイム検索最適化:Reactアプリにdebounce処理を組み込む hatenablog://entry/6801883189058387465 2023-11-20T09:00:00+09:00 2023-11-20T09:00:00+09:00 Reactアプリケーションでdebounce処理を実装する際に考慮したことについて紹介します。 <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/shzawa/20231113/20231113161834.png" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"><ul class="table-of-contents"> <li><a href="#はじめに">はじめに</a></li> <li><a href="#そもそも-debounce-処理って">そもそも debounce 処理って?</a></li> <li><a href="#本題">本題</a></li> <li><a href="#今回作りたかったもの">今回作りたかったもの</a></li> <li><a href="#検討したこと">検討したこと</a><ul> <li><a href="#useDebounce">useDebounce</a></li> <li><a href="#useDebouncedValue">useDebouncedValue</a></li> </ul> </li> <li><a href="#実装したもの">実装したもの</a></li> <li><a href="#最後に">最後に</a></li> </ul> <h2 id="はじめに">はじめに</h2> <p>テックタッチのフロントエンドエンジニアの ozaan (<a href="https://twitter.com/shzawa">@shzawa</a>) です。関西 (兵庫県姫路市) 在住のため、普段はフルリモートのメンバーとしてサービス開発に取り組んでいます。</p> <p>最近はスクラム開発の一環でバックエンドのコード (golang) を触らせてもらっていました。golang だと書き方が統一されているためか GitHub Copilot の補完機能がかなり効くので書いていて面白かったです。</p> <h2 id="そもそも-debounce-処理って">そもそも debounce 処理って?</h2> <p>対象のイベントが発生してから指定した時間が経過するまでは、同じイベントの発生を抑制する仕組みです。</p> <p>input 要素の onChange イベントのような短い間隔で連続して発生するイベントに debounce 処理をかけて API 通信の頻度を抑えることで、パフォーマンスの向上を図ることができます。</p> <h2 id="本題">本題</h2> <p>弊社サービスの管理画面の開発を行う中で、それまでフロントエンドで行っていた検索処理を、バックエンドの検索用 API を経由して行う形に変更する必要が生まれました。それに合わせて、インクリメンタルサーチ機能が利用される際にバックエンドに送信するリクエスト数を抑制する、debounce 処理を掛ける必要が生まれました。 そこで今回はReact アプリケーションで debounce 処理を扱う際に考慮したことなどについて書いていきます。</p> <h2 id="今回作りたかったもの">今回作りたかったもの</h2> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/shzawa/20231113/20231113103142.png" alt="&#x30E6;&#x30FC;&#x30B6;&#x30FC;&#x304C;&#x6587;&#x5B57;&#x5165;&#x529B;&#x3059;&#x308B;&#x3068;&#x30A2;&#x30C9;&#x30EC;&#x30B9;&#x30D0;&#x30FC;&#x306E;&#x30AF;&#x30A8;&#x30EA;&#x30D1;&#x30E9;&#x30E1;&#x30FC;&#x30BF;&#x304C;&#x66F4;&#x65B0;&#x3055;&#x308C;&#x307E;&#x3059;&#x3002;&#x305D;&#x308C;&#x306B;&#x3088;&#x308A;&#x691C;&#x7D22;&#x304C;&#x5B9F;&#x884C;&#x3055;&#x308C;&#x691C;&#x7D22;&#x7D50;&#x679C;&#x304C;&#x63CF;&#x753B;&#x3055;&#x308C;&#x307E;&#x3059;&#x3002;" width="1200" height="163" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <ul> <li>キーワード検索ボックス</li> <li>ユーザーが文字を入力している間は検索の実行 (リクエスト送信) を抑制したい</li> <li>ナビゲーション処理は Atomic Design の Pages に相当するコンポーネントに記述したい。そういったコンポーネントで扱うステートの数はなるべく減らしたい</li> </ul> <h2 id="検討したこと">検討したこと</h2> <p>React アプリケーションで検索処理に掛ける debounce 処理の実装は、Lodash の debounce 関数 (<a href="https://lodash.com/docs/4.17.15#debounce">https://lodash.com/docs/4.17.15#debounce</a>) を使って行いました。今回のように処理だけを debounce させたいケースでは、コンポーネントのステート管理をシンプルに保つことができるためです。</p> <p>他に検討したものとして、useDebounce と useDebouncedValue が挙がりました。</p> <h3 id="useDebounce">useDebounce</h3> <p>例: react-use の useDebounce <a href="https://github.com/streamich/react-use/blob/master/docs/useDebounce.md">https://github.com/streamich/react-use/blob/master/docs/useDebounce.md</a></p> <p>useDebounce を用いる場合、ユーザーの入力を反映するものとユーザーが入力中かどうかを示すものの2種類のステートを用意する必要があり、今回のケースでは Lodash の debounce 関数で書くよりもコードが冗長になってしまうことが判明したため、採用しませんでした。</p> <h3 id="useDebouncedValue">useDebouncedValue</h3> <p>例: Mantine UI の useDebouncedValue <a href="https://mantine.dev/hooks/use-debounced-value/">https://mantine.dev/hooks/use-debounced-value/</a></p> <p>useDebouncedValue はステートを複製した値の更新に debounce 処理を掛けます。これを使って処理に debounce 処理を掛けるには useEffect と組み合わせる必要があり、コンポーネントの内部が複雑になってしまうため採用しませんでした。</p> <p>既存の React アプリケーションに debounce 処理を組み込むに当たって、ユーザーが入力してから最新の検索結果が描画されるまでの流れを整理し、以下のような実行フローになるよう設計しました。 (以下は実際のフローの簡略版です)</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/shzawa/20231113/20231113104101.png" alt="&#x30E6;&#x30FC;&#x30B5;&#x3099;&#x30FC;&#x304B;&#x3099;&#x6587;&#x5B57;&#x5165;&#x529B;&#x3059;&#x308B;&#x3053;&#x3068;&#x3066;&#x3099;React&#x30A2;&#x30D5;&#x309A;&#x30EA;&#x304B;&#x3099;&#x6301;&#x3064;&#x5165;&#x529B;&#x5024;&#x30B9;&#x30C6;&#x30FC;&#x30C8;&#x304B;&#x3099;&#x66F4;&#x65B0;&#x3055;&#x308C;&#x307E;&#x3059;&#x3002;&#x305D;&#x308C;&#x306B;&#x5408;&#x308F;&#x305B;&#x3066;&#x30AF;&#x30A8;&#x30EA;&#x30CF;&#x309A;&#x30E9;&#x30E1;&#x30FC;&#x30BF;&#x3092;&#x66F4;&#x65B0;&#x3059;&#x308B;setSearchParams&#x304C;&#x5B9F;&#x884C;&#x3055;&#x308C;&#x307E;&#x3059;&#x3002;React&#x30A2;&#x30D7;&#x30EA;&#x306E;&#x4E2D;&#x306E;&#x30B3;&#x30F3;&#x30DD;&#x30FC;&#x30CD;&#x30F3;&#x30C8;&#x304C;&#x518D;&#x30EC;&#x30F3;&#x30C0;&#x30EA;&#x30F3;&#x30B0;&#x3055;&#x308C;&#x308B;&#x3053;&#x3068;&#x3067;&#x30E6;&#x30FC;&#x30B6;&#x30FC;&#x304C;&#x691C;&#x7D22;&#x7D50;&#x679C;&#x3092;&#x95B2;&#x89A7;&#x3067;&#x304D;&#x308B;&#x3088;&#x3046;&#x306B;&#x306A;&#x308A;&#x307E;&#x3059;&#x3002;" width="1200" height="292" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="実装したもの">実装したもの</h2> <p>以下が実装したものの簡易版になります。文字入力されると、検索 API リクエスト用 Hooks が走るようになっています。また、文字入力されている間は、検索 API リクエストを debounce させるようになっています。</p> <iframe src="https://stackblitz.com/edit/stackblitz-starters-enzh9b?embed=1&file=src%2FPage.tsx" width="680" height="400"></iframe> <p>React アプリケーションで debouce 関数を呼び出す場合、上記のサンプルのようにメモ化する必要があります。メモ化をしていないと、コンポーネントのスナップショットが更新 (再レンダリング) される度に debounce 関数内で保持している最後に入力された値がリセットされてしまうため、実質的に debounce 処理が機能しなくなってしまいます。</p> <h2 id="最後に">最後に</h2> <p>この機能を作り始めた当初は、useDebouncedValue を使おうとして、挙動の理解が浅かったため上手くいかず詰まっていました。解決したいものが状態なのか振る舞いなのかを切り分けるのは大事ですね。ありがとうございました。</p> shzawa Amazon Verified Permissionsとgolangで認可処理を実装してみた hatenablog://entry/6801883189052007087 2023-11-08T09:00:00+09:00 2023-11-08T09:00:02+09:00 先日 GA (一般利用可能)になった AWS のサービス Amazon Verified Permissions を、 golang で実装した簡単なサンプルを交えて紹介します。 <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yamanoi-y/20231106/20231106193101.png" alt="" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"><ul class="table-of-contents"> <li><a href="#Amazon-Verified-Permissions-とは">Amazon Verified Permissions とは</a></li> <li><a href="#従来の認可処理">従来の認可処理</a></li> <li><a href="#Cedar-言語の使い方">Cedar 言語の使い方</a><ul> <li><a href="#基本的な記述方法">基本的な記述方法</a></li> <li><a href="#RBAC-の例">RBAC の例</a></li> <li><a href="#ABAC-の例">ABAC の例</a></li> </ul> </li> <li><a href="#golang-で動かしてみる">golang で動かしてみる</a><ul> <li><a href="#1-ポリシーストアを-AWS-コンソールから作成する">1. ポリシーストアを AWS コンソールから作成する</a></li> <li><a href="#3-サンプルアプリケーションの実装">3. サンプルアプリケーションの実装</a></li> </ul> </li> <li><a href="#最後に">最後に</a></li> </ul> <p>こんにちは、2023年5月にバックエンドエンジニアとしてジョインした yamanoi です。<br/> 最近は Cloudflare スタックに注目しており、新機能を触ったりアップデートを眺めたりしています。</p> <p>今回は先日 GA (一般利用可能)になった AWS のサービス Amazon Verified Permissions を、 golang で実装した簡単なサンプルを交えて紹介したいと思います。</p> <h2 id="Amazon-Verified-Permissions-とは">Amazon Verified Permissions とは</h2> <p><a href="https://aws.amazon.com/jp/verified-permissions/">Amazon Verified Permissions</a> とは、ポリシーベースの認可処理の実装を肩代わりしてくれるマネージドサービスです。</p> <p>ロールベースのアクセス制御(RBAC)や属性ベースのアクセス制御(ABAC)を実現することができ、アプリケーションから認可の処理を一部分離することが可能になります。</p> <p>Amazon Verified Permissions ではポリシー言語として Cedar 言語を採用しており、この言語を用いてアプリケーションに必要な認可のルールを記述していきます。<br/> <span style="font-size: 80%">*ポリシー言語というのはAWSのIAM Policyのような認可に関するポリシーを記述するための言語となっています。 </span></p> <p>この記事では、サービスの使いどころ、 Cedar 言語の使い方、サンプルアプリケーションの実装までを解説していきたいと思います。</p> <h2 id="従来の認可処理">従来の認可処理</h2> <p>認可処理は従来サービスの開発する開発者自身で実装することが多いと思います。 認可処理を実装する課題点として以下のようなものが挙げられます。</p> <ul> <li>複雑性の問題</li> <li>セキュリティのリスク</li> <li>パフォーマンスへの影響</li> </ul> <p>そんな認可処理を一部置き換えてくれるサービスが AWS Verified Permissions になります。</p> <p>認可処理の実装については <a href="https://www.osohq.com/academy">Authorization Academy</a> という資料が非常に分かりやすく参考になります。 <span style="font-size: 80%">日本語でわかりやすく解説している記事はこちら </span><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fzenn.dev%2Fshe_techblog%2Farticles%2F6eff1f28d107be" title="認可のアーキテクチャに関する考察(Authorization Academy IIを読んで)" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://zenn.dev/she_techblog/articles/6eff1f28d107be">zenn.dev</a></cite></p> <p>この資料でも言及されていますが、 認可処理は極力アプリケーションのロジックから切り離して実装することで複雑度を下げることが可能です。<br/> 中でもインターフェースとして認可の判断(Decision)と適用(Enforcement)に分離するという考え方があります。</p> <p><a href="https://assets.website-files.com/5f1483105c9a72fd0a3b662a/60516eaf35e59b6c28d3f34c_WFrzEd5ZWHDKF7jFh7Px7-0sPYCEMmosPJRKCeuy4QSjg9EoIfKGThEg-0OkBVFUZBELsirpT61LL1a57VyheldVOBH9BGBJ2c4cxJhcY2Hw6B4xkDjo34SQOLS9Nsz7QjYms6ZJ.png" class="http-image"><img src="https://assets.website-files.com/5f1483105c9a72fd0a3b662a/60516eaf35e59b6c28d3f34c_WFrzEd5ZWHDKF7jFh7Px7-0sPYCEMmosPJRKCeuy4QSjg9EoIfKGThEg-0OkBVFUZBELsirpT61LL1a57VyheldVOBH9BGBJ2c4cxJhcY2Hw6B4xkDjo34SQOLS9Nsz7QjYms6ZJ.png" class="http-image" alt="https://assets.website-files.com/5f1483105c9a72fd0a3b662a/60516eaf35e59b6c28d3f34c_WFrzEd5ZWHDKF7jFh7Px7-0sPYCEMmosPJRKCeuy4QSjg9EoIfKGThEg-0OkBVFUZBELsirpT61LL1a57VyheldVOBH9BGBJ2c4cxJhcY2Hw6B4xkDjo34SQOLS9Nsz7QjYms6ZJ.png"></a> 出典: <a href="https://www.osohq.com/academy/what-is-authorization">Authorization Academy - What is Authorization?</a></p> <p>Amazon Verified Permissions は、ここで言う認可の判断(Decision)を行う際に利用できます。 そしてアプリケーションはその結果を受け、どう適用(Enforcement)させるかに集中できます。</p> <h2 id="Cedar-言語の使い方">Cedar 言語の使い方</h2> <p>Amazon Verified Permissions で利用される Cedar 言語は、 Rust で開発しているオープンソースの言語となります。<br/> 公式サイト: <a href="https://www.cedarpolicy.com/en">https://www.cedarpolicy.com/en</a></p> <p>公式サイトにはチュートリアルに加えて <a href="https://www.cedarpolicy.com/en/playground">Playground</a> もあるため、サクッと動作を確認することができます。</p> <p>それでは Cedar 言語の書き方を紹介していきたいと思います。</p> <h3 id="基本的な記述方法">基本的な記述方法</h3> <pre class="code" data-lang="" data-unlink>permit ( principal == PhotoApp::User::&#34;alice&#34;, action == PhotoApp::Action::&#34;viewPhoto&#34;, resource == PhotoApp::Photo::&#34;vacationPhoto.jpg&#34; ); </pre> <p>Cedar 言語では上記のように適用させたいポリシーを書いていきます。</p> <p>こちらの例は <br/> <code>principal(PhotoApp::User::"alice")</code>が <code>resource(PhotoApp::Photo::"vacationPhoto.jpg")</code>に対して <code>action(PhotoApp::Action::"viewPhoto")</code>を行うことを許可するポリシーとなります。</p> <p>普段 AWS を使っている方であれば、 IAM Policy と似ているため直感的に理解しやすいかと思います。<br/> Cedar では認可に関するポリシーをまず <code>permit</code> または <code>forbid</code> で指定します。<br/> 何も記載されていない場合は権限が無いものとして扱われます。<br/> IAM Policy の Deny と同様に forbid のルールは permit ルールを上書きするため、絶対に許可したくない場合等に利用することができます。<br/> 次に principal, action, resource と記述し認可の具体的な内容を記載していきます。</p> <h3 id="RBAC-の例">RBAC の例</h3> <pre class="code" data-lang="" data-unlink>permit( principal in Role::&#34;vacationPhotoJudges&#34;, action == Action::&#34;view&#34;, resource == Photo::&#34;vacationPhoto94.jpg&#34; ); </pre> <pre class="code" data-lang="" data-unlink>// entity [ { &#34;uid&#34;: { &#34;type&#34;: &#34;User&#34;, &#34;id&#34;: &#34;Bob&#34; }, &#34;attrs&#34;: {}, &#34;parents&#34;: [ { &#34;type&#34;: &#34;Role&#34;, &#34;id&#34;: &#34;vacationPhotoJudges&#34; }, { &#34;type&#34;: &#34;Role&#34;, &#34;id&#34;: &#34;juniorPhotographerJudges&#34; } ] }, { &#34;uid&#34;: { &#34;type&#34;: &#34;Role&#34;, &#34;id&#34;: &#34;vacationPhotoJudges&#34; }, &#34;attrs&#34;: {}, &#34;parents&#34;: [] }, { &#34;uid&#34;: { &#34;type&#34;: &#34;Role&#34;, &#34;id&#34;: &#34;juniorPhotographerJudges&#34; }, &#34;attrs&#34;: {}, &#34;parents&#34;: [] } ] </pre> <p>ロールベースアクセスを実現するには principal に特定のユーザーを指定するのではなく、ロールを指定 する必要があります。<br/> RBAC を実現するには User が複数の Role をもてるような principal の構造にします。<br/> そして、 principal に許可する Role を in で指定します。</p> <h3 id="ABAC-の例">ABAC の例</h3> <pre class="code" data-lang="" data-unlink>permit( principal, action == Action::&#34;view&#34;, resource ) when {resource.accessLevel == &#34;public&#34; &amp;&amp; principal.location == &#34;USA&#34;}; </pre> <pre class="code" data-lang="" data-unlink>// entity [ { &#34;uid&#34;: { &#34;type&#34;: &#34;Photo&#34;, &#34;id&#34;: &#34;VacationPhoto94.jpg&#34; }, &#34;attrs&#34;: { &#34;accessLevel&#34;: &#34;public&#34; }, &#34;parents&#34;: [] }, { &#34;uid&#34;: { &#34;type&#34;: &#34;User&#34;, &#34;id&#34;: &#34;alice&#34; }, &#34;attrs&#34;: { &#34;location&#34;: &#34;USA&#34; }, &#34;parents&#34;: [] } ] </pre> <p>principal や resource に何も指定しない場合は、全ての principal または resource に対してアクセスを許可することができます。<br/> さらに when 句で条件を指定してあげることで、 principal や resource に対して付与している属性ベースで権限を絞り込む事ができます。<br/> これまでの説明に出てきた "PhotoApp**" のようなものは、事前に Schema として定義することができます。</p> <p>他にも様々な記述の仕方や仕様があります。<br/> さらに詳しい内容は公式ドキュメントに記載しているので、気になった方はぜひ覗いてみてください。<br/> <a href="https://docs.cedarpolicy.com/.">https://docs.cedarpolicy.com/.</a></p> <h2 id="golang-で動かしてみる">golang で動かしてみる</h2> <p>実際にサンプルプロジェクトを題材に AWS Verified Permissions を利用して、<br/> 認可処理を golang で実装したいと思います。</p> <p>今回は PhotoFlash というサンプルプロジェクトを利用します。<br/> PhotoFlash とは、個々のユーザーが写真やアルバムを共有できるサービスとなります。<br/> <a href="https://docs.aws.amazon.com/ja_jp/verifiedpermissions/latest/userguide/getting-started-first-policy-store.html">https://docs.aws.amazon.com/ja_jp/verifiedpermissions/latest/userguide/getting-started-first-policy-store.html</a></p> <h3 id="1-ポリシーストアを-AWS-コンソールから作成する">1. ポリシーストアを AWS コンソールから作成する</h3> <p>サンプルポリシーストアから PhotoFlash を選択して作成します。 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yamanoi-y/20231009/20231009123209.png" width="1200" height="983" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>ポリシーストアを作成すると以下のスキーマが作成されます。 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yamanoi-y/20231009/20231009123647.png" width="1200" height="1135" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><details> <summary>JSON によるスキーマ定義はこちら</summary></p> <pre class="code lang-json" data-lang="json" data-unlink><span class="synSpecial">{</span> &quot;<span class="synStatement">PhotoFlash</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">entityTypes</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">Album</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">memberOfTypes</span>&quot;: <span class="synSpecial">[</span> &quot;<span class="synConstant">Album</span>&quot;, &quot;<span class="synConstant">Account</span>&quot; <span class="synSpecial">]</span>, &quot;<span class="synStatement">shape</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">attributes</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">Name</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">String</span>&quot; <span class="synSpecial">}</span> <span class="synSpecial">}</span>, &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">Record</span>&quot; <span class="synSpecial">}</span> <span class="synSpecial">}</span>, &quot;<span class="synStatement">User</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">shape</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">attributes</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">Email</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">String</span>&quot; <span class="synSpecial">}</span>, &quot;<span class="synStatement">Account</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">name</span>&quot;: &quot;<span class="synConstant">PhotoFlash::Account</span>&quot;, &quot;<span class="synStatement">required</span>&quot;: <span class="synConstant">true</span>, &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">Entity</span>&quot; <span class="synSpecial">}</span> <span class="synSpecial">}</span>, &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">Record</span>&quot; <span class="synSpecial">}</span>, &quot;<span class="synStatement">memberOfTypes</span>&quot;: <span class="synSpecial">[</span> &quot;<span class="synConstant">FriendGroup</span>&quot; <span class="synSpecial">]</span> <span class="synSpecial">}</span>, &quot;<span class="synStatement">FriendGroup</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">shape</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">attributes</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">Name</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">String</span>&quot; <span class="synSpecial">}</span> <span class="synSpecial">}</span>, &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">Record</span>&quot; <span class="synSpecial">}</span>, &quot;<span class="synStatement">memberOfTypes</span>&quot;: <span class="synSpecial">[</span> &quot;<span class="synConstant">Account</span>&quot; <span class="synSpecial">]</span> <span class="synSpecial">}</span>, &quot;<span class="synStatement">Account</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">memberOfTypes</span>&quot;: <span class="synSpecial">[]</span>, &quot;<span class="synStatement">shape</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">Record</span>&quot;, &quot;<span class="synStatement">attributes</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">Name</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">String</span>&quot; <span class="synSpecial">}</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span>, &quot;<span class="synStatement">Photo</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">memberOfTypes</span>&quot;: <span class="synSpecial">[</span> &quot;<span class="synConstant">Album</span>&quot;, &quot;<span class="synConstant">Account</span>&quot; <span class="synSpecial">]</span>, &quot;<span class="synStatement">shape</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">attributes</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">IsPrivate</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">Boolean</span>&quot; <span class="synSpecial">}</span>, &quot;<span class="synStatement">Name</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">String</span>&quot; <span class="synSpecial">}</span> <span class="synSpecial">}</span>, &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">Record</span>&quot; <span class="synSpecial">}</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span>, &quot;<span class="synStatement">actions</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">SetPrivacyFlag</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">appliesTo</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">principalTypes</span>&quot;: <span class="synSpecial">[</span> &quot;<span class="synConstant">User</span>&quot; <span class="synSpecial">]</span>, &quot;<span class="synStatement">resourceTypes</span>&quot;: <span class="synSpecial">[</span> &quot;<span class="synConstant">Photo</span>&quot; <span class="synSpecial">]</span>, &quot;<span class="synStatement">context</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">attributes</span>&quot;: <span class="synSpecial">{}</span>, &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">Record</span>&quot; <span class="synSpecial">}</span> <span class="synSpecial">}</span>, &quot;<span class="synStatement">memberOf</span>&quot;: <span class="synSpecial">[</span> <span class="synSpecial">{</span> &quot;<span class="synStatement">id</span>&quot;: &quot;<span class="synConstant">ManageAccount</span>&quot;, &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">PhotoFlash::Action</span>&quot; <span class="synSpecial">}</span> <span class="synSpecial">]</span> <span class="synSpecial">}</span>, &quot;<span class="synStatement">CloseAccount</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">appliesTo</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">principalTypes</span>&quot;: <span class="synSpecial">[</span> &quot;<span class="synConstant">User</span>&quot; <span class="synSpecial">]</span>, &quot;<span class="synStatement">resourceTypes</span>&quot;: <span class="synSpecial">[</span> &quot;<span class="synConstant">Account</span>&quot; <span class="synSpecial">]</span>, &quot;<span class="synStatement">context</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">attributes</span>&quot;: <span class="synSpecial">{}</span>, &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">Record</span>&quot; <span class="synSpecial">}</span> <span class="synSpecial">}</span>, &quot;<span class="synStatement">memberOf</span>&quot;: <span class="synSpecial">[</span> <span class="synSpecial">{</span> &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">PhotoFlash::Action</span>&quot;, &quot;<span class="synStatement">id</span>&quot;: &quot;<span class="synConstant">ManageAccount</span>&quot; <span class="synSpecial">}</span> <span class="synSpecial">]</span> <span class="synSpecial">}</span>, &quot;<span class="synStatement">LimitedPhotoAccess</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">appliesTo</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">resourceTypes</span>&quot;: <span class="synSpecial">[</span> &quot;<span class="synConstant">Photo</span>&quot; <span class="synSpecial">]</span>, &quot;<span class="synStatement">context</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">Record</span>&quot;, &quot;<span class="synStatement">attributes</span>&quot;: <span class="synSpecial">{}</span> <span class="synSpecial">}</span>, &quot;<span class="synStatement">principalTypes</span>&quot;: <span class="synSpecial">[</span> &quot;<span class="synConstant">User</span>&quot; <span class="synSpecial">]</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span>, &quot;<span class="synStatement">ManageAccount</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">appliesTo</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">principalTypes</span>&quot;: <span class="synSpecial">[</span> &quot;<span class="synConstant">User</span>&quot; <span class="synSpecial">]</span>, &quot;<span class="synStatement">context</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">Record</span>&quot;, &quot;<span class="synStatement">attributes</span>&quot;: <span class="synSpecial">{}</span> <span class="synSpecial">}</span>, &quot;<span class="synStatement">resourceTypes</span>&quot;: <span class="synSpecial">[</span> &quot;<span class="synConstant">Account</span>&quot;, &quot;<span class="synConstant">Photo</span>&quot; <span class="synSpecial">]</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span>, &quot;<span class="synStatement">ManageFriendGroup</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">memberOf</span>&quot;: <span class="synSpecial">[</span> <span class="synSpecial">{</span> &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">PhotoFlash::Action</span>&quot;, &quot;<span class="synStatement">id</span>&quot;: &quot;<span class="synConstant">ManageAccount</span>&quot; <span class="synSpecial">}</span> <span class="synSpecial">]</span>, &quot;<span class="synStatement">appliesTo</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">principalTypes</span>&quot;: <span class="synSpecial">[</span> &quot;<span class="synConstant">User</span>&quot; <span class="synSpecial">]</span>, &quot;<span class="synStatement">resourceTypes</span>&quot;: <span class="synSpecial">[</span> &quot;<span class="synConstant">FriendGroup</span>&quot; <span class="synSpecial">]</span>, &quot;<span class="synStatement">context</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">Record</span>&quot;, &quot;<span class="synStatement">attributes</span>&quot;: <span class="synSpecial">{}</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span>, &quot;<span class="synStatement">DeletePhoto</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">appliesTo</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">context</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">attributes</span>&quot;: <span class="synSpecial">{}</span>, &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">Record</span>&quot; <span class="synSpecial">}</span>, &quot;<span class="synStatement">resourceTypes</span>&quot;: <span class="synSpecial">[</span> &quot;<span class="synConstant">Photo</span>&quot; <span class="synSpecial">]</span>, &quot;<span class="synStatement">principalTypes</span>&quot;: <span class="synSpecial">[</span> &quot;<span class="synConstant">User</span>&quot; <span class="synSpecial">]</span> <span class="synSpecial">}</span>, &quot;<span class="synStatement">memberOf</span>&quot;: <span class="synSpecial">[</span> <span class="synSpecial">{</span> &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">PhotoFlash::Action</span>&quot;, &quot;<span class="synStatement">id</span>&quot;: &quot;<span class="synConstant">ManageAccount</span>&quot; <span class="synSpecial">}</span> <span class="synSpecial">]</span> <span class="synSpecial">}</span>, &quot;<span class="synStatement">SharePhoto</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">appliesTo</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">principalTypes</span>&quot;: <span class="synSpecial">[</span> &quot;<span class="synConstant">User</span>&quot; <span class="synSpecial">]</span>, &quot;<span class="synStatement">context</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">attributes</span>&quot;: <span class="synSpecial">{}</span>, &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">Record</span>&quot; <span class="synSpecial">}</span>, &quot;<span class="synStatement">resourceTypes</span>&quot;: <span class="synSpecial">[</span> &quot;<span class="synConstant">Photo</span>&quot; <span class="synSpecial">]</span> <span class="synSpecial">}</span>, &quot;<span class="synStatement">memberOf</span>&quot;: <span class="synSpecial">[</span> <span class="synSpecial">{</span> &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">PhotoFlash::Action</span>&quot;, &quot;<span class="synStatement">id</span>&quot;: &quot;<span class="synConstant">FullPhotoAccess</span>&quot; <span class="synSpecial">}</span> <span class="synSpecial">]</span> <span class="synSpecial">}</span>, &quot;<span class="synStatement">BlockAccountAccess</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">memberOf</span>&quot;: <span class="synSpecial">[</span> <span class="synSpecial">{</span> &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">PhotoFlash::Action</span>&quot;, &quot;<span class="synStatement">id</span>&quot;: &quot;<span class="synConstant">ManageAccount</span>&quot; <span class="synSpecial">}</span> <span class="synSpecial">]</span>, &quot;<span class="synStatement">appliesTo</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">principalTypes</span>&quot;: <span class="synSpecial">[</span> &quot;<span class="synConstant">User</span>&quot; <span class="synSpecial">]</span>, &quot;<span class="synStatement">context</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">attributes</span>&quot;: <span class="synSpecial">{}</span>, &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">Record</span>&quot; <span class="synSpecial">}</span>, &quot;<span class="synStatement">resourceTypes</span>&quot;: <span class="synSpecial">[</span> &quot;<span class="synConstant">Account</span>&quot; <span class="synSpecial">]</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span>, &quot;<span class="synStatement">ViewPhoto</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">appliesTo</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">resourceTypes</span>&quot;: <span class="synSpecial">[</span> &quot;<span class="synConstant">Photo</span>&quot; <span class="synSpecial">]</span>, &quot;<span class="synStatement">context</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">attributes</span>&quot;: <span class="synSpecial">{}</span>, &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">Record</span>&quot; <span class="synSpecial">}</span>, &quot;<span class="synStatement">principalTypes</span>&quot;: <span class="synSpecial">[</span> &quot;<span class="synConstant">User</span>&quot; <span class="synSpecial">]</span> <span class="synSpecial">}</span>, &quot;<span class="synStatement">memberOf</span>&quot;: <span class="synSpecial">[</span> <span class="synSpecial">{</span> &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">PhotoFlash::Action</span>&quot;, &quot;<span class="synStatement">id</span>&quot;: &quot;<span class="synConstant">LimitedPhotoAccess</span>&quot; <span class="synSpecial">}</span>, <span class="synSpecial">{</span> &quot;<span class="synStatement">id</span>&quot;: &quot;<span class="synConstant">FullPhotoAccess</span>&quot;, &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">PhotoFlash::Action</span>&quot; <span class="synSpecial">}</span> <span class="synSpecial">]</span> <span class="synSpecial">}</span>, &quot;<span class="synStatement">UploadPhoto</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">memberOf</span>&quot;: <span class="synSpecial">[</span> <span class="synSpecial">{</span> &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">PhotoFlash::Action</span>&quot;, &quot;<span class="synStatement">id</span>&quot;: &quot;<span class="synConstant">ManageAccount</span>&quot; <span class="synSpecial">}</span> <span class="synSpecial">]</span>, &quot;<span class="synStatement">appliesTo</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">resourceTypes</span>&quot;: <span class="synSpecial">[</span> &quot;<span class="synConstant">Photo</span>&quot; <span class="synSpecial">]</span>, &quot;<span class="synStatement">principalTypes</span>&quot;: <span class="synSpecial">[</span> &quot;<span class="synConstant">User</span>&quot; <span class="synSpecial">]</span>, &quot;<span class="synStatement">context</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">attributes</span>&quot;: <span class="synSpecial">{}</span>, &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">Record</span>&quot; <span class="synSpecial">}</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span>, &quot;<span class="synStatement">FullPhotoAccess</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">appliesTo</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">principalTypes</span>&quot;: <span class="synSpecial">[</span> &quot;<span class="synConstant">User</span>&quot; <span class="synSpecial">]</span>, &quot;<span class="synStatement">context</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">attributes</span>&quot;: <span class="synSpecial">{}</span>, &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">Record</span>&quot; <span class="synSpecial">}</span>, &quot;<span class="synStatement">resourceTypes</span>&quot;: <span class="synSpecial">[</span> &quot;<span class="synConstant">Photo</span>&quot; <span class="synSpecial">]</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span>, &quot;<span class="synStatement">CommentPhoto</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">memberOf</span>&quot;: <span class="synSpecial">[</span> <span class="synSpecial">{</span> &quot;<span class="synStatement">id</span>&quot;: &quot;<span class="synConstant">FullPhotoAccess</span>&quot;, &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">PhotoFlash::Action</span>&quot; <span class="synSpecial">}</span> <span class="synSpecial">]</span>, &quot;<span class="synStatement">appliesTo</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">resourceTypes</span>&quot;: <span class="synSpecial">[</span> &quot;<span class="synConstant">Photo</span>&quot; <span class="synSpecial">]</span>, &quot;<span class="synStatement">context</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">attributes</span>&quot;: <span class="synSpecial">{}</span>, &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">Record</span>&quot; <span class="synSpecial">}</span>, &quot;<span class="synStatement">principalTypes</span>&quot;: <span class="synSpecial">[</span> &quot;<span class="synConstant">User</span>&quot; <span class="synSpecial">]</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span>, &quot;<span class="synStatement">ManageAlbum</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">appliesTo</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">context</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">attributes</span>&quot;: <span class="synSpecial">{}</span>, &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">Record</span>&quot; <span class="synSpecial">}</span>, &quot;<span class="synStatement">resourceTypes</span>&quot;: <span class="synSpecial">[</span> &quot;<span class="synConstant">Album</span>&quot; <span class="synSpecial">]</span>, &quot;<span class="synStatement">principalTypes</span>&quot;: <span class="synSpecial">[</span> &quot;<span class="synConstant">User</span>&quot; <span class="synSpecial">]</span> <span class="synSpecial">}</span>, &quot;<span class="synStatement">memberOf</span>&quot;: <span class="synSpecial">[</span> <span class="synSpecial">{</span> &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">PhotoFlash::Action</span>&quot;, &quot;<span class="synStatement">id</span>&quot;: &quot;<span class="synConstant">ManageAccount</span>&quot; <span class="synSpecial">}</span> <span class="synSpecial">]</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span> </pre> <p></details></p> <p>ポリシーは、テンプレートを含め5つ作成されます。</p> <pre class="code" data-lang="" data-unlink>permit ( principal, action in PhotoFlash::Action::&#34;FullPhotoAccess&#34;, resource ) when { resource in principal.Account }; permit ( principal, action in PhotoFlash::Action::&#34;ManageAccount&#34;, resource ) when { resource in principal.Account }; permit ( principal in ?principal, action in PhotoFlash::Action::&#34;LimitedPhotoAccess&#34;, resource in ?resource ) unless { resource.IsPrivate }; forbid ( principal == ?principal, action, resource in ?resource ); permit ( principal in ?principal, action in PhotoFlash::Action::&#34;FullPhotoAccess&#34;, resource == ?resource ) unless { resource.IsPrivate }; </pre> <h3 id="3-サンプルアプリケーションの実装">3. サンプルアプリケーションの実装</h3> <pre class="code lang-go" data-lang="go" data-unlink><span class="synStatement">package</span> main <span class="synStatement">import</span> ( <span class="synConstant">&quot;fmt&quot;</span> <span class="synConstant">&quot;os&quot;</span> <span class="synConstant">&quot;github.com/aws/aws-sdk-go/service/verifiedpermissions&quot;</span> <span class="synConstant">&quot;github.com/aws/aws-sdk-go/aws/session&quot;</span> <span class="synConstant">&quot;github.com/aws/aws-sdk-go/aws&quot;</span> <span class="synConstant">&quot;github.com/gin-gonic/gin&quot;</span> ) <span class="synStatement">type</span> user <span class="synStatement">struct</span> { id <span class="synType">string</span> } <span class="synStatement">type</span> account <span class="synStatement">struct</span> { id <span class="synType">string</span> } <span class="synStatement">type</span> photo <span class="synStatement">struct</span> { id <span class="synType">string</span> owner <span class="synType">string</span> } <span class="synStatement">const</span> PERMISSION_ALLOW = <span class="synConstant">&quot;ALLOW&quot;</span> <span class="synStatement">var</span> currentUser = user{id: <span class="synConstant">&quot;test&quot;</span>} <span class="synStatement">var</span> currentAccount = account{id: <span class="synConstant">&quot;test&quot;</span>} <span class="synStatement">var</span> sess = session.Must(session.NewSession()) <span class="synStatement">var</span> vp = verifiedpermissions.New(sess, aws.NewConfig().WithRegion(<span class="synConstant">&quot;ap-northeast-1&quot;</span>)) <span class="synStatement">var</span> policyStoreId = os.Getenv(<span class="synConstant">&quot;POLICY_STORE_ID&quot;</span>) <span class="synStatement">var</span> photos = <span class="synType">map</span>[<span class="synType">string</span>]photo{ <span class="synConstant">&quot;1&quot;</span>: photo{id: <span class="synConstant">&quot;1&quot;</span>, owner: <span class="synConstant">&quot;test&quot;</span>}, <span class="synConstant">&quot;2&quot;</span>: photo{id: <span class="synConstant">&quot;2&quot;</span>, owner: <span class="synConstant">&quot;test&quot;</span>}, <span class="synConstant">&quot;3&quot;</span>: photo{id: <span class="synConstant">&quot;3&quot;</span>, owner: <span class="synConstant">&quot;other&quot;</span>}, } <span class="synStatement">type</span> authorizedHandler <span class="synStatement">interface</span> { getAction(c *gin.Context) <span class="synType">string</span> getResource(c *gin.Context) (<span class="synType">string</span>, <span class="synType">string</span>) getHandler() gin.HandlerFunc getEntities(c *gin.Context) []*verifiedpermissions.EntityItem } <span class="synStatement">type</span> uploadPhotoHandler <span class="synStatement">struct</span> {} <span class="synStatement">func</span> (h *uploadPhotoHandler) getAction(c *gin.Context) <span class="synType">string</span> { <span class="synStatement">return</span> <span class="synConstant">&quot;UploadPhoto&quot;</span> } <span class="synStatement">func</span> (h *uploadPhotoHandler) getResource(c *gin.Context) (<span class="synType">string</span>, <span class="synType">string</span>) { <span class="synStatement">return</span> <span class="synConstant">&quot;PhotoFlash::Photo&quot;</span>, <span class="synConstant">&quot;dummy&quot;</span> } <span class="synStatement">func</span> (h *uploadPhotoHandler) getEntities(c *gin.Context) []*verifiedpermissions.EntityItem { id := <span class="synConstant">&quot;dummy&quot;</span> <span class="synStatement">return</span> []*verifiedpermissions.EntityItem{ { Attributes: <span class="synType">map</span>[<span class="synType">string</span>]*verifiedpermissions.AttributeValue{ <span class="synConstant">&quot;Account&quot;</span>: &amp;verifiedpermissions.AttributeValue{ EntityIdentifier: &amp;verifiedpermissions.EntityIdentifier{ EntityType: aws.String(<span class="synConstant">&quot;PhotoFlash::Account&quot;</span>), EntityId: aws.String(currentAccount.id), }, }, }, Identifier: &amp;verifiedpermissions.EntityIdentifier{ EntityType: aws.String(<span class="synConstant">&quot;PhotoFlash::User&quot;</span>), EntityId: aws.String(currentUser.id), }, Parents: []*verifiedpermissions.EntityIdentifier{}, }, { Identifier: &amp;verifiedpermissions.EntityIdentifier{ EntityType: aws.String(<span class="synConstant">&quot;PhotoFlash::Photo&quot;</span>), EntityId: aws.String(id), }, Parents: []*verifiedpermissions.EntityIdentifier{ { EntityType: aws.String(<span class="synConstant">&quot;PhotoFlash::Account&quot;</span>), EntityId: aws.String(currentAccount.id), }, }, }, } } <span class="synStatement">func</span> (h *uploadPhotoHandler) getHandler() gin.HandlerFunc { <span class="synStatement">return</span> <span class="synType">func</span>(c *gin.Context) { <span class="synComment">// storageやDBへの書き込みをここで行う</span> c.JSON(<span class="synConstant">200</span>, gin.H{ <span class="synConstant">&quot;message&quot;</span>: <span class="synConstant">&quot;upload photo successful!&quot;</span>, }) } } <span class="synStatement">type</span> viewPhotoHandler <span class="synStatement">struct</span> {} <span class="synStatement">func</span> (h *viewPhotoHandler) getAction(c *gin.Context) <span class="synType">string</span> { <span class="synStatement">return</span> <span class="synConstant">&quot;ViewPhoto&quot;</span> } <span class="synStatement">func</span> (h *viewPhotoHandler) getResource(c *gin.Context) (<span class="synType">string</span>, <span class="synType">string</span>) { <span class="synStatement">return</span> <span class="synConstant">&quot;PhotoFlash::Photo&quot;</span>, c.Param(<span class="synConstant">&quot;id&quot;</span>) } <span class="synStatement">func</span> (h *viewPhotoHandler) getEntities(c *gin.Context) []*verifiedpermissions.EntityItem { photo, ok := photos[c.Param(<span class="synConstant">&quot;id&quot;</span>)] <span class="synStatement">if</span> !ok { <span class="synStatement">return</span> []*verifiedpermissions.EntityItem{} } <span class="synStatement">return</span> []*verifiedpermissions.EntityItem{ { Attributes: <span class="synType">map</span>[<span class="synType">string</span>]*verifiedpermissions.AttributeValue{ <span class="synConstant">&quot;Account&quot;</span>: &amp;verifiedpermissions.AttributeValue{ EntityIdentifier: &amp;verifiedpermissions.EntityIdentifier{ EntityType: aws.String(<span class="synConstant">&quot;PhotoFlash::Account&quot;</span>), EntityId: aws.String(currentAccount.id), }, }, }, Identifier: &amp;verifiedpermissions.EntityIdentifier{ EntityType: aws.String(<span class="synConstant">&quot;PhotoFlash::User&quot;</span>), EntityId: aws.String(currentUser.id), }, Parents: []*verifiedpermissions.EntityIdentifier{}, }, { Identifier: &amp;verifiedpermissions.EntityIdentifier{ EntityType: aws.String(<span class="synConstant">&quot;PhotoFlash::Photo&quot;</span>), EntityId: aws.String(photo.id), }, Parents: []*verifiedpermissions.EntityIdentifier{ { EntityType: aws.String(<span class="synConstant">&quot;PhotoFlash::Account&quot;</span>), EntityId: aws.String(photo.owner), }, }, }, } } <span class="synStatement">func</span> (h *viewPhotoHandler) getHandler() gin.HandlerFunc { <span class="synStatement">return</span> <span class="synType">func</span>(c *gin.Context) { <span class="synComment">// DBから取得した画像のURLを返す</span> photoUrl := <span class="synConstant">&quot;&lt;https://dummy.com/&gt;&quot;</span> + c.Param(<span class="synConstant">&quot;id&quot;</span>) + <span class="synConstant">&quot;/dummy.jpg&quot;</span> c.JSON(<span class="synConstant">200</span>, gin.H{ <span class="synConstant">&quot;image_url&quot;</span>: photoUrl, }) } } <span class="synStatement">func</span> authHandler(handler authorizedHandler) gin.HandlerFunc { <span class="synStatement">return</span> <span class="synType">func</span>(c *gin.Context) { rt, rs := handler.getResource(c) o, err := vp.IsAuthorized(&amp;verifiedpermissions.IsAuthorizedInput{ Action: &amp;verifiedpermissions.ActionIdentifier{ ActionId: aws.String(handler.getAction(c)), ActionType: aws.String(<span class="synConstant">&quot;PhotoFlash::Action&quot;</span>), }, Resource: &amp;verifiedpermissions.EntityIdentifier{ EntityType: aws.String(rt), EntityId: aws.String(rs), }, Principal: &amp;verifiedpermissions.EntityIdentifier{ EntityType: aws.String(<span class="synConstant">&quot;PhotoFlash::User&quot;</span>), EntityId: aws.String(currentUser.id), }, Entities: &amp;verifiedpermissions.EntitiesDefinition{ EntityList: handler.getEntities(c), }, PolicyStoreId: aws.String(policyStoreId), }) <span class="synStatement">if</span> err != <span class="synStatement">nil</span> { fmt.Printf(<span class="synConstant">&quot;err: %s </span><span class="synSpecial">\\\\</span><span class="synConstant">n&quot;</span>, err) c.AbortWithStatus(<span class="synConstant">400</span>) <span class="synStatement">return</span> } <span class="synStatement">if</span> *o.Decision != PERMISSION_ALLOW { fmt.Printf(<span class="synConstant">&quot;err: %v </span><span class="synSpecial">\\\\</span><span class="synConstant">n&quot;</span>, o) c.AbortWithStatus(<span class="synConstant">403</span>) <span class="synStatement">return</span> } (handler.getHandler())(c) c.Next() } } <span class="synStatement">func</span> main() { r := gin.Default() r.POST(<span class="synConstant">&quot;/photo/upload&quot;</span>, authHandler(&amp;uploadPhotoHandler{})) r.GET(<span class="synConstant">&quot;/photo/:id&quot;</span>, authHandler(&amp;viewPhotoHandler{})) r.Run() } </pre> <p>フレームワークは gin を利用して、写真のアップロード <code>POST: /photo/upload</code> と写真の取得 <code>GET: /photo/:id</code> を実装しています。<br/> サンプルのため、認証処理は省略し currentUser, currentAccount にて認証済みユーザーの情報が取れる前提です。</p> <p>各エンドポイント(handler)ごとに、認可に必要なデータ(Action, Resource, Entity)を取得できるようにしています。(Principal は今回リクエストユーザーに固定)</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synStatement">type</span> viewPhotoHandler <span class="synStatement">struct</span> {} <span class="synStatement">func</span> (h *viewPhotoHandler) getAction(c *gin.Context) <span class="synType">string</span> { <span class="synStatement">return</span> <span class="synConstant">&quot;ViewPhoto&quot;</span> } <span class="synStatement">func</span> (h *viewPhotoHandler) getResource(c *gin.Context) (<span class="synType">string</span>, <span class="synType">string</span>) { <span class="synStatement">return</span> <span class="synConstant">&quot;PhotoFlash::Photo&quot;</span>, c.Param(<span class="synConstant">&quot;id&quot;</span>) } <span class="synStatement">func</span> (h *viewPhotoHandler) getEntities(c *gin.Context) []*verifiedpermissions.EntityItem { photo, ok := photos[c.Param(<span class="synConstant">&quot;id&quot;</span>)] <span class="synStatement">if</span> !ok { <span class="synStatement">return</span> []*verifiedpermissions.EntityItem{} } <span class="synStatement">return</span> []*verifiedpermissions.EntityItem{ { Attributes: <span class="synType">map</span>[<span class="synType">string</span>]*verifiedpermissions.AttributeValue{ <span class="synConstant">&quot;Account&quot;</span>: &amp;verifiedpermissions.AttributeValue{ EntityIdentifier: &amp;verifiedpermissions.EntityIdentifier{ EntityType: aws.String(<span class="synConstant">&quot;PhotoFlash::Account&quot;</span>), EntityId: aws.String(currentAccount.id), }, }, }, Identifier: &amp;verifiedpermissions.EntityIdentifier{ EntityType: aws.String(<span class="synConstant">&quot;PhotoFlash::User&quot;</span>), EntityId: aws.String(currentUser.id), }, Parents: []*verifiedpermissions.EntityIdentifier{}, }, { Identifier: &amp;verifiedpermissions.EntityIdentifier{ EntityType: aws.String(<span class="synConstant">&quot;PhotoFlash::Photo&quot;</span>), EntityId: aws.String(photo.id), }, Parents: []*verifiedpermissions.EntityIdentifier{ { EntityType: aws.String(<span class="synConstant">&quot;PhotoFlash::Account&quot;</span>), EntityId: aws.String(photo.owner), }, }, }, } } <span class="synStatement">func</span> (h *viewPhotoHandler) getHandler() gin.HandlerFunc { <span class="synStatement">return</span> <span class="synType">func</span>(c *gin.Context) { <span class="synComment">// DBから取得した画像のURLを返す</span> photoUrl := <span class="synConstant">&quot;&lt;https://dummy.com/&gt;&quot;</span> + c.Param(<span class="synConstant">&quot;id&quot;</span>) + <span class="synConstant">&quot;/dummy.jpg&quot;</span> c.JSON(<span class="synConstant">200</span>, gin.H{ <span class="synConstant">&quot;image_url&quot;</span>: photoUrl, }) } } </pre> <p>取得したデータを <code>IsAuthorized</code> メソッドに渡し、認可が通るかを判定します。<br/> 認可が通らなかった場合は 403 を返し処理を終了させています。<br/> <code>IsAuthorized</code> メソッドに渡している PolicyStoreId はポリシーストアを作成した際に割り振られるIDになっており、 AWS コンソールから取得できます。</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synStatement">func</span> authMiddleware(handler authorizedHandler) gin.HandlerFunc { <span class="synStatement">return</span> <span class="synType">func</span>(c *gin.Context) { rt, rs := handler.getResource(c) o, err := vp.IsAuthorized(&amp;verifiedpermissions.IsAuthorizedInput{ Action: &amp;verifiedpermissions.ActionIdentifier{ ActionId: aws.String(handler.getAction(c)), ActionType: aws.String(<span class="synConstant">&quot;PhotoFlash::Action&quot;</span>), }, Resource: &amp;verifiedpermissions.EntityIdentifier{ EntityType: aws.String(rt), EntityId: aws.String(rs), }, Principal: &amp;verifiedpermissions.EntityIdentifier{ EntityType: aws.String(<span class="synConstant">&quot;PhotoFlash::User&quot;</span>), EntityId: aws.String(currentUser.id), }, Entities: &amp;verifiedpermissions.EntitiesDefinition{ EntityList: handler.getEntities(c), }, PolicyStoreId: aws.String(policyStoreId), }) <span class="synStatement">if</span> err != <span class="synStatement">nil</span> { fmt.Printf(<span class="synConstant">&quot;err: %s </span><span class="synSpecial">\\\\</span><span class="synConstant">n&quot;</span>, err) c.AbortWithStatus(<span class="synConstant">400</span>) <span class="synStatement">return</span> } <span class="synStatement">if</span> *o.Decision != PERMISSION_ALLOW { fmt.Printf(<span class="synConstant">&quot;err: %v </span><span class="synSpecial">\\\\</span><span class="synConstant">n&quot;</span>, o) c.AbortWithStatus(<span class="synConstant">403</span>) <span class="synStatement">return</span> } (handler.getHandler())(c) c.Next() } } </pre> <p>実際にリクエストを送ってみると、<br/> owner が自分ではない、id が 3 の photo にアクセスしようとすると 403 が返ります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yamanoi-y/20231009/20231009174022.png" width="754" height="585" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="最後に">最後に</h2> <p>まだ GA したばかりのサービスのため情報が少ないですが、少し複雑な認可処理でも柔軟に表現できました。<br/> 今後、新規で認可処理を実装する際の選択肢の 1 つとして考えていきたいです!</p> yamanoi-y 地方在住のQAエンジニアがフルリモートで『働き方改革』を実現した話 hatenablog://entry/6801883189051998465 2023-10-25T09:00:00+09:00 2023-10-25T13:32:13+09:00 地方在住のQAエンジニアがフルリモート勤務で実現した『働き方改革』を紹介します。 <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/makky_tyuyan/20231018/20231018191657.png" alt="" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <ul class="table-of-contents"> <li><a href="#働き方改革ってどういうこと">『働き方改革』ってどういうこと?</a><ul> <li><a href="#フルリモートで必要となる費用">フルリモートで必要となる費用</a></li> <li><a href="#コストパフォーマンスを支出のバランスで考える">コストパフォーマンスを支出のバランスで考える</a></li> </ul> </li> <li><a href="#フルリモート最強の作業環境はこれだ">フルリモート最強の作業環境はこれだ!</a></li> <li><a href="#フルリモートのコミュニケーションで工夫していることを知りたい">フルリモートのコミュニケーションで工夫していることを知りたい!</a><ul> <li><a href="#コミュニケーションの不安を軽減する3つのポイント">コミュニケーションの不安を軽減する3つのポイント</a></li> <li><a href="#フルリモートのコミュニケーションで活用できるTandem">フルリモートのコミュニケーションで活用できるTandem</a></li> </ul> </li> <li><a href="#まとめ">まとめ</a></li> </ul> <p>2023年1月にテックタッチへジョインしたQAエンジニアのMickeyです。 本記事では、私がフルリモート勤務でどのように『働き方改革』を実現したか、作業環境とコミュニケーションを中心に紹介します。 記事を読んでくれた方に『テックタッチ』でフルリモートで働くイメージを持っていただけたらうれしいです!</p> <h2 id="働き方改革ってどういうこと">『働き方改革』ってどういうこと?</h2> <p>前職までは、客先常駐※や自社勤務で、どちらも就業場所に制約がありました。<br/> テックタッチでは、エンジニアリングチームはフルリモート勤務が可能なため、今では、状況によって最適な場所で業務できる状態となりました。<br/> ※<strong>いわゆる SES のこと、System Engineering Service(システムエンジニアリングサービス)の略称です。本記事では、特定の業務に対して、技術者の労働を提供するIT業界における契約形態を指します。</strong></p> <p><strong>転職前後の変化</strong></p> <table> <thead> <tr> <th style="text-align:left;"> </th> <th style="text-align:left;"> 転職前 </th> <th style="text-align:left;"> 転職後 </th> </tr> </thead> <tbody> <tr> <td style="text-align:left;"> <strong>場所</strong> </td> <td style="text-align:left;"> 客先 or 自社 </td> <td style="text-align:left;"> コワーキングスペース or 自宅 </td> </tr> <tr> <td style="text-align:left;"> <strong>勤務体系</strong> </td> <td style="text-align:left;"> 裁量労働制 </td> <td style="text-align:left;"> フレックス制 </td> </tr> <tr> <td style="text-align:left;"> <strong>通勤</strong> </td> <td style="text-align:left;"> 電車 and 徒歩 </td> <td style="text-align:left;"> バス</td> </tr> <tr> <td style="text-align:left;"> <strong>通勤時間( door-to-door )</strong> </td> <td style="text-align:left;"> 60分</td> <td style="text-align:left;"> 40分</td> </tr> </tbody> </table> <p>『場所にとらわれない働き方を選択できる』ことが、私にとっての『働き方改革』でした。<br/> 40歳前後に訪れる家庭環境の変化(育児、介護など)により、働ける場所に制約があることで苦労することも多くなってきたことが背景でした。</p> <h3 id="フルリモートで必要となる費用">フルリモートで必要となる費用</h3> <p>さて、フルリモートで勤務する場合、気になるのが費用です。<br/> 電気代や周辺の作業環境を整えるためにはやはりお金がかかります。</p> <p><strong>フルリモート勤務で必要となった費用例</strong></p> <table> <thead> <tr> <th style="text-align:left;"></th> <th style="text-align:left;">フルリモート前</th> <th style="text-align:left;">フルリモート後</th> <th style="text-align:left;">費用</th> </tr> </thead> <tbody> <tr> <td style="text-align:left;">場所</td> <td style="text-align:left;">なし</td> <td style="text-align:left;">コワーキングスペース利用料</td> <td style="text-align:left;">¥18,000 / 月くらい</td> </tr> <tr> <td style="text-align:left;">家具 (机・椅子)</td> <td style="text-align:left;"> 会社備品 </td> <td style="text-align:left;">もともと保有していた家具を利用</td> <td style="text-align:left;">¥0</td> </tr> <tr> <td style="text-align:left;">周辺機器(ディスプレイなど)</td> <td style="text-align:left;">会社備品</td> <td style="text-align:left;">自分で用意※1</td> <td style="text-align:left;">¥15,000 くらい</td> </tr> <tr> <td style="text-align:left;">維持費(光熱費など)</td> <td style="text-align:left;"> なし </td> <td style="text-align:left;">プロバイダ利用料、電気代</td> <td style="text-align:left;">¥1,500 / 月くらい※2</td> </tr> </tbody> </table> <p>※1 入社時点で自宅に無いモノを購入、ディスプレイとヘッドフォンは会社支給品のため、費用に含めていません。<br/> ※2 自宅において通常発生する通信費、光熱費をざっくり按分した費用です。</p> <p>一般にフルリモート勤務の場合は家具や周辺機器の準備に一時的に費用がかかります。<br/> 私の場合、コワーキングスペースで勤務するため、日々の作業場所にも費用がかかっています。<br/> 光熱費などの維持費を鑑みて、企業がフルリモート勤務を推奨している場合は、フルリモート手当が支給される場合があります。<br/> テックタッチでは、フルリモート手当がありますが、上記で紹介した費用全額は賄えません。<br/> 知人に上記の負担について話すと「いやぁ、高いね。それだけ払うのはきついなぁ」という反応が大半です。</p> <h3 id="コストパフォーマンスを支出のバランスで考える">コストパフォーマンスを支出のバランスで考える</h3> <p>そこで、私が注目したのは日々の支出のバランスです。<br/> フルリモート前後の支出を比較してみましょう。</p> <p><strong>1日あたりの支出( MAX との比較)</strong></p> <table> <thead> <tr> <th style="text-align:left;"> </th> <th style="text-align:left;">フルリモート前</th> <th style="text-align:left;">フルリモート後</th> <th style="text-align:left;">差額</th> </tr> </thead> <tbody> <tr> <td style="text-align:left;">朝食(自宅で食べる)</td> <td style="text-align:left;">¥0</td> <td style="text-align:left;">¥0</td> <td style="text-align:left;">¥0</td> </tr> <tr> <td style="text-align:left;">昼食</td> <td style="text-align:left;">¥1,000</td> <td style="text-align:left;">¥500</td> <td style="text-align:left;">¥500↘</td> </tr> <tr> <td style="text-align:left;">夕食</td> <td style="text-align:left;">¥800</td> <td style="text-align:left;">¥500</td> <td style="text-align:left;">¥300↘</td> </tr> <tr> <td style="text-align:left;">間食(菓子類)</td> <td style="text-align:left;">¥400</td> <td style="text-align:left;">¥200</td> <td style="text-align:left;">¥200↘</td> </tr> <tr> <td style="text-align:left;">飲料(コーヒー、お茶類)</td> <td style="text-align:left;">¥500</td> <td style="text-align:left;">¥200</td> <td style="text-align:left;">¥300↘</td> </tr> <tr> <td style="text-align:left;">合計</td> <td style="text-align:left;">¥2,700</td> <td style="text-align:left;">¥1,400</td> <td style="text-align:left;">¥1,300↘</td> </tr> </tbody> </table> <p>フルリモート前までは、コンビニ弁当が苦手なため、食事は外食が中心。菓子類や飲料をコンビニで買うことが日常でした。特に、オフィスの近くにコンビニがあるため、休憩がてらに何かと飲食物を購入していました。<br/> 現在は、コワーキングスペースにドラッグストアが併設されており、飲料や菓子類の価格がほぼ半額。<br/> ドラッグストア内にある手作り弁当のお店はワンコイン。ざっくり、1日 1,300 円の支出減です。</p> <p><figure class="figure-image figure-image-fotolife" title="毎朝作りたて、10種類以上のお弁当がほぼワンコインで食べられます"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/makky_tyuyan/20231018/20231018194104.jpg" alt="&#x30C9;&#x30E9;&#x30C3;&#x30B0;&#x30B9;&#x30C8;&#x30A2;&#x5185;&#x306B;&#x3042;&#x308B;&#x624B;&#x4F5C;&#x308A;&#x5F01;&#x5F53;&#x306E;&#x5199;&#x771F;" width="560" height="420" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>毎朝作りたて、10種類以上のお弁当がほぼワンコインで食べられます</figcaption></figure> <figure class="figure-image figure-image-fotolife" title="最安値は297円(税抜)!!"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/makky_tyuyan/20231018/20231018192115.jpg" alt="&#x30C1;&#x30FC;&#x30BA;&#x30CF;&#x30F3;&#x30D0;&#x30FC;&#x30B0;&#x5F01;&#x5F53;&#x3068;&#x30CF;&#x30F3;&#x30D0;&#x30FC;&#x30B0;&#xFF06;&#x30B3;&#x30ED;&#x30C3;&#x30B1;&#x5F01;&#x5F53;&#x306E;&#x5199;&#x771F;" width="560" height="420" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>最安値は297円(税抜)!!</figcaption></figure></p> <p>さきほど紹介したフルリモート勤務で必要となった費用だけを見ると、月に 20,000 円の支出増ですが、1日 1,300 円つまり月に 26,000 円減ったため、相殺どころか月に 6,000 円の支出減※です。<br/> ※会社からはリモートワーク手当が支給されるため、実際には手当の範囲で備品購入をしています。<br/> そもそも、私は支出が多いタイプではありますが、フルリモート前の支出は異常な支出ではないと思います。<br/> フルリモートで『働き方改革』が実現、作業環境もお財布も豊かになりました。</p> <h2 id="フルリモート最強の作業環境はこれだ">フルリモート最強の作業環境はこれだ!</h2> <p>なるべくストレスのかからない作業環境を作るようにしています。<br/> 私のフルリモートワークを助けるガジェットたちを紹介します。<br/> ※本記事では小物全般のことをガジェットと表現しています。</p> <p><figure class="figure-image figure-image-fotolife" title="フルリモート最強の作業環境(自称)"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/makky_tyuyan/20231018/20231018192042.jpg" alt="" width="560" height="420" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>フルリモート最強の作業環境(自称)</figcaption></figure></p> <p>それでは紹介していきます。<br/> なお、これから紹介するガジェットはフルリモートワーク中に徐々に揃えました。</p> <p><strong>愛用しているガジェットたち</strong></p> <table> <thead> <tr> <th style="text-align:left;">ガジェット</th> <th style="text-align:left;">ポイント</th> <th style="text-align:left;">購入時の費用</th> </tr> </thead> <tbody> <tr> <td style="text-align:left;"> <figure class="figure-image figure-image-fotolife" title="IODATA モバイルモニター"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/makky_tyuyan/20231019/20231019214128.jpg" alt="" width="300" height="151" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption><a href="https://www.amazon.co.jp/dp/B08GKCD2NW">IODATA モバイルモニター</a></figcaption></figure> </td> <td style="text-align:left;">USB Type-C で給電可能なこと。背面スタンドで自立可能で、折りたたんでスマートに持ち運びしやすいサイズです。</td> <td style="text-align:left;"> ¥0(会社支給品)</td> </tr> <tr> <td style="text-align:left;"> <a href="https://www.amazon.co.jp/dp/B09RVPRC31">覗き見防止シート</a></td> <td style="text-align:left;"> ノングレアの覗き込み防止シートで、意図せず画面を見られるリスクを軽減しています。ほぼ真後ろで立ち止まって覗き込まないと見ることができません。</td> <td style="text-align:left;"> ¥2,000</td> </tr> <tr> <td style="text-align:left;"> <figure class="figure-image figure-image-fotolife" title="モバイルモニタースタンド"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/makky_tyuyan/20231019/20231019214121.jpg" alt="" width="300" height="400" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption><a href="https://www.amazon.co.jp/dp/B0BQHDT8Y3">モバイルモニタースタンド</a></figcaption></figure></td> <td style="text-align:left;"> 高さを調整可能なこと。</figure>15.6インチのフル HD ディスプレイをしっかりホールドしてくれます。 </td> <td style="text-align:left;"> ¥3,800</td> </tr> <tr> <td style="text-align:left;"> <figure class="figure-image figure-image-fotolife" title="冷却台"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/makky_tyuyan/20231019/20231019214117.jpg" alt="" width="300" height="400" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption><a href="https://www.amazon.co.jp/dp/B098M5Z97Y">冷却台</a></figcaption></figure> </td> <td style="text-align:left;"> 材質が鉄で冷却効果が高く、ファンが強いが音が静かなこと。USB の中継があるため、冷却台から別のガジェットに給電することも可能です。</td> <td style="text-align:left;"> ¥1,600</td> </tr> <tr> <td style="text-align:left;"> <figure class="figure-image figure-image-fotolife" title="スマホスタンド"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/makky_tyuyan/20231019/20231019214113.jpg" alt="" width="300" height="400" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption><a href="https://www.dot-st.com/lakole/disp/item/2187133/">スマホスタンド</a></figcaption></figure></td> <td style="text-align:left;"> ニュースやカレンダーのリマインダーにスマホを活用します。7段階に角度を調節でき、コンパクトにたためて持ち運びしやすいサイズです。</td> <td style="text-align:left;"> ¥400</td> </tr> <tr> <td style="text-align:left;"> <figure class="figure-image figure-image-fotolife" title="ACアダプタ"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/makky_tyuyan/20231019/20231019214135.jpg" alt="" width="300" height="298" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>ACアダプタ</figcaption></figure></td> <td style="text-align:left;"> USB Type-A とUSB Type-C が使えるアダプタ。コンパクトで可愛いデザインが特徴です。デザインはテックタッチのオリジナルです♪</td> <td style="text-align:left;"> ¥0(会社支給品)</td> </tr> <tr> <td style="text-align:left;"> <figure class="figure-image figure-image-fotolife" title="リストレスト"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/makky_tyuyan/20231019/20231019214146.jpg" alt="" width="300" height="400" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>リストレスト</figcaption></figure></td> <td style="text-align:left;"> 腕の疲れを軽減してくれる必需品です。様々なリストレストを使った結果、現在は妻お手製のリストレストを使ってます。絶妙な高さがお気に入りです。</td> <td style="text-align:left;"> ¥1,000(材料費)</td> </tr> <tr> <td style="text-align:left;"> <figure class="figure-image figure-image-fotolife" title="バッグハンガー"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/makky_tyuyan/20231019/20231019214139.jpg" alt="" width="300" height="283" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption><a href="https://www.amazon.co.jp/dp/B0998Z6Q2L">バッグハンガー</a></figcaption></figure></td> <td style="text-align:left;"> テーブルにバッグをかけられます。私はバッグを床や椅子に直接置きたくないため、重宝します。カラビナつきで持ち運びしやすいサイズです。</td> <td style="text-align:left;"> ¥1,200</td> </tr> <tr> <td style="text-align:left;"> <figure class="figure-image figure-image-fotolife" title="ヘッドホン"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/makky_tyuyan/20231019/20231019214106.jpg" alt="" width="300" height="400" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption><a href="https://www.amazon.co.jp/dp/B09YKQZHTM">ヘッドホン</a></figcaption></figure></td> <td style="text-align:left;"> 指向性のあるノイズキャンセリングつきのヘッドホンです。心地よくオンラインミーティングができます。</td> <td style="text-align:left;"> ¥0(会社支給品)</td> </tr> <tr> <td style="text-align:left;"> <figure class="figure-image figure-image-fotolife" title="卓上ホワイトボード"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/makky_tyuyan/20231019/20231019214142.jpg" alt="" width="300" height="400" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption><a href="https://www.amazon.co.jp/dp/B00F4VWG9I">卓上ホワイトボード</a></figcaption></figure></td> <td style="text-align:left;"> イメージアップはアナログ派です。パッと書きたいときにホワイトボードは欠かせません。ボードをとりはずして縦横に設置出来ます。</td> <td style="text-align:left;"> ¥2,000 </td> </tr> <tr> <td style="text-align:left;"> <figure class="figure-image figure-image-fotolife" title="ノートパソコン用インナーバッグ"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/makky_tyuyan/20231019/20231019214110.jpg" alt="" width="300" height="400" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption><a href="https://www.amazon.co.jp/dp/B07W3TZYBZ">ノートパソコン用インナーバッグ</a></figcaption></figure></td> <td style="text-align:left;">長年愛用しているインナーバッグ。シンプルだけどちょうどよい収納力。14インチの Mac Book Pro に加え、コード類やアダプタ、メモ帳などが入ります。 </td> <td style="text-align:left;"> ¥2,000</td> </tr> </tbody> </table> <p><strong>特にこだわっていること</strong></p> <p>サブディスプレイをノートパソコンの左右に配置するケースが多いと思います。<br/> サブディスプレイを左右に置くと、身体が不自然にねじれるため、肩や背中のコリに繋がります。<br/> 今回ご紹介したスタンドを使うと、視線がまっすぐに維持できるため、コリを軽減できます。<br/> (スタンドに出会うまでは右側に配置していたが、1日持たず背中が辛かった。。。)<br/> そこで、サブディスプレイを、さきほど紹介したスタンドにドッキングすると・・・</p> <p><figure class="figure-image figure-image-fotolife" title="がっちりドッキング"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/makky_tyuyan/20231018/20231018193115.jpg" alt="&#x30B5;&#x30D6;&#x30C7;&#x30A3;&#x30B9;&#x30D7;&#x30EC;&#x30A4;&#x3092;&#x30C9;&#x30C3;&#x30AD;&#x30F3;&#x30B0;&#x3057;&#x305F;&#x5199;&#x771F;" width="420" height="560" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>がっちりドッキング</figcaption></figure></p> <p>これで左右に身体をねじる必要なく作業ができます。<br/> 以上、私が考える最強の作業環境を紹介しました。</p> <p>フルリモートは自分の作業スタイルに合わせてコンパクトにしたいですね!<br/> 作業環境の参考になれば幸いです!</p> <h2 id="フルリモートのコミュニケーションで工夫していることを知りたい">フルリモートのコミュニケーションで工夫していることを知りたい!</h2> <p>作業環境のイメージができた!さっそくフルリモートで仕事をしたい! ・・・と思ったときに不安になるのがコミュニケーション。私は、フルリモートでもあまりストレスを感じていません。QAマネージャーからも「コミュニケーションに課題を感じたことはない!」とフィードバックされています。</p> <p>フルリモートに限らず、遠隔地の方と仕事をする場合に不安になるポイントがあると思います。</p> <p><strong>非対面によるコミュニケーション不足</strong></p> <p>対面での会話ができないため、情報共有や確認作業が難しくなります。また、電話やメール、チャットなどのコミュニケーションツールを駆使しても、表情や声のトーンなどから受け取れる微細な情報が欠落し、誤解が生じることがあります。</p> <p><strong>モチベーションの低下</strong></p> <p>職場の雰囲気に触れる機会が減るため仕事の意欲が得られにくいことが考えられます。</p> <p><strong>作業環境の準備</strong></p> <p>オフィス勤務と同等のパフォーマンスを出すための作業環境を作る必要があります。 「作業環境の準備」については、前述の「フルリモート最強の作業環境はこれだ!」で紹介しました。</p> <p>次に、「非対面によるコミュニケーション不足」を軽減するために工夫しているポイントを紹介します。</p> <h3 id="コミュニケーションの不安を軽減する3つのポイント">コミュニケーションの不安を軽減する3つのポイント</h3> <p>オフィス勤務では当たり前だと考えられることを、フルリモート勤務では意識的に実施することが大事です。</p> <p>そこで、私が大事にしているコミュニケーションの不安を軽減するポイントを3つ紹介します。</p> <p><strong>隣りにいる感覚</strong></p> <p>なるべくオフィスにいる感覚を持ちながら、お互いを尊重しあって「私はここにいる!」と存在を示すこと。例えば、Slack などのチャットツールのリアクションを積極的にする。などです。オンラインミーティング中も何かしらのリアクションを示すことに気をつけています。</p> <p><strong>集中できる環境</strong></p> <p>オフィスで勤務しているときも、集中して作業したいときがあると思います。</p> <p>その場合は、その意志を示すことが大事。例えば、Google カレンダーなどのスケジューリングツールで「作業時間」を登録し、他の方から見れるようにすること。ただし、その場合は調整可能な予定なのかどうか示すことが大事です。</p> <p>テックタッチでは「*」をスケジュール名に付与することで調整可能と示すようなルールがあります。</p> <p><figure class="figure-image figure-image-fotolife" title="調整可能な時間を &quot; * &quot; で見える化"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/makky_tyuyan/20231018/20231018194018.png" alt="&#x30B9;&#x30B1;&#x30B8;&#x30E5;&#x30FC;&#x30EB;&#x306E;&#x30B9;&#x30AF;&#x30EA;&#x30FC;&#x30F3;&#x30B7;&#x30E7;&#x30C3;&#x30C8;" width="560" height="255" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>調整可能な時間を &quot; * &quot; で見える化</figcaption></figure></p> <p><strong>声がけしやすい環境</strong></p> <p>一般的に、作業をしている様子を見ると、「話しかけていいのかな?」という気遣いが生まれます。 気遣い自体は悪いことでは全くないのですが、過剰になってしまうのはもったいない。リモートワークだと、オフィスにいるときよりもより顕著に現れます。</p> <p>集中する時でも「話しかけても大丈夫」と意思表示をすることが大事。 話しながら、いっしょにやろう!という環境を作るように気をつけています。</p> <h3 id="フルリモートのコミュニケーションで活用できるTandem">フルリモートのコミュニケーションで活用できるTandem</h3> <p>テックタッチでは、Tandem というビデオチャットツールを使っています。このツールを、紹介した3つのポイントを実現するために活用しています。</p> <p>Tandemについては、テックタッチプロダクトマネージャーのzakが紹介しているブログで Tandem への愛が感じられるはず!</p> <p>Tandem の詳しい機能にご興味ある方は是非ご覧ください!</p> <p><strong><strong><a href="https://tech.techtouch.jp/entry/why-we-love-tandem-1">リモートチームのコミュニケーションをレベルアップ!Tandemの紹介〜ごきげんに働こう編〜</a></strong></strong><br/> <strong><strong><a href="https://tech.techtouch.jp/entry/why-we-love-tandem-2">円滑なリモートワークを支えるツール Tandem への愛を語る〜効率よく働こう編〜</a></strong></strong></p> <p>私は、上記のブログで紹介されている機能のうち、Wave・クロストーク・リスナー機能を重宝しています。</p> <h2 id="まとめ">まとめ</h2> <p>オフィス勤務中心の環境からフルリモートの環境に『働き方改革』を実現したお話を紹介しました。</p> <p><strong>伝えたいポイント</strong></p> <ul> <li>フルリモートを実現するためのコストは全体の支出バランスを見て考えてみる</li> <li>フルリモートの作業環境は自分の作業スタイルに合わせてコンパクトにする</li> <li>コミュニケーションの不安は自分の意思表示をうまく伝える</li> </ul> <p>以上、フルリモートワークにおいて、作業環境のつくり方やコミュニケーションの不安に悩まれている方の参考になれば幸いです。</p> <p><strong>おまけ</strong></p> <p>記事中に出てきたドラッグストア併設のコワーキングスペース “EZOHUB SAPPORO” はこちら。</p> <p><a href="https://satudora-hd.co.jp/ezohubsapporo/">EZOHUB SAPPORO | 北海道・札幌最大級のコワーキングスペース</a></p> makky_tyuyan テックタッチにおけるSREの役割・課題感を紹介します hatenablog://entry/6801883189049925466 2023-10-19T09:00:00+09:00 2023-12-15T09:38:25+09:00 テックタッチという会社・サービスに触れつつ、SRE チームの働く環境や課題感を共有しながらチームの紹介をしていきます。 <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/i/ihirokyx/20231012/20231012211420.png" alt="" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"><ul class="table-of-contents"> <li><a href="#テックタッチという会社サービス">テックタッチという会社・サービス</a></li> <li><a href="#テックタッチの-SRE-チーム">テックタッチの SRE チーム</a><ul> <li><a href="#何をやっているの">何をやっているの</a></li> <li><a href="#インフラエンジニアというよりもソフトウェアエンジニア">インフラエンジニアというよりもソフトウェアエンジニア</a></li> <li><a href="#課題感">課題感</a></li> <li><a href="#SREチームの活動---大きなサイクル小さなサイクル">SREチームの活動 - 大きなサイクル・小さなサイクル</a></li> <li><a href="#コミュニケーション">コミュニケーション</a></li> <li><a href="#技術スタックツール">技術スタック・ツール</a></li> </ul> </li> <li><a href="#終わりに">終わりに</a></li> </ul> <p>こんにちは。SRE の roki です。暑い日はまだあるものの、朝はすっかり秋を感じるようになり子どもたちが登校しやすくなってホッとしている今日このごろです。</p> <p>この記事では、テックタッチという会社・サービスに触れつつ、SRE チームの働く環境や課題感を共有しながらチームの紹介をしていきます。興味を持っていただけたらぜひお声がけください。カジュアルに話し合う場を設けさせてもらっており、<a href="https://techtouch.jp/recruit/">採用情報ページ</a>にて受け付けています。</p> <h2 id="テックタッチという会社サービス">テックタッチという会社・サービス</h2> <p>テックタッチでは、社名と同じ「テックタッチ」という名前のサービスを運営しています。どのようなサービスかというと、対象とする Web システム上に、そのシステムを修正せずにノーコードで操作ガイドを設置することで、エンドユーザーに対象システムの活用を促すというサービスです。</p> <div> <video src="https://techtouch.jp/wp-content/themes/tt-theme/images/top.mp4" autoplay></video> <div style="text-align: center; margin: 10px 0px; color: #4E5156;">テックタッチのガイドを再生している様子</div> </div> <p>プロダクトの概要については、<a href="https://techtouch.jp/">サービスサイト</a>をご覧ください。また、先日<a href="https://techtouch.jp/news/202309133million/">プレスリリース</a>をうたせてもらったとおり MAU で 300 万人をこえる規模まで成長してきています。エンドユーザーからみるとシステムの一部として見える、そしていろいろなシステムでご利用いただいているため、高い可用性が求められます。</p> <p>そんなテックタッチを開発・運用する社内の雰囲気を表す 1 つの側面として、最近社内で使われる「Co-Developers」という考え方がとても象徴的です。開発者だけでなく、営業やカスタマーサクセスなどのビジネスチーム、コーポレートチームのメンバーを含めた全員を指しており、全社一丸となってテックタッチの展開に取り組んでいる状況をよく表している言葉だと思っています。</p> <h2 id="テックタッチの-SRE-チーム">テックタッチの SRE チーム</h2> <h3 id="何をやっているの">何をやっているの</h3> <p>テックタッチの SRE チームは、テックタッチにおけるソフトウェア開発・サービス運用における信頼性・生産性の向上と改善に努めています。ここでいう「ソフトウェア開発・サービス運用における信頼性・生産性」を分解すると、</p> <ul> <li>システムの安定性</li> <li>オブザーバビリティ</li> <li>パフォーマンス最適化</li> <li>セキュリティ</li> <li>拡張性</li> <li>保守性</li> <li>コスト最適化(FinOps)</li> </ul> <p>の周辺を指していて、SRE チームはこれらを向上させるために必要なことに日々取り組んでいます。たとえば、</p> <ul> <li>統合モニタリング環境の構築・運用</li> <li>コンテンツ配信の最適化</li> <li>外部からの攻撃に対する防御・検知機構の整備、脆弱性診断とその対処</li> <li>利用サービス・ミドルウェアの EOL 対応</li> <li>AWS Well-Architected Framework を参考にした運用の見直し</li> <li>CI/CD の構築・運用改善</li> <li>DevEx 向上のための開発基盤整備</li> <li>インフラコストのモニタリング・最適化</li> </ul> <p>などを通して体現しています。また、DevEx に関連する具体的な話は <a href="https://note.com/dnx_vc/n/nfd9377ec6109">DNX Ventures 主催の採用イベント</a>でも紹介させてもらいました。</p> <h3 id="インフラエンジニアというよりもソフトウェアエンジニア">インフラエンジニアというよりもソフトウェアエンジニア</h3> <p> SRE といってしまうと、いわゆる「<a href="https://cloud.google.com/sre?hl=ja">Google の SRE</a>」を想起されると思います。しかし、テックタッチは Google のような巨大なインフラを抱えているわけではないので、「Google の SRE」とまったく同じではありません。テックタッチの SRE は、ソフトウェア開発・サービス運用における信頼性・生産性向上のために状況を見直したり、ソフトウェア開発を行ったりすることがメインです。SRE というと、まずインフラエンジニアを想像する方が少なくないと思います。クラウドインフラ要素は多分にありますが、上述の理由からサーバーサイドエンジニアとして Web アプリケーション含めたソフトウェア開発にあたっている方のほうが活躍の幅は広いと認識しています。</p> <h3 id="課題感">課題感</h3> <p><a href="https://techtouch.jp/cases/">導入事例</a>で紹介させてもらっている通り、テックタッチは社内向け、自社プロダクト向け、中央省庁や自治体のシステム向けの領域でご活用いただいています。テックタッチは、顧客システムのエンドユーザー数とテックタッチへのリクエストの数が比例する構造になっているので、エンドユーザー数は多くなります。</p> <p><figure class="figure-image figure-image-fotolife" title="テックタッチを導入していただいたシステムのすべてのエンドユーザーからアクセスが発生する"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/i/ihirokyx/20231012/20231012213627.png" width="354" height="331" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>テックタッチを導入していただいたシステムのすべてのエンドユーザーからアクセスが発生する</figcaption></figure></p> <p>そしてエンドユーザーからすると対象システムの一部とも見えるため、「テックタッチが停止する = 顧客システムの一部が見えなくなる」ように見えます。導入事例(たとえば、<a href="https://techtouch.jp/cases/digitalagency/">「デジタル庁の「調達ポータル」にシステム活用支援サービス 「テックタッチ」を利用」</a> のナビゲーション)をご覧いただくと理解していただけるのではと思います。このような状態が数多くのシステムにわたり起こり得るため、エンドユーザーに見える機能が停止するようなメンテナンスを行うのは簡単ではありません。このような状況に耐えるアーキテクチャが必要で、そこに向かっていけるように改善を行っています。</p> <h3 id="SREチームの活動---大きなサイクル小さなサイクル">SREチームの活動 - 大きなサイクル・小さなサイクル</h3> <p>大きなサイクルとして、毎年、全社で掲げるビジネス目標と現在のギャップと各種リソースの利用状況(リクエスト数や DB レコード数など)から、将来どれくらいまで利用リソースが拡大するかを予測します。それに基づき何をしなければならないか考えたり、負荷テストなどを行いボトルネックを探りにいきます。</p> <p>小さなサイクルとして、日常業務における基本サイクルは、「先 3 週間のプランニング → デイリーミーティング → ふりかえり」の形をとっています。チケットの管理はカンバン方式で行っています。プランニングでは大まかにどの部分を見ていくかというレベルで方針を決め、デイリーで日々の進捗を確認しつつ状況を見ておかわりしてく方針でタスクに取り組みます。サービス運用をしているかたわら、一定の割り込みは許容しているところからこのような方針をとっています。ふりかえりでは KPT + Concern(Problem ではないけど気になること)方式で、共有・改善を図っています。3 人の小さなチームですがふりかえりスパンが長いこともあり、1 時間では収まらないくらいの話し合いになることはままあります。その他、検討・決めなければいけないことがあればチーム内外問わず適宜ミーティングを開催しています。SRE として他チームに働きかけることは多いです。</p> <h3 id="コミュニケーション">コミュニケーション</h3> <p>テックタッチの開発・運用を行っている組織はフルリモートで業務にあたっています。このため、基本的にコミュニケーションは、Slack, <a href="https://tandem.chat/">Tandem</a>, Google Meet あたりをメインで使っています。文章化したほうが良かったり非同期で十分な場合は Slack を使いますが、ちょっとした相談なんだけどテキストではコストが高いことでは音声チャットも多用しています。オフィスでちょっと話しかける雰囲気をリモートでも失わないようにしたいという思いから来ています。</p> <h3 id="技術スタックツール">技術スタック・ツール</h3> <p>インフラとして AWS を全面的に利用しています。サービスとしては Amazon ECS Fargate, Amazon Aurora, Amazon CloudFront などを中心にマネージドサービスを積極的に採用しています。インフラの管理は AWS CDK や Amazon CloudFormation を用いています。主要アプリケーションはコンテナで管理されておりその管理もおなっています。監視基盤として Datadog を導入・運用しています。CI/CD 環境として AWS 関連以外では CircleCI, GitHub Actions を利用しています。必要に応じてアプリケーションに手を入れることもありプログラミング言語については既存スタックの影響で Go を使うことが多いです。また、周辺スタックをさわる際に TypeScript/JavaScript, Python など適宜使い分けています。</p> <h2 id="終わりに">終わりに</h2> <p>SRE チームの様子や取り組み、テックタッチの雰囲気などまとめさせてもらいました。テックタッチの雰囲気については<a href="https://techtouch.jp/recruit/">こちら</a>にバリュー・行動指針がまとまっています。気になる方や、中の人と話してみたいかもと思った方はぜひご一読ください。バリューの中にある「いつでもごきげん」については、<a href="https://tech.techtouch.jp/entry/aws-security-workshop-event-2023">セキュリティインシデント疑似体験調査ワークショップに参加すべき3つの理由</a> を見ていただくと、さらに雰囲気を理解していただけると思います(SRE も参加しています!)。</p> ihirokyx Goコンパイラのお勉強(3)~配列の効率的な操作に関する最適化~ hatenablog://entry/6801883189049878197 2023-10-16T09:00:00+09:00 2023-12-15T09:37:16+09:00 「配列の効率的な利用」と題してgolang/go CompilerOptimizations で扱われている最適化の紹介をしていきます。 <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/i/izzii/20231012/20231012203251.png" alt="" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <ul class="table-of-contents"> <li><a href="#はじめに">はじめに</a></li> <li><a href="#string-と-byte-のキャスト最適化">string と byte のキャスト最適化</a><ul> <li><a href="#1-map-のキー指定のためのキャスト">1. map のキー指定のためのキャスト</a></li> <li><a href="#2-string-を-byte-毎に処理するためのキャスト">2. string を byte 毎に処理するためのキャスト</a></li> <li><a href="#3-比較のためのキャスト">3. 比較のためのキャスト</a></li> </ul> </li> <li><a href="#memclr-による配列ゼロクリア最適化">memclr による配列ゼロクリア最適化</a></li> <li><a href="#ガベージコレクションのマークスキャン回避">ガベージコレクションのマークスキャン回避</a></li> <li><a href="#おわりに">おわりに</a></li> <li><a href="#参考文献">参考文献</a></li> </ul> <h2 id="はじめに">はじめに</h2> <p>SRE の <a href="https://twitter.com/ahneahneahne"></a>izzii (𝕏: <a href="https://twitter.com/ahneahneahne">@ahneahneahne</a>) です。今回をもって「Go コンパイラのお勉強」と題した連載ブログが完結です!仕事の話とは直接関係がないネタだったので、書く内容に悩むということはなく気持ちよくかけました。さて、今回は「配列の効率的な利用」と題して</p> <p><a href="https://github.com/golang/go/wiki/CompilerOptimizations">golang/go CompilerOptimizations</a> で扱われている残り物の紹介をしていきます笑 残り物とは言っても知ると知らないとでは Go コードの読み方が変わるかと思いますので、ぜひ!</p> <ul> <li>[第 1 回] <a href="https://tech.techtouch.jp/entry/go-compiler-study-optimization-undefined-behaviour">Goコンパイラのお勉強(1)~ゼロ幅の型によるメモリ利用の最適化と未定義動作~</a></li> <li>[第 2 回] <a href="https://tech.techtouch.jp/entry/go-compiler-study-inline-escape-analysis">Goコンパイラのお勉強(2)~高階関数のためのインライン展開とエスケープ解析~</a></li> <li>[本記事] Goコンパイラのお勉強(3)~配列の効率的な操作に関する最適化~</li> </ul> <h2 id="string-と-byte-のキャスト最適化">string と byte<span data-unlink="data-unlink"> のキャスト最適化</span></h2> <p></span>byte から string 、string から <span data-unlink>byte へのキャストは、コンパイラの最適化によってメモリアロケーションが発生しない場合があります。逆を言うと基本的に string と </span>byte へのキャストの際にはメモリアロケーションが走るということです。百聞は一見に如かずということでまずはコードを見てみましょう。</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synComment">// go1.21.1 darwin/arm64</span> ... <span class="synStatement">func</span> printTotalAlloc() { ... <span class="synComment">// メモリアロケーションの合計を出力</span> } <span class="synStatement">func</span> main() { m := <span class="synStatement">make</span>(<span class="synType">map</span>[<span class="synType">string</span>]<span class="synStatement">struct</span>{}, <span class="synConstant">1</span>) <span class="synComment">// 入れ物を作る</span> <span class="synStatement">for</span> i := <span class="synConstant">0</span>; i &lt; <span class="synConstant">10</span>; i++ { b := <span class="synStatement">make</span>([]<span class="synType">byte</span>, <span class="synConstant">1024</span> * <span class="synConstant">1024</span>) <span class="synComment">// 1MiB のアロケーション</span> rand.Read(b) <span class="synComment">// 簡単のためエラーハンドリングしません</span> <span class="synStatement">if</span> _, ok := m[<span class="synType">string</span>(b)]; ok { <span class="synComment">// []byte -&gt; string のキャスト</span> fmt.Println(<span class="synConstant">&quot;never called&quot;</span>) } } printTotalAlloc() <span class="synComment">// TotalAlloc = 10 MiB</span> } </pre> <p>キャストstring(b) を別出ししてみます。</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synComment">// go1.20.5 darwin/arm64</span> ... <span class="synComment">// if _, ok := m[string(b)]; ok { // []byte -&gt; string のキャスト</span> s := <span class="synType">string</span>(b) <span class="synComment">// コピーで 1MiB のアロケーション</span> <span class="synStatement">if</span> _, ok := m[s]; ok { fmt.Println(<span class="synConstant">&quot;never called&quot;</span>) } } printTotalAlloc() <span class="synComment">// TotalAlloc = 20 MiB</span> } </pre> <p><span data-unlink>byte から string へのキャストを別行に外出ししただけで、メモリアロケーションのサイズが 2 倍に変わりました!正直自分も今までこれを知らずに勿体無いコードを書いていたかもしれません。</p> <p>たとえば、HTTP リクエストやファイルからのデータ読み取りといった、I/O 処理の場面で活躍することが多いのではないでしょうか。string と byte</span> のキャストでアロケーションが走らないのは以下の 3 つのパターンがあります。</p> <h3 id="1-map-のキー指定のためのキャスト">1. map のキー指定のためのキャスト</h3> <p>先ほどの例です。</p> <pre class="code lang-go" data-lang="go" data-unlink>m := <span class="synStatement">make</span>(<span class="synType">map</span>[<span class="synType">string</span>]<span class="synStatement">struct</span>{}) <span class="synStatement">var</span> b []<span class="synType">byte</span> ... ... <span class="synComment">// アロケートは発生しない</span> m[<span class="synType">string</span>(b)] </pre> <h3 id="2-string-を-byte-毎に処理するためのキャスト">2. string を byte 毎に処理するためのキャスト</h3> <p>string を for 文で 1byte ずつ処理するコードを書こうとした場合もメモリアロケーションの回避ができます。もちろんキャストを外出ししちゃダメですよ!</p> <pre class="code lang-go" data-lang="go" data-unlink>s := <span class="synConstant">&quot;...&quot;</span> <span class="synStatement">for</span> i, c := <span class="synStatement">range</span> []<span class="synType">byte</span>(s) { <span class="synComment">// アロケートは発生しない</span> <span class="synComment">// ...</span> } </pre> <h3 id="3-比較のためのキャスト">3. 比較のためのキャスト</h3> <p>もちろんキャストを外出ししちゃダメですよ!</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synStatement">var</span> s <span class="synType">string</span> <span class="synStatement">var</span> b []<span class="synType">byte</span> <span class="synStatement">var</span> x = <span class="synType">string</span>(s) == <span class="synType">string</span>(b) <span class="synStatement">var</span> y = <span class="synType">string</span>(s) &lt; <span class="synType">string</span>(b) </pre> <h2 id="memclr-による配列ゼロクリア最適化">memclr による配列ゼロクリア最適化</h2> <p>slices や array でゼロクリアする際にはその操作に最適化された <code>memclr</code> が使われます。 <code>memclr</code>とは Go の標準関数ではなく、ランタイムでのみ利用される関数です。早速実験してみましょう。</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synComment">// go1.21.0 darwin/arm64 </span> <span class="synStatement">const</span> N = <span class="synConstant">1</span> &lt;&lt; <span class="synConstant">20</span> <span class="synComment">// 1,048,576</span> <span class="synStatement">func</span> memclrRange(a []<span class="synType">int</span>) { <span class="synStatement">for</span> i := <span class="synStatement">range</span> a { <span class="synComment">// この for ループが最適化される</span> a[i] = <span class="synConstant">0</span> } } <span class="synStatement">func</span> memclrIndex(a []<span class="synType">int</span>) { <span class="synStatement">for</span> i := <span class="synConstant">0</span>; i &lt; <span class="synStatement">len</span>(a); i++ { <span class="synComment">// この for ループは最適化されない</span> a[i] = <span class="synConstant">0</span> } } <span class="synStatement">func</span> memset1(a []<span class="synType">int</span>) { <span class="synStatement">for</span> i := <span class="synStatement">range</span> a { a[i] = <span class="synConstant">1</span> } } <span class="synStatement">func</span> memsetRand(a []<span class="synType">int</span>) { r := rand.New(rand.NewSource(time.Now().UnixNano())) <span class="synStatement">for</span> i := <span class="synStatement">range</span> a { a[i] = r.Int() } } <span class="synStatement">func</span> BenchmarkMemclrRange(b *testing.B) { b.Run(strconv.Itoa(N), <span class="synType">func</span>(b *testing.B) { <span class="synStatement">var</span> a = <span class="synStatement">make</span>([]<span class="synType">int</span>, N) memsetRand(a) <span class="synComment">// 一応値を埋めておく</span> b.ResetTimer() <span class="synStatement">for</span> i := <span class="synConstant">0</span>; i &lt; b.N; i++ { memclrRange(a) } }) } <span class="synStatement">func</span> BenchmarkMemclrIndex(b *testing.B) { b.Run(strconv.Itoa(N), <span class="synType">func</span>(b *testing.B) { <span class="synStatement">var</span> a = <span class="synStatement">make</span>([]<span class="synType">int</span>, N) memsetRand(a) <span class="synComment">// 一応値を埋めておく</span> b.ResetTimer() <span class="synStatement">for</span> i := <span class="synConstant">0</span>; i &lt; b.N; i++ { memclrIndex(a) } }) } <span class="synStatement">func</span> BenchmarkNoMemclr(b *testing.B) { b.Run(strconv.Itoa(N), <span class="synType">func</span>(b *testing.B) { <span class="synStatement">var</span> a = <span class="synStatement">make</span>([]<span class="synType">int</span>, N) memsetRand(a) <span class="synComment">// 一応値を埋めておく</span> b.ResetTimer() <span class="synStatement">for</span> i := <span class="synConstant">0</span>; i &lt; b.N; i++ { memset1(a) } }) } </pre> <pre class="code lang-go" data-lang="go" data-unlink>BenchmarkMemclrRange/<span class="synConstant">1048576</span>-<span class="synConstant">8</span> <span class="synConstant">5482</span> <span class="synConstant">243813</span> ns/op BenchmarkMemclrIndex/<span class="synConstant">1048576</span>-<span class="synConstant">8</span> <span class="synConstant">2767</span> <span class="synConstant">497839</span> ns/op BenchmarkNoMemclr/<span class="synConstant">1048576</span>-<span class="synConstant">8</span> <span class="synConstant">2176</span> <span class="synConstant">537496</span> ns/op </pre> <p>range によるスキャンでは約 2 倍の速さに最適化されています。<strong>一方、range でないスキャンでは最適化が効かないことには注意です!</strong></p> <p><code>memclr</code> はなぜ早いのか?というと、実装が OS や CPU アーキテクチャに合わせて最適化されているからです。より一般的な <code>memset</code> によるゼロ値以外の値でも最適化も検討されたことがあるようですが、こちらは実装されていません。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fgolang%2Fgo%2Fissues%2F5373" title="cmd/gc: optimize memset idiom · Issue #5373 · golang/go" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/golang/go/issues/5373">github.com</a></cite></p> <p>明示的にデータをクリアして GC を効かせたい時、メモリアロケーションされた領域の使い回しの際に、ゼロクリアで初期化する場合などで活躍してくれるでしょう。</p> <p>下記のような書き方で部分 slice のゼロクリアができることも確認しています。</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synComment">// go1.21.0 darwin/arm64 </span> <span class="synStatement">func</span> memclr(a []<span class="synType">int</span>) { b := a[<span class="synStatement">len</span>(a)/<span class="synConstant">3</span> : <span class="synStatement">len</span>(a)/<span class="synConstant">2</span>] <span class="synStatement">for</span> i := <span class="synStatement">range</span> b { <span class="synComment">// この for ループが最適化される</span> b[i] = <span class="synConstant">0</span> } } </pre> <h2 id="ガベージコレクションのマークスキャン回避">ガベージコレクションのマークスキャン回避</h2> <p>ガベージコレクション (GC) のコストは小さいに越したことはありません。Go はトレースガベージコレクションという仕組みを採用している関係で slices, channels, maps … といったコレクション内部で参照がないポインターをスキャニングします。もしもコレクションの要素にポインターが含まれていなければスキャンしなくなるというのがこの最適化です。たとえば以下のような型を要素に持つ配列が具体例です。</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synStatement">type</span> Value <span class="synStatement">struct</span> { Name [<span class="synConstant">32</span>]<span class="synType">byte</span> Balance <span class="synType">uint64</span> Timestamp <span class="synType">int64</span> } <span class="synComment">// GC は m のポインタだけ管理する。中身を見る必要はない。</span> m := <span class="synStatement">make</span>([]Value, <span class="synConstant">1e8</span>) </pre> <p>完全に固定長でヒープ上でも連続したところに収まってくれそうなデータですね。逆に Name を string とすると実体としてはポインターなのでスキャンの対象となってしまいます。もう少し Go のトレースガベージコレクションの仕組みが知りたいという方がいらっしゃれば roki さんの記事、<a href="https://tech.techtouch.jp/entry/golang-gogc-gomemlimit">A Guide to the Go Garbage Collectorを読んで GOGC と GOMEMLIMIT を理解する</a> をぜひ読んでみてください!</p> <p>本章でも実験を通して、スキャン実効の有無や実行速度の差を確認したかったのですが、難しかったです。今後方法がわかれば、ぜひ記載したいと思います。大きなデータをメモリ上にストアする必要がある場合に、この最適化が有効な場面があるでしょう。</p> <h2 id="おわりに">おわりに</h2> <p>Go コンパイラのお勉強と題しまして 3 つ記事を書いてみました。公式が出している資料があっさりなので、行間を補いつつ、実験コードを交えつつ解説してみました。コンパイラによる最適化が効く条件というのはコード上に明示されないので、知ると知らないとで Go コードの読み方が変わると思っています。 自分も知らないことがあったので、非常に学びが多くて良い取り組みでした。また何か知識を整理するようなブログを書いて、勉強したいと思いました。</p> <p>ぜひ連載のほか 2 つのブログもみていただけますと嬉しいです!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.techtouch.jp%2Fentry%2Fgo-compiler-study-optimization-undefined-behaviour" title="Goコンパイラのお勉強(1) ~ゼロ幅の型によるメモリ利用の最適化と未定義動作 ~ - Techtouch Developers Blog" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tech.techtouch.jp/entry/go-compiler-study-optimization-undefined-behaviour">tech.techtouch.jp</a></cite></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.techtouch.jp%2Fentry%2Fgo-compiler-study-inline-escape-analysis" title="Goコンパイラのお勉強(2)~高階関数のためのインライン展開とエスケープ解析~ - Techtouch Developers Blog" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tech.techtouch.jp/entry/go-compiler-study-inline-escape-analysis">tech.techtouch.jp</a></cite></p> <h2 id="参考文献">参考文献</h2> <ul> <li><a href="https://github.com/golang/go/wiki/CompilerOptimizations">golang/go CompilerOptimizations</a></li> <li><a href="https://github.com/golang/go/issues/5373">cmd/gc: optimize memset idiom #5373</a></li> <li><a href="https://techtechtouch.jp/entry/golang-gogc-gomemlimit">A Guide to the Go Garbage Collectorを読んで GOGC と GOMEMLIMIT を理解する</a></li> </ul> izzii Microsoft Intune で Mac を管理するときに躓いたこと hatenablog://entry/820878482969356446 2023-10-02T09:00:00+09:00 2023-10-02T09:00:04+09:00 Microsoft Intune 導入の際、Mac 関係で躓いたことを紹介します。 <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tt_kacchan/20230926/20230926200705.png" alt="" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"><ul class="table-of-contents"> <li><a href="#あいさつ">あいさつ</a></li> <li><a href="#Mac-のパスワードポリシーを変更すると必ずローカルユーザーのパスワードを変更する必要がある">Mac のパスワードポリシーを変更すると必ずローカルユーザーのパスワードを変更する必要がある</a></li> <li><a href="#Mac-へブラウザ拡張機能を配布するにはplistを配布する">Mac へブラウザ拡張機能を配布するにはplistを配布する</a></li> <li><a href="#おわりに">おわりに</a></li> </ul> <h2 id="あいさつ">あいさつ</h2> <p>こんにちは、CSE(Customer Success Engineer) の kacchan です。先日、大学時代からファンであるプロのジャズギタリストからレッスンを受けたこともあり、毎週末とりあえずギターをさわるようになってきました。アドリブをいいかんじで取れるように練習するぞ!</p> <p>さて、半年くらい前にはなるのですが社内へ Microsoft Intune 導入を進めました。そのときに Mac 関係で躓いたことがあったのですが、あまりインターネット上に情報がなかったので紹介します。</p> <h2 id="Mac-のパスワードポリシーを変更すると必ずローカルユーザーのパスワードを変更する必要がある">Mac のパスワードポリシーを変更すると必ずローカルユーザーのパスワードを変更する必要がある</h2> <p>結論から述べると、以下の macOS の仕様があるためです。</p> <blockquote><p>パスワード ポリシーが更新されるたびに、これらの macOS バージョンを実行しているすべてのユーザーは、現在のパスワードが新しい要件に準拠している場合でも、パスワードを変更する必要があります。</p></blockquote> <p><a href="https://learn.microsoft.com/ja-jp/mem/intune/configuration/device-restrictions-macos#password">Microsoft Intune&#x306E; macOS &#x30C7;&#x30D0;&#x30A4;&#x30B9;&#x8A2D;&#x5B9A; | Microsoft Learn</a></p> <p>躓いたポイントは「ポリシー適用したら今まで使っていたパスワードが使えなくなった!」でした。 同じ事象に悩まれている方のために、ここから上記の情報にたどり着くまでの経緯を少し書いておきます。</p> <p>Mac にパスワード制約(桁数制限を入れただけの単純なもの)をかけるために検証用のポリシーを作成・適用したら、今まで使っていたパスワードが使えなくなりました。</p> <p>Mac 組み込みのパスワードマネージャーは今までのパスワードで利用可能(つまりパスワードは合っている)なのに、ファイアウォール設定など強い権限が必要な設定変更だとパスワードが通らない、という現象でした。</p> <p>一度 Mac をロック状態にしたら2度と開けないかも、、、と一瞬思いましたが、ローカルユーザーのパスワード変更はちゃんと今まで設定していたパスワードを”古いパスワード”として認識してくれたので助かりました。とりあえずパスワードを変更すれば良かったです。</p> <p>その後パスワードポリシーの検証を進めてわかったことは以下の通りです。</p> <ul> <li>パスワード関連のポリシーを適用すると必ずローカルユーザーのパスワード変更が必要になる</li> <li>パスワード関連のポリシーを外した際も同様にパスワード変更が必要になる</li> </ul> <p>ここからいろいろとググったもののなかなか答えにたどり着けず、結局 Microsoft のサポートへ問い合わせました。</p> <h2 id="Mac-へブラウザ拡張機能を配布するにはplistを配布する">Mac へブラウザ拡張機能を配布するにはplistを配布する</h2> <p>Mac 端末 × Google Chrome × Intune では、以下のような設定で実現できました。(Windows 環境ではいろいろ情報は出てきますが、Mac は少なかった。。。)</p> <ol> <li><p>macOS で環境設定用のプロパティを記載するplistファイルを作成します。今回の例では ExtensionInstallForcelist へ設定します。(ブラウザ拡張機能管理用の別のポリシーを設定できます。)</p> <pre><code class="`:xml"> &lt;key&gt;ExtensionInstallForcelist&lt;/key&gt; &lt;array&gt; &lt;string&gt;(インストールしたい拡張機能のIDを記載)&lt;/string&gt; &lt;/array&gt; </code></pre></li> <li><p>Intune でデバイス構成プロファイルを作成します。デバイス > macOS > 構成プロファイル から「プロファイルの作成」をクリックします。プロファイルの種類 は<code>テンプレート</code>を選択し、テンプレート名は<code>設定ファイル</code>を選択し作成します。</p></li> <li><p>構成プロファイルに plist ファイルを設定します。優先ドメイン名 には<code>com.google.Chrome</code>と入力、プロパティファイルリスト には作成したplistファイルをアップロードします。</p></li> </ol> <h2 id="おわりに">おわりに</h2> <p>社内で Intune 導入を進めたときに調査・検証したことの紹介でした。Mac 端末については Intune で管理するのはまれかと思いますが、ご紹介した内容は他の MDM 製品でも読み替えることは可能かなと思います。</p> <p>もし同様の事象で悩んでいる方がいたら、参考にしていただけると嬉しいです!</p> tt_kacchan えぇっ、Nx Cloud を知らない!?――前編:「CIで分散並列実行とキャッシュ利用ができるだって!?」―― hatenablog://entry/820878482967221575 2023-09-19T09:00:00+09:00 2023-12-26T11:30:34+09:00 この記事では Nx Cloud の基本でありそして目玉でもある「CIの自動分散並列実行」と「CI結果のキャッシング」を紹介するヨ👶CIを速くしたい人、Nx Cloud に興味がある人はぜひ読んでみてね!! <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20231225/20231225163603.png" alt="&#x8A18;&#x4E8B;&#x306E;&#x30BF;&#x30A4;&#x30C8;&#x30EB;&#x753B;&#x50CF;&#x3002;&#x8A18;&#x4E8B;&#x306E;&#x30BF;&#x30A4;&#x30C8;&#x30EB;&#x3068;&#x3001;&#x7B46;&#x8005;&#x3067;&#x3042;&#x308B;canalun&#x306E;&#x30A2;&#x30A4;&#x30B3;&#x30F3;&#x304C;&#x306E;&#x3063;&#x3066;&#x3044;&#x308B;&#x3002;" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>さあさあ、面白そうなタイトルに惹かれて(?)やってきたそこのあなた!ありがとうございます!!</p> <p>はじめまして、テックタッチの <a href="https://twitter.com/i_am_canalun">canalun</a> と言います👶<br/> 普段は DOM について考えたり、CI/CD の改善をしたりな毎日です!<br/> <strong>DOM が好きすぎて<a href="https://zenn.dev/canalun">週刊DOMDOMタイムス</a>というのをやってます</strong>ので、興味がある方はぜひ見てみてくださいねえ🌞</p> <p>この記事では<strong>前後編2回に分けて、Nx Cloud の主な機能の紹介と実際の導入の仕方を解説します</strong>!!<br/> 前編にあたる本稿では、<strong>Nx Cloud の2大最強ポイントである「CIの自動分散並列実行」と「CI結果のキャッシング」を紹介します</strong>。</p> <p>ちなみに後編ももうあるよ。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.techtouch.jp%2Fentry%2Fhow_to_adopt_nx_cloud" title="えぇっ、Nx Cloud を知らない!?――後編:「実際の導入の仕方とハマりどころ」―― - Techtouch Developers Blog" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tech.techtouch.jp/entry/how_to_adopt_nx_cloud">tech.techtouch.jp</a></cite></p> <p>では早速行きましょう!<br/> 目次を見てオモロそうなところだけ読むもよし、とりあえず上から読んでみるもよしです。自由にやってくれ!!</p> <p>👇👇</p> <ul class="table-of-contents"> <li><a href="#これは一体なんの記事なんやあらためて一応">これは一体なんの記事なんや!?(あらためて一応)</a></li> <li><a href="#そもそも-Nx-ってなにちょっとだけ説明するよ">そもそも Nx ってなに?(ちょっとだけ説明するよ)</a></li> <li><a href="#オーケーわかったで結局そのクールなソフトウェアは一体何をしてくれるって言うんだ">オーケー、わかった。で、結局そのクールなソフトウェアは一体何をしてくれるって言うんだ?</a><ul> <li><a href="#1-コマンドの自動分散並列実行">1. コマンドの自動分散並列実行</a></li> <li><a href="#2-コマンド実行結果のキャッシング">2. コマンド実行結果のキャッシング</a></li> </ul> </li> <li><a href="#えんもたけなわ">えんもたけなわ</a></li> </ul> <h2 id="これは一体なんの記事なんやあらためて一応">これは一体なんの記事なんや!?(あらためて一応)</h2> <p>さて、今日のソフトウェア開発においてはCI/CDがめちゃくちゃ大事なものとして取り沙汰されがちです。<br/> 実際どのくらい重視するかは現場によりけりですが、テックタッチのように toB の SaaS を開発する場合はもう説明の必要がないくらい大事です。<br/> <figure class="figure-image figure-image-fotolife" title="[Mallaidh Mleziva; Why the World Needs CI/CD ](https://flexagon.com/blog/why-the-world-needs-ci-cd/) より"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20230913/20230913201643.png" alt="&#x6D77;&#x5916;&#x306E;&#x30A2;&#x30CB;&#x30E1;&#x3088;&#x308A;&#x3001;&#x71C3;&#x3048;&#x3066;&#x3044;&#x308B;&#x90E8;&#x5C4B;&#x306E;&#x4E2D;&#x306B;&#x30B3;&#x30DF;&#x30AB;&#x30EB;&#x306A;&#x72AC;&#x306E;&#x30AD;&#x30E3;&#x30E9;&#x30AF;&#x30BF;&#x30FC;&#x304C;&#x5EA7;&#x3063;&#x3066;&#x5446;&#x7136;&#x3068;&#x3057;&#x3066;&#x3044;&#x308B;&#x30B7;&#x30FC;&#x30F3;&#x3002;&#x305D;&#x3053;&#x306B;&#x30AD;&#x30E3;&#x30D7;&#x30B7;&#x30E7;&#x30F3;&#x3067;&#x300C;A WORLD WITHOUT CI/CD&#x300D;&#x3068;&#x66F8;&#x3044;&#x3066;&#x3042;&#x308B;&#x3002;" width="768" height="433" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption><a href="https://flexagon.com/blog/why-the-world-needs-ci-cd/">Mallaidh Mleziva; Why the World Needs CI/CD </a> より</figcaption></figure></p> <p><strong>そして特に、CI を如何に高速にまわすかはめちゃんこ重要なワケです</strong>。</p> <p>……でも誰しもがこんな悩みを抱えたことがあるはずです👇</p> <ul> <li><strong>「え〜ん😭 CI がぜんぜん速くならないよお、、、」</strong></li> <li><strong>「遅いよお、、、」</strong></li> <li><strong>「どんだけがんばっても自動テストに3日かかっちゃうよお、、、」</strong></li> </ul> <p>「自動テストに3日かかっちゃうよお」はなかなか珍しいかもしれませんが、いずれにせよ<strong> CI の高速化はエンジニアを熱くする永遠のテーマの1つです</strong>(知らんけど。でもそうでしょう!?)。</p> <p>さて、ここに <strong>Nx Cloud という便利な道具があります</strong>。<br/> Nx Cloud とは、<strong>モノレポツール Nx と組み合わせて使用されることで CI を爆速にするクラウドサービスです。</strong></p> <pre class="code" data-lang="" data-unlink> _____ ./     /| | ̄ ̄ ̄ ̄ ̄| | &lt; 実際はソフトウェア、実体はコードでは? |Nx Cloud| | |_____|/ 本当の自分に気づき、韻を踏む Nx Cloud の図</pre> <p>👆なにこの図!?</p> <p>さて「Nx Cloud ?なんだいそれは?一体どんくらい速くなるってんだい??」という声が聞こえてきます。うんうん。<br/> なんと<strong>テックタッチでは Nx Cloud のおかげで CI の1回あたり実行時間が12分から7分になりました!</strong><br/> 厳密には Nx Cloud を入れたあとに Nx Cloud の効果を最大化するためのフローの組み直しやコンテナスペック調整など、色々な打ち手を実施しているので「入れただけ」ではありません👶<br/> ただ、御手製スクリプトによるテスト並列実行、Jest の CPU 使用数調整、CI コンテナのスペック調整などなどなど<strong>よく言われる打ち手はだいたい実施済みだったところから40%近い削減効果を捻出した</strong>のは CI ツールとして相当優秀なのではないかと思います。</p> <p>さあそこで本題です。<br/> この記事では<strong>前後編2回に分けて、Nx Cloud の主な機能の紹介と実際の導入の仕方を解説します</strong>!!<br/> 前編にあたる本稿では、Nx Cloud の2大最強ポイントである<strong>「CIの分散並列実行」</strong>と<strong>「CIのキャッシュ利用」</strong>を紹介します。</p> <p>というわけで、あたしゃこんな人に読んでいってほしいよ!</p> <ul> <li>Nx Cloud の使い方が分からない</li> <li>Nx の並列実行処理に興味がある</li> <li>CI が遅くて CI の並列実行処理を入れてみたい</li> <li>なんでもいいからマイナーなツールの話が知りたい</li> <li>他に読むものがない(👈そんな人いる?)</li> </ul> <h2 id="そもそも-Nx-ってなにちょっとだけ説明するよ">そもそも Nx ってなに?(ちょっとだけ説明するよ)</h2> <p>大前提として Nx Cloud は Nx というツールを使用しているレポジトリで使うものなのですが、その Nx についても一瞬だけ説明しておきます!!</p> <p>Nx とは、いわゆる「モノレポフレームワーク」です。<br/> 要するに、<strong>色々なアプリケーションを同じレポジトリで開発(=モノレポ開発)するときに使うと便利なアプリケーション</strong>👶</p> <p>モノレポ開発において便利な機能が色々そろっており、例えば下記のように色々な場面で Nx の機能が活躍します。</p> <ul> <li><p>あるアプリで使っているコンポーネントを他のアプリケーションでも使いたい!<br/> <strong>👉 Nx はコンポーネントの使い回しを念頭においたモノレポ開発が得意だよ</strong><br/> <a href="https://nx.dev/concepts/more-concepts/applications-and-libraries">Nx 公式ドキュメント; Applications and libraries</a></p></li> <li><p>あるアプリで使っているコンポーネントに変更を入れたけど、他のアプリに影響ないかな?<br/> <strong>👉 Nx の「変更検知機能(show --affected)」で分かるよ</strong><br/> <a href="https://nx.dev/packages/nx/documents/print-affected#print-affected">Nx 公式ドキュメント; print-affected</a></p></li> <li><p>これどこに依存しているの??<br/> <strong>👉 Nx の「依存グラフ描画機能(graph)」で分かるよ</strong><br/> <a href="https://www.youtube.com/watch?v=m2vagUiiArM&amp;ab_channel=Nx-Smart%2CFast%2CExtensible">Nx 公式動画; Nx Affected Graph</a></p></li> <li><p>変更があった部分だけテストしたい<br/> <strong>👉 Nx の「差分範囲のみコマンド実行機能(affected)」でできるよ</strong><br/> <a href="https://nx.dev/concepts/affected">Nx 公式ドキュメント; How Affected Works</a></p></li> <li><p>マイクロフロントエンドがやりたい<br/> <strong>👉 Nx はマイクロフロントエンドもいけるよ</strong><br/> <a href="https://nx.dev/concepts/more-concepts/micro-frontend-architecture">Nx 公式ドキュメント; Micro Frontend Architecture</a></p></li> </ul> <p>まあこんな感じで便利なワケ!</p> <h2 id="オーケーわかったで結局そのクールなソフトウェアは一体何をしてくれるって言うんだ">オーケー、わかった。で、結局そのクールなソフトウェアは一体何をしてくれるって言うんだ?</h2> <p>さてさて、ようやく Nx Cloud の話が始まります……<br/> Nx Cloud の機能は色々あるのかもしれませんが、目玉は要するに2つ。</p> <ul> <li>コマンドの自動分散並列実行</li> <li>コマンド実行結果のキャッシュ</li> </ul> <p>これを聞いて「ふ〜ん、興味ないわ」ってなった方(いまさらそんな人いる?)も、もうすこし読んでいってください!!<strong>こっからが面白いんです!!</strong></p> <p>順番に見ていきまっせ〜〜〜👶👶</p> <h3 id="1-コマンドの自動分散並列実行">1. コマンドの自動分散並列実行</h3> <p>これは「<strong>複数プロジェクトを対象にあるコマンドを実行したとき、自動で分散して並列実行してくれる</strong>」という機能です。</p> <p>これだけだとよくわからないと思うので、ちょっと具体例で考えてみましょう。<br/> 今回の話は<strong>モノレポが前提ですから、レポジトリに多くのプロジェクトが存在するとします</strong>。<br/> だから、例えばこんなディレクトリ構成だったりするわけです。映画館の席管理システム的な?</p> <pre class="code bash" data-lang="bash" data-unlink># サブディレクトリ1つ1つが Nx において「プロジェクト」と呼ばれる単位です apps/ booking/ check-in/ libs/ ticket/ api/ seatmap/ profile/</pre> <p>このときテストをapps/booking, libs/ticket, libs/profileで実行する場合、Nx は便利なのでこんな書き方ができます。</p> <pre class="code tsx" data-lang="tsx" data-unlink>nx test -t booking, ticket, profile</pre> <p>便利ですねえ〜!</p> <p>さて、これをリモート CI 環境で実行したとしましょう。<br/> もし<strong>何の工夫もなければ3つのプロジェクトのテストすべてが1台のマシンで順番に実行されます</strong>。<br/> しかし<strong>テストは並列で実施したいのが人の心であり世の常であります</strong>。実際、こういった3つのプロジェクトは並列にテストができるように作ってあることがほとんどでしょう。</p> <p>ここで、そのような並列実行をがんばって<strong>スクリプトを書いて実現する</strong>という手段もあります。<br/> ただ、忘れてはならないのは<strong>テストしたいプロジェクトは毎回毎回変わる</strong>ということです。<br/> 実際これはモノレポなんだから、せっかくなら「前回コミットと差分があったところとそれに依存する部分だけピックアップしてテストする」のがオイCやり方なわけで、<strong>テストしたいプロジェクトも、その個数も当然変わるわけです</strong>。<br/> また、<strong>工夫しないとログが散らかってしまったり</strong>、テストに限らず CI 全体まで考慮すると<strong>場合によっては順序依存を持つジョブ群があったり</strong>するでしょう。</p> <p>というわけで<strong>やっぱりスクリプトはちょっとがんばる必要が出てきます</strong>(テックタッチはがんばってやっていました👶)。</p> <p>そこで <strong>Nx Cloud が登場</strong>。</p> <pre class="code" data-lang="" data-unlink> _____ ./     /| | ̄ ̄ ̄ ̄ ̄| | &lt; 任せて下さい分散実行、かかってきなさい瞬間一掃 |Nx Cloud| | |_____|/ 上機嫌で韻を踏みながら並列実行する Nx Cloud の図</pre> <p>👆なにこの図!?</p> <p><strong>Nx Cloud は並列実行を、ジョブを自動で分散して実現してくれます</strong>。<br/> 具体的には、こちらが指定した数のマシンを立ち上げて、<strong>マシンごとの計算状況をモニタリングしながら、手が空いているマシンを見つけ次第タスクを入れてくれます</strong>。すごいねえ👶</p> <p>下記は公式による並列実行の解説の図ですが、こんな風に<strong>クラウド経由でマシン間の成果物共有も必要に応じて行われるのです</strong>(下図のケースでは、e2e には build の成果物が必要だということでマシン間で成果物が共有されている)!嬉しいですねえ👶<br/> なお、<strong>分散して実行されるのにログはちゃんと1つのコンテナに集約できますよ!</strong> <figure class="figure-image figure-image-fotolife" title="[Nx 公式ドキュメント; Distribute Task Execution (DTE)](https://nx.dev/core-features/distribute-task-execution#distribute-task-execution-(dte) より"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20230913/20230913215601.png" alt="build, test, lint, e2e&#x3068;&#x3044;&#x3046;4&#x3064;&#x306E;&#x30B8;&#x30E7;&#x30D6;&#x304C;Nx Cloud&#x3092;&#x901A;&#x3057;&#x3066;4&#x3064;&#x306E;&#x30DE;&#x30B7;&#x30F3;&#x306B;&#x5206;&#x6563;&#x3055;&#x308C;&#x4E26;&#x5217;&#x3067;&#x5B9F;&#x884C;&#x3055;&#x308C;&#x308B;&#x3053;&#x3068;&#x306E;&#x56F3;&#x89E3;&#x3002;&#x3042;&#x308B;&#x30DE;&#x30B7;&#x30F3;&#x3067;build&#x30B8;&#x30E7;&#x30D6;&#x306B;&#x3088;&#x308A;&#x4F5C;&#x3089;&#x308C;&#x305F;&#x6210;&#x679C;&#x7269;&#x304C;&#x3001;&#x4ED6;&#x30DE;&#x30B7;&#x30F3;&#x304C;e2e&#x30B8;&#x30E7;&#x30D6;&#x3092;&#x5B9F;&#x884C;&#x3059;&#x308B;&#x969B;&#x306B;&#x30AF;&#x30E9;&#x30A6;&#x30C9;&#x3092;&#x901A;&#x3058;&#x3066;&#x5171;&#x6709;&#x3055;&#x308C;&#x308B;&#x3053;&#x3068;&#x3082;&#x56F3;&#x89E3;&#x3055;&#x308C;&#x3066;&#x3044;&#x308B;" width="1200" height="432" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption><a href="https://nx.dev/core-features/distribute-task-execution#distribute-task-execution-(dte">Nx 公式ドキュメント; Distribute Task Execution (DTE)</a> より</figcaption></figure></p> <p>テックタッチは実際に CircleCI で利用していますが、下のような雰囲気です(initialize で CI の初期化を終えたあと、タスクを分散実行している)。<br/> Nx Cloud が立ち上げるマシンには好きな名前をつけられるので、それも楽しいヨ🌝 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20230913/20230913220515.png" alt="CircleCI&#x4E0A;&#x3067;Nx Cloud&#x3092;&#x4F7F;&#x3063;&#x305F;&#x969B;&#x306E;&#x5B9F;&#x969B;&#x306E;&#x753B;&#x9762;&#x3002;&#x5206;&#x6563;&#x5B9F;&#x884C;&#x3092;&#x62C5;&#x3046;&#x5404;&#x30DE;&#x30B7;&#x30F3;&#x306E;&#x540D;&#x524D;&#x304C;&#x8272;&#x3005;&#x306A;&#x8868;&#x60C5;&#x306E;&#x9854;&#x6587;&#x5B57;&#x306B;&#x306A;&#x3063;&#x3066;&#x3044;&#x308B;&#x3002;" width="1200" height="799" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3 id="2-コマンド実行結果のキャッシング">2. コマンド実行結果のキャッシング</h3> <p>さあ、2つめの目玉機能です。これは読んで字の如く。<br/> ビルド成果物、テスト結果(出力はもちろんカバレッジファイルも)、VRT で撮影したスクショデータ、型チェック結果などなど、<strong>コマンド実行の結果や成果物をキャッシュしてくれます</strong>!</p> <p>これも、もう少し具体的な流れを見てみましょう。再びこのディレクトリに戻ります。</p> <pre class="code tsx" data-lang="tsx" data-unlink>apps/ booking/ check-in/ libs/ ticket/ api/ seatmap/ profile/</pre> <p>これらが下記のような依存関係を持つとします。 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20230913/20230913225301.png" alt="&#x8A08;6&#x3064;&#x306E;&#x30D7;&#x30ED;&#x30B8;&#x30A7;&#x30AF;&#x30C8;&#x306E;&#x4F9D;&#x5B58;&#x95A2;&#x4FC2;&#x306E;&#x56F3;&#x3002;&#x30D7;&#x30ED;&#x30B8;&#x30A7;&#x30AF;&#x30C8;&#x540D;&#x306F;checkIn, ticket, api, booking, profile, searmap&#x306E;6&#x3064;&#x3002;checkIn&#x306F;ticket&#x3068;api&#x306B;&#x4F9D;&#x5B58;&#x3057;&#x3001;booking&#x306F;profile&#x3068;seatmap&#x3068;api&#x306B;&#x4F9D;&#x5B58;&#x3057;&#x3001;profile&#x306F;ticket&#x306B;&#x4F9D;&#x5B58;&#x3057;&#x3066;&#x3044;&#x308B;" width="1103" height="651" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>さて、あなたは Nx Cloud を導入したばかりの開発環境で、ticket プロジェクトに手を入れる作業をしているとします。<br/> そしていま<strong> Nx Cloud が導入されてから初めて自分のブランチでテストを実施しました</strong>。<br/> このとき Nx によれば main ブランチとの差分は ticket プロジェクトのみだったようで、<strong>ticket およびそれに依存するプロジェクトののべ4プロジェクトに対してテストが実行されました</strong>。</p> <ul> <li>ticket</li> <li>check-in</li> <li>profile</li> <li>booking</li> </ul> <p>このとき、<strong> Nx Cloud のキャッシュはまだ何もありませんから、4つのプロジェクトすべてのテストが順当に実行されます</strong>。</p> <p>では、その後 <strong>seatmap を変更して、もう一度テストを実行します</strong>!<br/> seatmapを変更したので、 <strong>main ブランチとの差分は ticket に加えて seatmap にも存在する</strong>ことになりますね。<br/> Nx は再び main ブランチとの差分が存在する場所およびそれに依存する部分をテストの実行対象とします。つまり、<strong>先ほどの4つのプロジェクトに seatmap が追加された、のべ5プロジェクトでテストが実行されます</strong>。</p> <ul> <li>ticket</li> <li>check-in</li> <li>profile</li> <li>booking</li> <li><strong>seatmap 👈 あたら©</strong></li> </ul> <p>さあ、ここでクイズです。このとき Nx Cloud さんはどうするでしょうか?</p> <ol type="a"> <li>seatmap だけテストをやって、残りはさっきの結果を持ってくる</li> <li>seatmap と booking だけテストをやって、残りはさっきの結果を持ってくる</li> <li>なんかもうワケわかんなくなってテストしない</li> <li>逆に全部テストする</li> </ol> <p>チクタクチクタク ……</p> <p>……</p> <p>さあ、そろそろよいでしょうか?</p> <p>……</p> <p>……</p> <p>正解は b の<strong>「seatmap と booking だけテストをやって、残りはさっきの結果を持ってくる」</strong>です!</p> <p>c と d は冗談なので置いておいて、ここでは a の<strong>「seatmap だけテストをやって、残りはさっきの結果を持ってくる」ではない</strong>のがポイントかと思います。<br/> なぜ新しく変更を加えた seatmap だけでなく booking のテストも実行されるのでしょうか?<br/> 先ほどの依存関係をもう一度見てみましょう。 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20230913/20230913225301.png" alt="&#x8A08;6&#x3064;&#x306E;&#x30D7;&#x30ED;&#x30B8;&#x30A7;&#x30AF;&#x30C8;&#x306E;&#x4F9D;&#x5B58;&#x95A2;&#x4FC2;&#x306E;&#x56F3;&#x3002;&#x30D7;&#x30ED;&#x30B8;&#x30A7;&#x30AF;&#x30C8;&#x540D;&#x306F;checkIn, ticket, api, booking, profile, searmap&#x306E;6&#x3064;&#x3002;checkIn&#x306F;ticket&#x3068;api&#x306B;&#x4F9D;&#x5B58;&#x3057;&#x3001;booking&#x306F;profile&#x3068;seatmap&#x3068;api&#x306B;&#x4F9D;&#x5B58;&#x3057;&#x3001;profile&#x306F;ticket&#x306B;&#x4F9D;&#x5B58;&#x3057;&#x3066;&#x3044;&#x308B;" width="1103" height="651" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><strong>ポイントは booking が seatmap に依存していること</strong>です!</p> <p>そうです、<strong>プロジェクト間の依存関係をもとに判断すれば seatmap に変更を加えた以上 booking もテストすべきなのです</strong>。seatmap に依存する booking のテスト結果が既にオシャカになっていることに Nx Cloud は気づいているのです。<br/> なんでも知っているんだねえ!?</p> <p>さて、具体例を通じてイメージをつかんで頂けましたか?👶<br/> 要するに「コマンド実行結果のキャッシュ」とは、あるプロジェクトに対してCIジョブが実行されたとき、<strong>そのプロジェクトおよびそのプロジェクトの依存先ともにソースコードが以前と同じなら以前の結果をノータイムで返してくれる</strong>し、逆に、<strong>依存先含めてどこかが変わっているなら、ちゃんと再実行されてキャッシュが更新される</strong>という話なのです🌝</p> <p>これだけでも十分に嬉しいのですが、まだもう2つ下記のうれしポイントがあります。<br/> 2とかナカナカ刺激的じゃないですか?👶</p> <ol> <li><strong>キャッシュは Nx のサーバーに勝手にいい感じに置いてくれます</strong>。自分たちのレポジトリで管理する必要も、キャッシュの置き場やディレクトリ構成に悩む必要も一切ないです!</li> <li><strong>キャッシュはローカルとリモート、両方の実行が出処になります</strong>。要するに、誰かが自分のマシンでローカルでテストを実行すれば――もちろんネット接続は必須ですが――その結果は他の誰かのローカルでのテスト実行時も、CircleCI のような CI でのテスト実行時もキャッシュとして参照されます!</li> </ol> <p>さて、ここまで話した上でもう一度書いておきます。</p> <ul> <li>VRT で撮影したスクショ</li> <li>ビルド成果物</li> <li>Jest によるテスト結果の出力、カバレッジファイル</li> <li>tsc による型チェック結果の出力</li> </ul> <p>Nx Cloud は、<strong>こういったものをすべてキャッシュして即座に返してくれます</strong>。<br/> 「ビルドが遅いなあ」とか「VRT のネックはスクショやな……」とか、そんなこと言っている場合じゃなくなるかもしれませんヨ!</p> <h2 id="えんもたけなわ">えんもたけなわ</h2> <p>さてさて、楽しい時間は一瞬で過ぎ去ってしまうものでありまして、前編はここでおしまいとなります😭<br/> 「こんな面白い、すごいツールがあるんだねえ!?」となった方も、「ふ〜ん」で終わってしまった方もぜひ後半戦を楽しみに待っておいて頂けると嬉しいです!<br/> 後編では――Nx をすでに導入している前提になるとは思いますが――<strong>この激ヤバツール Nx Cloud の導入方法とハマりどころ(テックタッチCIチームがハマったところ!!!)を解説しちゃうゾ☆</strong></p> <p>ではでは今日はここまでであります、またね〜👶👶</p> canalundayo Goコンパイラのお勉強(2)~高階関数のためのインライン展開とエスケープ解析~ hatenablog://entry/820878482963378509 2023-09-04T09:00:00+09:00 2023-12-15T09:37:16+09:00 「インライン展開」と「エスケープ解析」に関する、 Go コンパイラの最適化に関する理解が、高階関数を適切に利用する上でも重要であることを実験を通してみていきます。 <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/i/izzii/20230831/20230831171753.png" alt="" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <ul class="table-of-contents"> <li><a href="#はじめに">はじめに</a></li> <li><a href="#インライン展開">インライン展開</a><ul> <li><a href="#インライン展開とは">インライン展開とは</a></li> <li><a href="#高階関数のパフォーマンスが落ちる例">高階関数のパフォーマンスが落ちる例</a></li> <li><a href="#インライン展開の制約">インライン展開の制約</a></li> </ul> </li> <li><a href="#エスケープ解析">エスケープ解析</a><ul> <li><a href="#エスケープ解析とは">エスケープ解析とは</a></li> <li><a href="#高階関数のパフォーマンスが落ちる例-1">高階関数のパフォーマンスが落ちる例</a></li> </ul> </li> <li><a href="#さいごに">さいごに</a></li> <li><a href="#参考文献">参考文献</a></li> </ul> <h2 id="はじめに">はじめに</h2> <p>先々月にも <a href="https://tech.techtouch.jp/entry/go-compiler-study-optimization-undefined-behaviour">Go コンパイラの最適化に関するブログ記事</a>を書いたのですが、多くのブックマークを頂けて感謝です! SRE の izzii です。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fgolang%2Fgo%2Fwiki%2FCompilerOptimizations" title="CompilerOptimizations" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/golang/go/wiki/CompilerOptimizations">github.com</a></cite></p> <p>上のドキュメントを参考にしつつ Go コンパイラの最適化に関する記事を連載していきます。本記事は 2 本目です。</p> <ul> <li>[第 1 回] <a href="https://tech.techtouch.jp/entry/go-compiler-study-optimization-undefined-behaviour">Goコンパイラのお勉強(1)~ゼロ幅の型によるメモリ利用の最適化と未定義動作~</a></li> <li>[本記事] Goコンパイラのお勉強(2)~高階関数のためのインライン展開とエスケープ解析~</li> <li>[第 3 回] <a href="https://tech.techtouch.jp/entry/go-compiler-study-array-object-operation">Goコンパイラのお勉強(3)~配列の効率的な操作に関する最適化~</a></li> </ul> <p>文字数の関係でタイトルでは高階関数という言葉を使いましたが、私が言及したいのは 1.21.0 で追加された <a href="https://pkg.go.dev/slices">slices パッケージ</a> や、<a href="https://github.com/golang/go/issues/61898">こちらのプロポーザル</a>で盛り上がっている Map、Reduce、Filter といったイディオマティックな高階関数のことです。こうしたイディオムは便利である反面、 使い方によっては for 文よりもパフォーマンスが悪化します。本記事では「インライン展開」と「エスケープ解析」に関する、 Go コンパイラの最適化に関する理解が、高階関数を適切に利用する上でも重要であることを実験を通してみていきます。</p> <h2 id="インライン展開">インライン展開</h2> <h3 id="インライン展開とは">インライン展開とは</h3> <p>インライン展開とは関数の実装を呼び出し元に展開することで、関数呼び出しのオーバーヘッドを削減する手法です。 <a href="https://ja.wikipedia.org/wiki/インライン展開">&#x30A4;&#x30F3;&#x30E9;&#x30A4;&#x30F3;&#x5C55;&#x958B; - Wikipedia</a> に具体的な例が載っているので私の説明で分かりづらい場合はご覧ください。Go コンパイラはよしなに判断して関数をインライン展開してくれます。</p> <p>では以下のコードでインライン展開の効果を体験してみましょう。<code>NoInline</code> 関数は <code>//go:noline</code> ディレクティブによって、インライン展開が無効化されていることに注意です。</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synStatement">package</span> main <span class="synStatement">import</span> ( <span class="synConstant">&quot;fmt&quot;</span> <span class="synConstant">&quot;time&quot;</span> ) <span class="synStatement">func</span> YesInline() any { <span class="synStatement">return</span> <span class="synStatement">struct</span>{}{} } <span class="synComment">//go:noinline</span> <span class="synStatement">func</span> NoInline() any { <span class="synStatement">return</span> <span class="synStatement">struct</span>{}{} } <span class="synStatement">func</span> main() { <span class="synStatement">var</span> start time.Time <span class="synStatement">var</span> elapsed time.Duration     N := <span class="synType">int</span>(<span class="synConstant">10e8</span>) <span class="synComment">// インライン展開される場合</span> start = time.Now() <span class="synStatement">for</span> i := <span class="synConstant">0</span>; i &lt; N; i++ { YesInline() } elapsed = time.Since(start) fmt.Println(<span class="synConstant">&quot;YesInline() took&quot;</span>, elapsed) <span class="synComment">// インライン展開されない場合</span> start = time.Now() <span class="synStatement">for</span> i := <span class="synConstant">0</span>; i &lt; N; i++ { NoInline() } elapsed = time.Since(start) fmt.Println(<span class="synConstant">&quot;NoInline() took&quot;</span>, elapsed) } </pre> <p>まずはビルドオプション <code>-gcflags="-m -m"</code> をつけてビルド時の最適化の挙動を見てみます。</p> <pre class="code bash" data-lang="bash" data-unlink># go version go1.21.0 darwin/arm64 go build -gcflags=&#34;-m -m&#34; sample1.go ./sample.go:8:6: can inline YesInline with cost 3 as: func() any { return struct {}{} } ./sample.go:13:6: cannot inline NoInline: marked go:noinline ./sample.go:17:6: cannot inline main: function too complex: cost 507 exceeds budget 80 ./sample.go:25:12: inlining call to YesInline ./sample.go:28:13: inlining call to fmt.Println ./sample.go:36:13: inlining call to fmt.Println ... 略 ...</pre> <p><code>./sample.go:8:6: inlining call to YesInline...</code> とあるので <code>Yesinline</code> 関数がインライン展開されることがわかります。一方 <code>./sample.go:13:6: cannot inline NoInline: marked go:noinline</code> とあるので<code>Noinline</code> 関数はインライン展開されません。</p> <p>では実行してみましょう。</p> <pre class="code bash" data-lang="bash" data-unlink># go version go1.21.0 darwin/arm64 % go run sample.go YesInline() took 317.732291ms NoInline() took 2.073789375s</pre> <p>インライン展開が有効な <code>Yesinline</code> の方が高速です。一方、 <code>Noinline</code> は 10<sup>8</sup> 回も関数が呼び出されるため、遅くなっています。</p> <p>逆にインライン展開によるデメリットも存在します。たとえばコード内部の多くの場所で呼ばれる関数がインライン化されることで、バイナリが大きくなってしまうことなどが挙げられます。一般にはこうしたデメリットは意識することなく、Go コンパイラの判断にお任せで問題はないでしょう。</p> <h3 id="高階関数のパフォーマンスが落ちる例">高階関数のパフォーマンスが落ちる例</h3> <p>せっかくなので Go 1.21.0 で追加された <a href="https://pkg.go.dev/slices">slices パッケージ</a>で実験してみたいところですが、現在実装されている関数はすべて条件分岐を含むもので、 CPU の実行時最適化への影響によってインライン展開の効果がぼやけてしまいます。なので今回は <a href="https://github.com/samber/lo#reduce">lo ライブラリの Reduce </a>を使ってみます。</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synStatement">package</span> main <span class="synStatement">import</span> ( <span class="synConstant">&quot;fmt&quot;</span> <span class="synConstant">&quot;time&quot;</span> <span class="synConstant">&quot;github.com/samber/lo&quot;</span> ) <span class="synStatement">func</span> YesInline(a, b, _ <span class="synType">int</span>) <span class="synType">int</span> { <span class="synStatement">return</span> a + b } <span class="synComment">//go:noinline</span> <span class="synStatement">func</span> NoInline(a, b, _ <span class="synType">int</span>) <span class="synType">int</span> { <span class="synStatement">return</span> a + b } <span class="synStatement">func</span> main() { <span class="synStatement">var</span> start time.Time <span class="synStatement">var</span> elapsed time.Duration N := <span class="synType">int</span>(<span class="synConstant">10e8</span>) items := <span class="synStatement">make</span>([]<span class="synType">int</span>, N, N) <span class="synStatement">for</span> i := <span class="synConstant">0</span>; i &lt; N; i++ { items[i] = i } <span class="synComment">// for 文</span> start = time.Now() sum := <span class="synConstant">0</span> <span class="synStatement">for</span> _, item := <span class="synStatement">range</span> items { sum += item } elapsed = time.Since(start) fmt.Println(<span class="synConstant">&quot;for took&quot;</span>, elapsed) <span class="synComment">// インライン展開される場合</span> start = time.Now() _ = lo.Reduce(items, YesInline, <span class="synConstant">0</span>) elapsed = time.Since(start) fmt.Println(<span class="synConstant">&quot;YesInline() took&quot;</span>, elapsed) <span class="synComment">// インライン展開されない場合</span> start = time.Now() _ = lo.Reduce(items, NoInline, <span class="synConstant">0</span>) elapsed = time.Since(start) fmt.Println(<span class="synConstant">&quot;NoInline() took&quot;</span>, elapsed) } </pre> <p>実行結果は以下です。</p> <pre class="code bash" data-lang="bash" data-unlink># go version go1.21.0 darwin/arm64 % go run sample.go for took 312.769459ms YesInline() took 313.664083ms NoInline() took 6.693548083s</pre> <p>高階関数を使う場合でも、インライン展開されない場合はコレクションのサイズのぶんだけ関数呼び出しが発生します。ちなみに以下が lo の Reduce 実装です。slices パッケージの実装もほぼ一緒で、中身は for 文です。</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synComment">// https://github.com/samber/lo/blob/3ae362dbb60ff6a2a1cb226a35079ea182c5f7c2/slice.go#L67C3-L67C3</span> <span class="synComment">// Reduce reduces collection to a value which is the accumulated result of running each element in collection</span> <span class="synComment">// through accumulator, where each successive invocation is supplied the return value of the previous.</span> <span class="synComment">// Play: https://go.dev/play/p/R4UHXZNaaUG</span> <span class="synStatement">func</span> Reduce[T any, R any](collection []T, accumulator <span class="synType">func</span>(agg R, item T, index <span class="synType">int</span>) R, initial R) R { <span class="synStatement">for</span> i, item := <span class="synStatement">range</span> collection { initial = accumulator(initial, item, i) } <span class="synStatement">return</span> initial } </pre> <p>コンパイラがインライン展開を最適化してくれるからこそ、大抵の場合はオーバーヘッドを気にせず高階関数を使えますが、チューニングしたい時は for 文に書き換えた方が見通しがよいかもしれません。</p> <h3 id="インライン展開の制約">インライン展開の制約</h3> <p>すべての関数がインライン展開されるわけではありません。(先述しましたがインライン展開によるデメリットもあります) <a href="https://github.com/golang/go/wiki/CompilerOptimizations#function-inlining">CompilerOptimizations &middot; golang/go Wiki &middot; GitHub</a> によると以下のすべての条件を満たす場合にインライン展開されることになります。</p> <ol> <li><strong>関数が短くシンプルであること。</strong> 具体的には、関数が作成する AST(抽象構文木と呼ばれるプログラムの構造を表すデータ構造)のノード数が 80 以下であることです。</li> <li><strong>関数が複雑な要素を含まないこと。</strong> 関数がクロージャ、defer、recover、select などの複雑な要素を含んでいないことです。</li> <li><strong>関数に <code>go:noinline</code> ディレクティブがついていないこと。</strong> そのためのディレクティブですからね。</li> <li><strong>関数に <code>go:uintptrescapes</code> ディレクティブがついていないこと。</strong> 自分の Go 力では <a href="https://pkg.go.dev/cmd/compile">https://pkg.go.dev/cmd/compile</a> を、よりわかりやすく説明できません。低レベルのシステムコールの実装以外では使用が非推奨のようです。</li> <li><strong>関数が Body を持っていること。</strong> 逆に関数の中身が Go で実装されていない場合は Body を持たないことがあります。</li> <li><strong>その他。</strong> その他ってなんやねん!全部知りたい!と思われた方はこちらのコードを読んでみてもよいかもしれません。(私は途中で諦めました笑)<a href="https://github.com/golang/go/blob/go1.21.0/src/cmd/compile/internal/inline/inl.go">https://github.com/golang/go/blob/go1.21.0/src/cmd/compile/internal/inline/inl.go</a></li> </ol> <p>余談ですが上記に書かれない条件でいうと、例えば再帰関数はインライン展開されません。</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synStatement">package</span> main <span class="synComment">// 末尾再帰ですが普通の再帰でも結果は変わりません</span> <span class="synStatement">func</span> Fn(i <span class="synType">int</span>) <span class="synType">int</span> { <span class="synStatement">if</span> i == <span class="synConstant">0</span> { <span class="synStatement">return</span> <span class="synConstant">0</span> } <span class="synStatement">return</span> Fn(i - <span class="synConstant">1</span>) } <span class="synStatement">func</span> main() { Fn(<span class="synConstant">100</span>) } </pre> <pre class="code bash" data-lang="bash" data-unlink># go version go1.21.0 darwin/arm64 % go build -gcflags=&#34;-m -m&#34; sample.go ./sample.go:3:6: cannot inline Fn: recursive ./sample.go:10:6: can inline main with cost 60 as: func() { Fn(100) }</pre> <p>さらに余談ですが、相互再帰の場合は片方だけがインライン展開されることがあります。</p> <h2 id="エスケープ解析">エスケープ解析</h2> <h3 id="エスケープ解析とは">エスケープ解析とは</h3> <p>Go で「変数がエスケープする」とは変数へのポインタがスタック領域からヒープ領域に退避することを言います。エスケープする必要がなければエスケープしない方が変数のライフタイムを GC が管理する必要がないのでベターです。エスケープしない変数はスタックがポップされるタイミングで一緒に消せばよいからです。逆にある関数内で作られたポインタが関数を超えて利用されたりする場合は、エスケープする必要が出てきます。コンパイラによるエスケープ解析とは、変数をエスケープさせるかさせないかを判断することを言います。Go に限らなければエスケープ解析はもう少し広い意味を持ちます。(<a href="https://ja.wikipedia.org/wiki/%E3%82%A8%E3%82%B9%E3%82%B1%E3%83%BC%E3%83%97%E8%A7%A3%E6%9E%90">&#x30A8;&#x30B9;&#x30B1;&#x30FC;&#x30D7;&#x89E3;&#x6790; - Wikipedia</a>) Go のエスケープ解析は、多くの場面では解析を諦めて変数をエスケープしているとみなすとのことです。またエスケープ解析のルールは非常に複雑なため、詳細な情報を確認する場合は、<code>-m</code>オプションをつけてコンパイラの出力をチェックするよう助言がされています。</p> <blockquote><p>Gc compiler does global escape analysis across function and package boundaries. However, there are lots of cases where it gives up. For example, anything assigned to any kind of indirection (<code>*p = ...</code>) is considered escaped. Other things that can inhibit analysis are: function calls, package boundaries, slice literals, subslicing and indexing, etc. Full rules are too complex to describe, so check the <code>-m</code> output.</p></blockquote> <p>(<a href="https://github.com/golang/go/wiki/CompilerOptimizations#escape-analysis">https://github.com/golang/go/wiki/CompilerOptimizations#escape-analysis</a>)</p> <p>Go は変数管理を強力なガベージコレクションに任せることで、変数スコープについてプログラマが頑張る必要がないのが素敵な言語です。しかし変数管理に無頓着すぎると、悪いコードに気がつけません。まずは簡単な実験をしてみましょう。</p> <p><a href="https://qiita.com/kyfk/items/fc532d7dae29381e8186">Golang &#x30A8;&#x30B9;&#x30B1;&#x30FC;&#x30D7;&#x89E3;&#x6790; - Qiita</a> を参考にしています 。</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synStatement">package</span> main <span class="synStatement">import</span> ( <span class="synConstant">&quot;fmt&quot;</span> <span class="synConstant">&quot;time&quot;</span> ) <span class="synComment">//go:noinline</span> <span class="synStatement">func</span> YesEscape() *<span class="synType">int</span> { i := <span class="synConstant">1000</span> <span class="synComment">// 違いの見やすさのために変数 i としています</span> <span class="synStatement">return</span> &amp;i } <span class="synComment">//go:noinline</span> <span class="synStatement">func</span> NoEscape() <span class="synType">int</span> { j := <span class="synConstant">1000</span> <span class="synComment">// 違いの見やすさのために変数 j としています</span> <span class="synStatement">return</span> j } <span class="synStatement">func</span> main() { <span class="synStatement">var</span> start time.Time <span class="synStatement">var</span> elapsed time.Duration <span class="synComment">// for 文</span> start = time.Now() <span class="synStatement">for</span> i := <span class="synConstant">0</span>; i &lt; <span class="synConstant">10e8</span>; i++ { k := <span class="synConstant">1000</span> _ = k } elapsed = time.Since(start) fmt.Println(<span class="synConstant">&quot;for took&quot;</span>, elapsed) <span class="synComment">// エスケープされない場合</span> start = time.Now() <span class="synStatement">for</span> i := <span class="synConstant">0</span>; i &lt; <span class="synConstant">10e8</span>; i++ { _ = NoEscape() } elapsed = time.Since(start) fmt.Println(<span class="synConstant">&quot;NoEscape() took&quot;</span>, elapsed) <span class="synComment">// エスケープされる場合</span> start = time.Now() <span class="synStatement">for</span> i := <span class="synConstant">0</span>; i &lt; <span class="synConstant">10e8</span>; i++ { _ = YesEscape() } elapsed = time.Since(start) fmt.Println(<span class="synConstant">&quot;YesEscape() took&quot;</span>, elapsed) } </pre> <p>インライン展開が有効だとエスケープされなくなる場合があるため、 <code>//go:noinline</code> ディレクティブを使いつつ、最適化の挙動をみてみましょう。変数 <code>i</code> はエスケープされて、変数 <code>j</code> はエスケープされていないのが見て取れます。</p> <pre class="code bash" data-lang="bash" data-unlink># go version go1.21.0 darwin/arm64 % go build -gcflags=&#34;-m&#34; sample.go ./sample.go:31:13: inlining call to fmt.Println ./sample.go:39:13: inlining call to fmt.Println ./sample.go:47:13: inlining call to fmt.Println ./sample.go:10:2: moved to heap: i ... 略 ...</pre> <p>以下が実行結果です。 <code>for</code> と <code>NoEscape()</code> の実行時間の差はインライン展開による違い、 <code>NoEscape()</code> と <code>YesEscape()</code> の実行時間の差は変数のエスケープによる違いになります。</p> <pre class="code bash" data-lang="bash" data-unlink># go version go1.21.0 darwin/arm64 % go run sample1.go for            took 320.203792ms NoEscape() took 2.075623625s YesEscape() took 11.633245875s</pre> <h3 id="高階関数のパフォーマンスが落ちる例-1">高階関数のパフォーマンスが落ちる例</h3> <p>インライン展開と同様に lo ライブラリの Reduce 関数を用いて議論をします。</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synStatement">package</span> main <span class="synStatement">import</span> ( <span class="synConstant">&quot;fmt&quot;</span> <span class="synConstant">&quot;time&quot;</span> <span class="synConstant">&quot;github.com/samber/lo&quot;</span> ) <span class="synComment">//go:noinline</span> <span class="synStatement">func</span> YesEscape(ay *<span class="synType">int</span>, by <span class="synType">int</span>, _ <span class="synType">int</span>) *<span class="synType">int</span> { i := *ay + by <span class="synComment">// 違いの見やすさのために変数 i としています</span> <span class="synStatement">return</span> &amp;i } <span class="synComment">//go:noinline</span> <span class="synStatement">func</span> NoEscape(an, bn, _ <span class="synType">int</span>) <span class="synType">int</span> { j := an + bn <span class="synComment">// 違いの見やすさのために変数 j としています</span> <span class="synStatement">return</span> j } <span class="synStatement">func</span> main() { <span class="synStatement">var</span> start time.Time <span class="synStatement">var</span> elapsed time.Duration <span class="synStatement">var</span> sum <span class="synType">int</span> N := <span class="synType">int</span>(<span class="synConstant">10e8</span>) items := <span class="synStatement">make</span>([]<span class="synType">int</span>, N, N) <span class="synStatement">for</span> i := <span class="synConstant">0</span>; i &lt; N; i++ { items[i] = <span class="synConstant">1</span> } <span class="synComment">// for 文</span> sum = <span class="synConstant">0</span> start = time.Now() <span class="synStatement">for</span> _, v := <span class="synStatement">range</span> items { sum += v } elapsed = time.Since(start) fmt.Println(<span class="synConstant">&quot;for took&quot;</span>, elapsed) <span class="synComment">// エスケープされない場合</span> start = time.Now() _ = lo.Reduce(items, NoEscape, <span class="synConstant">0</span>) elapsed = time.Since(start) fmt.Println(<span class="synConstant">&quot;NoEscape() took&quot;</span>, elapsed) <span class="synComment">// エスケープされる場合</span> sum = <span class="synConstant">0</span> start = time.Now() _ = lo.Reduce(items, YesEscape, &amp;sum) elapsed = time.Since(start) fmt.Println(<span class="synConstant">&quot;YesEscape() took&quot;</span>, elapsed) } </pre> <p>実行結果は以下です。それぞれの実行時間の違いは前述の通りです。</p> <blockquote><p><code>for</code> と <code>NoEscape()</code> の実行時間の差はインライン展開による違い、 <code>NoEscape()</code> と <code>YesEscape()</code> の実行時間の差は変数のエスケープによる違いになります。</p></blockquote> <pre class="code bash" data-lang="bash" data-unlink># go version go1.21.0 darwin/arm64 % go run sample1.go for took 1.098197333s NoEscape() took 2.175875542s YesEscape() took 18.416683584s</pre> <p>高階関数を使う場合もエスケープが発生する場合はコレクションのサイズのぶんだけヒープ領域にメモリを確保することになってパフォーマンスが悪化します。多少であれば「Go のガベージコレクションを信じよう!」で切り抜けて良いと思います。ただし、高階関数に限らずサイズの大きなデータを対象としたプログラムを書く場合は気を付けましょう。ちなみに本気で変数のスコープやライフタイムが管理されたコードを書きたい、という場合は Go ではなく Rust など他の言語を使う方がよいでしょう。</p> <h2 id="さいごに">さいごに</h2> <p>インライン展開とエスケープ解析について、今後利用が拡大していく可能性のある Map, Reduce, Filter といった高階関数と絡めて調査してみました。本記事の内容を頭の片隅に置きつつ、便利な高階関数たちを適切に使っていきましょう。コンパイラにも限界があるので、for 文に直した方が早い!なんてことも往々にしてあるんじゃないかと思っています。以下でも Go コンパイラの最適化について記事にしているので興味があればご覧ください!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.techtouch.jp%2Fentry%2Fgo-compiler-study-optimization-undefined-behaviour" title="Goコンパイラのお勉強(1) ~ゼロ幅の型によるメモリ利用の最適化と未定義動作 ~ - Techtouch Developers Blog" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tech.techtouch.jp/entry/go-compiler-study-optimization-undefined-behaviour">tech.techtouch.jp</a></cite></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.techtouch.jp%2Fentry%2Fgo-compiler-study-array-object-operation" title="Goコンパイラのお勉強(3)~配列の効率的な操作に関する最適化~ - Techtouch Developers Blog" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tech.techtouch.jp/entry/go-compiler-study-array-object-operation">tech.techtouch.jp</a></cite></p> <h2 id="参考文献">参考文献</h2> <ul> <li><a href="https://github.com/golang/go/wiki/CompilerOptimizations">CompilerOptimizations &middot; golang/go Wiki &middot; GitHub</a></li> <li><a href="https://github.com/golang/go/issues/61898">https://github.com/golang/go/issues/61898</a></li> <li><a href="https://github.com/samber/lo">samber/lo</a></li> <li><a href="https://ja.wikipedia.org/wiki/%E3%82%A4%E3%83%B3%E3%83%A9%E3%82%A4%E3%83%B3%E5%B1%95%E9%96%8B">&#x30A4;&#x30F3;&#x30E9;&#x30A4;&#x30F3;&#x5C55;&#x958B; - Wikipedia</a></li> <li><a href="https://pkg.go.dev/cmd/compile">compile command - cmd/compile - Go Packages</a></li> <li><a href="https://ja.wikipedia.org/wiki/%E3%82%A8%E3%82%B9%E3%82%B1%E3%83%BC%E3%83%97%E8%A7%A3%E6%9E%90">&#x30A8;&#x30B9;&#x30B1;&#x30FC;&#x30D7;&#x89E3;&#x6790; - Wikipedia</a></li> <li><a href="https://qiita.com/kyfk/items/fc532d7dae29381e8186">Golang &#x30A8;&#x30B9;&#x30B1;&#x30FC;&#x30D7;&#x89E3;&#x6790; - Qiita</a></li> </ul> izzii セキュリティインシデント疑似体験調査ワークショップに参加すべき3つの理由 hatenablog://entry/820878482956584688 2023-08-17T09:00:00+09:00 2023-12-15T09:41:05+09:00 本記事では特に、このイベントに参加すべき3つの理由を私の視点で解説していきます! <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/i/izzii/20230808/20230808183211.png" width="1200" height="678" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="はじめに">はじめに</h2> <p>こんにちは。最近はテックタッチの同僚とボルダリング同好会のようなものを作ってワイワイしてます!SRE の izzii です。</p> <p>7月27日、社内の有志を集めて AWS ジャパン主催の<a href="https://pages.awscloud.com/security-gameday-reg.html">&#x30BB;&#x30AD;&#x30E5;&#x30EA;&#x30C6;&#x30A3;&#x30A4;&#x30F3;&#x30B7;&#x30C7;&#x30F3;&#x30C8;&#x7591;&#x4F3C;&#x4F53;&#x9A13; &#x8ABF;&#x67FB;&#x30EF;&#x30FC;&#x30AF;&#x30B7;&#x30E7;&#x30C3;&#x30D7;</a>に参加しました。このイベントは、AWS 環境上の典型的なセキュリティインシデントを再現したログを用いて、CTF (Capture The Flag、旗取りゲーム) 形式で AWS のセキュリティで気をつけるべきことを学べるイベントです。 テックタッチからは、izzii (SRE), roki (SRE), canalun (フロントエンド), kacchan (コーポレートセキュリティ) が参加し、その4名で構成されたチーム 「gokigen」 は<strong>約40チーム中で3位に入賞することができました!</strong>(記事のトップ画像はその時のキャプチャです ※AWS 様に掲載許可をいただいています。)予想外のことにはしゃいでいたところ、他の参加者の皆様から温かいツッコミをいただきました。</p> <p><figure class="figure-image figure-image-fotolife" title="チーム名が gokigen なのでこのような温かいツッコミが!"><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/izzii/20230808183526" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/i/izzii/20230808/20230808183526.png" width="740" height="1146" loading="lazy" title="" class="hatena-fotolife" style="width:400px" itemprop="image"></a></span><figcaption>チーム名が「gokigen」なのでこのような温かいツッコミが!</figcaption></figure></p> <p>ちなみに 「gokigen」 というチーム名は、<a href="https://speakerdeck.com/techtouch/we-are-hiring?slide=20">テックタッチのバリューである「いつでもごきげん」</a>が由来です。 このイベントは何度か開催しているらしいので、「AWS の上でサービス構築をされている方は機会があれば是非参加した方がいいですよ!」という AWS 様への感謝を込めたレコメンド記事です笑 本記事では特に、このイベントに参加すべき3つの理由を私の視点で解説していきます!</p> <h2 id="セキュリティインシデント疑似体験調査ワークショップに参加すべき3つの理由">セキュリティインシデント疑似体験調査ワークショップに参加すべき3つの理由</h2> <h3 id="CTF-形式で競技性があって楽しい">CTF 形式で競技性があって楽しい!</h3> <p>このイベントはただ擬似体験をするだけでなく、いわゆる Capture The Flag 形式となっているため、競技性もモチベーションになり集中して楽しく取り組むことができました。イベント中はリアルタイムにスコアや順位を見ることができ、参加しているチーム名によっては所属する組織が予想できたりしたため、「あの会社に勝ってる!負けてる!」なんてことも分かって燃えたりします笑</p> <h3 id="GUI-で完結するので敷居が低い">GUI で完結するので敷居が低い!</h3> <p>イベント中は AWS OpenSearch Service に収集された監査ログやアプリケーションログを用いて、 AWS で仮想的に発生したセキュリティインシデントと対峙することになります。初めて使うツールだったので最初は苦労しましたが、数十分もすれば困らないくらいには使いこなすことができました。事実、普段はプロダクトの開発業務に関わらない社内のITセキュリティ担当のメンバーも活躍し、点を稼いでおりました!ネタバレ禁止ということで当日の画面ではなく <a href="https://opensearch.org/docs/2.4/dashboards/quickstart-dashboards/">OpenSearch の公式ドキュメント</a>に載っている画面を転載します。なんとなくこんなツールなんだというのが伝われば!</p> <p><figure class="figure-image figure-image-fotolife" title="https://opensearch.org/docs/2.4/dashboards/quickstart-dashboards/"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/i/izzii/20230808/20230808184109.png" width="1200" height="428" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></figure></p> <h3 id="題材が具体的なシナリオなので学びが深い">題材が具体的なシナリオなので学びが深い!</h3> <p>イベント中は競技に熱中するので、自分たちのアクションを俯瞰視することはできませんでしたが、イベント終了後にインシデントシナリオの解説をいただけたことで、より深く自分達のアクションを振り返ったり、題材の意図などを理解することができました。AWS やミドルウェアの設定不備、脆弱性の放置によって起こるインシデントのシミュレーションを通して、</p> <ul> <li>AWS SecurityHub や Amazon Inspector などの CSPM や脆弱性管理ツール を利用した脅威の予防の大切さ</li> <li>監査ログやアプリケーションログを AWS OpenSearch Service などの SIEM に集めることで脅威の検知や分析ができることの大切さ</li> </ul> <p>を肌で感じられることが、今回参加したイベントの素晴らしさだと感じています。</p> <h2 id="おわりに">おわりに</h2> <p>セキュリティインシデント疑似体験 調査ワークショップは</p> <ul> <li>楽しくて</li> <li>参加の敷居が低くて</li> <li>深い学びのある</li> </ul> <p>素晴らしいプログラムでした!スタートアップのインフラ/セキュリティ担当者としての一意見ではございますが、私の本ブログが皆様の参加に向けた一押しになると嬉しい限りです!</p> izzii Goコンパイラのお勉強(1) ~ゼロ幅の型によるメモリ利用の最適化と未定義動作 ~ hatenablog://entry/820878482946788123 2023-07-13T09:00:00+09:00 2023-12-15T09:37:15+09:00 「interface 値にゼロ幅の型を入れた場合にメモリアローケーションを回避する」という Go コンパイラの最適化と、とある未定義動作について見てみます。 <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/i/izzii/20230904/20230904090607.png" alt="" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <ul class="table-of-contents"> <li><a href="#はじめに">はじめに</a></li> <li><a href="#メモリアロケーションの回避">メモリアロケーションの回避</a></li> <li><a href="#ゼロ幅の型ゼロサイズの型とは">ゼロ幅の型(=ゼロサイズの型)とは</a></li> <li><a href="#アドレスの同一性に関する未定義動作">アドレスの同一性に関する未定義動作</a></li> <li><a href="#この最適化が有効な場面">この最適化が有効な場面</a><ul> <li><a href="#1-map">1. map</a></li> <li><a href="#2-chan">2. chan</a></li> <li><a href="#3-interface-の実装">3. interface の実装</a></li> </ul> </li> <li><a href="#おわりに">おわりに</a></li> <li><a href="#参考文献">参考文献</a></li> </ul> <h2 id="はじめに">はじめに</h2> <p>こんにちは。SRE の izzii です。 最近は某フィットネスゲームが習慣だったり、ボルダリングを再開したり、登山シーズンが到来したりと心身ともに調子が良いです。</p> <p>さてつい先日、Go のコンパイラによる最適化について勉強したまとめを社内で共有したところ、メンバーが面白がってくれたため、細かいところを自分の手で触ってみたり、Go Forum で質問を投稿したりした上で記事にしてみました。Go コンパイラの最適化について知りたいという方だけでなく、Go に慣れた方でも意外と知らない挙動を垣間見ることのできる内容かと思います。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fgolang%2Fgo%2Fwiki%2FCompilerOptimizations" title="CompilerOptimizations" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/golang/go/wiki/CompilerOptimizations">github.com</a></cite></p> <p> を元にしているのですが、なかなかボリュームが大きくなりそうなので何個かの記事に分けます。本記事では「interface 値にゼロ幅の型を入れた場合にメモリアローケーションを回避する」という Go コンパイラの最適化と、とある未定義動作について見てみます。</p> <ul> <li>[本記事] Goコンパイラのお勉強(1)~ゼロ幅の型によるメモリ利用の最適化と未定義動作~</li> <li>[第 2 回] <a href="https://tech.techtouch.jp/entry/go-compiler-study-inline-escape-analysis">Goコンパイラのお勉強(2)~高階関数のためのインライン展開とエスケープ解析~</a></li> <li>[第 3 回] <a href="https://tech.techtouch.jp/entry/go-compiler-study-array-object-operation">Goコンパイラのお勉強(3)~配列の効率的な操作に関する最適化~</a></li> </ul> <h2 id="メモリアロケーションの回避">メモリアロケーションの回避</h2> <pre class="code" data-lang="" data-unlink>// https://github.com/golang/go/wiki/CompilerOptimizations Putting a zero-width type in an interface value doesn&#39;t allocate.</pre> <p>ここでいう interface 値とは Go 1.18+ では any 型の値と言い換えられます。ゼロ幅の型とは <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgo.dev%2Fref%2Fspec%23Size_and_alignment_guarantees" title="The Go Programming Language Specification - The Go Programming Language" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://go.dev/ref/spec#Size_and_alignment_guarantees">go.dev</a></cite></p> <p>で定義されたゼロサイズの型のことです。実験の後に詳しく説明します。まずは具体的なユースケースを見てみましょう。gopher のみなさんは以下のようなコードをどこかで見た or 使ったことがあるのではないでしょうか?</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synComment">// nums からユニークな値を uniqs のキーとして貯める処理</span> nums := []<span class="synType">int</span>{<span class="synConstant">3</span>,<span class="synConstant">4</span>,<span class="synConstant">5</span>,<span class="synConstant">13</span>,<span class="synConstant">20</span>,<span class="synConstant">1</span>,<span class="synConstant">20</span>,<span class="synConstant">4</span>} uniqs := <span class="synStatement">make</span>(<span class="synType">map</span>[<span class="synType">int</span>]<span class="synStatement">struct</span>{}) <span class="synComment">// 空の struct 型を利用することを宣言</span> <span class="synStatement">for</span> _, n := <span class="synStatement">range</span> nums { <span class="synStatement">if</span> _, ok := uniqs[n]; ok { <span class="synStatement">continue</span> } uniqs[n] = <span class="synStatement">struct</span>{}{} } </pre> <p>上の例では map の値として空の struct 型を利用することでメモリアロケーションを回避しています。</p> <p> 次に bool 型の配列と struct 型の配列を比較して実際にメモリアロケーションが回避されている様子を見てみましょう。map で比較しないのは、ハッシュ衝突を避けるためのメモリアロケーションが説明を面倒にしてしまう可能性があるためです。</p> <p>まずは空の struct の場合</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synComment">// go1.20.5 darwin/arm64</span> <span class="synStatement">const</span> M = <span class="synConstant">1024</span> * <span class="synConstant">1024</span> <span class="synStatement">func</span> printTotalAlloc() { <span class="synStatement">var</span> m runtime.MemStats runtime.ReadMemStats(&amp;m) fmt.Printf(<span class="synConstant">&quot;TotalAlloc = %v MiB</span><span class="synSpecial">\n</span><span class="synConstant">&quot;</span>, m.TotalAlloc/M) } <span class="synStatement">func</span> printCap[T any](a []T) { fmt.Printf(<span class="synConstant">&quot;Capacity of Slice = %v</span><span class="synSpecial">\n</span><span class="synConstant">&quot;</span>, <span class="synStatement">cap</span>(a)) } <span class="synStatement">func</span> printLen[T any](a []T) { fmt.Printf(<span class="synConstant">&quot;Length of Slice = %v</span><span class="synSpecial">\n</span><span class="synConstant">&quot;</span>, <span class="synStatement">len</span>(a)) } <span class="synStatement">func</span> printSize[T any](a []T) { fmt.Printf(<span class="synConstant">&quot;Size of Slice = %v</span><span class="synSpecial">\n</span><span class="synConstant">&quot;</span>, unsafe.Sizeof(a)) } <span class="synStatement">func</span> main() { a := <span class="synStatement">make</span>([]<span class="synStatement">struct</span>{}, M) printLen(a) <span class="synComment">// Length of Slice = 1048576</span> printCap(a) <span class="synComment">// Capacity of Slice = 1048576</span> printSize(a) <span class="synComment">// Size of Slice = 24</span> printTotalAlloc() <span class="synComment">// TotalAlloc = 0 MiB</span> } </pre> <p>次に bool 型の場合</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synComment">// go1.20.5 darwin/arm64</span> . . . <span class="synStatement">func</span> main() { a := <span class="synStatement">make</span>([]<span class="synType">bool</span>, M) printLen(a) <span class="synComment">// Length of Slice = 1048576</span> printCap(a) <span class="synComment">// Capacity of Slice = 1048576</span> printSize(a) <span class="synComment">// Size of Slice = 24</span> printTotalAlloc() <span class="synComment">// TotalAlloc = 1 MiB</span> } </pre> <p>ドンピシャな結果が出ました!</p> <p>bool 型を利用した場合、1 MiB つまり 1024 * 1024 Byte 分のメモリアロケーションが発生しているのに対して、空の struct 型を利用した場合には余計なメモリアローケーションがありません。(注:あえてオーバーヘッド的に発生する細かいアロケーションを無視するように丸めてしまっています。)slice 自体はポインタと長さと容量の情報を持った 24 バイトの構造体でありサイズを持ちます。しかし長さと容量に関わらず slice の参照する中身に関してはメモリアロケートされません。</p> <h2 id="ゼロ幅の型ゼロサイズの型とは">ゼロ幅の型(=ゼロサイズの型)とは</h2> <p>ゼロ幅の型とはゼロサイズの型と同一であると前述しました。ゼロサイズのデータとはゼロよりも大きなサイズのフィールドを持たない struct や配列だと書かれています。</p> <pre class="code" data-lang="" data-unlink>// https://go.dev/ref/spec#Size_and_alignment_guarantees A struct or array type has size zero if it contains no fields (or elements, respectively) that have a size greater than zero. Two distinct zero-size variables may have the same address in memory.</pre> <p>何個か簡単な例を使って実験してみましょう。</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synComment">// go1.20.5 darwin/arm64</span> <span class="synStatement">func</span> main() { <span class="synComment">// 空のstructはゼロサイズの型</span> a := <span class="synStatement">struct</span>{}{} fmt.Println(unsafe.Sizeof(a)) <span class="synComment">// 0</span> <span class="synComment">// 長さゼロの配列はゼロサイズの型</span> b := [<span class="synConstant">0</span>]<span class="synType">int</span>{} fmt.Println(unsafe.Sizeof(b)) <span class="synComment">// 0</span> <span class="synComment">// ゼロサイズの型を要素に持つ配列もゼロサイズの型</span> <span class="synStatement">var</span> c [<span class="synConstant">100</span>]<span class="synStatement">struct</span>{} <span class="synStatement">for</span> i := <span class="synConstant">0</span>; i &lt; <span class="synConstant">100</span>; i++ { c[i] = <span class="synStatement">struct</span>{}{} } fmt.Println(unsafe.Sizeof(c)) <span class="synComment">// 0</span> <span class="synComment">// bool型の配列はサイズを持つ型</span> <span class="synStatement">var</span> d [<span class="synConstant">100</span>]<span class="synType">bool</span> <span class="synStatement">for</span> i := <span class="synConstant">0</span>; i &lt; <span class="synConstant">100</span>; i++ { d[i] = <span class="synStatement">true</span> } fmt.Println(unsafe.Sizeof(d)) <span class="synComment">// 100</span> <span class="synComment">// slice はポインタと長さと容量を持つ構造体</span> <span class="synComment">// ゼロサイズの型に当てはまらない</span> e := <span class="synStatement">make</span>([]<span class="synStatement">struct</span>{}, <span class="synConstant">100</span>) <span class="synStatement">for</span> i := <span class="synConstant">0</span>; i &lt; <span class="synConstant">100</span>; i++ { e[i] = <span class="synStatement">struct</span>{}{} } fmt.Println(unsafe.Sizeof(e)) <span class="synComment">// 24</span> } </pre> <p><code>[100]struct{}</code> がゼロサイズの型ということは、それを要素にした slice を作っても内容に関してはメモリアロートされません。</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synComment">// go1.20.5 darwin/arm64</span> <span class="synStatement">func</span> main() { a := <span class="synStatement">make</span>([][<span class="synConstant">100</span>]<span class="synStatement">struct</span>{}, M) printLen(a) <span class="synComment">// Length of Slice = 1048576</span> printCap(a) <span class="synComment">// Capacity of Slice = 1048576</span> printSize(a) <span class="synComment">// Size of Slice = 24</span> printTotalAlloc() <span class="synComment">// TotalAlloc = 0 MiB</span> } </pre> <p>さらにいうと、空の struct と空の配列を組み合わせた型を作ってもそれはゼロ幅の型となります。</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synComment">// go1.20.5 darwin/arm64</span> <span class="synStatement">type</span> MyStruct0 <span class="synStatement">struct</span> { x [<span class="synConstant">0</span>]<span class="synType">int</span> } <span class="synStatement">type</span> MyStruct1 <span class="synStatement">struct</span> { x MyStruct0 y <span class="synStatement">struct</span>{} } <span class="synStatement">func</span> main() { a := MyStruct0{} fmt.Println(unsafe.Sizeof(a)) <span class="synComment">// 0</span> b := MyStruct1{ x: MyStruct0{}, y: <span class="synStatement">struct</span>{}{}, } fmt.Println(unsafe.Sizeof(b)) <span class="synComment">// 0</span> <span class="synStatement">var</span> c [<span class="synConstant">100</span>]MyStruct1 <span class="synStatement">for</span> i := <span class="synConstant">0</span>; i &lt; <span class="synConstant">100</span>; i++ { c[i] = MyStruct1{ x: MyStruct0{}, y: <span class="synStatement">struct</span>{}{}, } } fmt.Println(unsafe.Sizeof(c)) <span class="synComment">// 0</span> } </pre> <p>キモいですね。実用で出会うことはなさそうです。</p> <p>ゼロ幅の型というのは</p> <ol> <li>空の struct</li> <li>空の配列</li> <li>1 と 2 を要素に持つ配列や struct</li> </ol> <p>であることを実際に見てみました。3. のような複雑なゼロ幅の型が利用できる場面を見る機会はないとは思いますが(笑)ただし慣習的には空の struct 型が使われるので、それを利用する方が可読性の高いコードが書けることでしょう。</p> <h2 id="アドレスの同一性に関する未定義動作">アドレスの同一性に関する未定義動作</h2> <p>2014 時点での struct{} に関する記事</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdave.cheney.net%2F2014%2F03%2F25%2Fthe-empty-struct%23comment-2815" title="The empty struct | Dave Cheney" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://dave.cheney.net/2014/03/25/the-empty-struct#comment-2815">dave.cheney.net</a></cite></p> <p>に、以下のような実行例があるのですが</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synStatement">var</span> a, b <span class="synStatement">struct</span>{} fmt.Println(&amp;a == &amp;b) <span class="synComment">// true</span> </pre> <p>私の環境(Go 1.20.5 darwin/arm64)においては正しくありません。</p> <p>以下のようになります。</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synStatement">func</span> main() { <span class="synStatement">var</span> a, b <span class="synStatement">struct</span>{} fmt.Println(&amp;a == &amp;b) <span class="synComment">// false</span> } </pre> <pre class="code lang-go" data-lang="go" data-unlink><span class="synStatement">func</span> main() { <span class="synStatement">var</span> a, b <span class="synStatement">struct</span>{} fmt.Println(&amp;a == &amp;b) <span class="synComment">// true</span> fmt.Printf(<span class="synConstant">&quot;%p</span><span class="synSpecial">\n</span><span class="synConstant">&quot;</span>, &amp;a) <span class="synComment">// 0x1172f60 *depends on env</span> fmt.Printf(<span class="synConstant">&quot;%p</span><span class="synSpecial">\n</span><span class="synConstant">&quot;</span>, &amp;b) <span class="synComment">// 0x1172f60 *depends on env</span> } </pre> <p>キモいですね。(本日2回目)</p> <p>先ほどの2つそれぞれを <code>-gcflags="-m -N -l"</code>でビルドした時を比較してみると、「ポインタを必要としない限りはスタック上の別々のアドレスに変数が入る」のに対して、「ポインタが必要になるとヒープの同一のアドレスにエスケープされる」のだろうなということが予測できます。</p> <pre class="code bash" data-lang="bash" data-unlink>go build -gcflags=&#34;-m -N -l&#34; -o output input.go # command-line-arguments ./input.go:9:13: ... argument does not escape ./input.go:9:17: &amp;a == &amp;b escapes to heap</pre> <pre class="code bash" data-lang="bash" data-unlink>go build -gcflags=&#34;-m -N -l&#34; -o output input.go # command-line-arguments ./input.go:8:6: moved to heap: a ./input.go:8:9: moved to heap: b ./input.go:9:13: ... argument does not escape ./input.go:9:17: &amp;a == &amp;b escapes to heap ./input.go:10:12: ... argument does not escape ./input.go:11:12: ... argument does not escape</pre> <p>未定義動作かバグかわからなかったので Go Forum に報告してみることにしました。すると数時間で skillian 氏から回答をいただけました。Go のコミュニティあったけぇ。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fforum.golangbridge.org%2Ft%2Fintriguing-pointer-equality-behavior-on-zero-width-types%2F32245" title="Intriguing Pointer Equality Behavior on Zero-Width Types" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://forum.golangbridge.org/t/intriguing-pointer-equality-behavior-on-zero-width-types/32245">forum.golangbridge.org</a></cite></p> <p>言語仕様として以下の二文が書かれており、つまり上の挙動は明確に保証はされていませんでした。</p> <pre class="code bash" data-lang="bash" data-unlink>Pointers to distinct zero-size variables may or may not be equal. Two distinct zero-size variables may have the same address in memory.</pre> <p>ちなみに自分の環境ではゼロ幅の型を持つ変数のアドレスは、型に関係なく同一になりました。(もちろんこの動作は保証されているわけではないけれど!)</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synStatement">func</span> main() { <span class="synStatement">var</span> a <span class="synStatement">struct</span>{} <span class="synStatement">var</span> b [<span class="synConstant">0</span>]<span class="synType">int</span> <span class="synComment">// fmt.Println(&amp;a == &amp;b) // 型が違うので比較はできません</span> fmt.Printf(<span class="synConstant">&quot;%p</span><span class="synSpecial">\n</span><span class="synConstant">&quot;</span>, &amp;a) <span class="synComment">// 0x1172f60 *depends on env</span> fmt.Printf(<span class="synConstant">&quot;%p</span><span class="synSpecial">\n</span><span class="synConstant">&quot;</span>, &amp;b) <span class="synComment">// 0x1172f60 *depends on env</span> } </pre> <h2 id="この最適化が有効な場面">この最適化が有効な場面</h2> <p>代表的なユースケースは以下かと思われます。この辺りは色々なウェブ上の記事で言及があるかと思いますのであっさりと。</p> <h3 id="1-map">1. map</h3> <p>本記事の最初に挙げた例です。キーに関してフラグ管理したいだけであれば、 map の値を struct{} にすると良いでしょう。</p> <h3 id="2-chan">2. chan</h3> <p>チャネルが値を送信するのではなく、単にシグナルを送信するような場合に有効でしょう。 例)サブゴルーチンの終了シグナルを待つメインゴルーチン。</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synStatement">import</span> ( <span class="synConstant">&quot;fmt&quot;</span> <span class="synConstant">&quot;time&quot;</span> ) <span class="synStatement">func</span> main() { signal := <span class="synStatement">make</span>(<span class="synType">chan</span> <span class="synStatement">struct</span>{}) <span class="synStatement">go</span> <span class="synType">func</span>() { time.Sleep(<span class="synConstant">2</span> * time.Second) <span class="synStatement">close</span>(signal) }() fmt.Println(<span class="synConstant">&quot;Waiting for signal...&quot;</span>) &lt;-signal fmt.Println(<span class="synConstant">&quot;Received signal!&quot;</span>) } </pre> <h3 id="3-interface-の実装">3. interface の実装</h3> <p>状態を必要としない interface について、そのメソッド群を実装する型として空の struct 型を用いることができます。</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synStatement">type</span> MyInterface <span class="synStatement">interface</span> { MyMethod() } <span class="synStatement">type</span> MyEmptyStruct <span class="synStatement">struct</span>{} <span class="synStatement">func</span> (mes MyEmptyStruct) MyMethod() { fmt.Println(<span class="synConstant">&quot;Do something&quot;</span>) } <span class="synStatement">func</span> main() { <span class="synStatement">var</span> i MyInterface = MyEmptyStruct{} i.MyMethod() <span class="synComment">// Do something</span> } </pre> <h2 id="おわりに">おわりに</h2> <p>しばらく個人の勉強がてら Go のコンパイラによる最適化を実際に手を動かしながら体験しつつ、連載形式で記事化していこうかと思います。パッと読んでわかった気になっても、触ることで思いもよらない発見があったりして楽しいですね。そしてプログラムのキモい挙動を見つけるのは楽しいです。</p> <p>連載形式で別の記事も出していますので是非ご興味があれば見てみてください!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.techtouch.jp%2Fentry%2Fgo-compiler-study-inline-escape-analysis" title="Goコンパイラのお勉強(2)~高階関数のためのインライン展開とエスケープ解析~ - Techtouch Developers Blog" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tech.techtouch.jp/entry/go-compiler-study-inline-escape-analysis">tech.techtouch.jp</a></cite></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.techtouch.jp%2Fentry%2Fgo-compiler-study-array-object-operation" title="Goコンパイラのお勉強(3)~配列の効率的な操作に関する最適化~ - Techtouch Developers Blog" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tech.techtouch.jp/entry/go-compiler-study-array-object-operation">tech.techtouch.jp</a></cite></p> <h2 id="参考文献">参考文献</h2> <p><a href="https://github.com/golang/go/wiki/CompilerOptimizations">https://github.com/golang/go/wiki/CompilerOptimizations</a> <a href="https://go.dev/ref/spec#Size_and_alignment_guarantees">https://go.dev/ref/spec#Size_and_alignment_guarantees</a> <a href="https://dave.cheney.net/2014/03/25/the-empty-struct#comment-2815">https://dave.cheney.net/2014/03/25/the-empty-struct#comment-2815</a> <a href="https://forum.golangbridge.org/t/intriguing-pointer-equality-behavior-on-zero-width-types/32245">https://forum.golangbridge.org/t/intriguing-pointer-equality-behavior-on-zero-width-types/32245</a></p> izzii テックタッチの社内勉強会紹介 hatenablog://entry/4207112889982042270 2023-05-08T09:00:00+09:00 2023-12-15T09:42:00+09:00 テックタッチで行われているエンジニア社内勉強会について紹介しています。 <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20230501/20230501140129.png" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"><ul class="table-of-contents"> <li><a href="#はじめに">はじめに</a></li> <li><a href="#勉強会のアップデート">勉強会のアップデート</a></li> <li><a href="#最後に">最後に</a></li> </ul> <p>久しぶりのブログ投稿させていただきます misu です。書いたのは <a href="https://tech.techtouch.jp/entry/meilisearch-introduction">meilisearch</a> 以来でしたが、その間に v1 になったようで今後がますます楽しみですね!</p> <h2 id="はじめに">はじめに</h2> <p>テックタッチで行われているエンジニア社内勉強会について紹介したいと思います。 当初は Biz/Dev 関係なく行っていた <a href="https://en-jp.wantedly.com/companies/company_4388704/post_articles/200953">Spark Lunch</a> というものが勉強会として存在していました。しかし、嬉しいことに事業成長とともに人が増え、業務がより忙しくなってくるにつれて開催は滞り、不定期開催となってしまいました。 一方私はエンジニアとして普段の業務で得た知見や趣味で勉強してみたことについてアウトプットしあうことで、技術力を高めていきたいという気持ちがありました。それを 1on1 のときに話してみるとやってみようかということで以下のような形で勉強会がスタートしました。</p> <p><strong>目的</strong><br/> 技術的なトップラインをあげる。そのために業務時間を多少使って発表準備を行う。</p> <p><strong>発表方法</strong></p> <ul> <li>発表は基本デモ、ライブコーディング(例/AWS CDK を使ったアプリケーション開発を実演。デモではなくふつうにスライドもあり)</li> <li>毎週 1 人 30 分。当初は 6-7 人で順番に回していたので 1 - 2 ヶ月に一回自分の番が来る。</li> </ul> <p><strong>発表内容</strong></p> <ul> <li>最新技術のキャッチアップ(新しい言語、フレームワーク、ミドルウェア、サービスなどなど)</li> <li>既存技術の深堀(例/RDB のファイルシステム、DDD のヘキサゴンアーキテクチャの実装例)</li> <li>業務で得た知見の共有</li> </ul> <p>2021 年の 10 月からバックエンドエンジニアチームでスタートし、2023 年の 4 月(この記事執筆時)まで続けられています。 勉強会は tandem というコミュニケーションツール上で行われていて、興味があれば誰でも入ることができます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20230414/20230414215510.png" width="339" height="716" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>リスナー参加ができるためフロントエンドメンバーやエンジニアマネージャーの方が数人聞いていることが多いです。 この取り組みからフロントエンドチームでも勉強会が 2022 年の 3 月から始まっています(こちらは隔週)。 パワフルなメンバーはバックエンド・フロントエンドどちらの勉強会でも発表していて頼もしいです笑</p> <p>以下のグラフは、フロントエンド・バックエンドを合計した勉強会開催頻度を表しています。 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20230414/20230414215524.png" width="1046" height="446" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> 実際には毎週できないことのほうが多いですが続けられていることがわかると思います。</p> <h2 id="勉強会のアップデート">勉強会のアップデート</h2> <p>最近では以下のような課題感があり、方針のアップデートがありました。</p> <p><strong>課題</strong></p> <ul> <li>ネタが無いと辛い</li> <li>勉強会の資料を作るのが大変</li> <li>ハードルが上がり過ぎちゃう(ハードルを低くしたい)</li> <li>ホットなネタがある場合にやりたいタイミングを逃しちゃう可能性あり</li> <li>30 分という時間は継続しやすくてよいが、たまに延長してもう少し話したいときがある</li> </ul> <p><strong>アップデート</strong></p> <ul> <li>金曜日の 17 時から、木曜日の 18 時に変更した。(時間は 30 分枠から 1 時間枠にした) <ul> <li>18 時にすることで、後のミーティングを気にしなくてよくなる</li> <li>終わったら業務終了位の気持ちで参加できる</li> </ul> </li> <li>発表者をローテーションから、何か発表ネタある人あるいはありそうな人にお願いというようにした <ul> <li>ローテーションだとその時ホットな内容を話す機会が先延ばしになってしまうため</li> </ul> </li> <li>LT 枠を設けた <ul> <li>30 分の資料作りは大変なので小ネタでも話せる場を作って共有できるようする。資料用意がなくてもよい。</li> </ul> </li> </ul> <p><strong>効果</strong></p> <p>まだこの方針にして 2 回しか経っていませんが、時間に余裕ができたのはよかったと思っています。せっかく盛り上がっても時間切れで不完全燃焼になってしまうのはもったいないからです。 LT 枠では技術的なコミュニケーションができる場として有意義でした。自分も発表者で話してみたのですが、色々知見を持ったメンバーから意見をもらえたりするので簡易でも何かネタをもっていって話してみると楽しいなと思いました。他のメンバーもこんなテンプレートの用意があるけどどう?みたいなコードベースの話から盛り上がったので今後も続けていきたいです。</p> <h2 id="最後に">最後に</h2> <p>テックタッチではサービス開発の傍ら、エンジニア同士で勉強会を行なうことで業務で使った技術の共有や新しい技術の共有をしスキルアップを続けています。 セキュリティ、ネットワークやコンピュータサイエンスに詳しいメンバーもいるので色々な観点からのツッコミがもらえて面白いと思います。 チームで技術的な成長も続けサービスに活かし、チームもサービスもより大きくしていきたいです。 少しでも興味を持っていただけましたらカジュアル面談からでもお話しましょう!</p> techtouch 初のマネージャー選任は公募方式で! hatenablog://entry/4207112889947584619 2022-12-25T18:06:50+09:00 2023-12-15T09:38:25+09:00 「フラットな組織からマネージャーを設置するフォーマルな体制への変更」の過程で、マネージャーを公募方式で選任した話です。 <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20221225/20221225122316.png" alt="" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>テックタッチ CTO の Jun です。</p> <p>5 歳の息子はサンタさんに「シンカリオン Z のこまち」をお願いしていたようですが、妻が間違えて「シンカリオン エヴァ 02」を用意したらしく、当日の反応がどうなるのか期待と不安でいっぱいです。</p> <p><a href="https://qiita.com/advent-calendar/2022/techtouch">テックタッチアドベントカレンダー</a>の最終日の記事になります。 テックタッチ創業が 2018 年、数名の開発チームから、30 名を超えるプロダクトチームに成長してきた中で、いままで「あえて」運用していなかったマネージャー制度に移行し、公募という形でマネージャーを選任したことを書きたいと思います。</p> <ul class="table-of-contents"> <li><a href="#話のきっかけ">話のきっかけ</a></li> <li><a href="#マネージャー選考フロー">マネージャー選考フロー</a><ul> <li><a href="#公募方式">公募方式</a></li> <li><a href="#選考基準と選考">選考基準と選考</a></li> <li><a href="#選考方法">選考方法</a></li> <li><a href="#結果">結果</a></li> <li><a href="#リーダーシップ研修へ">リーダーシップ研修へ</a></li> </ul> </li> <li><a href="#振り返ってみて">振り返ってみて</a></li> </ul> <h2 id="話のきっかけ">話のきっかけ</h2> <p>テックタッチのプロダクトチームでは、創業から今まで、マネージャー職の運用はしてきませんでした。 これは、職場環境の風通しの良さを底上げするために「上下のないフラットな組織づくり」を目指したのと、マネージャーを生むことで各自の自立した行動が抑制されることを危惧した背景があります。特に後者は、事業立ち上げフェーズにおいてクリティカルになってくるという考えを持っていました。</p> <p>目指すは「フラットな組織」ということで、ティール組織やホラクラシーを参考に、これまでオリジナリティあふれる組織運用を行ってきました。</p> <p>しかし、社内から集めたエンゲージメントサーベイの結果や、退職者からの FB を元に、<strong>組織運用がうまくできていないと考えられる点がいくつか出てきた</strong>為、社内の有志でタスクフォースを組み、課題の整理と改善策の検討を行いました。</p> <blockquote><p>☝ テックタッチでは組織上の課題がある場合、誰でも課題提起でき、周囲の賛同者とともにタスクフォースを組んで解消に動くというカルチャーがあります。社内ではワーキングチームと呼ばれている制度です。</p></blockquote> <p>今回もこの制度を利用し、社内から 8 名の賛同者を得て、ワーキングチームとして課題の特定を進めました。 この WT の話は、<a href="https://tech.techtouch.jp/entry/2022/12/19/194959">canalun が記事</a>にしてくれています。 すごく長いですが、是非お読みください。</p> <p>このワーキングチームで出した結論の一つに、<strong>「フラットな組織からマネージャーを設置するフォーマルな体制への変更」</strong>があります。 こちらの記事では、テックタッチのプロダクトチームで初となるマネージャーをどのように選考したのかを書いていきたいと思います。</p> <h2 id="マネージャー選考フロー">マネージャー選考フロー</h2> <h3 id="公募方式">公募方式</h3> <p>企業によってさまざまなマネージャー選考フローがあると思いますが、テックタッチでは公募方式で進めることにしました。 これは、マネージャーのような役割は、<strong>自らの意志や判断に基づいて、自らの責任のもとで行動しようとする性質(主体性)</strong>が必要になり、主体性は<strong>自分で選択し決断する</strong>ことで高められると考えられるためです。 もちろん、テックタッチは自ら手を上げチャレンジすることを称賛するカルチャーもありますので、その意味でも公募方式はカルチャーを体現できている選考フローかもしれません。</p> <p>公募開始にあたり、マネージャーの具体的な役割、責任、権限をしっかり定義し、全メンバーに公開しました。 テックタッチは 100%中途採用になります。それぞれが前職でマネージャーという役割を見てきており、それ故にそれぞれが違った形のマネージャー像をもっています。ここを言語化することが新設されるマネージャーの期待値をメンバー全員で統一し、「フラット to フォーマル」への組織体制の移行をスムーズにしてくれると考えました。</p> <p>また、今回マネージャーとなった場合でも昇格ではなく、スペシャリストからマネージャーへの役割の移行という事にしました。 スペシャリスト気質の多いエンジニア集団の中でマネージャーになりたいと思ってくれるほうがどの程度応募してくれるのかはすごく不安でしたが、6 名もの方がマネージャーに立候補してくれました。 一緒に組織運営を行ってくれるという意思ですので、とても嬉しい限りです。</p> <p>また、今回は自薦だけでなく、メンバーからの他薦も受け付けるようにしました。 過去にマネージャー経験がないほうは、自分がその役割を担えるのか不安視されているほうも多く、他薦を設けることで周囲の方から後押しをもらい、自身の新しいチャレンジのきっかけにになればと思っています。実際に 2 名の方が、他薦からのノミネートという形になりました。 もちろん他薦を受けたほうにも最終の意思(<strong>自分で選択し決断したという事</strong>)を確認しています。</p> <p>自薦 6 名 &amp; 他薦 2 名 の計 8 名の選考がスタートです。</p> <h3 id="選考基準と選考">選考基準と選考</h3> <p>次は選考基準についてです。 今回の選考基準を作るうえで、<strong>マネージャーの役割を担う時点で必要なマスト要件</strong>と、<strong>マネージャーになってから身に着けていけばよいウォント要件</strong>を分けて考え、前者のみを選考観点とすることにしました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20221222/20221222224034.png" width="1200" height="669" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><strong>マネージャーの役割を担う時点で必ず必要なマスト要件としては以下のものを設定しました。</strong></p> <ul> <li>人から信頼され、正しくポジティブなコミュニケーションができるか <ul> <li>言うことと行動に矛盾がない</li> <li>偏らず、適切・健全な考え方</li> <li>実務能力・業務に必要な幅広い知識</li> <li>言語化スキル・想像力</li> </ul> </li> <li>明確なビジョンを持ち、効率よく実行できるか <ul> <li>戦略的思考力・分析力</li> <li>課題設定力・実行力</li> </ul> </li> </ul> <h3 id="選考方法">選考方法</h3> <p>次に選考方法です。選考は一次と二次に分けて実施しました。</p> <p>一次は「人から信頼され、正しくポジティブなコミュニケーションができるか」という観点を軸に置いた選考基準となっております。 こちらを適切に評価するため、候補者の方と日ごろ業務を行っているメンバーから、360 度 FB という形でいくつかのアンケートに答えてもらいました。 いただいたメンバーからのアンケート結果を読み込んだ後、僕(CTO)と井無田(CEO)でインタビュー形式で実施しました。</p> <p>二次は「明確なビジョンを持ち、効率よく実行できるか」という観点で実施しました。 一次とは違い、より能力面にフォーカスがあたっています。 ここは一緒に働いた経験が無くても見極めやすいという側面もあり、第三者的な立場の方の意見をもらうことで納得感も出せると考え、経験豊富で信頼のおける外部の人事アドバイザーの方と僕(CTO)で実施しました。 候補者の方からすると、初めましての場で選考がスタートするという、入社前の面接に近い体験をしてもらったと思います。</p> <p>選考官は合否を決めることも一つのゴールなのですが、もし今回は見送りとなったときにどういった点を延ばしてほしいか、また、マネージャーではなく別の役割を担うほうがその方の強みを生かせると考えたのであれば、今後のキャリアプランを一緒に考え、提案できるようにしたいと思っています。 そのため、インタビューの内容を録画をさせてもらい、翌日以降に何度も聞きなおし、言葉一つ一つの理解に努めました。</p> <h3 id="結果">結果</h3> <p>当初から選考基準をしっかり定めていたため、合否判断はスムーズに進むと思っておりましたが、そんなことはもちろんなく、すごく迷う部分もありました。 マネージャーは、マネージメントされる方への影響も大きいため、マネージャーの役割を担う時点で必要なマスト要件を評価すると決めており、募集枠を埋めるような相対的な評価ではなく、選考基準を厳格に運用した絶対評価で行なうことを強く意識しました。 「候補者 + 選考官 VS 選考基準」という構図になります。</p> <p>結果として、8 名の候補者の方から、3名の方にマネージャーをお願いすることとなりました。</p> <h3 id="リーダーシップ研修へ">リーダーシップ研修へ</h3> <p>テックタッチに限らず、マネジメントに必要なことを体系的に学んだことがなく、自己流で奮闘されているマネージャーの方が多くいらっしゃると思います。(僕調べ) 特にスタートアップをキャリアの中心の進められているほうだと、社内教育環境は整っていないことが多く、そのような機会を得るのは難しいと思います。 そんな状態で進めていると、マネージメントをする側も、される側もつらい状況になる可能性が高くなります。今回のマネージャー選考フローは、このリーダーシップ研修の受講もセットで設計しました。 幸いテックタッチでは、信頼できる人事領域のスペシャリストの方にアドバイザーとなってもらうことができ(上記の二次面接の方)、質の高い研修をお願いできることになりました。 ですので、今回マネージャーとなったほうは必須で、見送りとなったほうも任意でリーダーシップ研修を受けていただきました。(結果、全員参加でした。)</p> <h2 id="振り返ってみて">振り返ってみて</h2> <p>こうして、ワーキングチーム発足から数か月をかけ、マネージャーのいない組織からマネージャーのいる組織に変わることができました。 マネージャー発足から 3 ヶ月程度しかたっていないため、結果の振り返りにはまだ早いかもしれませんが、エンゲージメントサーベイの結果は改善傾向にあります。 課題が 0 になることは無いのですが、チーム内の課題に向き合う中心人物が明確になり、それぞれのチームで課題をテーブルに上げ議論し、改善策を実行する土壌はできたのかなと思っています。 マネージャーになられたほうも、新しいキャリアを楽しんでもいるようで、心強く思っています。</p> <p>また、自分自身の変化もありました。 選考プロセスを通して、マネージャーとして適任かどうかはもちろんなのですが、その人の強み弱み、どういったキャリアがもっとも向いているのだろうかを僕なりに本気で考えました。 不思議なことに、考えれば考えるほど、その人の人間的な魅力も再認識できました。 そんな中で、自分も本心で、ポジティブな FB に限らずネガティブな FB をしっかり伝えたいと思えるようになり、またこういった姿勢が伝わったのか、すべての方がしっかり僕らの FB に耳を傾けてくれたと思っています。 そして、マネージャー選考に入ってもらった全員の方に、事後に FB をもらいましたが、総じて良い体験だったという言葉をいただいており、それが何よりうれしかったです。</p> <p>今年も無事乗り切ることができ、来年もっと飛躍するモチベーションを持って年末を迎えられることを嬉しく思っています。 メリークリスマス!</p> techtouch 優れたPMになるために必要な「プロダクトセンス」と「システム化とアドリブの使い分け」 hatenablog://entry/4207112889951759843 2022-12-24T09:25:28+09:00 2023-12-15T09:38:56+09:00 PM1年生の目線から優れたPMになるために必要な能力について執筆しました。 <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20230107/20230107003827.png" alt="" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>アドベントカレンダー24日目を担当するShuです。<br> プロダクトマネージャーの皆さん、普段利用する道具を自らでデザインしていますか? Uenoさんの<a href="https://twitter.com/manabuueno/status/1341774692605075462?ref_src=twsrc^tfw">ツイート</a>から引用させていただきますが、「Use to make」することは知的生産することと同等に重要かつ魅力的な行為であり、プロダクトマネージャーとして必要な感性を磨いてくれる!と私は考えています。 <blockquote data-conversation="none" class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">道具をデザインすることには際限のない魅力がある。それは道具が本有する再帰性への展望的な投機だ。道具の中には、使うことと作ることが、同時的に、生き生きと連関している。その永遠なるランループに、私たちは耽溺してしまうのである。 <a href="https://t.co/OsaSUZnMui">pic.twitter.com/OsaSUZnMui</a></p>&mdash; Manabu Ueno (@manabuueno) <a href="https://twitter.com/manabuueno/status/1341774692605075462?ref_src=twsrc%5Etfw">2020年12月23日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> そんな私は最近社内の人に教えてもらった「<a href="https://scrapbox.io/shu-san/Raycast%E3%81%A8%E3%81%84%E3%81%86%E7%A5%9E%E3%83%84%E3%83%BC%E3%83%AB">Raycast</a>(ランチャーツール)」を拡張して、Scrapboxの記事をRaycastから検索できるようにしました。Raycastの機能そのものにも感動したのですが、ユーザー自身が好きに”デザイン”できるようになっている設計にとても魅力を感じましたので、是非興味ある人は利用してみてください!(OSS最高🎉)<br></p> <p>関係ない前置きが長くなってしまいましたが、これより本編です。</p> <ul class="table-of-contents"> <li><a href="#はじめに">はじめに</a></li> <li><a href="#プロダクトセンス">プロダクトセンス</a><ul> <li><a href="#なぜ重要なのか">なぜ重要なのか?</a></li> <li><a href="#発揮される場面">発揮される場面</a></li> </ul> </li> <li><a href="#システム化とアドリブの使い分け">システム化とアドリブの使い分け</a><ul> <li><a href="#なぜ重要なのか-1">なぜ重要なのか?</a></li> <li><a href="#発揮される場面-1">発揮される場面</a></li> </ul> </li> <li><a href="#おわりに">おわりに</a></li> </ul> <h2 id="はじめに">はじめに</h2> <p>本日お話したいことは、プロダクトマネージャーとして重要な「プロダクトセンス」と「システム化とアドリブの使い分け」についてです。 1年前の2021年12月にプロダクトマネージャー(以下、PM)にジョブチェンジし、どうすればPMとして活躍できるか?どういった能力が重要になるか?を考え続けました。勿論、土台となる<a href="https://scrapbox.io/shu-san/%E3%83%97%E3%83%AD%E3%83%80%E3%82%AF%E3%83%88%E3%83%9E%E3%83%8D%E3%83%BC%E3%82%B8%E3%83%A3%E3%83%BC%E3%81%AE%E3%83%91%E3%83%83%E3%82%B7%E3%83%96%E3%82%B9%E3%82%AD%E3%83%AB">パッシブスキル</a>の高さや経験の豊富さが重要ですが、活躍されているプロダクトマネージャーを見ていると上記2つがとても重要であることに気づきました。 そのため今回は、「プロダクトセンス」と「システム化とアドリブの使い分け」の重要性について私なりの見解をお話していきたいと思います。</p> <h2 id="プロダクトセンス">プロダクトセンス</h2> <p>私が考えるプロダクトセンスとは、<strong>「不確実性が高い中で筋のよい仮説を立てられる力」</strong>です。「筋のよい仮説」とは「真の課題や論点に当たりをつけられている仮説」を指します。そのため、プロダクトセンスは仮説思考力の発展的なもので、先天的な才能ではなく後天的に向上させることができる能力であると考えています。</p> <h3 id="なぜ重要なのか">なぜ重要なのか?</h3> <p>理由は以下2つです。</p> <ol> <li>限られた時間の中で本当に解かなければいけない課題を見つけ出すため</li> <li>不必要な開発や手戻りを減らすため</li> </ol> <p>反対にプロダクトセンスがなければ、しらみつぶし的なやり方で課題を洗い出すため、それなりの時間を要してしまい、素早い検証と意思決定を行うことができません。そのため、プロダクトセンスはPMが身につけるべき重要な能力の1つだと言えます。<br> ただ前提として、不確実性は定性・定量の情報を用いて可能な限り下げることが望ましいですし、”当たりをつける”といっても非論理的なアプローチが許されるわけではありません。また、課題の内容やリスクがある要件においては慎重に網羅的なやり方が好ましい場合もあります。</p> <h3 id="発揮される場面">発揮される場面</h3> <p>ユーザーの課題が複雑かつ複数の解決策があるケースにおいてとても有効的に発揮されます。 皆さんもご経験があるとおもいますが、課題が複雑であればあるほど機能仕様も複雑になったり、開発スコープが広がってしまいがちです。しかし、プロダクトセンスがあればmust haveの機能仕様に絞り込むことができたり、本用途とは異なりますが既存機能で解決できたりすることがあります(このことをテックタッチでは、”<u>生活の知恵</u>”と呼んでいます)。</p> <h2 id="システム化とアドリブの使い分け">システム化とアドリブの使い分け</h2> <p>次に、「システム化とアドリブ」についてお話します。 「システム化とアドリブ」という概念については深津さんの「<a href="https://note.com/fladdict/n/n2a9fa2ffd2a8">図解 システム化とアドリブのよい使い分けとは?</a>」という記事でかなりわかりやすくご解説頂いています。<br>(以降、お読み頂いた前提で話を進めます) <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fnote.com%2Ffladdict%2Fn%2Fn2a9fa2ffd2a8" title="図解 システム化とアドリブのよい使い分けとは?|深津 貴之 (fladdict)|note" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://note.com/fladdict/n/n2a9fa2ffd2a8">note.com</a></cite></p> <h3 id="なぜ重要なのか-1">なぜ重要なのか?</h3> <p>そもそもプロダクトマネージャーの役割は「<strong>プロダクトの成長に責任をもつこと</strong>」であり、成長できるようなシステム設計を考えなければいけません。しかし、世の中のシステムの大半はユーザーの利用方法・場面・環境が多種多様で凹凸になっており、そんな中でプロダクトを成長させるためには絶対に「システム化とアドリブの使い分け」が必要です。この能力が欠けていると高度なソリューションを用いてもPdMの役割である「プロダクトの成長に責任を持つ」を達成できなくなります。</p> <h3 id="発揮される場面-1">発揮される場面</h3> <p>事業形態がToC・ToBに関わらず、仕様書の策定や開発へ踏み切る際に発揮されます。 課題がある程度見えてきた段階ですぐにソリューション化するのではなく、まずはオペレーション上で試してみたり、プロトタイプをお客さんへ当てることで勝ちパターンを探るようにします。そして、勝ちパターンが見えてきたらプロダクトとして開発する(システム化)、特定ユーザーだけのニーズであればプロダクトとしては開発せずオペレーションで対応する(アドリブ)など、適切な選択肢を取ることが可能になります。 まさにこれが深津さんが提唱している以下のアプローチであり、活躍されているプロダクトマネージャーはこれらを駆使してサービス設計をされています。 <figure class="figure-image figure-image-fotolife" title="(画像出典:深津 貴之 (fladdict) 「図解 システム化とアドリブのよい使い分けとは?)"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20230107/20230107001256.png" width="620" height="351" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>(画像出典:深津 貴之 (fladdict) 「図解 システム化とアドリブのよい使い分けとは?)</figcaption></figure> <figure class="figure-image figure-image-fotolife" title="(画像出典:深津 貴之 (fladdict) 「図解 システム化とアドリブのよい使い分けとは?)"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20230107/20230107001431.png" width="620" height="351" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>(画像出典:深津 貴之 (fladdict) 「図解 システム化とアドリブのよい使い分けとは?)</figcaption></figure></p> <h2 id="おわりに">おわりに</h2> <p>本日お話した「プロダクトセンス」と「システム化とアドリブの使い分け」は一長一短で身につくものではなく、土台となるパッシブスキルが磨かれてこそだと思います。私自身もまだまだ発揮できるものになっていないため、いち早く自分のものにしてプロダクトの成長に貢献できるプロダクトマネージャーになりたいと思います。<br> p.s 来年は投稿間に合わせられるようにします orz</p> techtouch いい感じの typescript-eslint ルールを探してみた hatenablog://entry/4207112889946921758 2022-12-23T12:00:00+09:00 2023-03-08T10:35:30+09:00 テックタッチアドベントカレンダー23日目を担当するフロントエンドエンジニアの ozaan です。 最近ハマっているものは、カルディなどで売られているホーマーロイヤルミルクティベース無糖です。よろしくお願いします。 弊社では、フロントエンド領域の技術スタックとして TypeScript, React, Nx, etc. を採用しており、JavaScript の Linter としてデファクトスタンダード(2022年現在)である ESLint も導入しています。 当記事では、ESLint が TypeScript 固有の構文などを解析可能になるプラグインである typescript-eslint … <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20221222/20221222141543.png" alt="" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><a href="https://qiita.com/advent-calendar/2022/techtouch">テックタッチアドベントカレンダー</a>23日目を担当するフロントエンドエンジニアの ozaan です。<br/> 最近ハマっているものは、カルディなどで売られているホーマーロイヤルミルクティベース無糖です。よろしくお願いします。</p> <hr /> <p>弊社では、フロントエンド領域の技術スタックとして TypeScript, React, Nx, etc. を採用しており、JavaScript の Linter としてデファクトスタンダード(2022年現在)である ESLint も導入しています。</p> <p>当記事では、ESLint が TypeScript 固有の構文などを解析可能になるプラグインである <a href="https://typescript-eslint.io/">typescript-eslint</a> の <a href="https://typescript-eslint.io/rules/#supported-rules">Supported Rules</a> に着目し、社内リポジトリでまだ適用していないルールのうち一定の効果が見込めるものを紹介します。</p> <ul class="table-of-contents"> <li><a href="#今回の方針">今回の方針</a></li> <li><a href="#ルールの探し方">ルールの探し方</a><ul> <li><a href="#現在適用中のルール">現在適用中のルール</a></li> </ul> </li> <li><a href="#ルールの紹介">ルールの紹介</a><ul> <li><a href="#まだ適用していない--一定の効果が見込める--導入が楽そうなルール">まだ適用していない &amp; 一定の効果が見込める &amp; 導入が楽そうなルール</a></li> <li><a href="#まだ適用していない--一定の効果が見込める--導入までの道のりが険しいルール">まだ適用していない &amp; 一定の効果が見込める &amp; 導入までの道のりが険しいルール</a></li> </ul> </li> <li><a href="#道中で遭遇した問題とその解決など">道中で遭遇した問題とその解決など</a></li> <li><a href="#ふりかえり">ふりかえり</a></li> </ul> <h2 id="今回の方針">今回の方針</h2> <p>Linter のルールを追加することによって、以下の効果が得られます。</p> <ul> <li>既存のコードの形式に対して警告を出す、または自動で訂正を行なう</li> <li>今後追加されるコードの形式を一定に保つ</li> </ul> <p>前者はリポジトリに適用することで即座に効果が確認できる一方で、後者は既存のコード構成が合意された経緯などに対する広い視野での理解が必要であり、入社3か月程度の自分の視野では後者に基づいた判断は難しいと考えました。</p> <p>そのため今回は、前者の「既存のコードの形式に対して警告を出す、または自動で訂正を行なう」という点を重視し、警告の数や自動訂正によるコードの差分が比較的多いルールを調べることにしました。</p> <h2 id="ルールの探し方">ルールの探し方</h2> <p>ESLint の CLI を使って現在適用中のルール名を列挙したものと、公式リファレンスの Supported Rules のページからルール名のリストを抽出してそれらを突き合わせて… というような手順で進めたかったのですが、<br/> 社内リポジトリで利用している typescript-eslint のバージョンと公式リファレンスのそれが一致していなかった<br/> => 一部のルール名が変更されている可能性があった<br/> => 未適用のルール名を洗い出すことが困難<br/> という理由から、ルールの詳細ページを1つずつ開きながら適用できそうなものを泥臭く確認していく形を取りました。</p> <p>弊社のフロントエンド用リポジトリの Linting には、Nx が提供している <a href="https://www.npmjs.com/package/@nrwl/eslint-plugin-nx">@nrwl/eslint-plugin-nx</a> プラグインに含まれている <a href="https://github.com/nrwl/nx/blob/master/packages/eslint-plugin-nx/src/configs/typescript.ts">@nrwl/nx/typescript</a> プラグイン、および .eslintrc.json ファイルでルール名を直接指定する形で適用しているため、それらは今回の検討からは外しました。<br/> 以下は、ルール名を直接指定して適用している typescript-eslint ルールの紹介になります。</p> <h3 id="現在適用中のルール">現在適用中のルール</h3> <p><strong>@typescript-eslint/no-non-null-assertion</strong><br/> 非 null アサーション演算子 <code>!</code> の利用を許可しません。<br/> <code>null</code> または <code>undefined</code> が返却される可能性のある式を取り除き、null 安全を実現します。</p> <p><strong>@typescript-eslint/consistent-type-assertions</strong><br/> <code>as</code> 句を使った型アサーションを許可しません。(<code>any</code> 型や <code>unknown</code> 型へのアサーションは無視されます)</p> <p><strong>@typescript-eslint/interface-name-prefix</strong><br/> interface の命名で先頭に <code>I</code> をつけるよう強制します。 (2022年12月現在の最新バージョンである v5.46.1 では、 <a href="https://typescript-eslint.io/rules/naming-convention/#enforce-that-interface-names-do-not-begin-with-an-i">naming-convention ルールにオプションを加えることで再現可能</a>です)</p> <p><strong>@typescript-eslint/no-unused-vars</strong><br/> <code>var</code> 句を使った変数宣言を許可しません。(ESLint にある同名のルールの拡張版)</p> <p><strong>@typescript-eslint/ban-types</strong><br/> <code>String</code> オブジェクトや <code>Boolean</code> オブジェクトなどの、JavaScript で定義されているプリミティブ値のラッパーオブジェクトを型定義に利用することを許可しません。</p> <p><strong>@typescript-eslint/no-extra-semi</strong><br/> セミコロンの記述を必要最低限(例:即時関数の定義 <code>;(() =&gt; {})()</code>)のもの以外許可しません。(ESLint にある同名のルールの拡張版)</p> <p><strong>@typescript-eslint/prefer-ts-expect-error</strong><br/> 型検査を回避するための <code>@ts-ignore</code> ディレクティブの利用を許可しません。(その上位互換である <code>@ts-expect-error</code> ディレクティブは無視されます)</p> <p><strong>@typescript-eslint/no-explicit-any</strong><br/> 明示的な <code>any</code> 型の利用を許可しません。</p> <p>(@nrwl/nx/typescript プラグイン自体にはルールの適用は無く(<a href="https://github.com/nrwl/nx/blob/master/packages/eslint-plugin-nx/src/configs/typescript.ts">厳密には4つのルールが無効化されています</a>)、そこに含まれている <a href="https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/src/configs/recommended.ts">@typescript-eslint/recommended</a> プラグインによってルールが適用されています)</p> <h2 id="ルールの紹介">ルールの紹介</h2> <h3 id="まだ適用していない--一定の効果が見込める--導入が楽そうなルール">まだ適用していない &amp; 一定の効果が見込める &amp; 導入が楽そうなルール</h3> <p><strong>@typescript-eslint/prefer-optional-chain</strong><br/> 連鎖したプロパティの存在判定式にオプショナルチェーン <code>?.</code> の利用を強制します。</p> <p><strong>@typescript-eslint/prefer-includes</strong><br/> 文字列や配列の中に指定する値が存在しているかを判定する式として <code>array.indexOf(value) !== -1</code> の代わりに <code>array.includes(value)</code> の記述を強制します。</p> <h3 id="まだ適用していない--一定の効果が見込める--導入までの道のりが険しいルール">まだ適用していない &amp; 一定の効果が見込める &amp; 導入までの道のりが険しいルール</h3> <p><strong>@typescript-eslint/no-unsafe-member-access</strong><br/> <code>any</code> 型のオブジェクトのメンバー変数への参照を許可しません。<br/> 外部ライブラリやDOM周りの型定義が追いついていない部分もあるため、網羅するにはかなり苦労しそうです。</p> <p><strong>@typescript-eslint/restrict-template-expressions</strong><br/> テンプレートリテラル <code>${arg}</code> を使った暗黙的な型変換を許可しません。<br/> 実際に適用する場合は、allowNumber などのオプションを指定して実装しやすさと安全性のバランスを取るのがベターであると見ています。</p> <p>上記計4つは、 <a href="https://typescript-eslint.io/rules/">公式リファレンス</a>にて strict や recommended として指定されているルールから、実際に手元で適用してみてエラーや差分が多く出たものから挙げました。<br/> ルールを追加 &amp; 修正を施した Pull Request は、影響範囲が広いためリリース作業などが落ち着き次第提出するつもりです。</p> <h2 id="道中で遭遇した問題とその解決など">道中で遭遇した問題とその解決など</h2> <ul> <li>CLI 実行時に以下のエラーが表示される</li> </ul> <pre><code class="``shell"> Error: You have attempted to use a lint rule which requires the full TypeScript type-checker to be available, but you do not have `parserOptions.project` configured to point at your project tsconfig.json files in the relevant TypeScript file "overrides" block of your project ESLint config `apps/broker/.eslintrc.json` </code></pre> <ul> <li><p>.eslintrc.json の最上位のプロパティとして、もしくは <code>overrides</code> プロパティの1つの要素として以下のプロパティを追加する必要がある</p> <pre><code class="json">"parserOptions": { "project": ["./tsconfig.base.json"] }, </code></pre></li> <li><p>CLI 実行時に以下のようなエラーが表示される</p></li> </ul> <pre><code class="``shell"> Definition for rule '@typescript-eslint/&lt;ルール名&gt;' was not found </code></pre> <ul> <li>最新のバージョンではルール名が変わっていることがある</li> <li><a href="https://typescript-eslint.io/rules">リファレンス</a>にはルール名の変遷について記載されていない...</li> <li><a href="https://github.com/typescript-eslint/typescript-eslint">リファレンスのリポジトリ</a> の当該のルールの .md ファイルの差分を追う必要がある</li> <li>CLI 実行時の <code>--fix</code> オプションが効かない <ul> <li><a href="https://typescript-eslint.io/rules/">https://typescript-eslint.io/rules/</a> で 🔧 がついてるルールのみ自動修正可能(執筆当初はこの仕様に気づきませんでした;;)</li> </ul> </li> </ul> <h2 id="ふりかえり">ふりかえり</h2> <p>個人的に ESLint の導入(または Lint ルールの適用)にはあまり良い思い出がなかったのですが、今回の調査を通じて以下のような学びを得られました。</p> <ul> <li>typescript-eslint ルールの Example から TypeScript におけるバッドプラクティス</li> <li>ESLint プラグインの実体 (GitHub 上にあるコード) の追い方</li> <li>ルールのうち、どれが推奨されているものなのか・自動修正が可能なものなのかといったことを把握できた &amp; 継続的に把握できるようになった <ul> <li>typescript-eslint 公式リファレンスの <a href="https://typescript-eslint.io/rules/">Overview</a> から確認できます</li> </ul> </li> </ul> <p>typescript-eslint だけでなく本家の ESLint のルールも追いながら、今後も継続的に社内リポジトリに貢献していきたいです。<br/> 最後まで読んでいただきありがとうございました。</p> techtouch いますぐできる!Webアクセシビリティ改善3選 hatenablog://entry/4207112889946937028 2022-12-22T10:03:32+09:00 2023-03-08T10:43:10+09:00 テックタッチアドベントカレンダー 22 日目を担当する、フロントエンドエンジニアの shoko です。クリスマスが大好きで、12 月に入ってからは毎日うきうきしています! 本日のテーマは「アクセシビリティ」です。 アクセシビリティとは? アクセシビリティとは、Web システムやサイトを可能な限り多くの人が利用できるようにすることを意味します。 「可能な限り多くの人」というのは、ハンディキャップを持つ人々だけではなく、さまざまな状況や環境も含まれます。例えば、モバイルデバイスや回線の遅いネットワークを利用する場合、屋外の光が眩しい場所でデバイスを利用する場合、機械の故障でマウスが使えずキーボード… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20221222/20221222142849.png" alt="" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>テックタッチアドベントカレンダー 22 日目を担当する、フロントエンドエンジニアの shoko です。クリスマスが大好きで、12 月に入ってからは毎日うきうきしています!</p> <p>本日のテーマは「アクセシビリティ」です。</p> <h2 id="アクセシビリティとは">アクセシビリティとは?</h2> <p>アクセシビリティとは、Web システムやサイトを<strong>可能な限り多くの人が利用できるようにする</strong>ことを意味します。</p> <p>「可能な限り多くの人」というのは、ハンディキャップを持つ人々だけではなく、さまざまな状況や環境も含まれます。例えば、モバイルデバイスや回線の遅いネットワークを利用する場合、屋外の光が眩しい場所でデバイスを利用する場合、機械の故障でマウスが使えずキーボードで操作する場合…などです。このような状況や環境になることは誰でも考えられることであり、アクセシビリティを改善することは多くの人にとってメリットがあります。</p> <p>しかし、アクセシビリティに興味を持っても、「何から始めたらいいのかわからない」「興味はあるけど、工数がかかりそうで手をつけられない」と、なかなかはじめの一歩を踏み出せない方も少なくないのではないでしょうか?</p> <p>今回は、特別な工数をかけることなく、実装時に意識するだけで簡単に取り組むことができるアクセシビリティ改善を3つご紹介します。</p> <h2 id="1画像に代替テキストalt属性をつける">1.画像に代替テキスト(alt属性)をつける</h2> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20221219/20221219220747.jpg" alt="&#x30AF;&#x30EA;&#x30B9;&#x30DE;&#x30B9;&#x30C4;&#x30EA;&#x30FC;&#x306E;&#x5199;&#x771F;" width="640" height="958" loading="lazy" title="" class="hatena-fotolife" style="width:240px" itemprop="image"></span></p> <pre class="code lang-html" data-lang="html" data-unlink><span class="synIdentifier">&lt;</span><span class="synStatement">img</span><span class="synIdentifier"> </span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;christmas_tree.jpg&quot;</span><span class="synIdentifier"> </span><span class="synType">alt</span><span class="synIdentifier">=</span><span class="synConstant">&quot;クリスマスツリーの写真&quot;</span><span class="synIdentifier">&gt;</span> </pre> <p>画像の alt 属性に画像の内容を表す文言を指定します。 alt 属性を指定しておくと、スクリーンリーダー<a href="#f-bb50d9da" name="fn-bb50d9da" title="画面に表示された内容を音声や点字など視覚以外の方法で伝える機能のこと">*1</a>を利用しているユーザーが画像の内容を認識できるようになります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20221221/20221221111407.png" alt="&#x5BDD;&#x3066;&#x3044;&#x308B;&#x732B;&#x306E;&#x5199;&#x771F;&#x3068;&#x305D;&#x306E;&#x8AAC;&#x660E;&#x6587;&#x306E;&#x30A4;&#x30E1;&#x30FC;&#x30B8;&#x753B;&#x50CF;" width="490" height="258" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>alt 属性には必ず文字を指定しなければいけないわけではなく、意味を持たない装飾的な画像や、上記画像のように代替テキストが隣接する画像(寝ている猫)の場合には、空で指定します。このような場合に代替テキストを指定すると、スクリーンリーダーを利用しているユーザーに不要な情報や重複した情報を与えてしまう可能性があるためです。</p> <p>また、空だからといって alt 属性を省略してしまうと、ファイル名が読み上げられるようになってしまい、ユーザーの混乱を招く可能性があるため注意が必要です。</p> <pre class="code lang-html" data-lang="html" data-unlink>// OK ✅ 空のalt属性 alt=&quot;&quot; を指定する <span class="synIdentifier">&lt;</span><span class="synStatement">div</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">img</span><span class="synIdentifier"> </span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;sleeping_cat.jpg&quot;</span><span class="synIdentifier"> </span><span class="synType">alt</span><span class="synIdentifier">=</span><span class="synConstant">&quot;&quot;</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">strong</span><span class="synIdentifier">&gt;</span>寝ている猫<span class="synIdentifier">&lt;/</span><span class="synStatement">strong</span><span class="synIdentifier">&gt;</span> ... <span class="synIdentifier">&lt;/</span><span class="synStatement">div</span><span class="synIdentifier">&gt;</span> // NG ❌ alt属性を省略する <span class="synIdentifier">&lt;</span><span class="synStatement">div</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">img</span><span class="synIdentifier"> </span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;sleeping_cat.jpg&quot;</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">strong</span><span class="synIdentifier">&gt;</span>寝ている猫<span class="synIdentifier">&lt;/</span><span class="synStatement">strong</span><span class="synIdentifier">&gt;</span> ... <span class="synIdentifier">&lt;/</span><span class="synStatement">div</span><span class="synIdentifier">&gt;</span> </pre> <p>代替テキストに指定する文言に悩んだときは、W3C の画像チュートリアルにある<a href="https://www.w3.org/WAI/tutorials/images/decision-tree/">Alt Decition Tree</a> をチェックしてみることをおすすめします。チュートリアル内にたくさん例が載っているので、きっと参考になると思います。</p> <h3 id="メリット">メリット</h3> <ul> <li>スクリーンリーダーで代替テキストが読み上げられるので、視覚障がいのある人など視覚的に情報を知覚することが難しいユーザーにも画像コンテンツの情報を伝えることができる。</li> <li>回線速度の遅いデバイスを利用していて画像の表示をオフにしていたり、何らかの理由で画像がリンク切れになってしまっていたりして画像が表示されない場合でも、代替テキストによって画像コンテンツの情報を伝えることができる。 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20221221/20221221132236.png" alt="&#x753B;&#x50CF;&#x304C;&#x30EA;&#x30F3;&#x30AF;&#x5207;&#x308C;&#x3057;&#x3066;&#x3044;&#x308B;&#x6642;&#x306E;&#x30A4;&#x30E1;&#x30FC;&#x30B8;" width="354" height="137" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></li> </ul> <h2 id="2ラベルと入力フォームを紐づける">2.ラベルと入力フォームを紐づける</h2> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20221221/20221221111324.png" alt="&#x30E9;&#x30D9;&#x30EB;&#x3068;&#x5165;&#x529B;&#x30D5;&#x30A9;&#x30FC;&#x30E0;&#x306E;&#x30A4;&#x30E1;&#x30FC;&#x30B8;&#x753B;&#x50CF;" width="490" height="200" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <pre class="code lang-html" data-lang="html" data-unlink><span class="synIdentifier">&lt;</span><span class="synStatement">label</span><span class="synIdentifier"> </span><span class="synType">for</span><span class="synIdentifier">=</span><span class="synConstant">&quot;title&quot;</span><span class="synIdentifier">&gt;</span>タイトル<span class="synIdentifier">&lt;/</span><span class="synStatement">label</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">input</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;text&quot;</span><span class="synIdentifier"> </span><span class="synType">id</span><span class="synIdentifier">=</span><span class="synConstant">&quot;title&quot;</span><span class="synIdentifier">&gt;</span> </pre> <p>label の for 属性、 input の id 属性に同じ値を指定することで、ラベルと入力フォームを紐づけることができます。これにより、スクリーンリーダーを利用しているユーザーがラベルを認識しやすくなる、フォームのクリック可能領域の拡大によりマウス利用時の操作性が向上するといった効果が期待できます。</p> <pre class="code lang-html" data-lang="html" data-unlink><span class="synIdentifier">&lt;</span><span class="synStatement">label</span><span class="synIdentifier"> htmlFor=</span><span class="synConstant">&quot;title&quot;</span><span class="synIdentifier">&gt;</span>タイトル<span class="synIdentifier">&lt;/</span><span class="synStatement">label</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">input</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;text&quot;</span><span class="synIdentifier"> </span><span class="synType">id</span><span class="synIdentifier">=</span><span class="synConstant">&quot;title&quot;</span><span class="synIdentifier"> /&gt;</span> </pre> <p>for は JavaScript での予約語であるため、React では代わりに htmlFor を使用します。</p> <pre class="code lang-html" data-lang="html" data-unlink><span class="synIdentifier">&lt;</span><span class="synStatement">label</span><span class="synIdentifier">&gt;</span> タイトル <span class="synIdentifier">&lt;</span><span class="synStatement">input</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;text&quot;</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;/</span><span class="synStatement">label</span><span class="synIdentifier">&gt;</span> </pre> <p>また、input を直接 label の内側に入れることでも同様の効果が得られます。この場合は関連付けが明確なため、for および id 属性は必要ありません。</p> <h3 id="メリット-1">メリット</h3> <ul> <li>スクリーンリーダーなどの支援技術で入力フォームにフォーカスが当たった時に、ラベルの名前が読み上げられるので何のためのフォームなのかが理解しやすい。</li> <li>ラベル部分をクリックした時に入力フォームにフォーカスが当たるようになる。クリック可能領域を拡大できるので、マウス利用時の操作性が向上する。</li> </ul> <h2 id="3フォーカスを見えるようにする">3.フォーカスを見えるようにする</h2> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20221221/20221221111525.png" alt="&#x30D5;&#x30A9;&#x30FC;&#x30AB;&#x30B9;&#x306E;&#x30A4;&#x30E1;&#x30FC;&#x30B8;&#x753B;&#x50CF;&#x3002;&#x5229;&#x7528;&#x898F;&#x7D04;&#x3068;&#x3044;&#x3046;&#x6587;&#x8A00;&#x306E;&#x30EA;&#x30F3;&#x30AF;&#x306B;&#x9752;&#x3044;&#x67A0;&#x7DDA;&#x304C;&#x8868;&#x793A;&#x3055;&#x308C;&#x3066;&#x3044;&#x308B;&#x3002;" width="490" height="138" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>ブラウザは、フォーカスされた要素を可視化するために<strong>フォーカスリング</strong>を実装しています。フォーカスリングとは、上記画像でいうと「利用規約」という要素を囲っている枠線のことです。キーボードで操作をする時、フォーカスが見えていたほうが迷わず操作がしやすいですよね。</p> <p>このように通常はブラウザがフォーカスを見えるようにしているので問題がないのですが、クリックした後にもこのフォーカスリングが表示されることを嫌って、CSS で疑似クラス <code>:focus</code> に<code>outline: none;</code> を指定し、フォーカスリングを消してしまっている場合があります。</p> <p>そんな時には、<a href="https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible"><code>:focus-visible</code></a> を使うと良いでしょう。<code>:focus-visible</code> は、キーボードを使用してタブ移動をしてフォーカスが当たった時にのみフォーカスリングを表示できる疑似クラスです。</p> <pre class="code lang-css" data-lang="css" data-unlink><span class="synStatement">a</span>:<span class="synPreProc">focus</span>-visible <span class="synIdentifier">{</span> <span class="synType">outline</span>: <span class="synConstant">4px</span> <span class="synConstant">dashed</span> <span class="synConstant">brown</span>; <span class="synType">background-color</span>: <span class="synConstant">floralwhite</span>; <span class="synIdentifier">}</span> </pre> <p>また、CSS で指定すれば、フォーカス時に独自のデザインをあてることもできます。</p> <h3 id="メリット-2">メリット</h3> <ul> <li>キーボードで操作をしているユーザーが、画面上にある複数の要素のうち、どの要素がキーボードフォーカスを持っているかを認識しやすくなる。</li> <li>注意力欠如、短期記憶の制約などのあるユーザーが、フォーカスがどこにあるのかを見つけることができるようになる。</li> </ul> <h2 id="おわりに">おわりに</h2> <p>テックタッチでも少しずつアクセシビリティ改善を進めていますが、まだまだ道半ばです。テックタッチのミッションである「すべてのユーザーが、システムを使いこなせる世界に」を真に実現するため、これからも日々改善に取り組んでいきます。</p> <p>最後まで読んでいただきありがとうございました。メリークリスマス!🎄</p> <div class="footnote"> <p class="footnote"><a href="#fn-bb50d9da" name="f-bb50d9da" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text">画面に表示された内容を音声や点字など視覚以外の方法で伝える機能のこと</span></p> </div> techtouch グッドデザイン賞を受賞するまでのあゆみ hatenablog://entry/4207112889943568846 2022-12-21T07:00:00+09:00 2023-12-15T09:42:19+09:00 テックタッチアドベントカレンダー21 日目を担当するデザイナーの toshi です。 今年、テックタッチはグッドデザイン賞に応募しました。本記事では、審査でプロダクトの魅力を伝えるために考えた点を紹介します。 特に、二次審査は非公開で 3 日間行われ、いつ審査されるか分からない緊張感がありました。 テックタッチとは? テックタッチの目指す世界 グッドデザイン賞の5つの理念 テックタッチを伝える 一次審査 二次審査 さいごに テックタッチとは? 始めに、グッドデザイン賞にエントリーしたテックタッチを紹介します。 テックタッチは、ノーコードであらゆる Web システムに操作ガイドを作成・再生できた… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20221215/20221215182216.png" alt="" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><a href="https://qiita.com/advent-calendar/2022/techtouch">テックタッチアドベントカレンダー</a>21 日目を担当するデザイナーの toshi です。<br/> 今年、テックタッチはグッドデザイン賞に応募しました。本記事では、審査でプロダクトの魅力を伝えるために考えた点を紹介します。<br/> 特に、二次審査は非公開で 3 日間行われ、いつ審査されるか分からない緊張感がありました。</p> <ul class="table-of-contents"> <li><a href="#テックタッチとは">テックタッチとは?</a></li> <li><a href="#テックタッチの目指す世界">テックタッチの目指す世界</a></li> <li><a href="#グッドデザイン賞の5つの理念">グッドデザイン賞の5つの理念</a></li> <li><a href="#テックタッチを伝える">テックタッチを伝える</a><ul> <li><a href="#一次審査">一次審査</a></li> <li><a href="#二次審査">二次審査</a></li> </ul> </li> <li><a href="#さいごに">さいごに</a></li> </ul> <h2 id="テックタッチとは">テックタッチとは?</h2> <p>始めに、グッドデザイン賞にエントリーした<a href="https://techtouch.jp/">テックタッチ</a>を紹介します。<br/> テックタッチは、ノーコードであらゆる Web システムに操作ガイドを作成・再生できたり、システムの利用状況などを可視化・分析できます。</p> <h2 id="テックタッチの目指す世界">テックタッチの目指す世界</h2> <p><strong>すべてのユーザーがシステムを使いこなせる世界に</strong><br/> 世の中には、ユーザーの課題を解決する魅力的なサービスが数多くあります。<br/> 一方で、ユーザーに価値を届けていても「UI が複雑」「使い方忘れる」「入力ミスが起こる」など、すべてのユーザーに対して「価値の最大化」を行う難しさがあります。</p> <p>また、開発側が Web システムの問題を理解し UI/UX を改修したくても、開発工数がかかるなど柔軟に対応できない現実があります。</p> <p>テックタッチは、ユーザーと Web システムの架け橋をノーコードで実現し、すべてのユーザーが Web システムの価値を最大化できることを目指しているサービスです。</p> <h2 id="グッドデザイン賞の5つの理念">グッドデザイン賞の5つの理念</h2> <p>テックタッチの紹介で長くなりましたが、ここでグッドデザイン賞の理念を確認します。</p> <blockquote><p>人間(HUMANITY) もの・ことづくりを導く創発力<br/> 本質(HONESTY) 現代社会に対する洞察力<br/> 創造(INNOVATION) 未来を切り開く構想力<br/> 魅力(ESTHETICS) 豊かな生活文化を想起させる想像力<br/> 倫理(ETHICS) 社会・環境をかたちづくる思考力</p></blockquote> <p>テックタッチはグッドデザイン賞の理念にも共感できるサービスだと思います。<br/> 共感できるサービスだからこそ、良さを伝えられなければデザイナーの力不足ですので、受賞できるかと内心ヒヤヒヤでした。</p> <h2 id="テックタッチを伝える">テックタッチを伝える</h2> <p>2022 年度グッドデザイン賞の審査対象数は 5,715 件でした。膨大な数です。<br/> 審査員は数多くのエントリーシートをチェックするので、伝わりやすいように<br/> <strong>* テックタッチを端的に言語化する<br/> * テックタッチで実現したい世界を伝える</strong><br/> ためにはどう表現するかを考えました。</p> <h3 id="一次審査">一次審査</h3> <p>グッドデザイン賞の事務局が用意したエントリーシートに、基本情報や審査情報を記入します。 代表の naka、CTO の jun、広報の yocchi、デザイナーの keita、toshi でテックタッチが生まれた背景や大切にしたことを共有しました。<br/> Digital Adoption Platform (DAP) 市場のテックタッチを知らない人にも価値が伝わるように、ポイントをまとめてライティングの調整をし申し込みました。</p> <h3 id="二次審査">二次審査</h3> <p>一次審査に通過したあとは、展示情報の登録、実機展示の準備などがあります。 テックタッチを展示するということは、別の Web システムも同時に見せる必要があります。 しかし、他社の Web システム上にテックタッチを表示して展示することはできません。</p> <p>ここで、考えなくてはいけない点は、<br/> <strong>展示の見せ方</strong><br/> 1. 静的な情報のパネル<br/> 2. 動的な情報の動画<br/> 3. 動作デモを確認できる PC の用意<br/>  ・動作デモからテックタッチはこれだと審査員が分かること<br/>  ・テックタッチでできることが伝わること<br/> 4. 72 時間常に表示する設定とネットワーク</p> <p>また、情報設計の際には審査時間も考慮しました。BEST100 のプレゼンテーション時間は 4 分ですので、4 分以内で審査員にテックタッチが伝わることを目指しました。</p> <p><strong>静的な情報のパネル</strong><br/> パネル構成は簡潔に4つの構成にまとめました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20221215/20221215184213.png" alt="&#x30B0;&#x30C3;&#x30C9;&#x30C7;&#x30B6;&#x30A4;&#x30F3;&#x8CDE;&#x306E;&#x30D1;&#x30CD;&#x30EB;" width="1200" height="422" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><strong>動的な情報の動画</strong><br/> テックタッチの作成・再生方法と紹介動画を3分に編集し、ループ再生でどの場面から見てもテックタッチが伝わるよう作成し、タブレットで表示しました。</p> <p><strong>動作デモ画面</strong><br/> 審査員はグッドデザイン賞の Web サイトを見たことがあると仮定しました。その前提のもと、グッドデザイン賞の Web サイト上にテックタッチの動作デモを作成しプレゼンテーションしました。<br/> 動作デモの構成は、テックタッチのガイドをメインに定型文の自動入力ができることなども、代表の naka や広報の yocchi からのフィードバックをもとにブラッシュアップしました。 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20221215/20221215184218.gif" alt="&#x30B0;&#x30C3;&#x30C9;&#x30C7;&#x30B6;&#x30A4;&#x30F3;&#x8CDE;&#x306E;&#x30C7;&#x30E2;&#x52D5;&#x753B;" width="1200" height="1125" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><strong>72 時間常に表示する設定とネットワーク</strong><br/> 忘れてはいけないのが、展示する PC・タブレットの設定です。審査は3日間なのでいつでも見れるように常時表示の設定にしました。<br/> 常時表示の設定に本当に問題がないか、事前に自宅でも確認しました。(画面ロックやスリープ解除を忘れずに)</p> <h2 id="さいごに">さいごに</h2> <p>結果、グッドデザイン賞を受賞できました! 審査員コメントは以下になります。</p> <blockquote><p>ノーコードで様々なウェブサイト上に操作ガイドを載せることの出来るサービス。 リテラシーの限られたユーザーと、対応の工数を抑制し業務効率化を図りたい企業双方のニーズに合致している。 社会全体でのより包括的なデジタル活用を限られたコストの中で実現するという文脈での更なる活躍を期待したい。</p></blockquote> <p>審査員にもテックタッチの目指している世界が伝わりほっとしました。 これまでテックタッチに関わってきた Biz / Corp / Prod のメンバーはもちろんのこと、テックタッチをよりよくするために意見を届けてくれたユーザーといっしょに生み出した「<strong>CO-DEVELOPERS</strong>」の成果だと思います。</p> <p>これからも、すべてのユーザーがシステムを使いこなせる世界にするために「<strong>CO-DEVELOPERS</strong>」で価値あるサービスにしていきます!</p> <p>「<strong>CO-DEVELOPERS</strong>」に込めた想いについては<a href="https://www.wantedly.com/companies/company_4388704/post_articles/380429#_=_">こちら</a></p> techtouch パフォーマンス最適化を目指したフレームワーク「Qwik」について hatenablog://entry/4207112889946385178 2022-12-20T19:00:00+09:00 2023-01-12T12:06:39+09:00 パフォーマンス最適化を目指したフレームワーク「Qwik」について学んでみました。 <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20221219/20221219131726.png" alt="" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>テックタッチアドベントカレンダーの 20 日目を担当させていただく matchy です。<br /> 最近は趣味でアプリケーションの UI デザインを Figma で作るのが楽しくてつい時間を忘れてしまいます。<br /> 近ごろ <a href="https://astro.build/">Astro</a> や今回紹介する <a href="https://qwik.builder.io/">Qwik</a> など、MPA フレームワークの話をよく SNS などで見かけるので、気になり調べてみました。</p> <ul class="table-of-contents"> <li><a href="#Qwikとは">Qwikとは?</a></li> <li><a href="#公式">公式</a></li> <li><a href="#特徴をまとめてみる">特徴をまとめてみる</a></li> <li><a href="#ん-Hydration-を使用しない">ん? Hydration を使用しない?</a></li> <li><a href="#Hydration-の課題">Hydration の課題</a></li> <li><a href="#Resumable-という概念">Resumable という概念</a></li> <li><a href="#プロジェクト作成をしてみる">プロジェクト作成をしてみる</a><ul> <li><a href="#Qwik-City-とは">Qwik City とは</a></li> </ul> </li> <li><a href="#最後に">最後に</a></li> <li><a href="#参考資料">参考資料</a></li> </ul> <h2 id="Qwikとは">Qwikとは?</h2> <ul> <li><a href="https://www.builder.io/">builder.io</a> が開発している Web フレームワーク</li> <li>Web サイトやアプリケーションの構築に使用できる</li> <li><strong>SSR ベース</strong></li> </ul> <h2 id="公式">公式</h2> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fqwik.builder.io%2F" title="Framework reimagined for the edge! - Qwik" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p> <h2 id="特徴をまとめてみる">特徴をまとめてみる</h2> <ul> <li>約 1 KB の JS で起動できる</li> <li>最小限のアプリケーションをダウンロードして実行する <ul> <li>JavaScript の実行とダウンロードを可能な限り遅らせる</li> <li>アプリケーションとフレームワークの実行状態をサーバーでシリアライズし、クライアントで Resume させる</li> </ul> </li> <li><strong>Hydration を使用しない</strong></li> <li>jsx、tsx で書ける</li> </ul> <h2 id="ん-Hydration-を使用しない">ん? Hydration を使用しない?</h2> <p><img src="https://storage.googleapis.com/zenn-user-upload/13290fb10876-20221219.webp" alt="Digging deeper into hydration" /></p> <center><a href="https://www.builder.io/blog/hydration-is-pure-overhead">https://www.builder.io/blog/hydration-is-pure-overhead</a></center> <p><br /> SSR では、まずサーバー上でページのレンダリングを行い、生成された HTML をクライアントに返却します。<br /> その後、そのままだとインタラクティブなコンテンツの操作が行えないため、 Hydration という処理が走り、レンダリング済みの DOM と比較しながらイベントハンドラをアタッチしていきます。<br /> SSR ベースのフレームワークにも関わらず、その Hydration を使用しないということで???となる方もいるのではないでしょうか。</p> <h2 id="Hydration-の課題">Hydration の課題</h2> <ul> <li>ページの表示までは行われるが、TTI(Time to Interactive = コンテンツがインタラクティブになるまでの時間)がユーザーの環境に依存してしまう</li> <li>コンポーネントの量が増えていくとそれだけ初期ロード時の JS サイズが大きくなってしまう</li> <li>インタラクティブな部分が少ないページや静的なページも関係なく、コンポーネントの JS をすべてダウンロードし DOM の計算を行うコストがかかってしまう</li> </ul> <h2 id="Resumable-という概念">Resumable という概念</h2> <p><img src="https://storage.googleapis.com/zenn-user-upload/029485116893-20221219.jpeg" alt="Resumability: a no-overhead alternative to hydration" /></p> <center><a href="https://www.builder.io/blog/hydration-is-pure-overhead">https://www.builder.io/blog/hydration-is-pure-overhead</a></center> <p><br /> Hydration の問題の解消として React 18 の <a href="https://twitter.com/reactjs/status/1508852384373563398?ref_src=twsrc%5Etfw%7Ctwcamp%5Etweetembed%7Ctwterm%5E1508852384373563398%7Ctwgr%5Ebe9310b2718d913ae3d11ebe4b6d7cebee512fce%7Ctwcon%5Es1_c10&amp;ref_url=https%3A%2F%2Fwww.publickey1.jp%2Fblog%2F22%2Freact_18.html">Selective Hydration</a> や、冒頭で少し触れた Astro では <a href="https://jasonformat.com/islands-architecture/">Islands Architecture</a> という Hydration を行う箇所をインタラクティブな部分限定にし、パフォーマンスを向上させるといったアプローチを取っています。<br /> それに対して Qwik では <strong>Resumable</strong> というアプローチを採用しました。<br /> そもそも <strong>Hydration の処理を行わず</strong>、ダウンロードする JS ファイルを最小限にすることで、ページのレンダリングコストを下げることが可能になっています。<br /> また、DOM にイベントハンドラを仕込んでおき、ユーザーの操作に応じて必要な JS のみダウンロードを行うので、結果 TTI も短縮できるといったアイデアです。<br /> TTI を短縮できることによって、UXの向上や page speed Insights の指標の改善が狙えます。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span> component$<span class="synStatement">,</span> useStore <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">'@builder.io/qwik'</span><span class="synStatement">;</span> <span class="synStatement">export</span> <span class="synType">const</span> Counter <span class="synStatement">=</span> component$<span class="synStatement">(()</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synType">const</span> store <span class="synStatement">=</span> useStore<span class="synStatement">(</span><span class="synIdentifier">{</span> count: <span class="synConstant">0</span> <span class="synIdentifier">}</span><span class="synStatement">);</span> <span class="synStatement">return</span> <span class="synStatement">(</span> <span class="synStatement">&lt;</span>div<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>p<span class="synStatement">&gt;</span>Count: <span class="synIdentifier">{</span>store.count<span class="synIdentifier">}</span><span class="synStatement">&lt;</span>/p<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>p<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>button onClick$<span class="synStatement">=</span><span class="synIdentifier">{</span><span class="synStatement">()</span> <span class="synStatement">=&gt;</span> store.count<span class="synStatement">++</span><span class="synIdentifier">}</span><span class="synStatement">&gt;</span>Increment<span class="synStatement">&lt;</span>/button<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>/p<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>/div<span class="synStatement">&gt;</span> <span class="synStatement">);</span> <span class="synIdentifier">}</span><span class="synStatement">);</span> </pre> <p>(Qwik でインタラクティブなボタンを実装した例)<br /></p> <pre class="code lang-html" data-lang="html" data-unlink><span class="synIdentifier">&lt;</span><span class="synStatement">button</span><span class="synIdentifier"> on:click=</span><span class="synConstant">&quot;/src/counter_component_div_p_button_onclick_ziketbnzetm.js#Counter_component_div_p_button_onClick_ZIkEtbnzEtM[0]&quot;</span><span class="synIdentifier"> q:</span><span class="synType">id</span><span class="synIdentifier">=</span><span class="synConstant">&quot;3&quot;</span><span class="synIdentifier">&gt;</span>Increment<span class="synIdentifier">&lt;/</span><span class="synStatement">button</span><span class="synIdentifier">&gt;</span> </pre> <p>(レンダリングされた HTML では button 要素に on:click の属性が付与されて出力される。値には遅延読み込みする JS ファイルが指定されており、イベントが発火したら JS ダウンロードが行われる)</p> <h2 id="プロジェクト作成をしてみる">プロジェクト作成をしてみる</h2> <p>Qwik の <a href="https://qwik.builder.io/docs/getting-started/">Getting Started</a> に沿ってプロジェクトを 1 つ作成してみたいと思います。</p> <pre class="code js" data-lang="js" data-unlink>npm create qwik@latest</pre> <p><br /> 次にプロジェクトを作成する場所とプロジェクト名を決めます。</p> <pre class="code js" data-lang="js" data-unlink>? Where would you like to create your new project? › ./qwik-app</pre> <pre class="code js" data-lang="js" data-unlink>? Select a starter › (use ↓↑ arrows, hit enter) ❯ Basic App (QwikCity) └─App with Routing built-in (recommended) Documentation site (QwikCity) Component library</pre> <p>ここで starter を選択します。(今回は Basic App) <br /></p> <pre class="code js" data-lang="js" data-unlink>? Would you like to install npm dependencies? › (Y/n)</pre> <p>npm の依存パッケージをインストールするか聞かれるので enter(Yes)を押します。 <br /></p> <pre class="code js" data-lang="js" data-unlink>🦄 Success! Project created in qwik-app directory 🐰 Next steps: cd learning/SUBSCLiZE/subsclize_app npm start 🔌 Integrations? Add Netlify, CloudFlare, Tailwind... npm run qwik add 📚 Relevant docs: https://qwik.builder.io/docs/getting-started/ 💬 Questions? Start the conversation at: https://qwik.builder.io/chat https://twitter.com/QwikDev 📺 Presentations, Podcasts and Videos: https://qwik.builder.io/media/</pre> <p>無事インストールされました! <br /> ディレクトリを移動して<code>npm run dev</code>で起動してみます。</p> <pre class="code js" data-lang="js" data-unlink>cd /Users/username/qwik-app</pre> <pre class="code js" data-lang="js" data-unlink>❯ npm run dev &gt; dev &gt; vite --mode ssr Port 5173 is in use, trying another one... VITE v4.0.1 ready in 641 ms ➜ Local: http://127.0.0.1:5174/ ➜ Network: use --host to expose ➜ press h to show help</pre> <p><img src="https://storage.googleapis.com/zenn-user-upload/2f7b27f22b66-20221219.png" alt="Welcome-to-Qwik" /> 起動できました!<br /> ちなみに、Qwik では Vite がビルドツールとしてデフォルトで入っているのもあり開発環境の起動もビルドも爆速でした。</p> <h3 id="Qwik-City-とは">Qwik City とは</h3> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fqwik.builder.io%2Fqwikcity%2Foverview%2F" title="Qwik City - Overview - Qwik" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p> <blockquote><p>Learning Qwik and Qwik City are not two different things, Qwik City is built on top of Qwik, bringing opinionated routing and goodies to make your life easier.<br /> We call it a meta-framework for Qwik. Qwik City is to Qwik, what Next.js is to React, what Nuxt is to Vue, or SvelteKit to Svelte.</p></blockquote> <p>Qwik のメタフレームワークで、React に対しての Next.js、 Vue に対しての Nuxt.js ようなもの。<br /> 独自のファイルベースのルーティングで、高速でアプリケーションを構築できるとのことです。 (ざっくり調べた感じ、ルーティングディレクトリの制約が Next.js などとは異なるみたいです)</p> <h2 id="最後に">最後に</h2> <p>色々な角度からパフォーマンス向上させるためのアプローチをしかけているライブラリやフレームワークが続々と出てきていて、面白いなと思いました。<br /> また、SSR における Hydration の問題を学ぶ良い機会にもなりました。<br /> 今回は内容が膨大化してしまうため Getting Started までの紹介となってしまいましたが、これを機に Qwik を利用したアプリケーションを作ってみようと思います。</p> <h2 id="参考資料">参考資料</h2> <ul> <li><a href="https://www.builder.io/blog/hydration-is-pure-overhead">Hydration is Pure Overhead</a></li> <li><a href="https://dev.to/builderio/a-first-look-at-qwik-the-html-first-framework-af">A first look at Qwik - the HTML first framework</a></li> <li><a href="https://www.docswell.com/s/kawamataryo/K4GW15-qwik-resumable">Resumable な JavaScript フレームワーク Qwik を学ぶ</a></li> <li><a href="https://zenn.dev/aiji42/articles/fafa354f79660d">Qwikの基本概念である Resumable を理解する</a></li> <li><a href="https://speakerdeck.com/yosuke_furukawa/mpahua-suruspatospahua-surumpa">MPA化するSPAとSPA化するMPA</a></li> </ul> techtouch 遊星からの抽象X――抽象的な課題との闘いおよびその技法の記録―― hatenablog://entry/4207112889944852349 2022-12-19T19:49:59+09:00 2023-12-15T09:38:25+09:00 これは、テックタッチの開発チームが「開発組織の体制をどのように変えるべきか」という抽象的な課題に立ち向かった過程の記録。課題解決のよく知られたノウハウからマニアックな技法まで登場するこの記録は、組織や経営の抽象的な課題の解決に広く役に立つポテンシャルを秘めているはずだ。 <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20221219/20221219132211.png" alt="" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image" /></p> <p>本稿は、テックタッチアドベントカレンダー 19 日目の記事です。担当は canalun (twitter: <a href="https://twitter.com/i_am_canalun_">@i_am_canalun_</a>) です。</p> <p>……</p> <blockquote> <p>Why don't we just... wait here for a little while... see what happens?<br /><span style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;">「もう少しここで待って、何が起きるか見てみよう」<em><br />―― MacReady, from The Thing(邦題: 遊星からの物体X)</em></span></p> </blockquote> <p>1982年、南極大陸にて基地隊員が得体のしれない物体との死闘を展開。その正体は未だ明らかになっておらず、<ruby><rb>それ</rb><rp>(</rp><rt><strong>・・</strong></rt><rp>)</rp></ruby>はひとまず「<a href="https://ja.wikipedia.org/wiki/%E9%81%8A%E6%98%9F%E3%81%8B%E3%82%89%E3%81%AE%E7%89%A9%E4%BD%93X">遊星からの物体X</a>」という名前を与えられた。</p> <p>残念ながら、南極大陸で未知の怪物と闘うようなチームにとって、これから記されることはまったく役に立たないだろう。</p> <p>しかし、スタートアップの宇宙を旅する私たちのようなチームからすれば事情は違うはずだ。なにしろその宇宙には恐怖の破滅的怪物すなわち物体Xに代わって、難解な抽象的課題すなわち抽象Xが数多く潜んでいるというのだから。</p> <p>本稿は、<strong>テックタッチのプロダクトチームが「開発組織の体制をどのように変えるべきか」という抽象的な課題(問い)に立ち向かった過程を記録したもの</strong>である<a href="#f-1498c109" name="fn-1498c109" title="本旨が伝わりやすくなるよう、実際の時系列に対して編集を加えた記録です">*1</a>。ただし、ただの日記ではスタートアップ宇宙を旅する仲間の助けにはならないだろうと考え、<strong>課題解決に関する有名な書籍等</strong><a href="#f-82d5440a" name="fn-82d5440a" title="「イシューからはじめよ」や「考える技術・書く技術」、はたまた「コンセプトのつくりかた」などなど">*2</a><strong>を参考にしながら取り入れたり考えたりした課題解決の技法を併せて記した</strong>。登場するいくつかの技法――「課題が解決策の裏返しで語られていないか確認する」「経過観察を必ず行う」などなど――は<strong>組織や経営の抽象的な課題に広く適用できるポテンシャルを秘めているはずだ</strong>。</p> <p>あのまま抽象Xに全滅させられることなく生還し、これを書き残せているのも勇敢な仲間たちのおかげである。同じ宇宙の仲間に届くことを願って、ここにボトルメッセージを流す。</p> <p>次の楽しい予定まで時間がないのなら、興味があるセクションやまとめだけでも読んでいってほしい。もしスケジュール帳につまらない予定しかないのなら、ぜひじっくり読んでほしい――ちょっとした退屈しのぎにはなるはずだ。</p> <ul class="table-of-contents"> <li><a href="#day-0-プロローグ-組織の成長痛としての抽象X">day 0: プロローグ ――組織の成長痛としての抽象X――</a></li> <li><a href="#day-1-活動計画の策定-抽象Xに立ち向かうための作戦会議">day 1: 活動計画の策定 ――抽象Xに立ち向かうための作戦会議――</a></li> <li><a href="#day-2-課題の特定-抽象Xよお前はいったい何なんだ">day 2: 課題の特定 ――抽象Xよ、お前はいったい何なんだ――</a></li> <li><a href="#day-3-課題の整理-抽象Xは姿を変えるいや抽象Xの姿を変える">day 3: 課題の整理 ――抽象Xは姿を変える?いや抽象Xの姿を変える――</a><ul> <li><a href="#day-3-A-課題の分類">day 3-A: 課題の分類</a></li> <li><a href="#day-3-B-課題内容の精緻化">day 3-B: 課題内容の精緻化</a></li> </ul> </li> <li><a href="#day-4-課題の優先順位付け-抽象Xを倒しきろうという無理難題">day 4: 課題の優先順位付け ――抽象Xを倒しきろうという無理難題――</a></li> <li><a href="#day-5-ソリューション仮説の作成-抽象Xに対する具体Y">day 5: ソリューション仮説の作成 ――抽象Xに対する具体Y――</a></li> <li><a href="#day-6-ソリューション仮説の検証-さらば抽象X後ろに戻る勇気をこめて">day 6: ソリューション仮説の検証 ――さらば抽象X、後ろに戻る勇気をこめて――</a></li> <li><a href="#afterward-エピローグ-冒険のおわりにいやはじまりに">afterward: エピローグ ――冒険のおわりに……いやはじまりに――</a></li> </ul> <h4 id="day-0-プロローグ-組織の成長痛としての抽象X">day 0: プロローグ ――組織の成長痛としての抽象X――</h4> <p><ruby><rb>それ</rb><rp>(</rp><rt><strong>・・</strong></rt><rp>)</rp></ruby>がどうして私たちの前に姿を現したのか、少し昔の話から始めなければならない。</p> <p>すべてのユーザーがシステムを使いこなせる世界に――このテックタッチなる宇宙船がそんな壮大な理念を掲げて旅を始めた2018年、そのプロダクトチームは自分たちが理想とする組織像とは何かを雄弁に語った。</p> <p>役職や階級といった上下関係を規定する制度と決別し、組織の意思決定に関わる権限があらゆるメンバーに均等に分散されている組織。組織と個人の目指すところが寸分違わず一致しており、完全に対等なメンバーが要所要所で必要な意思決定をすることで成功をおさめる組織。</p> <p>組織形態に関心のある人であればこのようなコンセプトに聞き覚えがあるだろう。これは他ならぬティール組織である。そう、<strong>テックタッチのプロダクトチームはティール組織を理念としてきた</strong>。そしてそれは4年間変わってこなかった。</p> <p>さて、これは機能したのか。答えは、YESであった。これまではこれで良かった、いや、これが良かったのだ。マネージャーを置かずに誰もが同質の大きな裁量を持つことで、組織は成長してきた。</p> <p>では、これは機能し続けるのか。答えは、YESだとは思えなかった。</p> <p>成長する組織の宿命からは逃れられなかったのだ。成長には痛みが伴う。<strong>プロダクトが育ち、組織の規模が大きくなる中で、ティールの理念はその現実的な持続可能性を問われなければならなかった</strong>。既に開発チームのメンバーは30人ほど。これだけ多くの人間がいる中で、ティールを組織の理想として掲げ続け、取り入れ続けることが本当にやるべきことなのか。事実、組織としてティールを貫徹できているかと聞かれれば、それも既に判然としない状態だった。</p> <p>組織の在り方について抱かれる疑問は、ティールかそうでないかという理念の問題の範囲にとどまらなかった。<strong>実際の現場でも様々な課題の感覚が持たれ始めていた</strong>。</p> <p>「いろいろな意思決定をもっと加速させられないか」<br />「役割分担をもっと明確にしていった方がいいのではないか」<br />「個人がチームに縛られていないか。チーム間の垣根がないか」</p> <p>幸いなことに、<strong>テックタッチのプロダクトチームのメンバーは思ったことをとりあえず議論してみるというやり方を好んだ</strong>。日常的に交わされる議論の甲斐あって、組織の在り方について皆がなんとなく疑問を抱いているということを、皆がなんとなく知っていた。知ることができていた。</p> <p>だからこそ、いつの間にか抽象Xがいた。いや、私たちの側が呼び寄せるべくして呼び寄せたのだ。<strong>「開発組織の体制をどのように変えるべきか」</strong>。そんな抽象的な問いが生まれていた。<strong>この問いは二重の意味で抽象的だった。第一に形あるものを扱った問いではないという意味で。そして第二にその問いの背後にある課題が具体的に何であるか、すなわち何を解決しなければいけないのかがまだ明確に見通せないという意味で。</strong>その怪物は他ならぬ私たちの手によって、宇宙船に招き入れられていたのだ。</p> <p> </p> <p>5月の暑い日にCTOからこんなSlackメッセージが投稿された。</p> <blockquote> <p><strong>プロダクトチームの方へ、「組織マネジメントWT」発足のご案内</strong><br />プロダクトチームはティールとまではいかないまでも、マネージャー職を設置しておらず、階層のないフラットな組織運用になっておりますが、今後上場準備を進めていく中で <em>...略... </em>統制の仕組みが求められてきます。<br /><em>...略...</em><br />今までは事業全振り(立ち上げ最優先)でやってきた感がありますが、より経営を安定させ、もっと大きなビジョンを持てるように、冒頭の「組織マネジメントWT」を発足し、現状の課題の整理と、次の組織設計に必要な改善案を一緒に考えてくれる方を募集します。</p> <p><em>...略...</em><br />募集人数が少なければ、こちらからお声がけさせてもらい、大勢いた場合は、その時進め方検討させてくださいw<br />※WTに入らなくとも、一通り皆さんの意見を聞きつつ進めていく予定なのと、しっかり議論をオープンにしていきたいと思っています。<br />やりたいという方は、是非スレッドでコメントください!</p> <p>――<em>テックタッチCTO 日比野淳, 社内Slackの実際のメッセージより一部抜粋</em></p> </blockquote> <p>ティールを掲げていたおかげかは定かではないが、<strong>皆のことを皆で議論して決めるという文化はしっかりと根付いていた</strong>ようだった。驚くことに、投稿して1日が経つころには30人のチームのうち既に8人が協力したい旨を表明していた。</p> <p>こうして、この抽象Xに相対するチームが結成された。このチームは週に1回会社に集まり、1ヶ月から1ヶ月半でこの闘いに蹴りを付けることを目標とした。</p> <p>これは、その闘いの記録である。</p> <h4 id="day-1-活動計画の策定-抽象Xに立ち向かうための作戦会議">day 1: 活動計画の策定 ――抽象Xに立ち向かうための作戦会議――</h4> <p>当たり前だが、ことはそう単純ではなかった。</p> <p>私たちは、まずこの怪物にどう立ち向かうかを考えなければならなかった。この抽象的な課題の答えが必要な時期まで、残された時間はそう長くなかったからだ。1ヶ月から1ヶ月半。要するに、無駄な検討や手戻りによるロスは極小化される必要があった。</p> <p>そこで、<strong>下記の流れに従おうという話になった</strong>のである。</p> <ol> <li>課題の特定</li> <li>課題の整理</li> <li>課題の優先順位付け</li> <li>ソリューション仮説の作成</li> <li>ソリューション仮説の検証・具体化</li> </ol> <p>これは<a href="https://www.leadershipnow.com/leadingblog/2019/10/7_steps_to_bulletproof_problem.html">元マッキンゼーコンサルタントCharles ConnとRob McLeanが唱えるフロー</a>や、有名どころでは<a href="http://www.eijipress.co.jp/book/book.php?epcode=2085">「イシューからはじめよ」</a>で言われているようなことの変奏である。私も<a href="https://www.wantedly.com/companies/company_4388704/post_articles/395654">この会社で働いていた経験がある</a>が、上の流れに違和感はなかった。</p> <blockquote> <p>1. 今本当に答えを出すべき問題=「イシュー」を見極める</p> <p>2. イシューを解けるところまで小さく砕き、それに基づいてストーリーの流れを整理する</p> <p>3. ストーリーを検証するために必要なアウトプットのイメージを描き、分析を設計する</p> <p>4. ストーリーの骨格を踏まえつつ、段取り良く検証する</p> <p>5. 論拠と構造を磨きつつ、報告書や論文をまとめる</p> <p><em><span style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;">――</span>安宅和人「イシューからはじめよ」P34</em></p> </blockquote> <p>ところで、これら一つ一つのフェーズで言われていることは当たり前のように思えるかもしれないが、<strong>こうして明文化することでメンバー全員がフェーズをきっちりと切り分け、常に議論の目的を共有することができる</strong>。ここに大きな価値がある。</p> <p><strong>「いま何のために何を議論しているのか」はもちろん、もっと言えば「いま議論しなかったことはいつ議論されるのか」も含めて、皆が共通認識を持っている</strong>ことの価値――裏を返せばそれを持っていないときの問題――は誰もが身を以て知っている。</p> <p>分からなくなったら、いつだって先の流れに立ち返って「いまどのステップだったっけ?」と問い直せばよい。問題解決ステップの明文化は、ともすれば迷宮に迷い込む私たちに与えられたアリアドネの糸なのだ。</p> <p>さあ準備が整ったところで、抽象Xとの闘いを進めよう。</p> <h4 id="day-2-課題の特定-抽象Xよお前はいったい何なんだ">day 2: 課題の特定 ――抽象Xよ、お前はいったい何なんだ――</h4> <p>まずは初めのステップ、課題の特定である。</p> <p>このステップの目的を平たく言えば、自分たちの組織がどのような課題を持っているのか洗い出すことである。<strong>これはすべてのスタート地点となるため、とにかく出しきることが重要だった</strong>。ここで何か言いそびれれば、後々になってすべてがひっくり返ったり、重要なことが話せないまま進んでいるモヤモヤ感を誰かが抱いたり、といった展開が生まれてしまう。もちろん散らかったままでは対処ができないので整理はするが、それは次のステップの話なのだ。「発散と収束」なるクリシェを拝借するならば、ここでは<strong>もっぱら発散に集中するべきなのだ</strong>。全集中・発散の呼吸である。</p> <p>というわけで各々が課題だと思っていることをとにかくmiroに付箋で貼っていく作戦に出た。実際に出た意見をいくつか列挙してみる<a href="#f-31fcacd4" name="fn-31fcacd4" title="一部改変しています">*3</a>。</p> <p>「物事を進めようとすると色々なグループをまたいでしまう。そして会議になると参加者が爆増して、意思決定のスピードがどんどん落ちている」<br />「スクラムで管理していないプロジェクトの進捗が見えづらい」<br />「もっと簡単にチーム移動や職務移動ができるようになってもいいと思う」</p> <p>などなど。多種多様な意見が出た。</p> <p>あとになって思えば、とても良い雰囲気で意見出しができていたと思う。整理はあとでするのだからこの段階ではまだ感覚に基づく話でもいいのだ――そんなふうに皆がなんとなく思っていたのかは分からないが、<strong>否定したり反論したりは誰もしなかった</strong>。</p> <p>ざっくばらんでもいいから、とにかく思っていることを出せる。このような「酒場」的な雰囲気について、Wiiの企画担当であった玉樹真一郎はこう述べている。</p> <blockquote> <p>アイデアを出し合う場所では、多かれ少なかれ人はプレッシャーを感じているものです。</p> <p>そんな状態で「何でもいいから発言してください」と言われても、参加者は逆に困ってしまいます。現に、私自身が「怖い」と感じてしまっているぐらいです。...略...</p> <p>冒険の酒場にふさわしい雰囲気を作るところから、始めましょう。</p> <p>――<em>玉樹真一郎「コンセプトのつくりかた 「つくる」を考える方法」P104-105</em></p> </blockquote> <p>もちろんこの書籍で言われるコンセプトワークと私たちがやっていることとは少し異なるが、アイデアを出し合うという点では共通しており、やはり「酒場」的な雰囲気は大事だろう。</p> <p>幸い、<strong>普段から社内やメンバーの雰囲気が明るい組織であることも相まって、その場はすぐに</strong><strong>「冒険の酒場」へと姿を変えた</strong>。これは大きかった。</p> <p>また、もしかしたらここでも、いまは何をやるフェーズであるかの共通認識を持っていることが効いていたのかもしれない。<strong>あとで収束すると分かっているからこそ安心して発散できる</strong>、そんな側面もあったのかもしれない。</p> <h4 id="day-3-課題の整理-抽象Xは姿を変えるいや抽象Xの姿を変える">day 3: 課題の整理 ――抽象Xは姿を変える?いや抽象Xの姿を変える――</h4> <p>嬉しい悲鳴とはまさにこのこと。貼られた付箋の枚数は40枚ほどにのぼった。</p> <p>問題はどのように整理するのかであったが、私たちとしては、この後のステップで<strong>優先順位付けができる形になれば何でもよかった</strong>。そこでひとまず、この幾多の付箋を数個のグループに分け、グループごとの関係性を明らかにできればそれで十分ではないかと考えた。</p> <p>結論から言うとこれはうまく機能した。具体的には、<strong>一つ一つの付箋を丁寧に深堀りしながら、①課題の分類、②課題内容の精緻化を思いつく限りやっていった結果、良い構造化ができたのだ</strong>。思いつく限りというのは要するに「いまは分類の時間だ!」などと限定することなく、「これ似ているな」と思ったらすぐに分類してしまったり、「ところでさっきの付箋で気になることがあって」と好きなタイミングで精緻化してみたり。要するに<strong>自然な流れに任せた</strong>ということである。<strong>乱雑に散らばったアイデアを収束させていく作業は一定、発見的である</strong>から、定まった手順を踏むというよりはこのようなやり方が機能しやすいのかもしれない。もちろん分類も精緻化も、やってみた結果を眺めながらもう一度考えてみるという繰り返しが何度か必要だった。</p> <p>ここでは、それぞれの作業をどのように行ったかを具体的に書いてみる。</p> <h5 id="day-3-A-課題の分類">day 3-A: 課題の分類</h5> <p>これは<strong>似たようなものをまとめてみる</strong>ということである。</p> <p>まずは各付箋に書かれた事象について皆で理解する。そうすると割と似たような付箋があることに気づく。</p> <p>「自分を見つめ直して成長につなげられるようなフィードバックや評価がもっとあってもいいかも」<br />「普段から仕事のやり方や内容にもっとアドバイスがあると嬉しい」</p> <p>このような付箋同士は「成長のためのフィードバックが不足」のようなカテゴリーを作り、まとめることができる。</p> <p>ただ、これをやっただけではどのカテゴリーにも入らない単体がパラパラと残ってしまう場合もあるだろう。それらをそれぞれ独立した課題として扱うのも良いが、<strong>深堀りしてみると案外つながっていることが分かるときもある</strong>。すなわち、<strong>その課題の原因(なぜ起こっているのか)や影響(何を引き起こしているのか)を議論することで共通点が浮かび上がることもある</strong>のだ。エンジニアの開発現場に引き寄せた例で言えばこういうことである。</p> <p>「PRのレビューにめちゃ時間がかかる」<br />↓「なんでなんだろう??」<br />「そもそも時間が取りづらい気がする」<br />↓「なんでなんだろう??」<br />「誰か一人に集中しちゃうことがある。タイミングの問題かなあ」<br />↓「他の人はできないのか??」<br />「その部分はその人しか分からないことが多くて」</p> <p>「バグ修正が思っていた予定に間に合わないことがある」<br />↓「なんでなんだろう??」<br />「急に別のトラブルが発生して対応に追われることが多い」<br />↓「他の人に任せられないのか??」<br />「その部分は自分しか分からないことが多くて」</p> <p>現実にはもっと複雑なことも多いが、このように一見すると異なる問題が結びつくこともやはりある。「PRのレビューにめちゃ時間がかかる」という付箋と「バグ修正が思っていた予定に間に合わないことがある」という付箋を、「円滑な業務遂行に必要な知識の分散がなされていない」というカテゴリーの下に持っていけるのである。</p> <p>なお、上の例のように<strong>「なんでなんだろう?」という<a href="https://ja.wikipedia.org/wiki/%E3%81%AA%E3%81%9C%E3%81%AA%E3%81%9C%E5%88%86%E6%9E%90">five whys</a>にも通じるやり方で道を切り開けることが多い</strong>が、「結果、何が起きているんだろう?」という逆の問いかけも時には有効であろう。</p> <h5 id="day-3-B-課題内容の精緻化">day 3-B: 課題内容の精緻化</h5> <p>課題を分類しつつ、課題の内容への理解を深めることも非常に重要である。</p> <p>特にこのタイミングで、各課題について裏付けとなるファクトがあるのかを気にしてみても良いだろう。<strong>よくよく考えるとそこに書いてあることが単なる印象論だと分かることもある</strong>。例えば「会議の人数が多すぎる」という付箋を見たら「具体的にはいつのこと?」「いつもそうなの?」という問いかけをしてみよう。既に終わった単発プロジェクトでの話だったなら、そのプロジェクトの振り返りを別途してもらうとして、ここでは課題として取り上げなくてもよいはずだ。</p> <p>また、<strong>「解決策の裏返し」で語られている課題にも注意しよう</strong>。例えば先ほど出てきた「成長のためのフィードバックが不足」という課題カテゴリーは、この罠に陥っているかもしれない。このような問いかけをしてみよう。</p> <p>「成長のためのフィードバックが不足している」<br />↓「何がまずいの??」<br />「みんな仕事のやる気が出ていない」<br />↓「やる気が出ていない!?そんなことあるかなあ??」<br />「従業員のモチベーションアンケートがそれを示している」<br />↓「なるほど……そのアンケート結果からフィードバック不足が原因とまで分かるの?」<br />「うーん、自分とあの人はそう思っていたけど実際に確かめてはいないなあ」</p> <p>この会話によって下記の2点のことが明らかになった。</p> <ol> <li>実際に起きているのは、従業員のモチベーションの低下である</li> <li>モチベーション低下の原因がフィードバックの不足であるかは不明</li> </ol> <p>こうなると「成長のためのフィードバックが不足」という課題認識は妥当性を欠いていることになる。それは早とちりであるし、<strong>その認識のもとでは「従業員のモチベーション低下」という事象に対して、ソリューション案が「フィードバックを与える」ということだけに絞られてしまう</strong>のだ――<strong>モチベーション低下の原因がフィードバックの不足であるかは全くわからないのにもかかわらず</strong>。</p> <p>要するに<strong>「XXが存在しない」とか「XXが十分ではない」といった形式で語られる課題認識は、解決したい事象に対するソリューションの幅を、決め打ちで無根拠に狭めている可能性がある</strong>のだ。もちろん、念頭に置かれているソリューションは正しいかもしれない。ただ、本当に問題となっている状況やその本当の背景を正しく認識することに注意すべきである。「XXが不足している」という解決策の裏返しとして語られる課題に対して、私たちはその更に裏返しを読む必要があるのだ。</p> <p>ただ一方で、深堀りをしているうちに<strong>厳密さの囚人になってしまうことも避けたい</strong>。例えば次のようなフレーズを聞くことがある。</p> <p>「それは原因であって、課題ではない」<br />「それは単なる状況であって、問題ではない」</p> <p>至極当たり前だが、このような定義の厳密性は有用なときもあれば、全く役にたたないときも同様にあることに注意したい。</p> <p><strong>そもそも課題と原因を厳密に分けて考えるのは簡単ではない。</strong>「スクラムからはみ出しているプロジェクトが、ちゃんとマネジメントされていない」という事象は課題だろうか。それとも、これは「プロジェクトが遅延してしまう」ことの原因だろうか。もしくは本当の課題/原因は「すべてのプロジェクトをスクラムで管理していないこと」なのだろうか。もちろん課題と原因は全く異なる概念ではあるのだが、具体的な事象をどちらであるとみなすかについては微妙な判断になることが多々ある。</p> <p>事象を深ぼることはとても価値のあることである。先述のように事象間のつながりを見つけられることもあれば、そもそも事象の理解をするうえで不可欠な行為である。それと同じように、何が課題で何が原因かという議論も一定の共通認識を持つうえでは必要なことだ。</p> <p>しかし、<strong>私たちの課題分析が目指しているゴールはあくまでも「いま起きている悪い事象を取り除くこと」であり、物事を実際に前に進められなければ意味がないのだ</strong>。言葉を弄くり倒しても、事態が進展しないのなら無意味だ。この「課題内容の精緻化」ステップひいては多くのビジネス的な議論においては、<strong>ある程度の共通認識が揃えばそれで及第点、いや満点なのである</strong>。もちろんどこまで行ってもケースバイケースではあるが、先ほど例に挙げた問題も「スクラムからはみ出たプロジェクトが遅れがち」くらいでひとまずまとめておけばよい。このあとのステップで<strong>高い優先順位が付いたなら、そこで初めてもっと真面目に「原因」とやらの究明を始めればよいはずだ</strong>。</p> <p>厳密さは大事なときもあるが、特にグループで話し合っているような場合にそれを気にして議論活動が縮小してしまうのはもったいない。</p> <p>とにかく、このフェーズの目的は優先順位を付けるための整理である。<strong>適当な大きさで意味のある単位に課題をまとめることができれば、そしてその各課題の内容について共通見解を持っていれば、それで十分なはずだ。</strong>このあたりはバランス感覚が重要だと感じる。</p> <p>さて、このフェーズの結果として当初「開発組織の体制をどのように変えるべきか」と語られていた抽象Xは7つほどの小課題に形を変えた。下記は実際の結果をもとにしたイメージである。</p> <figure class="figure-image figure-image-fotolife mceNonEditable" title="整理された課題(イメージ)"> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20221218/20221218203218.png" width="1200" height="698" loading="lazy" title="" class="hatena-fotolife" itemprop="image" alt="各メンバーが課題だと感じる事象が書かれた付箋がグループ分けされ、各グループにグループ名が与えられているようなmiroホワイトボードの様子" /></p> <figcaption class="mceEditable">整理された課題(イメージ)</figcaption> </figure> <p>解決すべきことは言語化され、抽象Xの輪郭がくっきりとした。漠然とした抽象的課題はいまや一定の具体性を持った小課題にブレークダウンされ、それら小課題についてのしっかりとした共通認識も深堀りを通じて持つことができていた。</p> <h4 id="day-4-課題の優先順位付け-抽象Xを倒しきろうという無理難題">day 4: 課題の優先順位付け ――抽象Xを倒しきろうという無理難題――</h4> <p>ではさっそく解決策を議論しよう!といきたいところではあったが、ソリューションの議論にはまだ触れられない――いまだ<a href="https://www.youtube.com/watch?v=otCpCn0l4Wo">u can't touch this</a>である。</p> <p><iframe width="560" height="315" src="https://www.youtube.com/embed/otCpCn0l4Wo?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen="" title="M.C. Hammer - U Can't Touch This" id="widget2"></iframe></p> <p>疲れた頭をヒップホップで癒やせば明らかだ――いまここで<strong>必要なのは優先順位である</strong>。というのも先述の通り、抽象Xは今や議論可能な7つの小課題に分裂したが、<strong>すべてのことに同時に取り組めるほどのリソースを私たちが抱えているわけではないから</strong>だ。</p> <p>この段階では実直に、比較のための軸をいくつか言語化したうえで、各小課題について評価をしていった。振り返ってみてここで大事だったと感じるのは――当たり前なのだが――<strong>物事の根拠を確認しながら議論をすること</strong>(day 3-B セクションを参照)、そして<strong>軸の定義を明らかにしておくこと</strong>である。特に、分かりやすい言葉で明確な議論をする(「ふわっとした言葉」を極力使わないようにする)ことは、議論に参加しているメンバー数が多いこともあって大事だった。</p> <p>ところでこの優先順位付けのステップは、今回の課題解決フロー(下記)の中で明示的に意思決定をする初めてのフェーズであった。</p> <blockquote> <ol> <li>課題の特定</li> <li>課題の整理</li> <li>課題の優先順位付け</li> <li>ソリューション仮説の作成</li> <li>ソリューション仮説の検証・具体化</li> </ol> </blockquote> <p><strong>解決したい課題を選ぶことは、同時に解決しない課題を選ぶことでもある</strong>。優先順位付けは課題解決の流れを大きく規定するので、ここは特に慎重になった。また、これは組織全体を巻き込む意思決定でもあったため、他メンバーへの説明責任を果たせるようにしっかりと議論することも意識した。</p> <p>結果、主に影響範囲の広さや緊急性を鑑みて、私たちは「各チーム・部門がより迅速な意思決定をできるようにするために何をすべきか」という小課題のソリューションについて考えることにしたのであった。</p> <h4 id="day-5-ソリューション仮説の作成-抽象Xに対する具体Y">day 5: ソリューション仮説の作成 ――抽象Xに対する具体Y――</h4> <p>私たちはとうとう具体的なアイデアを考える段階にたどり着いた。</p> <p>このステップでは<strong>ソリューションの仮説をなるべく多く持つことが目的</strong>である。検証はあくまで次のステップでの話だ。発散と収束を切り分けよう。要するにここでは「こんなアイデアもある、あんなアイデアもある」という状態になればよい。</p> <p>なお、このとき私たちが検討対象としていた課題は先述の通り、「テックタッチにおいて各チーム・部門がより迅速な意思決定をできるようにするために何をすべきか」であった。些細なことではあるが、<strong>課題の書き方を「答えを出したい問い」の形に変えることで少し問題が考えやすくなっていたように思う</strong>。もともとは「各チーム・部門の意思決定が遅い」という文言で考え始めたが、「各チーム・部門がより迅速な意思決定をできるようにするために何をすべきか」という<strong>将来の目標(「各チーム・部門がより迅速な意思決定をできる」)を埋め込んだ文章にすることでイメージがしやすくなったように思う</strong><a href="#f-022837c0" name="fn-022837c0" title="加えて、末尾を「何をすべきか」とアクションを問う形に書くことも良いと言われる。理由はいくつかあるが、ここでは割愛">*4</a>。</p> <p>さて、このように課題の形を定めたうえでアイデアの発散に入った。付箋に思いつくソリューションを書いていく。ここまで来ると手慣れたもので、その後は課題の特定・整理と同様に、<strong>各付箋の深堀り・グルーピングという流れへと自然に移っていった</strong>。</p> <p>ところで、ソリューション案を出す際は取りこぼしがないよう、色々な切り口で考えてみるとよい。例えば「各チーム・部門がより迅速な意思決定をできるようにするために何をすべきか」という今回の例について、「チーム間連携時のコミュニケーションラインの確立」という解決策があったとする。このとき、これがチーム間のいわば水平的な話であるとして、では垂直の話はありうるかと発想してみる。そこで例えば「CTOを巻き込んだ意思決定フローの明文化」のようなアイデアが出れば儲けものである。このように、<strong>いろいろな軸を以てアイデア群を眺めることで網羅的に考えることが有用である</strong><a href="#f-ce6f9d2a" name="fn-ce6f9d2a" title="もちろんこれはあらゆるアイデア出しに有効である">*5</a>。</p> <p>さて、このようにして実際に多くの案が出せた。そこから似たようなソリューション同士はより本質的な新しい一つに置き換えてみたり、成功する見込みがないことが既に明らかなものは消してみたりを通じて、3つのソリューション案が残った。下記に、実際の議論の記録を一部掲載する(編集済み)。ちなみに、ここではバーバラ・ミント著「考える技術・書く技術」第2章「ピラミッドの内部構造はどうなっているのか?」あたりの内容を意識したメンバーもいた。</p> <figure class="figure-image figure-image-fotolife mceNonEditable" title="ソリューション仮説の整理結果(イメージ)"> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20221218/20221218222010.png" width="1200" height="567" loading="lazy" title="" class="hatena-fotolife" itemprop="image" alt="仮説の数と同じ行数かける4列で構成された表があるmiroホワイトボードの様子。すなわちソリューションの仮説を書いた付箋が縦に並んでおり、各仮説の横に内容の詳細、うまくいく根拠、実際の検証を行うかどうかの3つのことを書き並べている表。なお表の左横に、いま取り組んでいる課題を書いた付箋も貼られている" /></p> <figcaption class="mceEditable">ソリューション仮説の整理結果(イメージ)</figcaption> </figure> <p>ところで<strong>専門家の意見はこのフェーズにおいて非常に有用である</strong>。もちろん課題の特定や整理のフェーズでも話を聞けるなら聞きたいのだが、如何せんその段階ではこちら側もまだ状況を整理しきれていないことがある<a href="#f-b7a32aac" name="fn-b7a32aac" title="それでも巻き込めるならどんどん巻き込めばよいのだが、コストや時間の関係でコネクションが限られることもやはりある">*6</a>。一方で、<strong>この段階まで来るとチームの議論も地に足のついたものになっていることが多い</strong>はずで、<strong>知識が豊富な人間に適切な質問や深堀りがしやすくなっている</strong>。実際テックタッチでもこのくらいのタイミングで外部の組織コンサルティング会社に意見を伺い、素晴らしいインサイトを得られた。</p> <p>さて、こうした議論の果てに私たちは下記のソリューションを検討することにした。</p> <ul> <li>何かを決定するときに、誰とどういう流れで決定のステップを踏んでいけばよいかをルールとして定める</li> <li>各チームが責任を負っていることを明確にして、その決定権も与える</li> <li>チームの意見を取りまとめる人・チームへの問い合わせに最初に応える人を決める、チームの権限と責任の範囲内で実行を決定する</li> </ul> <h4 id="day-6-ソリューション仮説の検証-さらば抽象X後ろに戻る勇気をこめて">day 6: ソリューション仮説の検証 ――さらば抽象X、後ろに戻る勇気をこめて――</h4> <p>ここからはソリューションの仮説を検証する。課題を解決するためのアイデアが本当に機能するかを考えてみるのだ。この段階までくると、<strong>抽象的な課題との闘いという色合いも薄れ、通常の業務でも多く行われるような具体性の高い議論になってくる</strong>。</p> <p>例えば今回は「各チームが責任を負っていることを明確にして、その決定権も与える」「チームの意見を取りまとめる人・チームへの問い合わせに最初に応える人を決める、チームの権限と責任の範囲内で実行を決定する」といった仮説に対して、それらが実現されるような新しい組織設計を検討した。「チームはこう分けた方が良い」「各チームの権限はこうすると良さそう」といった議論を通じて、これらのソリューションが本当に機能するかを確かめたのだ。</p> <p>ところで、このような<strong>ソリューション具体化の過程において、これで絶対にうまくいくという確信を持つことは当然不可能である</strong>。異なる文脈でなされた哲学者ジジェクの発言がここでも当てはまる――「我々は常に、確固たる知識をもたずに人生を運命づけるような事柄について決定を下さなければならないという立場に置かれる」のだ。有限の時間の中で有限の情報とともに生きる私たちは、どこかで決断して前に進む勇気を奮わなければならない。</p> <p>一方で<strong>経過観察が重要な理由はここにある</strong>。組織課題に限った話ではないが、意思決定に対する振り返りは必須だ。テックタッチでも今回導入するソリューションが機能しているかの検証を、導入後6ヶ月で実施する運びになった。このように経過観察をして、<strong>もし失敗していたらやり直せばよい</strong>のだ。むしろ<strong>やり直す余地が残るようなソリューション設計ができるならそうすべきである</strong>。勇気は前に進むためだけにあるのではない。勇気は後ろを振り返り、後ろに進むためにも奮われるべきだ。</p> <h4 id="afterward-エピローグ-冒険のおわりにいやはじまりに">afterward: エピローグ ――冒険のおわりに……いやはじまりに――</h4> <p><span class="s1">このようにして「開発組織の体制をどのように変えるべきか」という漠然とした問いを具体化し、構造化し、最終的に具体的な策に落ち着けるに至った。<strong>この流れの中で登場した</strong></span><strong>レッスンを再度ここにまとめてみよう</strong>。</p> <ul> <li>day 1: 最初から最後までどう進めるかを明文化することで、メンバー全員がフェーズをきっちりと切り分けて常に議論の目的を共有することができる</li> <li>day 2: 発散のフェーズでは何も気にせず発散する。否定したり反論したりはあとにする</li> <li>day 3-A: 「なんでなんだろう?」「それで何が起きているのか?」をキーワードに課題を深ぼっていく</li> <li>day 3-B: 「解決策の裏返し」で語られている課題に注意する。一方で、言葉の定義に拘泥しない</li> <li>day 4: 解決したい課題を選ぶことは解決しない課題を選ぶことである</li> <li>day 5: いろいろな軸でアイデア群を眺め、網羅的なアイデア出しをする。遅くとも、ある程度状況が整理されてきた段階で専門家の意見を取り入れる</li> <li>day 6: ソリューション実施後、経過観察をする。やり直す余地をなるべく残しておく</li> </ul> <p>課題も状況も組織によって千差万別なのだから、一般化されたレッスンは抽象的にならざるを得ない。実際、このように書いてみるとどれも当たり前のことに思えてしまう。ただそうは言っても、今回の旅を振り返ると、<strong>基本をしっかりとおさえて議論を進めたことが成功の鍵だった</strong>実感がある。</p> <p>また、メンバーの当事者意識が強かったことも大きかった。どのステップのどんな話し合いのどんな些細なことであっても、皆が真剣に向き合っていた。<strong>組織全体に関わる話に多くのメンバーが関心をもって、積極的に議論する雰囲気がひとまずの怪物撃退につながった</strong>のだと思う。<strong>これは皆の、皆による、皆のためのチームなのだ</strong>。</p> <p>……</p> <p>さて、私たちが抽象Xと闘ったときの話はこれでおしまいだ。この記録が、今日も至るところで繰り広げられているだろう闘いのちょっとした助けになることを願う。</p> <p>だいぶ長くなってしまったが、そろそろ私たちのもとにまた次の抽象Xが現れる頃だろう。スタートアップの宇宙を旅する私たちは何回でも闘わなければならない。マクレディのように「もう少しここで待って、何が起きるか見てみよう」などと悠長なことを言っている余裕は全く無いのである。</p> <blockquote> <p><span style="text-decoration: line-through;">Why don't we just... wait here for a little while... see what happens?</span><br /><span style="text-decoration: line-through;"><span style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;">「もう少しここで待って、何が起きるか見てみよう」<em><br />―― MacReady, from The Thing(邦題: 遊星からの物体X)</em></span></span></p> </blockquote><div class="footnote"> <p class="footnote"><a href="#fn-1498c109" name="f-1498c109" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text">本旨が伝わりやすくなるよう、実際の時系列に対して編集を加えた記録です</span></p> <p class="footnote"><a href="#fn-82d5440a" name="f-82d5440a" class="footnote-number">*2</a><span class="footnote-delimiter">:</span><span class="footnote-text">「<a href="http://www.eijipress.co.jp/book/book.php?epcode=2085">イシューからはじめよ</a>」や「<a href="https://www.diamond.co.jp/book/9784478490273.html">考える技術・書く技術</a>」、はたまた「<a href="https://www.diamond.co.jp/book/9784478022399.html">コンセプトのつくりかた</a>」などなど</span></p> <p class="footnote"><a href="#fn-31fcacd4" name="f-31fcacd4" class="footnote-number">*3</a><span class="footnote-delimiter">:</span><span class="footnote-text">一部改変しています</span></p> <p class="footnote"><a href="#fn-022837c0" name="f-022837c0" class="footnote-number">*4</a><span class="footnote-delimiter">:</span><span class="footnote-text">加えて、末尾を「何をすべきか」とアクションを問う形に書くことも良いと言われる。理由はいくつかあるが、ここでは割愛</span></p> <p class="footnote"><a href="#fn-ce6f9d2a" name="f-ce6f9d2a" class="footnote-number">*5</a><span class="footnote-delimiter">:</span><span class="footnote-text">もちろんこれはあらゆるアイデア出しに有効である</span></p> <p class="footnote"><a href="#fn-b7a32aac" name="f-b7a32aac" class="footnote-number">*6</a><span class="footnote-delimiter">:</span><span class="footnote-text">それでも巻き込めるならどんどん巻き込めばよいのだが、コストや時間の関係でコネクションが限られることもやはりある</span></p> </div> techtouch JavaScriptランタイム事情 2022冬 hatenablog://entry/4207112889945392600 2022-12-18T11:38:33+09:00 2022-12-19T18:25:07+09:00 今年になって JavaScript の新しいランタイムである Bun をよく目にするようになりました。Node.js、Deno に続く JavaScript ランタイムの勢力となるわけですが、それぞれどのように違うのか、それぞれが生まれた背景やコンセプトから理解していこう!というのがこの記事の趣旨です。 <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20221215/20221215204506.png" alt="JavaScript &#x30E9;&#x30F3;&#x30BF;&#x30A4;&#x30E0;&#x4E8B;&#x60C5; 2022 &#x51AC;" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>テックタッチアドベントカレンダー 18 日目担当の yokochin です。</p> <p>今年になって JavaScript の新しいランタイムである Bun をよく目にするようになりました。</p> <p>Node.js、Deno に続く JavaScript ランタイムの新勢力となるわけですが、それぞれどのように違うのか、それぞれが生まれた背景やコンセプトから理解していこう!というのがこの記事の趣旨です。</p> <ul class="table-of-contents"> <li><a href="#Nodejs-開発の背景">Node.js 開発の背景</a><ul> <li><a href="#余談ブロッキングとSSR">余談:ブロッキングとSSR</a></li> </ul> </li> <li><a href="#Nodejs-の後悔と-Deno-の登場">Node.js の後悔と Deno の登場</a><ul> <li><a href="#Deno-のモジュールシステム">Deno のモジュールシステム</a></li> <li><a href="#そのほかの特徴">そのほかの特徴</a></li> </ul> </li> <li><a href="#Bun-の登場">Bun の登場</a><ul> <li><a href="#Bun-のパフォーマンス">Bun のパフォーマンス</a></li> <li><a href="#そのほかの特徴-1">そのほかの特徴</a></li> </ul> </li> <li><a href="#JS-ランタイムの互換性">JS ランタイムの互換性</a><ul> <li><a href="#Deno-の-Nodejs-互換">Deno の Node.js 互換</a></li> <li><a href="#Bun-の-Nodejs-互換">Bun の Node.js 互換</a></li> </ul> </li> <li><a href="#終わりに">終わりに</a></li> </ul> <h2 id="Nodejs-開発の背景">Node.js 開発の背景</h2> <p>Node.js は 2009 年にリリースされ、現在最も広く使われている JavaScript ランタイム(以降 JS ランタイム)です。</p> <p>Node.js 開発の最初のモチベーションは「ノンブロッキングなネットワークプログラミングをサーバー開発に熟知していないユーザーにも可能にすること」でした。 ノンブロッキングという概念が重要になりますが、その前に Node.js が採用しているイベントループという実行モデルについて説明します。</p> <p>イベントループとはメインスレッドと呼ばれる 1 つのスレッドがイベントを待ち受けていて、イベントを感知したら処理を実行し、実行が終わったらまたイベントを待ち受けるというループを回し続ける機構です。</p> <p>基本的にイベントループは 1 スレッドですべての処理を実行するのですが、サーバーにおいては 1 つの HTTP リクエストを処理している間にほかのリクエストに対応できないのでは使い物になりません。ここで I/O(ディスクやネットワークとのやりとり)を並列に実行できるノンブロッキングが重要になります。</p> <p>ノンブロッキングな処理は I/O の応答を待つ間にイベントループを動かし続けることができます。たとえば、サーバーから DB にクエリを発行すると DB からの応答を待つことになります。この待ち時間の間にイベントループを再開し、ほかのリクエストを受け付けるなど別の処理ができます。DB からの応答が返ってきたら、後続の処理を再開します。こうすることにより 1 スレッドでも待ち時間を持て余すことなく効率良く処理を実行できます。</p> <p>ノンブロッキングなプログラミングをサーバー開発に取り入れたかった背景は 2 つあって、1 つはサーバーの傾向として起動時間に対して I/O の待機時間の占める割合が大きいため 1 スレッドで効率良く処理できるノンブロッキング、イベントループと相性が良かった点。2 つ目は I/O を並列化するための技術(epoll や kqueue といったシステムコール)は 1980 年代からあったものの、どのプログラミング言語からも容易に使えるインターフェースがなく、実用的にサーバー開発で使うにはハードルが高かった点です。</p> <p>そこで Node.js はサーバーサイド開発でノンブロッキングなプログラミングを容易にするインターフェースとともにリリースされました。 <code>Promise</code> ( 現代は <code>async / await</code> ) のようなシンプルな構文でノンブロッキング、イベントループを利用できるのは実はありがたいことなんですね。</p> <p>イベントループをサーバーサイド開発に取り入れたいのであれば、新しいプログラミング言語を作るという手もありましたが、JavaScript を選んだのは I/O に関する定義がない ECMAScript を拡張することができ、かつ JavaScript の普及とともに Node.js を認知する機会を得ることができる、というメリットがあったからのようです。</p> <h3 id="余談ブロッキングとSSR">余談:ブロッキングとSSR</h3> <p>Node.js の背景から脱線しますが、最近のトレンドを踏まえてノンブロッキングをもう少し掘り下げてみます。</p> <p>前述の通り Node.js はノンブロッキングによって I/O の待機時間を利用して 1 スレッドで効率よく処理できますが、 I/O ではなく CPU 負荷の高いスレッドを専有される処理には弱いです。 たとえば <code>JSON.parse()</code> で巨大な JSON をパースするとイベントループはブロッキングされて、その間はほかのタスクが実行できなくなってしまいます。</p> <p>最近では Next.js などで一般的になった SSR(サーバーサイドレンダリング)ですが、コンポーネントをまとめあげ HTML としてレンダリングするという処理はイベントループをブロッキングする処理であるため、実は Node.js が得意な領域ではありません。にもかかわらずここまで SSR が発展しているという現状はおもしろいですね。</p> <p>実行効率よりもフロントエンドとサーバーサイドのコンテキストスイッチを少なく開発できるという開発体験が重要視されている点と、この弱点を補えるスケーラブルな実行環境(Vercel のようなエッジでスケールする環境や Kubernetes などのコンテナオーケストレーションによる水平スケーリングが容易な環境)が整ってきていることが背景にある気がします。</p> <h2 id="Nodejs-の後悔と-Deno-の登場">Node.js の後悔と Deno の登場</h2> <p>Deno は Node.js の開発者である Ryan Dahl 氏によって開発が進められている JS ランタイムで、 2018/08 のバージョン 0.1.0 からおよそ 2 年の開発を経て、 2020/05にバージョン 1.0 に到達しました。</p> <p>Node.js を開発した本人がなぜ新しい JS ランタイムを作ろうとしたのか。JSConf EU 2018 で Ryan Dahl 氏は Node.js の 10 の後悔を述べていて、最も大きな後悔としてモジュールシステムの失敗を挙げています。</p> <h3 id="Deno-のモジュールシステム">Deno のモジュールシステム</h3> <p>Node.js が登場したときにはモジュールシステムの仕組みがなかったため、CommonJS と呼ばれるモジュールシステムを採用しましたが、後に ES6 で標準化されたモジュールシステム(ES Module)は CommonJS とは異なるものでした。</p> <p>ES Moduleでは</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span> myFunction <span class="synIdentifier">}</span> from <span class="synConstant">'./libs/mymodule.js'</span> </pre> <p>のように <code>import ~ from …</code> の構文で拡張子まで含めた明示的なパスを要求しますが、CommonJS では</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">const</span> <span class="synIdentifier">{</span> myFunction <span class="synIdentifier">}</span> = require(<span class="synConstant">'mymodule'</span>) </pre> <p>のように <code>require</code> で外部スクリプトの識別子を要求し、内部的に <code>mymodule</code> が node_modules 配下にあるかなどを解決しにいきます。このように構文としてもモジュール解決の仕組みとしても ECMAScript の進化から Node.js は外れてしまっている、ということを後悔として挙げています。</p> <p>そもそも外部のスクリプトを使いたいのであれば、ES Moduleのように直接スクリプトのパスを指定すれば良く、ディレクトリに「モジュール」という新しい概念を与える必要はなかったし、それを管理するための package.json や npm リポジトリさえ生み出してしまうことはなかったと述べています。(今振り返ってもっとこうしたかった、というのはわかりますがここまで npm のエコシステムが大きくなっている現状を見ると少し悲観的すぎる気はしますが。)</p> <p>npm リポジトリにはすでに多くの資産があり、後戻りすることはできないため、JS ランタイムを 1 から作り直そうというのが Deno 開発のモチベーションになりました。</p> <p>リリース当初の Deno は既存の npm との互換性を切り捨てていて、モジュールへの依存も以下のように直接スクリプト内パスを指定する方式をとりました。</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">import</span> * as R from <span class="synConstant">&quot;https://deno.land/x/ramda@v0.27.2/mod.ts&quot;</span>; </pre> <p>外部のスクリプトを利用するのに npm, package.json, node_modules は不要で、 <code>deno run</code> でスクリプトを実行すると依存するモジュールがダウンロードされます。</p> <h3 id="そのほかの特徴">そのほかの特徴</h3> <p>Node.js の後悔の 1 つにセキュリティの問題を上げていました。Node のプログラムを実行するとあらゆるリソース(ファイルシステムやネットワーク)にアクセスできてしまいます。ですが、 linter を使うのにネットワークアクセスやファイルの書き込み権限は不要なはずです。そこで deno では実行時に</p> <ul> <li><code>--allow-net</code>(ネットワークアクセスの許可)</li> <li><code>--allow-write</code>(ファイル書き込み権限の許可)</li> </ul> <p>のようなオプションをつけない限りはリソースへアクセスできないようにすることで、サードパーティのスクリプトを安全に実行できる機構を提供しています。</p> <p>また、Node.js との違いとしてはデフォルトで TypeScript をサポートしている点です。TypeScript のコンパイルがなくとも <code>deno run</code>で直接 TypeScript を実行できます。</p> <h2 id="Bun-の登場">Bun の登場</h2> <p>Node.js や Deno と違って Bun は課題を起点に生まれたというよりは、Node.js や Deno を Bun に置き換えるという意欲から出発しています。</p> <h3 id="Bun-のパフォーマンス">Bun のパフォーマンス</h3> <p>後発で登場するからには Node.js から乗り換えてもらうメリットがないといけないわけですが、Bun はとにかくパフォーマンスを押しています。</p> <p><a href="https://bun.sh/">Bun の公式サイト</a>のトップページを見ると 2022/12/14 時点では React の SSR が Node.js の 4 倍以上速いというベンチマークの結果を載せています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20221215/20221215205443.png" alt="" width="417" height="398" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>この SSR のベンチマークでは <a href="https://github.com/oven-sh/bun/blob/b0a7f8df926e91d3b2f0b3b8833ddaf55073f30e/bench/react-hello-world/react-hello-world.jsx#L14-L21">Hello world レベルの 1 コンポーネントしかない</a>ので、React で SSR する部分はわずかで、stream を使ってレスポンスを返す部分がベンチマークスクリプトの重たい部分となっています。この stream の実装が Node.js と Deno は JavaScript で実装されていて、Bun は Zig で実装されているため、言語の違いでパフォーマンスの差が出ていると考えられます。数十〜数百のコンポーネントを使ったケースでは Node.js、Bun ともに JavaScript による処理の比重が大きくなるため、4 倍ほどの差は開かないと思います。</p> <p>Bun はとにかく知名度を上げて使ってもらうためにも勝てるフィールドでのベンチマーク結果を押し出していっているという状況です。</p> <h3 id="そのほかの特徴-1">そのほかの特徴</h3> <p>公式サイトで「all-in-one JavaScript runtime」を謳っているように、ランタイムでありながらもバンドラ、トランスパイラなどが付属してきます。TypeScript のトランスパイラも付いているため、Deno 同様 TypeScript を実行できます。</p> <p>また、npm クライアントを内蔵しているため npm パッケージを利用することもできます。</p> <h2 id="JS-ランタイムの互換性">JS ランタイムの互換性</h2> <p>さてここまで 3 つの JS ランタイムの背景やコンセプトについてお伝えしました。</p> <p>さっそく既存の Node.js プロジェクトから移行して試したくなりますが、Deno、Bun は Node.js とまったく同じ構文を解釈、実行できるわけではないので簡単には移行できません。とはいえ、Deno、Bun としては Node.js の利用者に使ってほしいので移行コストを下げる努力をしています。</p> <h3 id="Deno-の-Nodejs-互換">Deno の Node.js 互換</h3> <p>Deno のモチベーションの 1 つは Node.js で失敗したモジュールシステムを作り直すことでしたが、2022/11/14 v1.28 で正式に npm をサポートすることになりました。やはりこれまでの npm パッケージを使いたいという多数の声は無視できなくなったようです。競合である Bun が登場とともに npm サポートを謳っていたのも背景にあるとは思います。</p> <p>Deno v1.28.3 では Node.js の API の互換性のために <a href="https://deno.land/manual@v1.28.3/node/std_node"><code>std/node</code> という組込みライブラリ</a>があり、これが Node.js API の <code>fs</code> や <code>http</code>などの Polyfill となるようです。また、CommonJS のモジュールを Deno に読み込んでくれるようです。</p> <h3 id="Bun-の-Nodejs-互換">Bun の Node.js 互換</h3> <p>前述の通り npm パッケージは利用できます。 Bun v0.3.0 時点では <a href="https://github.com/oven-sh/bun#node-api-napi">Node.js の API は 90%ほど実装している</a>と言っています。 ES Module、CommonJS 両方にもサポートしているようです。</p> <h2 id="終わりに">終わりに</h2> <p>Deno、Bun の互換性を上げる取り組みは絶賛進行中なので既存のプロジェクトを置き換えるハードルはまだ高そうです。そのため Node.js がシェアを占める状態はすぐには変わらないと思いますが、新規プロジェクトについては Deno、Bun の導入を検討して見ても良いかもしれません。</p> <p>私自身はちょっとしたスクリプトであれば Deno を使って書くことがありますが、やはりゼロコンフィグで TypeScript が使える、package.json や node_module で汚れない、スクリプト 1 枚で共有できるなどメリットは大きいので新しい JS ランタイムの方が Node.js を上回るシーンは出始めていると実感します。</p> <p>継続的に JS ランタイム事情をウォッチしつつ、適材適所で使い分けていきたいですね!</p> <hr /> <p>参考</p> <ul> <li><a href="http://fukabori.fm">fukabori.fm</a> <ul> <li><a href="https://fukabori.fm/episode/82">https://fukabori.fm/episode/82</a></li> <li><a href="https://fukabori.fm/episode/83">https://fukabori.fm/episode/83</a></li> </ul> </li> <li>Node.js に関する 10 の反省点 <ul> <li><a href="https://www.youtube.com/watch?v=M3BM9TB-8yA">https://www.youtube.com/watch?v=M3BM9TB-8yA</a></li> <li><a href="https://yosuke-furukawa.hatenablog.com/entry/2018/06/07/080335">https://yosuke-furukawa.hatenablog.com/entry/2018/06/07/080335</a></li> </ul> </li> <li>Ryan Dahl: Original Node.js presentation <ul> <li><a href="https://www.youtube.com/watch?v=ztspvPYybIY">https://www.youtube.com/watch?v=ztspvPYybIY</a></li> </ul> </li> <li>東京 Node 学園祭 2011 <ul> <li><a href="https://www.publickey1.jp/blog/11/nodejs_node.html">https://www.publickey1.jp/blog/11/nodejs_node.html</a></li> <li><a href="https://www.nicovideo.jp/watch/1320669612">https://www.nicovideo.jp/watch/1320669612</a></li> </ul> </li> <li>Big Changes Ahead for Deno <ul> <li><a href="https://deno.com/blog/changes">https://deno.com/blog/changes</a></li> </ul> </li> </ul> techtouch useSyncExternalStoreを使ってみた hatenablog://entry/4207112889945290420 2022-12-17T12:22:01+09:00 2022-12-17T12:22:01+09:00 テックタッチアドベントカレンダー17日目を担当する kenshin です。 今年もあと少しで終わりですね。今年を振り返るのために2022年に食べたラーメンの杯数を数えてみると121杯(執筆時点)でした。年末までにあと何杯食べられるかな。 さて、今回は React18 で追加された useSyncExternalStore を使ってみました。 useSyncExternalStore とは 利用場面 使い方 実際に使ってみよう 最後に useSyncExternalStore とは React18 で新たに追加された React フックです。 React コンポーネントの多くは props、st… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20221215/20221215172019.png" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><a href="https://qiita.com/advent-calendar/2022/techtouch">テックタッチアドベントカレンダー</a>17日目を担当する kenshin です。 <br/>今年もあと少しで終わりですね。今年を振り返るのために2022年に食べたラーメンの杯数を数えてみると121杯(執筆時点)でした。年末までにあと何杯食べられるかな。</p> <p>さて、今回は React18 で追加された useSyncExternalStore を使ってみました。</p> <ul class="table-of-contents"> <li><a href="#useSyncExternalStore-とは">useSyncExternalStore とは</a></li> <li><a href="#利用場面">利用場面</a></li> <li><a href="#使い方">使い方</a></li> <li><a href="#実際に使ってみよう">実際に使ってみよう</a></li> <li><a href="#最後に">最後に</a></li> </ul> <h2 id="useSyncExternalStore-とは">useSyncExternalStore とは</h2> <p>React18 で新たに追加された React フックです。</p> <p>React コンポーネントの多くは props、state、context からデータを参照します。 <br/>ただし、React 外部のデータソースから値を参照する必要がある場合も存在します。 <br/>このような場合、 useSyncExternalStore を使うことによって、 React 外部のデータソースのストアを監視し、ストアの値を参照できます。</p> <h2 id="利用場面">利用場面</h2> <p><a href="https://beta.reactjs.org/apis/react/useSyncExternalStore#:~:text=Third%2Dparty%20state,to%20its%20changes.">React Docs BETA</a>に、 useSyncExternalStore をどのような場面で利用すればよいかが記載されていました。</p> <blockquote><p>①React の外部で状態を管理するライブラリを開発するとき(ライブラリ製作者向け)<br/>②ブラウザーのイベント API をサブスクライブするとき</p></blockquote> <p>基本的には useState、useReducer (または状態管理ライブラリ)を使って状態管理すべきで、もし上記ケースに該当する場合は useSyncExternalStore の利用を検討してみてもよいのかもしれません。</p> <h2 id="使い方">使い方</h2> <p>コンポーネントのトップレベルで、 useSyncExternalStore を呼び出します。 <br/> useSyncExternalStore の引数には、ストアが変更されるたびに呼び出されるコールバックを登録するための subscribe 関数、ストアの現在の値を返す getSnapshot 関数、そしてオプションで getServerSnapshot を渡します。 <br/> getServerSnapshot はサーバーレンダリング時にスナップショットを返すための関数です。</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span> useSyncExternalStore <span class="synIdentifier">}</span> from <span class="synConstant">'react'</span> <span class="synStatement">import</span> <span class="synIdentifier">{</span> subscribe, getSnapshot, getServerSnapshot <span class="synIdentifier">}</span> from <span class="synConstant">'./externalStore.js'</span> <span class="synIdentifier">function</span> ExternalStore() <span class="synIdentifier">{</span> <span class="synStatement">const</span> snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?) <span class="synComment">// snapshot: ストアの値</span> <span class="synComment">// subscribe: ストアが変更されるたびに呼び出されるコールバックを登録するための関数</span> <span class="synComment">// getSnapshot: ストアの現在の値を返す関数</span> <span class="synComment">// getServerSnapshot: サーバーレンダリング時にスナップショットを返すための関数</span> <span class="synComment">// ...</span> <span class="synIdentifier">}</span> </pre> <h2 id="実際に使ってみよう">実際に使ってみよう</h2> <p>今回は利用場面②ブラウザーのイベント API をサブスクライブするケースで、 resize イベントを監視し、ビューポートの幅を取得してみたいと思います。</p> <p>まず、 resize イベントをサブスクライブする subscribe 関数とビューポートの幅を返す getSnapshot 関数を用意します。 <br/>getSnapshot 関数は、 window.innerWidth から取得した値を返します。 <br/>subscribe 関数は、引数の callback をイベントリスナーに登録し、登録したイベントリスナーをクリーンアップする関数を返します。</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">const</span> getSnapshot = () =&gt; <span class="synIdentifier">{</span> <span class="synStatement">return</span> <span class="synStatement">window</span>.innerWidth; <span class="synIdentifier">}</span>; <span class="synStatement">const</span> subscribe = (callback) =&gt; <span class="synIdentifier">{</span> <span class="synStatement">window</span>.addEventListener(<span class="synConstant">&quot;resize&quot;</span>, callback); <span class="synStatement">return</span> () =&gt; <span class="synStatement">window</span>.removeEventListener(<span class="synConstant">&quot;resize&quot;</span>, callback); <span class="synIdentifier">}</span>; </pre> <p><br/><br/> 用意した2つの関数(subscribe、getSnapshot)を useSyncExternalStore の引数に渡すことで、 window.innerWidth の値を読み取り、変更をサブスクライブできます。</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">const</span> ViewportWidth = () =&gt; <span class="synIdentifier">{</span> <span class="synStatement">const</span> snapshot = useSyncExternalStore(subscribe, getSnapshot); <span class="synStatement">return</span> ( &lt;div&gt;<span class="synIdentifier">{</span>snapshot<span class="synIdentifier">}</span>&lt;/div&gt; ); <span class="synIdentifier">}</span> </pre> <p><br/><br/> 実際にビューポートの幅を変更してみると、snapshot の値が変わっているのがわかります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20221215/20221215134033.gif" width="503" height="219" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="最後に">最後に</h2> <p>今回は React18 で追加された useSyncExternalStore を試してみました。 <br/>useSyncExternalStore を使うことで、React コンポーネント内で外部の変更をサブスクライブし、値を参照できました。</p> <p>今回は、ブラウザのイベント API をサブスクライブするところまでしか試せていませんが、今後は useSyncExternalStore を使って、簡易な状態管理ライブラリも実装してみたいです。</p> <p>最後まで読んでいただきありがとうございました。</p> techtouch シナリオ作成のすゝめ hatenablog://entry/4207112889944447709 2022-12-16T09:00:00+09:00 2023-12-15T09:42:51+09:00 デザインチームでデザインプロセスの定義をしました。 <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20221212/20221212160014.png" alt="&#x30B7;&#x30CA;&#x30EA;&#x30AA;&#x4F5C;&#x6210;&#x306E;&#x3059;&#x309D;&#x3081;" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><a href="https://qiita.com/advent-calendar/2022/techtouch">テックタッチアドベントカレンダー</a>16日目を担当する、デザイナーの keita です。</p> <p>私の日課のひとつが NBA 観戦なんですが、今シーズン<a href="https://youtu.be/Tc-fq8CzAs4">大活躍中の渡邊雄太選手</a>から目が離せません!ネッツの背番号18のジャージ買って、一緒にブルックリンに応援しに行く人いませんか?</p> <h2 id="はじめに">はじめに</h2> <p>これまでの我々の開発プロセスは、兎にも角にも競合調査が中心でした。</p> <p>ユーザーの課題を解決するためにはどういうソリューションが必要なのかを検討し、要件が薄ら見え始めた頃、競合他社には同じような機能がないかを調査します。そして UI や技術要件などが固まったら開発スタート!という流れでした。</p> <h2 id="課題">課題</h2> <p>これまでのプロセスで特に問題ないと思っていたんですが、自分たちが「こうだ!」と自信満々にリリースした機能が、ユーザーの利用シーンを想定しきれていなかったためファーストリリースから芯を食った機能にできなかった、ということがありました(詳細な機能について記載できなくてごめんなさい🙇‍♂️)。</p> <p>もちろん、Nice to have なフィードバックがあれば折を見て追加開発するんですが、この機能の場合はそもそも設計の段階から少しズレてしまっていたため、次回の改修で大幅な変更が必要になりそうな気がしています。</p> <p>これの原因のひとつが、自分たちが「テックタッチ」ユーザーとして機能を使きれていないため検証が不十分だった、という点です。</p> <p>「ドッグフーディングしっかりやろうぜ!」だったり「ユーザー理解しようぜ!」も策としてはあると思うんですが(もちろん別で進行中)、今回は「検証プロセスをテコ入れしようぜ!」になりました。</p> <p><strong>補足</strong></p> <p>一般的なSaaSであれば、新機能や機能改善は実装が終われば週単位や月単位でリリースできると思うのですが、「テックタッチ」に関してはユーザーの利用するシステム上で動作するという特性があるためバグは許されず、そのリスクを減らすために「リリース頻度を減らし、品質が高い状態を維持しながらリリースする」というサイクルをとっています。</p> <p>なので、一度リリースしてユーザーの利用状況や反応を見てから機能改善する、というサイクルが回しづらいため、手前でいかに検証し切れるかが味噌になってきます。</p> <h2 id="シナリオとは">シナリオとは?</h2> <p>情報設計のなかでカスタマージャーニーマップの作成というプロセスがありますが、それと似て非なるものだと思っています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fja.wikipedia.org%2Fwiki%2F%25E3%2582%25AB%25E3%2582%25B9%25E3%2582%25BF%25E3%2583%259E%25E3%2583%25BC%25E3%2582%25B8%25E3%2583%25A3%25E3%2583%25BC%25E3%2583%258B%25E3%2583%25BC" title="カスタマージャーニー - Wikipedia" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://ja.wikipedia.org/wiki/%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%9E%E3%83%BC%E3%82%B8%E3%83%A3%E3%83%BC%E3%83%8B%E3%83%BC">ja.wikipedia.org</a></cite></p> <p>カスタマージャーニーマップは、サービスに触れる前からその後までの一連の道筋でどのタッチポイントでどういうコミュニケーションをとる必要があるのかを整理するのが目的だと思うのですが、我々の「シナリオ」は少し違います。</p> <p>※カスタマージャーニーマップも、ペルソナの行動と感情のシナリオですが、今回は違うものだとわかりやすくするために、敢えて「シナリオ」と呼んでいます。</p> <h3 id="カスタマージャーニーマップとの違い">カスタマージャーニーマップとの違い</h3> <p>シナリオの場合は、あくまでも実装予定の機能とそれに関係する機能についてのみ深掘りしていきます。</p> <p>ペルソナが、どういう状況下で「テックタッチ」の機能を利用し、誰とどのように連携し運用していくのか。これを想定できる数だけ作成します。シナリオの途中で別のシナリオに分岐したり、そこからまた元のシナリオに戻ったりと、道がいくつもできるイメージです。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20221212/20221212154322.jpg" alt="&#x30B7;&#x30CA;&#x30EA;&#x30AA;&#x306E;&#x30A4;&#x30E1;&#x30FC;&#x30B8;" width="1200" height="594" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>もちろん、ペルソナについての理解がないとシナリオは書けないので、そこについてはカスタマージャーニーマップと変わらないとは思いますが。</p> <p>このシナリオが固まり違和感がないことを確認できたら、それをもとにUIを作成します。</p> <p>そして、作成した UI をシナリオ通りに操作したとき、ペルソナの目的が達成できるかどうかを検証します。途中で操作に詰まったり、シナリオに書いてあることが実現できない場合は、UI が要件を満たせていないということなので再度設計しなおします。</p> <h3 id="メリット">メリット</h3> <p>なんとなく頭の中で考えていたものを目に見えるようにすることで、UI を作成する際に外してはいけないポイントを洗い出しやすくなりました。</p> <p>今までは「あー、その観点あったね」みたいな会話を、実装が終わってステークホルダー交えてのレビューでしていたんですが、それがもっと手前で、かつ自分たちで気づきやすくなったと思います。</p> <p>「プロトタイプでレビューしてもらえばいいじゃん」という意見もあると思いますが、ユーザーの行動パターンはその手前でデザイナーができる限り把握しておくべき、という強い意志と、プロトタイプ作成コストの兼ね合いでこのプロセスを試しています。</p> <p>かんたんな機能であれば Figma を見せるだけで伝わると思うんですが、複雑な機能になるとペーパープロトや Figma のプロトタイピングだと「よくわからない」という声もあり。結局のところ、動くものを触ってみないとよくわからないので、その動くものの確度をあげるための「シナリオ」というわけです。</p> <p>なので「シナリオ」+「(実際に動く)プロトタイプ」を作成しステークホルダーのレビュー、そのあとに本実装だったら完璧なプロセスかもしれません。</p> <h3 id="デメリット">デメリット</h3> <p>作成のコストがかかるので、小さな機能や改修には向かないと思います。大きめの新機能や、複数の機能が複雑に絡み合うものなどを設計するときには合っているのかなと。</p> <h2 id="最後に">最後に</h2> <p>シナリオ作成自体、正直なところまだ一周も回しきってはいないのですが、このようなデザインプロセスをチームで定義することはとても重要なことだと認識できました。</p> <p>デザインプロセスを定義しておくことで、属人化が避けられ、一貫性をもったユーザー体験を提供できるのではないかと考えるようになりました。</p> <p>さらに「テックタッチのデザインチームは、こういうプロセスでプロダクトデザインしているんだ」と社内で認識してもらうことができれば「この情報はデザインチームに渡しておかないと!」のように、組織がデザインをうまく利用するきっかけにもなるのではないかと思っています。</p> <p>プロダクトや組織の数だけデザインプロセスはあると思いますが、より価値ある体験を提供できるよう今後も我々にあったやり方を模索していければいいなと思っています。</p> techtouch dbt の導入、毎日30分の輪読会でチームに浸透させる hatenablog://entry/4207112889944865791 2022-12-15T09:00:00+09:00 2023-12-15T09:47:49+09:00 テックタッチアドベントカレンダー15 日目担当の teru です。今年の個人的ベスト家電はスマートフォンで見れるネットワークカメラでした。子ども達が寝室で寝ている様子を確認しながら家事ができるのでとても便利です。 きっかけ 輪読会の準備 輪読会の実施 やってみてどうだったか 良かった点 気になった点 終わりに きっかけ 13 日目の記事 でも触れているように、弊社でデータ分析基盤のモデリング用途に dbt の利用が始まりました。私の所属する分析運用チームでもこの流れに乗って、dbt を使って分析業務に関わる範囲のデータテーブル構築を自分たち自身で行えるように取り組むことにしました。 分析運用チ… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20221214/20221214113733.png" alt="dbt&#x306E;&#x5C0E;&#x5165;&#x3001;&#x6BCE;&#x65E5;30&#x5206;&#x306E;&#x8F2A;&#x8AAD;&#x4F1A;&#x3067;&#x30C1;&#x30FC;&#x30E0;&#x306B;&#x6D78;&#x900F;&#x3055;&#x305B;&#x308B;" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><a href="https://qiita.com/advent-calendar/2022/techtouch">テックタッチアドベントカレンダー</a>15 日目担当の teru です。今年の個人的ベスト家電はスマートフォンで見れるネットワークカメラでした。子ども達が寝室で寝ている様子を確認しながら家事ができるのでとても便利です。</p> <ul class="table-of-contents"> <li><a href="#きっかけ">きっかけ</a></li> <li><a href="#輪読会の準備">輪読会の準備</a></li> <li><a href="#輪読会の実施">輪読会の実施</a></li> <li><a href="#やってみてどうだったか">やってみてどうだったか</a><ul> <li><a href="#良かった点">良かった点</a></li> <li><a href="#気になった点">気になった点</a></li> </ul> </li> <li><a href="#終わりに">終わりに</a></li> </ul> <h2 id="きっかけ">きっかけ</h2> <p><a href="https://tech.techtouch.jp/entry/dbt-elementary-tutorial">13 日目の記事</a> でも触れているように、弊社でデータ分析基盤のモデリング用途に <a href="https://www.getdbt.com/">dbt</a> の利用が始まりました。私の所属する分析運用チームでもこの流れに乗って、dbt を使って分析業務に関わる範囲のデータテーブル構築を自分たち自身で行えるように取り組むことにしました。</p> <p>分析運用チームには、この記事を書いている時点で私を含む 2 名が在籍していました。二人とも BI ツールを用いて SQL クエリを書いたりレポートを作成したりといったデータアナリストの業務経験はありましたが、dbt についてはまったく経験がない状態でした。データ基盤チームのメンバーから dbt の説明とハンズオンを受け、とりあえず使い始められる状態にはなりましたが、さてこれから dbt をどう使っていこうかと全体設計や移行計画、開発フローを考えなければならない状況となりました。</p> <p>大まかな開発・運用・デプロイフローはデータ基盤チームが考えてくれていたのでそれに乗っかることとして、既存の DWH、データマートを dbt に移行していく手順については dbt のドキュメント、特に<a href="https://docs.getdbt.com/guides/best-practices">ベストプラクティス</a>などを読みながら進めていけばなんとなくはできそうに感じました。しかし、これまでの before dbt な世界のデータ基盤構築において中間テーブルの依存階層が深すぎたり計算ロジックが入り乱れていたり、データレイヤやグルーピングの概念があいまいであったりなど自ら苦労する技術的負債を生み出していた現状のことを思い出し、オフィシャルなドキュメントの内容を正しく理解して dbt の思想に準じた洗練されたデータ基盤を構築すべきであると思い直しました。</p> <p>加えて、チームとして dbt の理解をきちんとせずに進めた場合、以下のような課題が生まれる懸念がありました。</p> <ul> <li>メンバー間で dbt やデータ基盤の設計、実装するコードの基本構造についてなどの認識の差やずれがあると、各々で異なる構造や思想の SQL クエリやコードを書いてしまう</li> <li>コードレビュー時にも衝突が発生してしまう</li> <li>全くコードを書いて手を動かさずにドキュメントだけを読んでも、理論と実践のサイクルを回すことができず認識が深まらない</li> <li>現場の開発体験や運用に沿わない設計をしてしまう</li> </ul> <p>これらの課題をクリアしながら進める方法をチーム内で相談した結果、「<strong>dbt のベストプラクティスや業務上で掘りたくなったトピックをピックアップして、1 日 1 ページ 30 分で輪読会をやろう</strong>」と進めることにしました。</p> <h2 id="輪読会の準備">輪読会の準備</h2> <p>チームで dbt の理解を深めていこう、と考えた時から輪読会が適していそうだな、とは思ったのですが、一方で準備が大変だったりして開催が途絶えてしまったり誰かが置いて行かれてしまったり、期間が開いてモチベーションが下がったりしてはもったいないなと思いました。そのため、運用と効果のバランスを考えて以下の内容としました。</p> <ul> <li>1 日 1 ページだけやる(分量が多い場合は分割も可)</li> <li>30 分でコンパクトに納める</li> <li>記事を読んで、ほぼ直訳で訳すだけ。サマリ作成などはしない</li> <li>発表は当番制(2 人なので交互)</li> <li>平日に毎日やる。リスケはありだがスキップはできるだけしない</li> </ul> <p>弊チームの場合、開発フローはカンバン方式で、まだ結成してからの日にちも浅くメンバーの疎通向上を目的として朝会と昼会を毎日開催していたので、昼会の後に実施をするのがちょうど良さそうだったので Google カレンダーに繰り返しの予定として登録しました。</p> <p><figure class="figure-image figure-image-fotolife" title="dbt 勉強会、毎日昼に開催"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20221214/20221214004740.png" width="1044" height="136" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>dbt 勉強会、毎日昼に開催</figcaption></figure></p> <p>Notion にも輪読会のページを作成し、向こう 2 週間の当番表も埋めました。退路は絶つスタイル。</p> <p><figure class="figure-image figure-image-fotolife" title="輪読会の当番表"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/techtouch/20221214/20221214004829.png" width="1121" height="1200" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>輪読会の当番表</figcaption></figure></p> <h2 id="輪読会の実施">輪読会の実施</h2> <p>発表の当番は開始時間までに記事の翻訳を Notion に書いておき、時間になったらオンラインで集合して内容を読み上げる形で発表していきます。(個人的には、翻訳が間に合わない時は原文のまま読むとか訳しながら読むとかでもよいやという気持ちで臨んでいますが、幸いなことに今のところはきちんと記事を準備して開催できています)</p> <p>基本は斜め読みしながら重要なところをじっくり読んだり、前と被る部分は読み飛ばしたり、外部リンクや動画は開いて見て気になるところは確認してみたり。業務と絡む部分や実体験と重なる部分(あるいは異なる部分)など感想も交えたりしながら読み進めていって、最後まで読んでコメントを共有したら一日分の開催は終了となります。</p> <p>記事を読む中でこのページは深堀りしたいとか、最近の業務でこの機能を使ったので知りたいなど新しいトピックが見つかると当番表に追加したり翌日に差し込んだりして適宜内容を追加していっています。</p> <h2 id="やってみてどうだったか">やってみてどうだったか</h2> <p>この記事を書いている時点でまだ実施 3 週目突入したばかりですが、概ね期待通りの効果が出ていると感じています。</p> <h3 id="良かった点">良かった点</h3> <ul> <li>dbt のドキュメントがデータ分析運用の業務に即していて、整理もされており読みやすい</li> <li>無理なく理解しながら業務の進行とバランスを取りながら進められる</li> <li><strong>公式などのドキュメントを根拠にした建設的なディスカッションが行える</strong></li> </ul> <p>短期集中タイプの人やもともとデータプロセスの開発に明るい人などであれば一気にドキュメントを読み通して設計や実装まで一気に駆け抜けることもできそうな内容ですが、弊チームではこの 30 分輪読会での読み進め方がちょうどよいペースでハマりました。</p> <p>コードレビューにおいてもドキュメントをベースにした建設的な議論を行える場面が多く、実装者あるいはレビュアーの無理解や押しつけとなるような実装で進んでしまうようなこともほとんどなく進められています。(という自分の思い込みでなければよいのですが...)</p> <h3 id="気になった点">気になった点</h3> <ul> <li>業務の作業時間が減少する</li> <li>輪読会の準備や実施でコンテキストスイッチのコストが若干発生する</li> </ul> <p>始める時点ですぐ想像できるトレードオフではあるのですが、定期に輪読会に時間が取られるので業務の作業時間が減少しコンテキストスイッチのコストも発生していると感じます。開発の調子が乗ると 1 日とか集中したくなる時もやはりあるので、一定の量までドキュメントを読み進めたら勉強会の頻度を減らしたり開催を終了するなどの変更も必要になりそうです。とはいえオンボーディング期のチームの効率を高めているメリットと比べれば大きな問題ではないので、チームの習熟度を見てしかるべきタイミングで調整していけばよさそうです。</p> <h2 id="終わりに">終わりに</h2> <p>分析運用チームでの dbt の導入と、それを定着させるための毎日 30 分の輪読会という取り組みについての紹介でした。ほかの開発チームにも同様の取り組みが適用できるかはチームメンバーの傾向や開発のフロー、取り扱う題材などによって変わりますが、特に立ち上がったばかりなど認識や目線合わせが必要となるチームにおいては効果を発揮できるのではないかと思います。</p> techtouch