TECH PLAY

株式会社エブリー

株式会社エブリー の技術ブログ

385

目次 はじめに 2024年度の振り返りと改善点 成果 課題 課題の解消に向けた施策について 研修の目的・カリキュラム 研修の目的 カリキュラム CTO からのメッセージ 活躍した新卒2年目エンジニアからのメッセージ バックエンド/インフラ講義 モバイル講義 Web 講義 データ講義 次回に向けて おわりに はじめに こんにちは。 トモニテ開発部ソフトウェアエンジニア兼、CTO室Dev Enableグループの庄司( ktanonymous )です。 本記事では、2024年度から実施を始め、第2回目となる2025年新卒エンジニア向け研修の取り組みについてご紹介します。 昨年度実施した新卒研修については、以下をご覧ください。 2024年度の振り返りと改善点 昨年度のエンジニア新卒研修では、以下のような成果と課題が見られました。 成果 エブリーのエンジニアとしてのマインドセットの理解 各技術領域の全体像の把握 社内エンジニアとのネットワーク構築 課題 前提知識の不足による理解の困難さ 各種ツールの使い方に関する事前準備の不足 課題の解消に向けた施策について 昨年度の課題を踏まえ、運営チームでは如何に入社時の前提知識の不足やばらつきを解消するのかを検討していました。 その中で、入社前から新卒(当時は内定者)の意欲が高く、学習コンテンツがあれば積極的に活用したいという声が多くありました。 そこで、内定者研修という形で入社後のオンボーディングをスムーズにするための基礎的なキャッチアップをフォローする研修を実施しました。 内定者研修についてのブログも公開しておりますので、ぜひご覧ください。 研修の目的・カリキュラム 今年度の研修では、昨年度のフォーマットを踏襲しつつ、成果や課題を踏まえて、改善策なども検討しました。 研修の目的 研修自体の目的や方針については、昨年度と大きくは変えず、以下のように設定しました。 目的 『オーナシップを持ってプロダクト課題の解決に動けるエンジニアへの土台を作る』 方針 エブリーのエンジニアとして求められるマインドを理解する 専門領域を超えてエブリー全体で使われている技術スタックを理解する カリキュラム 研修のカリキュラムについても、昨年度と大枠は変わっていません。 新卒の皆さんには、5日間で以下のようなカリキュラムに従って研修に参加してもらいました。 マインド研修 CTO からのメッセージ 活躍した新卒2年目エンジニアからのメッセージ 技術スタックの把握(講義 + ハンズオン) バックエンド/インフラ モバイル Web データ ランチ会 前回からの変更点として、マインド面の研修に盛り込んでいたインシデントへの向き合い方のパートが変更になっています。 講義内容をより身近に感じてもらうために、活躍している新卒2年目エンジニアからのメッセージとして同様の要素を取り込みつつ、実際に入社直後から活躍している先輩がどのようなことを意識しながら業務に取り組んできたのかを伝えてもらいました。 CTO からのメッセージ この講義では、AI/LLM時代におけるエブリーのエンジニアとして重要となるマインドセットについて講義をしました。 生成AIが破竹の勢いで進歩していく中でエンジニアを取り巻く環境はどう変わっていくのか、また、エブリーのエンジニアとしてどういう姿勢で臨むべきかについて、CTO 自らの視点を交えた内容となり、配属に向けて刺激を得られる講義でした。 活躍した新卒2年目エンジニアからのメッセージ この講義では、入社直後から活躍している先輩がどのようなことを意識しながら業務に取り組んできたのかを伝えてもらいました。 実際に活躍している近い世代の先輩のマインドを伝えることで、実際に行動に移した例もあり、より具体的なイメージを持ってもらうことができたと思います。 バックエンド/インフラ講義 バックエンド/インフラ講義では、エブリーで共通的に用いられる技術や知識について、一通り触れて理解することを主な目標としました。 ハンズオンを通して以下の内容を学びました。 社内の技術スタック Go を利用した簡易的な API サーバの開発体験 CLIによる手動デプロイからCDによる自動デプロイまでを体験 モバイル講義 モバイル講義では、アプリの開発の基礎や実装に触れてもらい、API 設計やログ解析、不具合解析の際にアプリサイドの開発観点も考慮できるようになることを目標としました。 主な内容: モバイルアプリ開発の基本概念 基本的な UI 実装 アプリログや分析の実装 API接続ハンズオン 組織開発における注意点 Web 講義 Web 講義では、Webの歴史から現在の技術を理解することやWeb開発の基礎知識を身につけることを主な目標としました。 講義内容: 社内の技術スタック Web 開発の歴史と基礎知識 開発における重要なポイント デリッシュキッチンアプリを題材として画面実装ハンズオン データ講義 データ講義では、データ領域の各分野について体系的に学びました。 主な内容: 各種職能の役割 社内での業務への関わり方 AIの概観/社内での活用事例 データ基盤 データ品質の管理 実践的なデータ処理ハンズオン 次回に向けて 研修後の調査で、受講者からは以下のように好意的なフィードバックが寄せられました。 同期と楽しみながら進められた 研修を通して学ぶべきことを知ることができた 全領域を横断的に触れることで業務でのコミュニケーションにも繋げられると感じた 研修を楽しみながら、エブリーのエンジニアとして必要となるものを認識してもらうことができ、目的は達成できたのではないかと思います。 しかし、改善点がなくなったわけではありません。 特に、生成AIの進歩に対応したコンテンツ作りは1つの大きな懸念材料になると思います。 弊社では、AIエディタである Cursor がエンジニア(およびPdM)を対象に導入されました 1 。 組織全体としても模索中である開発業務におけるAI活用はもちろん、受講者のAI利用を前提としたコンテンツをどのように構成するべきかを検討する必要があると考えています。 講義風景 講義風景 おわりに 2025年度のエンジニア新卒研修では、2024年度の課題を踏まえた改善を実施し、より充実した研修プログラムを提供することができました。 配属前のタイミングで横断的なコンテンツを提供することで、エンジニアとしてだけではなく業務に携わるメンバーとしても、早期成長の手助けをできていると感じています。 今後も、研修などを通じてエブリー開発組織全体の活性化・強化を目指していきたいと思います。 最後に、本研修の実施にあたり、講義に快くご協力いただいた皆様に感謝申し上げます。 エブリー、AIエディタ「Cursor」を全エンジニアおよびプロダクトマネージャーに導入 ↩
アバター
はじめに こんにちは、デリッシュキッチンでクライアントエンジニアを担当している kikuchi です。 デリッシュキッチンの Android アプリ開発チームでは新規機能の開発だけでなく、日々アプリの改善のために不具合の修正や挙動の改善についても力を入れており、アプリのパフォーマンスの部分など細かい部分も数値が改善されているかシビアに計測データで確認しています。 今回はそのパフォーマンスの部分で 「アプリの起動時間の改善」 の観点で特に効果が高かった対応をまとめてみたいと思います。 アプリの起動時間を確認する方法 アプリ起動時間は以下で調べることが出来ます。 Google Play Console の Android Vitals でコールドスタートなどの時間を確認する アプリの TTID (Time To Initial Display) と TTFD (Time To Full Display) を確認する ログを仕込む 1 については、Google Play Console 上で Android Vitals という機能を使ってコールドスタートなどの数値を確認する、というものになります。 こちらは OS のバージョン、CPU、RAM などで細かくフィルタリングができ、また何より 類似するサービスのアプリや、任意で選択したアプリと比較できる という点が他社のアプリと直接比較できる数少ない指標になるため重宝しています。 (なお、実値で比較できるわけではなく、あくまでコールドスタートなどの時間が長いユーザの割合が比較できるのみ) 2 については Google Developers の公式サイトで詳しく紹介されています。 TTID : 初期表示までの時間のことで、アプリの UI の最初のフレームを表示するまでにかかる時間 https://developer.android.com/topic/performance/vitals/launch-time?hl=ja#time-initial TTFD : 完全表示までの時間のことで、アプリがユーザ操作可能な状態になるまでの時間 https://developer.android.com/topic/performance/vitals/launch-time?hl=ja#time-full TTID についてはどのアプリについても Logcat で簡単に確認することが出来ます。 以下は API Level 36 のエミュレータで Chrome アプリを起動した場合のログになります。 Displayed でフィルタをかけて表示したログで、「1s312ms」という数値が TTID となります。 上記のようにアプリを起動するだけで出力されるログのため、自社で開発したアプリ、ストアからインストールしたアプリなど関係なく確認することが出来ます。 なお、TTFD については ComponentActivity の reportFullyDrawn() というメソッドを意図的に呼ぶ必要があるため、基本は自社で開発したアプリのみで確認できます。 TTFD については今回まとめる内容と深く関わる部分ではないため、割愛させていただきます。 3 については、細かく Log クラスなどを駆使してログを仕込んでいく方法になります。 こちらはあくまでアプリが起動してから (つまりは Application を継承したクラスの onCreate メソッドがコールされてから) の処理にログを仕込めるものとなるため、怪しい実装が無いかを特定する際に最適な方法だと言えます。 調査観点 起動時間を改善する、と一言で言っても、手当たり次第調査をしても効果は出にくいと考えたため、以下に観点を絞って調査をしました。 アプリ起動時に不要な処理を実行していないか 不要なライブラリが存在しないか 起動時間そのものを最適化して短縮できないか 結果を先にお伝えすると、1 についてはログを細かく仕込んで調査したものの全く問題なく、余計な処理や大きく短縮できそうな処理は存在せず、2 と 3 に大きく効果がある改善ポイントが存在していました。 以降の項目で 2 と 3 について細かくまとめていきたいと思います。 不要ライブラリの調査 不要ライブラリが存在するかについては、導入しているライブラリを一つ一つ確認していきました。 とはいえ、デリッシュキッチンのプログラムの規模はかなり大きく全部で 100 個近くのライブラリを導入しているため、AndroidX など必須のライブラリは除いて画像関連のライブラリなどサードパーティ製のライブラリに絞って調査することにしました。 結果、 Firebase In-App Messaging がかなり起動時間に悪影響を与えていたということが分かりました。 複数端末で Firebase In-App Messaging を除外した状態で TTID を検証した結果は以下のようになりました。 デバイス A / OS 8.1.0 / RAM 4GB … 180msec 程度改善 デバイス B / OS 12 / RAM 6GB … 120msec 程度改善 デバイス C / OS 13 / RAM 8GB … 30msec 程度改善 OS が古い、RAM が少ない端末ほど改善幅が大きい、という結果になりました。 こちらについてはデリッシュキッチンチーム全体で確認した結果、今は使用しておらず今後も使う可能性は無い、ということが分かったためライブラリを除外する結論となりました。 何故 Firebase In-App Messaging が起動時間に悪影響を与えていたのか In-App Messaging ですが、どこでもメッセージを表示できるという特性上、 WebView で作られていたことが起動時間を長くしていた要因でした。 WebView の初期化、生成は非常にコストがかかるうえ、Firebase In-App Messaging は ライブラリを導入しているだけで問答無用でアプリ起動時に初期化される 仕組みとなっており、意図的にプログラムで初期化処理を呼ばなくても初期化処理が走っていました。 結果、アプリ起動時に WebView の初期化処理まで走って余計に時間がかかっているという状況に繋がっていました。 他にも In-App Messaging の機能を提供しているライブラリでも同様に初期化処理に時間がかかっていたため、In-App Messaging のライブラリを使用している場合はライブラリの見直しをしてみるのも良いかも知れません。 起動時間そのものを最適化する方法の調査 起動時間そのものを最適化する方法については、Android Gradle Plugin (AGP) と Android Jetpack の一部として提供されている Baseline Profiles という仕組みが導入コストが低く、改善の期待値が高いということが分かりました。 Baseline Profiles については Google Developers に概要がまとめられています。 https://developer.android.com/topic/performance/baselineprofiles/overview?hl=ja Baseline Profiles とは、アプリの起動に必要な情報を事前 (AOT) コンパイルしておくことで、アプリの起動時に実行するコードパスの解釈、ジャストインタイム (JIT) コンパイルを省略でき、初回起動からのコード実行速度が約 30% も向上するものとなります。 なお、Baseline Profiles を採用することで起動時間を短縮するだけでなく、ジャンク (かくつき) の軽減、全体的なパフォーマンスの向上にも繋がります。 Baseline Profiles の導入自体は容易です。次の項目で導入手順をまとめます。 Baseline Profiles の導入手順 実際にプロジェクトに追加する手順を画像を交えて紹介します。 ①Android Studio で「File > New > New Module」を選択 ②Templates で「Baseline Profile Generator」を選択し、必要に応じて値を設定し、Finish を選択 ※「Use Gradle Managed Device」にチェックを付けることで、Gradle で管理されているデバイス (GMD) を使用して Baseline Profiles を作成するための初期設定が自動的に行われます ③ツールバーの Run / Debug Configrations で、手順 2 にて生成した項目を選択して Run を実行 ④app モジュール > src > release / generated / baselineProfiles に baseline-prof.txt が出力される 以上で完了となります。 ※実際のプロジェクトの環境によっては手順が異なる可能性があるため、先述した公式サイトの情報を確認しながら設定を行うことをおすすめします。 Baseline Profiles の導入結果 Baseline Profiles を導入した結果の TTID は以下のようになりました。 デバイス A / OS 8.1.0 / RAM 4GB … 200msec 程度改善 デバイス B / OS 12 / RAM 6GB … 150msec 程度改善 デバイス C / OS 13 / RAM 8GB … 70msec 程度改善 こちらも不要ライブラリ除外と同様、OS が古い、RAM が少ない端末ほど改善幅が大きい、という結果になりました。 Baseline Profiles についてはプロジェクトの規模などにも大きく左右されますが、大きな改善が見込めることが分かります。 対応した結果 今回注力して対応した「不要ライブラリ除外」、「Baseline Profiles の導入」のみで、 最大で 400msec 近くもの起動時間を短縮 に成功しました。 調査や対応に当然工数はかかっているものの、検証込みで 1 週間程度の少ない工数の見返りとしてはかなり大きいものだったと感じています。 これ以外にも TTID 以外の指標を使用して細かい計測やソースのリファクタリングなどを経て、最終的には対応前と対応後で起動時間を半分にすることが出来ました。 なお、起動時間を短縮した結果、一番恩恵を受けたのがスプラッシュ画面になりました。 OS 12 以降はスプラッシュ画面 (Splash Screen) が表示されるようになりましたが、TTID が短いほどスプラッシュ画面の表示が短くなります。 スプラッシュ画面はアイコンだけの画面になっていて無機質な画面に見えることが多く、スプラッシュ画面の表示時間が長いとフリーズしているように見えるため、すぐにユーザが操作できる状態になることはユーザビリティの向上に繋がります。 おわりに Firebase In-App Messaging の除外、Baseline Profiles 導入といった工数が少ない対応で大幅な起動時間の短縮に繋げることが出来ました。 デリッシュキッチンのようにライブラリの見直しだけでも起動時間の改善に繋がることもあるので、バージョン更新だけでなく、使用有無の棚卸しをするだけでも非常に効果的だと考えます。 また Baseline Profiles についても、こういったことをやりたい、と漠然と気になったことでも調べてみると機能が提供されていることがあるため、エンジニアの原点ではありますが、気になったら調べる、という行動は必要不可欠だと考えます。 本記事の情報が皆様のお役に立てれば幸いです。
アバター
はじめに こんにちは。リテールハブ開発部の池です。 エブリーは 2025/05/02 にプレスリリースを出した通り Cursor を全エンジニアとプロダクトマネージャーに導入し、AI活用による生産性の向上に積極的に取り組んでいます。 corp.every.tv 現在、世の中では Cursor のような開発支援ツールを使ってLLMをベースとしたエージェントの開発ワークフローを構築する動きが進んでいます。 エージェントに安定した挙動をさせるには、一貫したコンテキストの提供が必要であり、永続的なルールやファイルを用いてその振る舞いを制御するアプローチが一般的になりつつあります。 これには単なるドキュメント整備ではなく、ルールの構造化された設計が必要です。 どのような種類のファイルが必要か どのフォーマット・テンプレートで記述するのか エージェントはどのタイミングでそれを参照するのか どのように内容を更新して永続化し続けるのか こうしたルール設計は現状はまだ標準が存在せず、黎明期にあると感じています。 そのような環境において、私にとって有用な情報源となっているのが Reddit や Discord、X などの Cursor コミュニティです。 コミュニティでは多くのユーザーが日々の試行錯誤を共有しあっているため、ユーザーの実体験に基づいた新しい知見をキャッチアップするには非常に役立ちます。 この記事では、コンテキスト管理の必要性を説明した後、コミュニティの中で興味を持った Cursor Custom modes を活用したワークフローの手法 vanzen01/cursor-memory-bank について、試してみたいと思います。 ※ Cursor 公式のワークフローではないのでご注意ください。 LLMを活用した開発における課題 まず前提として、LLMは、コンテキストウィンドウを超えた情報を保持できず、基本的には直近のトークンのみ参照します。 つまり、一定のトークン数を超えると文脈が途切れ、過去のやりとりを忘れてしまうような現象が起こります。 (ただし、モデルによっては古い情報が影響を弱めながら残るケースもあるようです。) この問題は、プロジェクトの開発を進める中で頻繁に起こるので、文脈の持続が求められるタスクにおいて進捗を悪くする要因になります。 そのため、コンテキストを効率よく管理して適切な量の情報をLLMに渡すことが鍵となってきます。 コンテキストウィンドウとは コンテキストウィンドウとは、モデルが一度のプロンプトや会話履歴として「認識・参照」できるトークンの最大量のことです。 コンテキストサイズの比較表(2025年5月時点)は次のようになっています。このウィンドウサイズに収まるようにコンテキスト管理を行うことが重要です。 また、コード生成において、コンテキストサイズを超えずとも回答精度が悪くなるという声もあるため、コンテキスト量を抑えることが大事になります。 2025年5月時点における、主要なモデルのウィンドウサイズは以下のようになっています。 モデル名 コンテキストウィンドウサイズ リリース時期 GPT-4.1 1,000,000トークン 2025年4月 GPT-4.5 128,000トークン 2025年2月 o3 200,000トークン(入力)/100,000トークン(出力) 2025年4月 o4-mini 200,000トークン(入力)/100,000トークン(出力) 2025年4月 o4-mini-high 200,000トークン(入力)/100,000トークン(出力) 2025年4月 Claude 3.5 Sonnet 200,000トークン 2024年6月 Claude 3.7 Sonnet 200,000トークン 2025年2月 Gemini 2.5 Pro 1,000,000トークン 2025年4月 また、トークン数に加えて各モデルにおいて得意/不得意な作業があるため、それを考慮してモデル選択を行うことが大事です。 コンテキスト管理のアプローチ コンテキスト管理は様々なアプローチが考えられます。 1. タスクの分割と設計 トークン制限を考慮して、タスクを小さく分割し、それぞれのタスクに必要なコンテキストだけを提供することで、精度を保つことができます。 詳しくは本記事では触れませんが、タスク管理については claude-task-master を活用する事例をコミュニティで よく目にするので、よかったら参照してください。 2. メモリバンクの活用 メモリバンクとは、エージェントが参照すべき知識や振る舞いのルールを構造化して保存する仕組みです。 ここで扱っているメモリバンクはあくまで外部ドキュメントを明示的にコンテキストとして毎回渡す設計であり、モデル自体に永続的に記憶されるわけではありません。 詳しくは Cline Memory Bank を参照ください。 上記はClineが提唱するメモリバンクですが、Cline専用の機能ではなく方法論であるため、同じ仕組みをCursorに適用することができます。 3. モデルの使い分け モデルごとに特性やコンテキストウィンドウサイズが異なるため、タスクの性質に応じて最適なモデルを選択することで、コンテキスト効率と精度の両方を向上させることができます。 私は直近では以下の二つを使い分けています。 Claude 3.7 Sonnet:比較的小さいコンテキストで高精度が求められる場面、特にコード理解や分析タスク Gemini 2.5 Pro:大規模な設計・全体構成把握など、複雑なプロジェクト構造の理解 4. ワークフローの定義 ワークフローを明確に定義し、作業ごとのコンテキストを効率的に引き継ぐことが重要です。 例えば、「計画→実装→テスト→レビュー」という流れで、各ステップに必要な情報だけを選択的に提供することで、トークン消費を抑えながら一貫性を保てます。 5. 外部からコンテキストを取得する コンテキスト管理からは少し外れますが、MCPやRAG(検索拡張生成)などを利用して、社内の文書管理システムやFigmaの設計データなどを動的に検索・取得し、必要に応じてLLMに渡すことで、リポジトリ外の情報も活用できます。 リポジトリで管理できない社内ドキュメントや、テキストとしてコピーしづらいようなFigmaデザインデータなどをMCPやRAGなどを通じて取得・更新すると便利です。 Cursor Custom modes を利用した cursor-memory-bank を試す 以上のコンテキスト管理の必要性とアプローチ方法を踏まえつつ、ここからは2025年3月 Cursor v0.48 で搭載されたカスタムモードを利用したコンテキスト管理のワークフローについて、私が興味を持った下記プロジェクトの手法を試してみます。 github.com このプロジェクトではCursorのカスタムモードを活用し、AIがセッション間での記憶を維持しながら、構造化された開発ワークフローを実現することを目的としています。 本記事は Cursor v0.48 時点での設定方法に基づいています。以降のバージョンでUIや設定項目が変更される可能性があるのでご注意ください。 また、cursor-memory-bank も v0.6-beta 時点のワークフローに基づいており、ちょうど執筆時点で v0.7-beta にアップデートされていたため、最新のワークフローとは多少異なっています。 Cursor Custom modes とは Custom modes とは、Cursor のモードを独自に定義できる機能です。使用するツール、モデル、ルール等を設定した独自のモードを作成することができます。 このカスタムモードを活用することで、例えばコード生成時とコードレビュー時で異なるルールセットを適用できるため、それぞれのワークフローに特化した作業を行わせることができるようになります。 Custom modes 始め方 1. Cursor Settings > Features の Custom modes を許可する 2. Chatセクションのモード選択 > Add custom mode を選択 3. 必要なルールを設定する cursor-memory-bank を試してみる cursor-memory-bank は Cursor のカスタムモードを通じて、以下の4つの開発フェーズに対応するモードを提供します。 VANモード:プロジェクトの初期化とメモリバンクの設定 PLANモード:タスクの計画と戦略の立案 CREATIVEモード:設計とアイデアの創出 IMPLEMENTモード:実装と進捗の管理 これにより、各ワークフローに特化した一貫性のある開発を行います。 ※ v0.6-beta 時点のワークフローです。 VAN → PLAN → CREATIVE → IMPLEMENT という順に実行して開発を行います。 設定方法 cursor-memory-bank をプロジェクトに適用する手順は以下の通りです: 1. リポジトリのクローン cursor-memory-bankをクローンし、作業するプロジェクトに移します。 git clone https://github.com/vanzan01/cursor-memory-bank.git 2. Custom modes の設定 各開発フェーズのモード(VAN, PLAN, CREATIVE, IMPLEMENT)を作成します。 例)PLANモードの設定 エージェントへの指示もPLANモード用の内容をカスタムモードに設定します。 詳細な設定方法はcursor-memory-bankのREADMEに記載があるのでそちらを参照ください。 実行方法 次に、各モードを使った実際の実行例を紹介します。 サーバー側で、モバイルアプリの強制アップデートが必要かどうかを判定するミドルウェア機能を実装するタスクが、ちょうど良い大きさのタスクだったので、その機能の実装で試します。 VANモード カスタムモードで設定したVANモードを選択して「VAN」と入力すると、プロジェクト全体を読み込み、メモリバンクの初期化を行います。 ただし、今回は使い方や設定に誤りがあったのか、この方法ではメモリバンクの各ファイルの内容が生成されませんでした。 そこで、今回は gemini-2.5-pro-exp-03-25 モデルを利用し、チャット上で指示をしてプロジェクト全体のコンテキストをドキュメント化しました。 Gemini 2.5 Pro はコンテキストウィンドウサイズが1Mと大きく、プロジェクト全体を解釈して設計や計画立てを行うことが得意であるため、 メモリバンクのコンテキスト生成時に利用すると比較的詳細にドキュメント化してくれます。 PLANモード PLANモードは memory-bank/tasks.md のタスク内容から設計や実装計画を作成します。 事前に要件や仕様を memory-bank/tasks.md に記述した上で、PLANモードを選択して「PLAN MODE」と入力すると、計画立てしてくれます。 実行する際は、新しいチャットを開いて行うのがおすすめです。 参考までに memory-bank/tasks.md の内容は以下です。 足りない部分はチャットしながら補う前提でざっくりとした記述となっています。 memory-bank/tasks.md を表示 ## 要件・仕様 - OSごとに強制アップデートのバージョンを指定できる - 強制アップデート条件 - サーバーで設定されている強制アップデートバージョン > アプリバージョン ## 設計 - アプリ - 全APIのリクエストヘッダーに下記情報を付与 - OS:XX-XXX-XXXX - `iOS` - `Android` - アプリバージョン:XX-XXX-XXXX - `1.0.0` - サーバーからHTTPステータスコード427が返却された場合に強制アップデートのダイアログを表示する - サーバー - アプリ向けの全APIにおける共通処理のミドルウェアとして実装 - リクエストヘッダーで必要な情報を受け取る - OS:XX-XXX-XXXX - アプリバージョン:XX-XXX-XXXX - OSもしくはアプリバージョン情報がリクエストヘッダーにない、もしくは不正だった場合はエラー - HTTPステータスコード 400? - OSおよびアプリバージョン情報をもとにバージョン判定を行う - 強制アップデート対象の場合 HTTPステータスコード427を返却 mermaid flowchart TD A[バージョン要件を判定するミドルウェア] --> B[HTTPリクエストヘッダーからアプリのバージョン情報を取得] B --> C{バージョン情報が有効か} C -- No --> D[エラー HTTPステータスコード 400 返却] C -- Yes --> F[HTTPリクエストヘッダーから端末のOS情報を取得] F --> G{バージョン情報が有効か} G -- No --> D G -- Yes --> H{バージョン情報とOS情報から強制アップデート対象判定} H -- No --> J[ミドルウェア終了。次の処理に進む。] H -- Yes --> I[エラー HTTPステータスコード 427 Require Update 返却] subgraph バージョン判定ロジック K[OS情報からサーバーで定義されているiOSもしくはAndroid のminVersion を取得] K --> L{アプリバージョン < minVersion} L -- No --> M[Return False] L -- Yes --> N[Return True] end H --> |バージョン判定ロジック| K 入力内容はこちら。 以下が実行結果です。今回は1回の指示のみで詳細な計画を立ててくれました。 memory-bank/ 配下のファイルを詳細に記述しておくなど、前段の準備をしっかりしておくことで、チャットへの入力はほとんど無く詳細な結果を返却してくれます。 PLANモード 実行結果を表示 # 強制アップデート機能 実装計画(PLAN MODE) ## 1. 要件分析(Requirements Analysis) - [x] OSごとに強制アップデートのバージョンをサーバー側で指定できる - [x] サーバーで設定された強制アップデートバージョン > アプリバージョンの場合、強制アップデートを要求 - [x] 必要なリクエストヘッダー(RA-Device-OS, RA-App-Version)がない/不正な場合は400エラー - [x] 強制アップデート対象の場合はHTTP 427(Require Update)を返却 ### 技術的制約 - [x] サーバー側は全APIに対して共通ミドルウェアとして実装 - [x] バージョン情報・OS情報はリクエストヘッダーで受け取る - [x] サーバー側でOSごとのminVersionを管理 --- ## 2. コンポーネント分析(Component Analysis) ### 影響範囲 - APIサーバー(Laravel想定) - 共通ミドルウェア層(全APIに適用) - バージョン管理設定(config or DB) - エラーハンドリング(400, 427) - クライアント(アプリ) - ヘッダー送信 - 427受信時のダイアログ表示(※サーバー側計画外) ### 変更点・依存関係 - ミドルウェア新規作成 - リクエストヘッダー検証 - バージョン判定ロジック - エラー/強制アップデート応答 - 設定管理 - OSごとのminVersionをどこで管理するか(configファイル or DB) - テスト - 単体テスト(ミドルウェア) - 統合テスト(API経由) --- ## 3. 設計判断(Design Decisions) ### アーキテクチャ - [x] LaravelのHTTPミドルウェアとして実装 - [x] 設定は `config/force_update.php` などで管理(初期はファイル、将来的にDB化も考慮) - [x] バージョン比較はSemver互換の比較ロジックを実装 ### UI/UX - [ ] サーバー側は非該当(アプリ側で427時にダイアログ表示) ### アルゴリズム - [x] バージョン比較ロジック(Semver対応) - [x] ヘッダー検証ロジック --- ## 4. 実装戦略(Implementation Strategy) ### フェーズ1: ミドルウェア実装 - [ ] ミドルウェア新規作成(例:`ForceUpdateMiddleware`) - [ ] ヘッダー検証 - [ ] バージョン判定ロジック実装 - [ ] 設定ファイル作成(`config/force_update.php`) ### フェーズ2: エラー・強制アップデート応答 - [ ] 400, 427レスポンスの実装 - [ ] エラーハンドリングの統一 ### フェーズ3: テスト・ドキュメント - [ ] 単体テスト(ミドルウェア) - [ ] 統合テスト(API経由) - [ ] API仕様書・README更新 --- ## 5. テスト戦略(Testing Strategy) - [ ] ヘッダーが正しい場合/不正な場合のテスト - [ ] バージョンがminVersion未満/以上のテスト - [ ] OSごとの分岐テスト - [ ] 400, 427レスポンスの検証 - [ ] テストカバレッジの確認 --- ## 6. ドキュメント計画(Documentation Plan) - [ ] API仕様書(リクエストヘッダー、エラーコード追加) - [ ] 実装手順・設定方法のREADME追記 - [ ] テストケース一覧 --- ## 7. クリエイティブフェーズ要否(Creative Phase Identification) - UI/UX: **不要**(サーバー側のみ) - アーキテクチャ: **必要**(設定管理方法、バージョン比較ロジック設計) - アルゴリズム: **必要**(Semver比較) --- ## 8. チェックリスト(Verification Checklist) - [x] 要件がすべて網羅されている - [x] 影響範囲・コンポーネントが明確 - [x] 設計判断・実装戦略が明記されている - [x] テスト・ドキュメント計画がある - [x] クリエイティブフェーズ(アーキテクチャ・アルゴリズム)が明示 --- ### 次モード推奨 - **アーキテクチャ設計・バージョン比較ロジックの詳細設計が必要なため、次は「CREATIVE MODE」推奨。** CREATIVEモード PLANモードで、アーキテクチャ設計・バージョン比較ロジックの詳細設計が必要ということだったので、CREATIVEモードを用いてその作業を行います。 今まで同様に、CREATIVEモードを選択して「CREATIVE MODE」を入力します。 この際に、claude-3.7-sonnet の Thinking を設定することでより高精度な設計を行ってくれます。 CREATIVEモード 実行結果を表示(一部) # 強制アップデート機能 バージョン比較アルゴリズム設計 🎨🎨🎨 ENTERING CREATIVE PHASE: ALGORITHM 🎨🎨🎨 Focus: 強制アップデート機能におけるバージョン比較ロジック Objective: 正確かつ効率的なバージョン比較アルゴリズムを設計する Requirements: - 基本的なバージョン比較(X.Y.Z形式) - リクエストヘッダーから取得したバージョン文字列の検証 - パフォーマンスを考慮した実装 - シンプルな実装を重視 ## コンテキスト ### アルゴリズム要件 - バージョン形式: `X.Y.Z`(メジャー.マイナー.パッチ)の形式のみ - 無効なバージョン文字列の検出と処理 - 単純なバージョン比較(新しいか古いか) ### 技術的制約 - PHPでの実装 - 高速な比較処理(全リクエストで実行されるため) - LaravelミドルウェアからのAPIとしての利用しやすさ ## バージョン比較アルゴリズムのオプション ### オプション1:独自実装(カスタムロジック) - **説明**: PHPの文字列操作と数値比較を用いた独自実装 - **手法**: 1. バージョン文字列を`.`で分割 2. メジャー、マイナー、パッチそれぞれを数値比較 - **長所**: - 完全にカスタマイズ可能 - 外部依存がない - 特定のユースケースに最適化可能 - **短所**: - 実装の複雑性 - エッジケースの見落としリスク - メンテナンスコスト - **計算量**: O(n)(nはバージョン文字列の長さ) - **実装複雑性**: 中 ### オプション2:既存のライブラリ活用(composer/semver) - **説明**: Composerが提供する`composer/semver`パッケージを使用 - **手法**: 1. `composer/semver`をインストール 2. `Comparator`クラスの`lessThan()`/`greaterThan()`メソッドを使用 - **長所**: - 広く使われており信頼性が高い - SemVerの仕様に準拠 - メンテナンスの心配がない - **短所**: - 外部依存関係の追加 - 多少のオーバーヘッド - シンプルなケースには過剰な機能 - **計算量**: ライブラリ内部実装に依存 - **実装複雑性**: 低 ### オプション3:PHP標準関数 version_compare - **説明**: PHPの標準関数`version_compare()`を使用 - **手法**: 1. `version_compare($version1, $version2, $operator)`を呼び出し 2. 結果に基づいて判定 - **長所**: - PHPに標準搭載 - 追加依存関係が不要 - シンプルなAPI - 高速な処理 - **短所**: - 複雑なバージョン比較ケースには不向き(今回は必要なし) - **計算量**: O(n)(PHPの内部実装による) - **実装複雑性**: 非常に低 ## 決定 - **選択したオプション**: オプション3(PHP標準関数 version_compare) - **理由**: - PHPに標準搭載されており外部依存関係が不要 - シンプルで高速な実装が可能 - 基本的なX.Y.Z形式のバージョン比較に十分対応 - 追加のパッケージ導入が不要でプロジェクトが軽量化 - 今回の要件に最適なシンプルさを提供 - **実装ガイドライン**: // バージョン比較 public function isUpdateRequired(string $clientVersion, string $requiredVersion): bool { return version_compare($clientVersion, $requiredVersion, '<'); } // バージョン検証(X.Y.Z形式のみ許可) public function isValidVersion(string $version): bool { // X.Y.Zの形式のみ許可する単純な正規表現 $pattern = '/^\d+\.\d+\.\d+$/'; return preg_match($pattern, $version) === 1; } ## 検証 - 要件の充足: - [✓] 基本的なバージョン比較(X.Y.Z形式) - [✓] バージョン文字列の検証 - [✓] パフォーマンスへの配慮 - [✓] シンプルな実装 - 技術的実現性: 高(PHP標準関数を使用) - リスク評価: 低(PHPの標準機能に依存) 🎨 CREATIVE CHECKPOINT: アルゴリズム設計確定 - 進捗: バージョン比較ロジックの決定完了 - 決定: PHPの標準関数version_compareと単純な正規表現を使用 - 次のステップ: ForceUpdateMiddlewareの実装 🎨🎨🎨 EXITING CREATIVE PHASE 🎨🎨🎨 Summary: バージョン比較アルゴリズムとして、シンプルさと実装の容易さを考慮してPHPの標準関数`version_compare`を採用。基本的なX.Y.Z形式のバージョン検証のみを行う単純な実装アプローチを決定。 Key Decisions: - PHPの標準関数version_compareを使用してバージョン比較を実装 - シンプルな正規表現によるX.Y.Z形式のみの検証 - 外部依存関係を減らしたシンプルな実装 Next Steps: - ForceUpdateMiddlewareへの実装 - 単体テストによる検証 複数の設計パターンを検討し、採用した設計の意思決定の内容まで記述してくれました。 IMPLEMENTモード 以上の実装計画と設計をもとに実装します。 IMPLEMENTモードを選択して「IMPLEMENT MODE」と入力すると実装に進みます。 ここで新しいチャットを開いて実行すると良いでしょう。 結果は長いので割愛しますが、1回のやり取りでは全て完璧な実装とはならず、テストまで全て成功するまでには何度かやり取りを繰り返しました。 ただ、計画のチェックリストに自動的にチェックをつけながら実装してくれるため、内容の網羅性が高く、また、効率的なやり取りを行えると感じました。 まとめ 今回は簡単な機能実装を例に Cursor Custom modes を利用した cursor-memory-bank のワークフローを試しました。 小さい内容のタスクであったため効率的に各フローを実行できましたが、もっと大きなプロジェクトで大きな課題に対して実行するとまた違った結果になるかと思います。また、今後も試してみたいと思います。 今回紹介した手法は、あくまで現時点でのアプローチの一例に過ぎません。重要なのは、自分たちのワークフローや開発スタイルに合わせて適切にカスタマイズし、継続的に改善していくことだと思います。 本記事が少しでも参考になれば幸いです。ありがとうございました!
アバター
Amazon CloudWatch Logs Insights を使ったログ調査〜最新機能を添えて まえがき 背景 Cloud Watch Logs Insightsを使った調査事例 最新のCloud Watch Logs Insights事情 新機能1:AIアシスタントによるクエリ自動生成 日本語プロンプトの制限事項 英語プロンプトの例(日本語文字列あり:エラー発生) 複雑なプロンプト例(エラー発生) 新機能2:OpenSearchSQLクエリ 総括 最後に  こんにちは、開発本部 RetailHUB開発部 NetSuperグループ兼、CTO 室 Dev Enable グループに所属するフルスタックエンジニアをやらせていただいています、ホーク🦅アイ👁️です。AWS Summit2025が来月に開催されますがいかがお過ごしでしょうか。 aws.amazon.com まえがき  弊社ではつい先だってプレスリリースされました、Cursorエディタの全エンジニア配布が社内で最もインパクトのある出来事となっており開発部内でもSlackで専用のチャンネルを用意して会話をしたり、社内勉強会の開催も近々予定していたりと大いに盛り上がっております。 prtimes.jp 背景  昨年、私が執筆したブログ記事でAmazon Data Firehoseについて触れさせていただきましたが、データウェアハウスなログ管理・分析手法として現在も所属チームで稼働中です。 tech.every.tv 一方、別のインフラ構成で稼働しているGraphQLシステムがありこちらはAWS Lambdaを導入しているだけでそのログは標準のAmazon CloudWatch Logsに全依存しているのが現状です。そんな中、とある調査が必要になりこのLambdaのロググループの大量のログを検索しなければならないことになったのです。構造化ログと非構造化ログが混在しておりS3に保存もしていないので過去のログを今すぐにAthenaを使ってクエリ発行もできないため仕方なくLogs Insightsを使って直接検索することになりました。Logs Insights自体は2018年にGAとなっているレガシー技術ですが、実は直近で大きな新しい機能が追加となっていたのでそれを紹介しつつ、本調査の体験談をさせたいただきます。 Cloud Watch Logs Insightsを使った調査事例  先ほど述べたLambdaログを調査するにあたり、現状システムのログ出力前提は以下のようになっています。 APIプログラム内で出力されるログの種類は、処理途中の変数値やタイムスタンプを記したinfoログと例外発生時のエラーログがあり、その2種類が同じロググループに混在している(構造化・非構造化ログの混在) 調査対象のAPIはログイン後に特定のユーザ(スマホアプリ)が叩くものだが一部のログにしかユーザIDが埋め込まれていない エラーログにはそのユーザIDは埋め込まれていない STG環境とPRD環境のログが同じロググループに混在している  この前提条件の下、1リクエストで出力させたログをまとめて、ある期間中にどのユーザが特定エラーを起こしたのか、またその件数はいくつなのか、何人がエラーを起こしたのかを抽出したいという要件がありました。  これらを踏まえてChatGPTと壁打ちの結果、なんとかLogs Insights QLのクエリ構文を完成させることができ、期待するデータを抽出することができました。クエリ例は以下のようになります。 fields @ timestamp , @message | parse @message " *\t*\t*\t* " as level , timestamp , request_id, body | parse body / ' user_id ' :\s* ' (?<usr_id>[^ ' ]+) ' / | parse body / ' shop_id ' :\s* ' (?<shp_id>[^ ' ]+) ' / | parse body / ' db_name ' :\s* ' (?<db_nme>[^ ' ]+) ' / | parse body / ' error_message ' :\s* ' (?<err_message>[^ ' ]+) ' / | stats latest(usr_id) as user_id, latest(shp_id) as shop_id, latest(db_nme) as db_name, latest(err_message) as error_message, latest( timestamp ) as log_timestamp by request_id | filter error_message = " 情報が取得できませんでした。 " and db_name != " stg_database "  簡単に説明すると、@messageでログ文字列全体を取得して、タブ区切りになっている部分を4分割してLambdaのリクエストを一意に決めるRequestIdを取得しつつ、ログ本体をbodyとしています。このRequestIdでグルーピングすることで1リクエストのログ全体を集約しました。ログは、plaintextの場合もあればJson形式の場合もあったり、またJson形式がネストしていたりとJsonパースを共通ルール化して記述できなかったので単純に正規表現でuser_idなどを抽出しています。最後にPRD環境のみとする条件を追加しています。 最新のCloud Watch Logs Insights事情  CloudWatch Logs では2024年12月、以下の3つの機能がGAとなって可能性が広がっております。 生成AIを用いた自然言語によるクエリ作成・更新機能 OpenSearch Service Piped Processing Language (PPL)のサポート OpenSearch Service Structured Query Language (SQL)のサポート  また、2025年7月31日以降は、IAMポリシーで logs:StartQuery,logs:GetQueryResults の権限追加が必須となりセキュリティ向上が見込まれます。  そこで、先ほどのLambdaログ調査で使ったクエリと同様の結果を出すためのSQL記述はどのようなものなのか、生成AIプロンプトでリクエストするとどのようなクエリを生成するのかも気になりました。もしかしたら、もっと簡単な文法で記述できていたのだろうか、GPTとの壁打ちをしてやっとの思いで完成させたクエリだったのでその時間短縮ができるのであれば良い選択肢になると思い試すことにしました。 新機能1:AIアシスタントによるクエリ自動生成 日本語プロンプトの制限事項  Cursorに上記の完成形のクエリを渡して「このクエリ結果と同じになるクエリを生成してもらうためのプロンプトを考えて」とお願いして作成してもらったものを使って実行してみたら以下のエラーになりました。日本語がそもそもダメだったようです。。 # 日本語での入力例(エラーになります) Lambda関数のログから以下の条件で分析をしたいです: 1. ログからuser_id, shop_id, db_name, error_messageを抽出 2. request_idでグループ化し、各フィールドの最新値を取得 3. error_messageが"情報が取得できませんでした。"で、 db_nameが"stg_database"以外のものを抽出 ⚠️ エラーメッセージ Bad Request - Query generation currently only supports prompts in English. Only english characters, numbers and standard punctuations are allowed. 英語プロンプトの例(日本語文字列あり:エラー発生)  日本語には対応していないようなので、英語プロンプトに変更してとCursorにお願いして作成し直してもらいました。それを使って実行するとまたもやエラーになってしまいました。。どうやら、メッセージ文字列そのものも日本語(マルチバイト?)が混じっているとエラーになるようです。 I want to analyze Lambda function logs with the following conditions: 1. Extract user_id, shop_id, db_name, and error_message from logs 2. Group by request_id and get the latest value for each field 3. Filter where error_message is "情報が取得できませんでした。" and db_name is not "stg_database" ⚠️ エラーメッセージ Bad Request - Query generation currently only supports prompts in English. Only english characters, numbers and standard punctuations are allowed. 複雑なプロンプト例(エラー発生)  というわけで全て英語のプロンプトをCursorにお願いして作成してもらい、それを使って実行してみましたが、以下のようにまたまたエラーになってしまいました。長文だったり要件が複数・複雑だとエラーが出てクエリ生成に失敗してしまったのでそもそも簡単なクエリ生成にしか対応できないようです。 I want to analyze Lambda function logs with the following conditions: 1. Extract user_id, shop_id, db_name, and error_message from logs 2. Group by request_id and get the latest value for each field 3. Filter where error_message is "Information could not be retrieved" and db_name is not "stg_database" ⚠️ エラーメッセージ Bad Request - The provided query prompt failed to generate a valid query. Try rephrasing the query prompt.  その後、簡単な文章プロンプトで少しづつクエリを組み立ていこうと、Generate New Query→Update Query→Update Query...と繰り返していきましたが単純にUpdateでプロンプトの結果を追加する時もあれば上書きする時もあったので中々一筋縄ではいかない結果となりました。。 新機能2:OpenSearchSQLクエリ  こちらも同様にCursorを使ってSQLを作ってもらってそれを実行する方法で試しました。確かに簡単なSQL構文では正しく動きましたし、元からある知見をそのまま使ってすぐにクエリ作成ができるのは便利でした。しかし、今回のようなちょっと複雑なクエリ要件になるとCursorが生成したSQL(以下、一例)では文法NGだったり実行できてもヒット数0件だったりと何度か壁打ちして試しましたが上手くいきませんでした。。 SELECT request_id, user_id, shop_id, db_name, error_message, `@ timestamp ` as log_timestamp FROM `/aws/lambda/FunctionA` WHERE regexp_extract(`@message`, ' error_message '' ?\s*:\s* '' ([^ '' ]+) ''' , 1 ) = ' 情報が取得できませんでした。 ' AND regexp_extract(`@message`, ' db_name '' ?\s*:\s* '' ([^ '' ]+) ''' , 1 ) != ' stg_database ' | fields regexp_extract(`@message`, ' user_id '' ?\s*:\s* '' ([^ '' ]+) ''' , 1 ) as user_id, regexp_extract(`@message`, ' shop_id '' ?\s*:\s* '' ([^ '' ]+) ''' , 1 ) as shop_id, regexp_extract(`@message`, ' db_name '' ?\s*:\s* '' ([^ '' ]+) ''' , 1 ) as db_name, regexp_extract(`@message`, ' error_message '' ?\s*:\s* '' ([^ '' ]+) ''' , 1 ) as error_message, split(`@message`, ' \t ' )[ 3 ] as request_id, `@ timestamp ` | sort `@ timestamp ` desc | limit 1000 ; 総括  本記事では、前半でCloudWatch Logs Insightsを使ったログ調査事例について話しました。後半では、最新機能についても触れました。生成AIによる自動生成はまだまだ改善の余地がありそうな結果でした。SQLの方は構文や関数は既に出揃っているのでそれらを全て理解すれば複雑なクエリ実行も成功することはできると思います。  そもそも、Logs Insights使用はマネーコストが掛かるためこれだけを使ったログ管理運用は最適解ではないかもしれません。そのため使用頻度は高くないと思いますが、今回のようなデータレイクないろんな形式のログが混在しているのを一括して検索可能であったり、複数のLogGroupsを横断して検索可能であったり、CloudWatch Dashboardsでリアルタイム監視ができたり、と使い分けとして使用機会自体はまだまだありますので新しい機能を駆使して快適なログライフを送れると良いと思いました。  また、新しいログ分析手法としてデータレイクとしてApache IcebergをサポートしたS3 Tablesを使ってAthenaで分析するパターンが出てきました。2025年5月執筆時点ではコンソール上でnamespace作成、テーブル作成も可能となっておりました。こちらは既存の弊社システムのログ分析手法をアップデートでき得るものとして後日検証して別途報告できればと思います。  これにて本記事の結びとさせていただきます。 最後に エブリーでは、ともに働く仲間を募集しています。 テックブログを読んで少しでもエブリーに興味を持っていただけた方は、ぜひ一度カジュアル面談にお越しください! corp.every.tv
アバター
はじめに こんにちは、開発本部のデータ&AIチームの蜜澤です。 Amazon QuickSight(以下quicksight)にはハイライト機能がありますが、インタラクティブにランキング表の指定したワードにハイライトすることはできません。 本記事ではquicksightのハイライト機能を使用せずに、ランキング表の同じワードにハイライトする方法を紹介したいと思います。 本記事はquicksightの機能を一通り知っている方向けの内容になっています。 使用するデータ 今回使用するデータは、レシピ動画サービスの検索データを集計して作成した「にんじんと一緒に検索された回数が多いワードの年度ごと(2021年〜2024年)のランキング」のデータです。 一緒に検索されたワードとは「にんじん しりしり」のようにスペース区切りで共に検索されたワードのことを指します。 quicksightの「テーブル」ビジュアルを使用してランキング表を表現するために、rankカラムと各年度のカラムを作成し、各年度のカラムには一緒に検索されたワードを格納します。 ※データはサンプルなので実際の傾向とは異なります。 実際にquicksightでテーブルを作成すると上記画像のようなテーブルが作成できます。 ハイライトをつける 先ほどのランキング表を見てみると、「しりしり」の順位が2021年から2024年にかけて上がっていることや、「大根」の順位が2021年から2024年にかけて下がっていることがわかります。 しかし、この表で「しりしり」や「大根」の順位の遷移を目で追うのは大変なので、特定のセルのみに色をつける(ハイライトする)というのが、今回の目標になります。 1.パラメータを作成 以下のようなパラメータを作成します。 このパラメータには、ハイライトしたいワードが入リます。 2.計算フィールドを作成 以下のような計算フィールドを作成します。 2021年カラムの値が、Highlightパラメータと同じ場合は1、異なる場合は0を返す計算フィールドになっており、この値によって後述する条件付き書式を設定します。 同様にして2022~2024用の計算フィールドも作成します。 3.条件付き書式を設定 プロパティ -> 条件付き書式設定 -> 列で「2021」を選択 -> 背景色を追加する で以下のように設定します。 条件の対象フィールドには先ほど作成した対応する計算フィールドを選択し、条件は「次と等しい」、値は1にします。 2022~2024にも同じ設定をします。 4.コントロールを追加 1.で作成したパラメータを以下の設定でコントロールに追加します。 5.完成 必要な工程は1~4になります。 作成したコントロールにハイライトしたいワードを入力すれば、ランキング表に色がつきます! ハイライトがつくことで順位の遷移が見やすくなりました! まとめ 本記事では、quicksightで特定のセルにハイライトをつけることができるランキング表の作成方法を紹介しました。 この記事がいつか誰かの助けになれば幸いです! 最後まで読んでいただきありがとうございました。
アバター
はじめに 背景 ADRについて ADRの始め方 ADRの実践例 ADRの具体例 おわりに はじめに デリッシュキッチンのiOSアプリ開発を担当している池田です。今回はiOSチームでADR(Architecture Decision Record)を用いてチームの意思決定の記録を残し始めた話をします。正確には個人でADRを記録していたものの、チームでの運用はしていなかったため、この機会にチームでの管理を開始しました。 背景 デリッシュキッチンのiOS開発は当初少人数で行っていたため、実装に関する詳細なドキュメントを作成するという習慣がありませんでした。しかし近年、メンバーの入れ替わりや新入社員の参加により、設計や実装の経緯を説明する機会が増えてきました。これを良い機会と捉え、私が個人で記録していたADRをチームに共有することにしました。 ADRについて ADRとは「Architecture Decision Record」の略で、プロジェクトにおける重要な意思決定を記録するためのドキュメントです。チームでの決定事項とその経緯が不明確になり、都度確認が必要になったり、経緯を知る人が退職してしまったりした経験は多くの方があるでしょう。ADRを残すことで、このような意思決定プロセスの消失を防ぐことができます。 意思決定は時間とともに消失する ADRにまとめる主な項目は以下の通りです。 タイトル:何についての決定か簡潔に表現 ステータス:提案中、承認済み、廃止、変更など コンテキスト:その決定が必要になった背景や状況 決定事項:採用した選択肢とその理由、検討した代替案 結果:この決定による影響(メリット・デメリット) ADRの始め方 ADRを始めるにはいくつかのステップがあります。 テンプレートの作成と合意 - 最小限の項目でシンプルに始めることが重要です。チームで合意したフォーマットがあれば、一貫性を維持しやすくなります。 記録場所の決定 - アクセスしやすく、気軽に編集できる場所が理想的です。GitHubやConfluenceなど、チームが日常的に使用するツールが最適です。 最初のADRの作成 - 完璧を目指すよりも、まずは記録することを優先しましょう。 ADRの実践例 私たちのチームではGitHub DiscussionにADRをまとめることにしました。everyではConfluenceを利用しているため、そこに記載する選択肢もありましたが、開発関連の内容とその他の情報を分離する目的からGitHub Discussionを選択しました。 ただ、振り返ってみると、PdMやデザイナーなどの非エンジニアとの共有や、複数リポジトリに対するADRの一元管理の観点からは、Confluenceでの管理の方が適していたかもしれません。 私たちのチームでは前述の5項目をベースとしたシンプルなテンプレートを作成しました。タイトルには決定事項を記載し、ステータスにはGitHub Discussionのカテゴリー機能を活用することにしました。実際のテンプレートは以下の通りです。 # 経緯 # 提案内容 # 承認した場合の結果 ## メリット ## デメリット # 備考 最初のADRはサンプルの意味合いも込めて「ADRを使って意思決定事項を残す」というテーマで作成しました。 ADRの具体例 実際のADRの例 おわりに デリッシュキッチンのiOSチームではADRの記録を始めました。これにより、新しいメンバーが参加した際にもスムーズに開発を進められるようになると考えています。 また、近年ではAIを活用した開発が進んでいますが、明確なドキュメントがあることでAIに対しても適切な指示を出しやすくなり、プロジェクトの文脈を理解した上でより質の高い提案を得られるようになるでしょう。 重要なのは完璧を目指すのではなく、まずは小さく始めて習慣化することです。最初は簡単な記録から始め、徐々に洗練させていけば十分です。意思決定はコードと同様に重要な資産であり、チーム全体で共有・管理することで、長期的な開発効率と品質向上につながります。 皆さんもぜひADRを取り入れ、誰もが開発しやすい環境を整備していきましょう。
アバター
はじめに こんにちは。デリッシュキッチン開発部の村上です。 弊社ではエンジニアとPdM全員にCursorを配布しており、生成AIを活用した開発を積極的に行っています。 prtimes.jp エンジニア組織では開発生産性10倍を目標としていますが、そこに到達するためには新しい技術やツールに触れながら、 ある意味でこれまでの開発のやり方を根底から疑ってみて、生成AIの活用を前提とした新しい組織・業務設計をしていく大胆さが求められていきます。 そんな中で、私の所属する組織ではちょうどLPの開発について考える機会がありました。 LPはそこまで複雑な機能はなく、基本的にはデザインに基づいたコーディングが中心になりますが、地味に時間がかかります。 そこで、最近話題のFigma-Context-MCPを使った検証をしているのでそこで得た知見を紹介しようと思います。 今月の初めには弊社CTOもMCPサーバーに関する記事をあげているのでぜひこちらもご覧ください。 tech.every.tv Figma-Context-MCPとは Figma-Context-MCPとは、FigmaのデザインデータをCursorなどのAIコーディングツールの "ツール呼び出し" として扱えるようにする Model Context Protocol (MCP) サーバーです。Figma APIから取得した複雑なJSONの情報を整理してLLMに橋渡しすることによって、効率的にデザイン解析やコード生成が可能になります。 github.com 始め方 セットアップは簡単で公式の クイックスタート を見ればすぐに利用することができますが、こちらでも簡単に説明します。 Figmaアクセストークンの取得 Figma-Context-MCPはFigma APIを通してFigmaの情報にアクセスするのでアクセストークンが必要になります。 アクセストークンはアカウントの設定メニューからSecurityタブを開けば、アクセストークン発行の導線が見つかります。公式ドキュメントによると File content と Dev resources の読み取り権限が実行に必要なため注意してください。 Cursor側でMCPサーバーを設定 こちらも公式ドキュメントからほとんど変えていないですが、 .cursor/mcp.json にMCPサーバーの設定を追加します。 (※2025年4月27日時点でバージョン指定をしていないと接続エラーとなるため、最新のv0.2.1を指定しています) { " mcpServers ": { " Framelink Figma MCP ": { " command ": " npx ", " args ": [ " -y ", " figma-developer-mcp@0.2.1 ", " --figma-api-key=<API KEY> ", " --stdio " ] } } } シンプルなLPデザイン構成でコード生成を試す まずは実際に弊社内でデザインされているLPのFigmaデータから部分的に切り出してシンプルな画像要素中心のLPで検証してみます。 今回は題材として、デリッシュキッチンが手掛けている冷凍宅配弁当の「 Meals 」のデザインデータを使います。 Figmaの構造 切り出したFigmaのLPデータは以下のような構造で画像が中心となっているため、とてもシンプルです。 test1(Frame) ├── Group 48(Group) │ └── kv(Group) │ ├── 帯(Group) │ │ ├── このページをご覧の方 限定!(Text) │ │ └── Rectangle 40(Rectangle) │ └── kv_food(Image) └── diet_onayami(Image) LP生成 Cursorから指示を出してみます。Figmaでは「Copy Link to Section」という形でセクションのリンクを取得できるのでそのリンクを使用します。 Figmaでページ全体を共有すると複数デザインがおいてある場合にすぐにContext上限で応答パフォーマンスも悪くなるため、生成したいものだけに絞ることをお勧めします。 以下のfigmaのデザインを参照して、コーディングを行ってください。 @<figmaの該当セクションのURL> デザインと生成されたLPのスクショを比較したのがこちらです。 ヘッダーの位置がずれてしまっていたり、marginが余計に入ったりと満足のいくものが出てきません。 Auto Layout機能を使ってレイアウトを認識させる 調べていくと公式ドキュメントの Best Practices で以下の言及がされていました。 Use auto layouts—the MCP isn't great at handling floating or absolutely positioned elements just yet どうやらMCPはGroupで指定された絶対値参照のレイアウトが得意ではないようでAuto Layoutを使うことが推奨されていました。私はFigmaについては初心者なので詳しいわけではないですが、Auto LayoutはCSSでいうflexboxのように要素を規則的に並べることが出来る機能で要素の並び方や要素間のgapを定義することができます。 さっそく、 kv と 全体( test1 )にAuto Layoutを適用して、Figmaを更新してみます。 test1(Auto Layout) ├── Group 48(Group) │ └── kv(Auto Layout) │ ├── 帯(Group) │ │ ├── このページをご覧の方 限定!(Text) │ │ └── Rectangle 40(Rectangle) │ └── kv_food(Image) └── diet_onayami(Image) この状態で先ほどと同じ指示でLP生成を行った結果が以下の画像です。 Auto Layoutを指定しただけでその他は構造も命名も変えていませんが、要素の位置を認識してデザイン通りに配置してくれています。画像中心のレイアウトであれば、ここから縦に数を増やしてもアウトプットの精度は大きく変化せず安定していました。 もう少し複雑なLPデザイン構成でコード生成を試す ほとんどのセクションが画像となっているデザイン構成であれば、前述の方法でもかなりLPの開発をショートカットできそうですが、そういったケースだけではなさそうです。もう少し構成要素が多いものでみてみましょう。 Figmaの構造 今回、Auto Layout設定は一部に設定していますが、要素やその配置は先ほどよりやや複雑です。 test3(Auto Layout) └── nutrition(Frame) ├── title(Group) │ ├── icon_point(Image) │ ├── nutrition_title(Image) │ └── nutrition_title_bg(Image) └── 栄養基準(Auto Layout) ├── Group 31(Group) │ ├── 糖質・脂質・塩分・野菜量など8項目の栄養基準(Text) │ └── Rectangle 25(Rectangle) ├── nutrition_ingredients_img(Image) └── ※上記に合わせて脂質(10~20g)、炭水化物(15~35g)の基準を設定(Text) LP生成 先ほどと同じ指示でLP生成をしてみます。 以下のfigmaのデザインを参照して、コーディングを行ってください。 @<figmaの該当セクションのURL> 生成されたLPをみてみるとデザインに比べて、大きく崩れていることがわかります。 絶対値で重なって構成されている要素は一つの画像として扱う 大きく崩れた部分は以下のtitleのGroup配下の要素でした。 test3(Auto Layout) └── nutrition(Frame) ├── title(Group) ├── icon_point(Image) ├── nutrition_title(Image) └── nutrition_title_bg(Image) 今回のFigmaデザインでは3つの画像で一つのヘッダーセクションを構成していましたが、これは前述したように絶対値を苦手とするMCPではコード生成のクオリティが下がります。可能な限り、こういった要素は一つの画像にまとめると自動生成が楽になります。 同一テキスト間で違う文字サイズ、色を使う場合はテキストごと分ける Figmaでは同一テキストパーツでも途中から別の文字サイズ、色を適用することが可能です。今回だと「糖質・脂質・塩分・野菜量など8項目の栄養基準」の部分はデザインだと「8項目の栄養基準」の部分のテキストサイズが大きく、テキストカラーも緑に変わっています。 しかし、MCPでデザインデータを取得するとその情報がLLMに渡す過程で欠落してしまいます。以下はMCPでのツール応答の該当箇所を抜粋したものです。Figmaとしてはoverrideしたstyle情報は持っているようですが、少なくともv0.2.1のFigma-Context-MCPではそのスタイリングには対応していなさそうです。 children: - id: '5414:5619' name: 糖質・脂質・塩分・野菜量など 8項目の栄養基準 type: TEXT textStyle: style_S5EO4M fills: fill_WTN9RR layout: layout_8PK5H6 text: |- 糖質・脂質・塩分・野菜量など 8項目の栄養基準 globalVars: styles: style_S5EO4M: fontFamily: Noto Sans fontWeight: 700 fontSize: 14 lineHeight: 1.3620000566755022em letterSpacing: 10% textAlignHorizontal: CENTER textAlignVertical: TOP fill_WTN9RR: - '#322012' そこでこうしたデザインをLPに反映させるためにはテキストパーツ自体を分けて配置することで認識することができます。 Figmaの構成を修正してLPを再生成 これら2点を改善できるようにFigmaの構成を見直していきます。 section_titleで1つの画像にする text_wrapper配下でそれぞれスタイルの違うテキストを配置 Figmaの最終構成は以下のようになります。 test3(Auto Layout) └── nutrition(Frame) ├── section_title(Image) └── 栄養基準(Auto Layout) ├── Group 31(Group) │ ├── text_wrapper(Auto Layout) │ │ ├── 糖質・脂質・塩分・野菜量など(Text) │ │ └── 8項目の栄養基準(Text) │ └── Rectangle 25(Rectangle) ├── nutrition_ingredients_img(Image) └── ※上記に合わせて脂質(10~20g)、炭水化物(15~35g)の基準を設定(Text) この状態で再度生成をしてみます。 生成された結果は、上部に無駄なpaddingが存在してしまっていますが、そこだけ消せば、再現度は高そうです。 カルーセル要素のあるLPデザイン構成でコード生成を試す 次にデザインもコードのスタイリングも複雑なカルーセル要素のデザインのコード生成を検証してみます。 Figmaの構成 Auto Layout機能を使って、menu_otherがカルーセルの要素として配置されています。 test5(Auto Layout) └── Frame 74(Auto Layout) ├── その他のメニュー(Text) └── menu(Frame) └── Frame(Auto Layout) ├── menu_other(Frame) ├── menu_other(Frame) ├── menu_other(Frame) ├── menu_other(Frame) ├── menu_other(Frame) ├── menu_other(Frame) ├── menu_other(Frame) ├── menu_other(Frame) └── menu_other(Frame) LP生成 LP生成をしてみます。 以下のfigmaのデザインを参照して、コーディングを行ってください。 @<figmaの該当セクションのURL> 生成されたLPをみてみるとそもそもカルーセルを認識できずに縦並びになっています。 意味のある要素名でコードの生成精度を高める 公式ドキュメントの Best Practices でAuto Layoutと一緒に語られているのがFigma上での命名についてです。 Name your frames and groups - Protip: Try Figma's AI to automatically generate names これは単純に名前をつければいいのではなく、LLMが生成するコードのhtml構造やスタイルが正しくなるようにその要素を適切に表した名前にしないといけないということです。 エンジニアであれば、変数やhtmlのclass名に対して気をつかうようにFigmaの要素に対する命名もコードの生成精度に関わってきます。 今回のカルーセルであればカルーセルの要素だとわかるように命名するのが望ましいでしょう。 test5(Auto Layout) └── Frame 74(Auto Layout) ├── その他のメニュー(Text) └── menu(Frame) └── carousel_container(Auto Layout) ├── carousel_item(Frame) ├── carousel_item(Frame) ├── carousel_item(Frame) ├── carousel_item(Frame) ├── carousel_item(Frame) ├── carousel_item(Frame) ├── carousel_item(Frame) ├── carousel_item(Frame) └── carousel_item(Frame) 命名だけの違いですが、LPを生成してみると以前と変わってカルーセル形式でのスタイリングが適用されます。ただし、2つの修正は必要になります。 item要素の画像の絶対値が認識できないのでitemごと画像化を検討 両側のpadding要素を排除 その他: 安定したコード生成をするためのCursorルール 今回のこれらの検証で何度か繰り返し生成をしていると挙動が不安定なケースがあったので最低限のルールとして./cursor/rulesに以下を配置しました。 ## ガイドライン - get_figma_dataでは必ずnodeIdを指定してください。 - download_figma_imagesが失敗した場合は正しいJSON形式で画像のダウンロードを再度試みてください。 - 画像はbackground要素でない限り、html上でimgタグを使ってください。 get_figma_dataでは必ずnodeIdを指定してください。 何回か指示を出しているとたまに渡したURLのセクション情報を無視して、ファイル指定のみでfigmaのデザインデータを取得しようとします。ファイルにデザインデータが多いとcontext情報が多くてスタックしてしまうので、忘れないようにルールを入れています。 download_figma_imagesが失敗した場合は正しいJSON形式で画像のダウンロードを再度試みてください。 2025年4月27日時点では画像のダウンロードはかなり不安定で「invalid parameter」という形でエラーが頻出します。ダメもとにはなりますが、リトライを明確に指示出しすることで成功確率は少し上がりました。 画像はbackground要素でない限り、htmlのimgタグを使ってください。 たまにfigmaの画像をcssのbackground-imageとして全て参照してしまい、スタイルが大きく崩れることがありました。基本的にはimgタグを使って画像を配置しつつ、命名で background_* と書いてあるときだけはbackground-imageとして使ってもらうなど使い分けると良さそうです。 まとめ まだまだ検証は足りないですが、こうしてFigmaのMCPを使ってみると現状では人間がデザインからコードを起こしているから大きな問題にならない部分でもAIに生成させるとなるとひと工夫必要なケースが多く見えてきました。 今後の業務で活用できるレベルまで到達するにはいかにAIやMCPに相性の良いFigmaのデザイン手法を見つけ、組み立てていけるかを1から考えることが大切になりそうなので、エンジニアもデザイナーもより自分たちの領域を超えて、コミュニケーションを取っていくべきでしょう。 こうした業務での活用に伴う検証は短期的に見れば、非効率で直接書いた方が早いと思う場面は何度もありました。しかし、ここで費やした労力が将来的な開発スピードの大きな差になってくると思うので、弊社では引き続き生成AIを積極的に活用した業務改善にチャレンジしていきます! ぜひ、この取り組みに興味を持った方は一度カジュアル面談でお話しましょう!
アバター
はじめに こんにちは。開発部でiOSエンジニアをしている野口です。 今回は挑戦WEEKにてiOS版デリッシュキッチンにウィジェット機能を実装した際の実装方法や、実装中に直面した課題とその解決方法についてお話しします。(※本記事ではiOS版について解説しますが、Android版にも同様の機能を追加しています) 弊社の挑戦WEEKの取り組みについては以下の記事をご覧ください! tech.every.tv ウィジェットについては以前まとめている記事があるので以下の記事もご覧ください! tech.every.tv 今回実装したウィジェットについて 今回実装したウィジェットの要件はこちらです。以下をゴールとして作成しました。 おすすめレシピを表示する レシピは15分毎に更新する ウィジェットをタップするとアプリのレシピ詳細で動画を再生(※タップ後のレシピ詳細の実装は本記事では省略します) 完成したウィジェットはこちらです。 実装の流れ 今回はXcode16.0を使用し、 WidgetKit フレームワークを用いてウィジェットを作成します。 実装の流れとしては以下のようになります。 ウィジェットターゲットの導入 レシピデータの取得とタイムライン管理 ウィジェット画面の作成 1. ウィジェットターゲットの導入 まず、既存のアプリプロジェクトにウィジェット機能を追加するためのターゲットを導入します。 Appleのドキュメント に従い、以下の手順で進めます。 File > New > Target を選択し、 Widget Extension を選択して Next を押します。 プロダクト名を指定します。今回はライブアクティビティやコントロール機能は使用しないため、 Include Live Activity 、 Include Control 、 Include Configuration App Intent のチェックは外して Finish を押します。 注意点: 新たに Target を追加するため、ウィジェットは既存アプリとは別のアプリとして扱われます。そのため、リリース時にはApple Developerサイトでウィジェット用の Identifiers (App ID)を登録する必要があります。 2. レシピデータの取得とタイムライン管理 ウィジェットに表示するデータを取得し、いつ更新するかを管理するのが TimelineProvider の役割です。 ターゲット作成時に、基本的な TimelineProvider のテンプレートコードが生成されます。 // 初期のテンプレートコード(抜粋) struct Provider : TimelineProvider { // データ取得不可時のプレースホルダー表示用データを定義 func placeholder ( in context : Context ) -> SimpleEntry { ... } // ウィジェットギャラリーでのスナップショット表示用データを定義 func getSnapshot ( in context : Context , completion : @escaping ( SimpleEntry ) -> () ) { ... } // ウィジェットの表示更新タイミングとデータ(タイムライン)を定義 func getTimeline ( in context : Context , completion : @escaping ( Timeline < Entry >) -> () ) { ... } } // ウィジェットに表示するデータの単位 (Entry) struct SimpleEntry : TimelineEntry { let date : Date // このデータが表示されるべき日時 let emoji : String // 表示するデータ(初期テンプレート) } 今回実装するにあたり、 TimelineProvider で以下の処理を行います。 placeholder : レシピデータがない場合の表示内容を定義します。(実装詳細は省略) getSnapshot : ウィジェット追加時のプレビュー用に、最初のレシピ情報を取得して表示します。 getTimeline : APIからレシピ情報を取得し、15分ごとに表示内容が切り替わるようなタイムラインを作成します。 以下が TimelineProvider 周りの実装コード全体像です。(API通信処理の実装は省略しています) import WidgetKit import SwiftUI struct Provider : TimelineProvider { // プレビュー用データ取得 func getSnapshot ( in context : Context , completion : @escaping @Sendable ( RecipeTimelineEntry ) -> Void ) { Task { let result = await fetchRecipes() // レシピ情報取得 switch result { case .success( let response ) : if let firstRecipe = response.recipes.first { // 最初のレシピ画像を取得 let fetchedImage = await loadImage(url : firstRecipe.imageURL ) let recipeEntry = RecipeEntry( id : firstRecipe.id , title : firstRecipe.title , image : fetchedImage // 他のレシピ情報も設定 ) completion(RecipeTimelineEntry(date : Date (), recipe : recipeEntry )) // 最初のレシピ情報を反映 } else { // レシピがない場合 completion(RecipeTimelineEntry(date : Date (), recipe : nil )) } case .failure : // APIエラーの場合 completion(RecipeTimelineEntry(date : Date (), recipe : nil )) } } } // タイムライン用データ取得・生成 func getTimeline ( in context : Context , completion : @escaping @Sendable ( Timeline < RecipeTimelineEntry >) -> Void ) { Task { let result = await fetchRecipes() // レシピ情報取得 switch result { case .success( let response ) : if response.recipes.isEmpty { // レシピがない場合は空のタイムラインを返す completion(Timeline(entries : [ RecipeTimelineEntry ( date: Date () , recipe: nil )] , policy : .atEnd)) return } let interval : Int = 15 // 更新間隔(分) let currentDate = Date() // --- レシピ表示の循環性を保つための計算 --- let calendar = Calendar(identifier : .gregorian) let midnight = calendar.startOfDay( for : currentDate ) // 今日の午前0時を取得 // 今日の0時から現在時刻までに経過した分数を計算し、15分間隔で割る // これにより、「今日が始まってから何番目の15分区間か」がわかる let currentTimelineOffset : Int = (calendar.dateComponents([.minute], from : midnight , to : currentDate ).minute ?? 0 ) / interval // --- ここまで --- var entries : [ RecipeTimelineEntry ] = [] // 現在の区間から8つ先まで(2時間分)のエントリーを作成 // ループの開始点を currentTimelineOffset にすることで、getTimeline がいつ呼ばれても // その時点からの適切なレシピが表示されるようにする for timelineOffset in currentTimelineOffset ..< currentTimelineOffset + 8 { // 表示するレシピのインデックスを計算 (リストの末尾まで行ったら先頭に戻る) let recipeIndex = timelineOffset % response.recipes.count let recipe = response.recipes[recipeIndex] // レシピ画像を取得 let fetchedImage = await loadImage(url : recipe.imageURL ) let recipeEntry = RecipeEntry( id : recipe.id , title : recipe.title , image : fetchedImage // 他のレシピ情報も設定 ) // このレシピを表示する日時を計算 (今日の0時から timelineOffset * 15 分後) if let entryDate = calendar.date(byAdding : .minute, value : timelineOffset * interval, to : midnight ) { entries.append(RecipeTimelineEntry(date : entryDate , recipe : recipeEntry )) } } // 生成したエントリーリストと、最後の表示が終わった後に更新するポリシーでタイムラインを作成 completion(Timeline(entries : entries , policy : .atEnd)) case .failure : // APIエラーの場合、とりあえず現在のデータで終了 completion(Timeline(entries : [ RecipeTimelineEntry ( date: Date () , recipe: nil )] , policy : .atEnd)) } } } // 非同期で画像を読み込む private func loadImage (url : URL ) async -> Image? { do { let (data, _) = try await URLSession.shared.data(from : url ) if let uiImage = UIImage(data : data ) { return Image(uiImage : uiImage ) } } catch { print( "Image loading error: \( error ) " ) } return nil } } // 1つのタイムラインエントリーが持つデータ構造 struct RecipeTimelineEntry : TimelineEntry { let date : Date // このデータが表示される日時 let recipe : RecipeEntry? // 表示するレシピ情報 } // レシピ情報の詳細を保持する構造体 struct RecipeEntry { let id : Int64 // レシピID let title : String // レシピタイトル let image : Image? // レシピ画像 (SwiftUI用) // 以下、必要に応じて他のレシピ情報 } getSnapshot の解説 getSnapshot は、ユーザーがウィジェットを追加しようとする際に表示されるプレビュー画面のためのものです。ここではレシピ情報の最初の1件 ( response.recipes.first ) を使ってプレビュー用の SimpleEntry を作成し、 completion ハンドラで返しています。 ちなみにプレビュー表示はこんな感じになります。 getTimeline の解説 getTimeline はウィジェットの表示内容とその更新タイミングを定義します。 課題: レシピの循環表示 要件は「15分ごとにレシピを更新する」ことなので、単純に15分間隔のエントリーをリストの先頭から順に生成することが考えられます。しかし、 WidgetKit のタイムラインの更新タイミングはOSによって最適化されており、必ずしも作成した全エントリー(今回は2時間分)が表示された後に次の getTimeline が呼ばれるとは限らず、OSによる更新が頻繁に起こった場合に、最初の数個のレシピばかりが表示され続ける、という問題が発生し得ます。 解決策: 現在時刻に応じた開始位置の計算 この問題を回避し、レシピを循環的に表示させるため、以下の計算を行っています。 let calendar = Calendar(identifier : .gregorian) let midnight = calendar.startOfDay( for : currentDate ) // 今日の午前0時 (基準点) // 今日の0時から現在時刻までに「15分間の区画」が何回経過したかを計算 let currentTimelineOffset : Int = (calendar.dateComponents([.minute], from : midnight , to : currentDate ).minute ?? 0 ) / interval Calendar を使って「今日の午前0時 ( midnight )」を取得します。これを1日の基準点とします。 dateComponents で、午前0時から現在時刻 ( currentDate ) までの経過時間を「分」で計算します。 その経過分数を interval (15) で割ります。この結果が currentTimelineOffset となり、「今日が始まってから現在時刻までに、15分の区切りが何回あったか」を示します。 タイムラインエントリーの生成 この currentTimelineOffset を使って、 for ループでタイムラインエントリーを生成します。 // 現在の区間 (currentTimelineOffset) から8つ先まで (2時間分) のエントリーを作成 for timelineOffset in currentTimelineOffset ..< currentTimelineOffset + 8 { // レシピリスト内で循環するようにインデックスを計算 let recipeIndex = timelineOffset % response.recipes.count let recipe = response.recipes[recipeIndex] // ... (画像取得、RecipeEntry作成) ... // エントリーが表示されるべき日時を計算 if let entryDate = calendar.date(byAdding : .minute, value : timelineOffset * interval, to : midnight ) { entries.append(SimpleEntry(date : entryDate , recipe : recipeEntry )) } } // 生成したエントリーリストでタイムラインを作成。.atEnd はリストの最後を表示後に更新を促す completion(Timeline(entries : entries , policy : .atEnd)) ループ開始点を currentTimelineOffset にすることで、OSがいつウィジェットを更新しても、その時刻に応じたレシピから表示が始まるように調整されます。これにより、常にリストの先頭からスケジュールが作られてしまうのを防いでいます。 さらに、ループ内で使うレシピ番号は let recipeIndex = timelineOffset % response.recipes.count で計算しています。これによって、レシピリストの最後まで表示したら次は先頭のレシピに戻るように循環的に表示できます。 画像の扱い 画像を表示する際に AsyncImage を使用することができませんでした。なので、 TimelineProvider 側で Image に変換してから SimpleEntry に含めるようにしています。 // 非同期で画像を読み込む private func loadImage (url : URL ) async -> Image? { do { let (data, _) = try await URLSession.shared.data(from : url ) if let uiImage = UIImage(data : data ) { return Image(uiImage : uiImage ) } } catch { print( "Image loading error: \( error ) " ) } return nil } ウィジェット画面作成 ウィジェットのUIは SwiftUI を使って構築します。 まず、 Widget プロトコルに準拠した構造体 (例: RecipeWidget ) でウィジェット全体の設定を行います。 ここで、 .supportedFamilies モディファイアを使って、サポートするウィジェットのサイズ(今回は .systemSmall と .systemMedium )を指定します。 また、 .contentMarginsDisabled() でデフォルトもマージンを無効して画面いっぱいに画像が表示できるようにしています。 struct RecipeWidget : Widget { let kind : String = "RecipeWidget" var body : some WidgetConfiguration { StaticConfiguration(kind : kind , provider : Provider ()) { entry in RecipeWidgetEntryView(entry : entry ) .containerBackground(.fill.tertiary, for : .widget) } .contentMarginsDisabled() // ウィジェットのデフォルトマージンを無効にする .configurationDisplayName( "本日のおすすめレシピ" ) .supportedFamilies([.systemSmall, .systemMedium]) // 対応サイズを指定 } } 次に、実際にウィジェットの内容を表示する View (例: RecipeWidgetEntryView ) を作成します。 サイズ取得: @Environment(\.widgetFamily) を使って、現在表示されているウィジェットのサイズ ( .systemSmall など) を取得します。 データ受け取り: var entry: Provider.Entry のように TimelineProvider から渡されたデータ( SimpleEntry )を受け取るプロパティを定義します。 Linkでタップ時の動作を設定: Link を使ってウィジェット全体または一部をラップし、タップされた際に指定した URL スキーム ("アプリのURLスキーム://レシピ詳細パス/(recipe.id)") を使ってアプリ本体の特定画面へ遷移させます。 サイズに応じて表示を切り替え: family に応じてSmall、Mediumウィジェットに切り替える struct RecipeWidgetEntryView : View { @Environment ( \.widgetFamily ) var family // サイズ取得 var entry : Provider.Entry // 2. データ受け取り var body : some View { if let recipe = entry.recipe { // 3. Linkでタップ時の動作を設定 Link(destination : URL (string : "アプリのURLスキーム://レシピ詳細パス/ \( recipe.id ) " ) ! ) { // 4. サイズに応じて表示を切り替え if family == .systemSmall { SmallWidgetView(recipe : recipe ) } else { MediumWidgetView(recipe : recipe ) } } } } } struct SmallWidgetView : View { let recipe : RecipeEntry // ... Smallサイズ用のレイアウト ... var body : some View { Text( "Medium: \( recipe.title ) " ) } // 省略 } struct MediumWidgetView : View { let recipe : RecipeEntry // ... Mediumサイズ用のレイアウト ... var body : some View { Text( "Medium: \( recipe.title ) " ) } // 省略 } このように、 Widget 構造体で全体設定を行い、表示用の View でデータやサイズに応じたUI構築することで、ウィジェット画面を作成します。 終わりに 今回の挑戦WEEKでのウィジェット機能追加により、ユーザーはアプリを起動せずともホーム画面で手軽におすすめレシピをチェックできるようになりました。これによってアプリの起動率の向上に繋がることを期待しています。 ウィジェット開発は、タイムライン管理など通常のアプリ開発とは異なる考慮点がありましたが、この挑戦を通じてWidgetKitの知見を深めることができ、今後の開発にも活かせる貴重な経験となりました。 今回実装したウィジェット機能は既にリリース済みですので、デリッシュキッチンをお使いの方は、ぜひホーム画面に追加して試してみてください! 最後までお読みいただき、ありがとうございました。
アバター
レバテックさんに弊社テックブログを紹介してもらいました 嬉しいニュース テックブログの価値 おすすめ記事のご紹介 バックエンド開発 フロントエンド開発 インフラ・クラウド アプリ開発 AI技術 機械学習 今後の展望 最後に  こんにちは、開発本部 RetailHUB開発部 NetSuperグループ兼、CTO 室 Dev Enable グループに所属するホーク🦅アイ👁️です。 嬉しいニュース  先日、嬉しいニュースが届きました。レバテック株式会社様が運営する「レバテックフリーランス」というプラットフォームで、弊社のテックブログを紹介してもらいました! freelance.levtech.jp  この記事では、エンジニアのスキルアップに役立つ様々な企業のテックブログが紹介されています。弊社以外にも、多くの企業のブログが掲載されており、技術情報の宝庫となっています。 テックブログの価値  テックブログは、単なる技術情報の共有だけでなく、各企業の開発文化や課題解決のアプローチを垣間見ることができる貴重なリソースです。弊社のブログも、そんな価値を提供できるよう心がけています。  弊社のテックブログは、実務に即した具体的な内容が中心で、バックエンドからフロントエンド、インフラ、アプリ開発、機械学習など多岐にわたる記事を高頻度で投稿しています。毎週2回のブログ公開を目標に、最新の技術動向や実践的な知見を共有しています。  特に、AI技術の活用は弊社の重要な戦略の一つであり、今後も積極的に情報発信していく予定です。 おすすめ記事のご紹介  弊社のテックブログから、各分野のおすすめ記事をご紹介します! バックエンド開発 tech.every.tv tech.every.tv フロントエンド開発 tech.every.tv tech.every.tv インフラ・クラウド tech.every.tv tech.every.tv アプリ開発 tech.every.tv tech.every.tv AI技術 tech.every.tv tech.every.tv 機械学習 tech.every.tv tech.every.tv 今後の展望  このような形で外部から評価いただけたことは、弊社のテックブログ活動の励みになります。今後も、より質の高い技術情報を発信し続けていきたいと考えています。  読者の皆様には、引き続き弊社のテックブログをお楽しみいただければ幸いです。また、ブログを通じて弊社に興味を持っていただけた方は、ぜひ採用情報もご覧ください。 最後に エブリーでは、ともに働く仲間を募集しています。 テックブログを読んで少しでもエブリーに興味を持っていただけた方は、ぜひ一度カジュアル面談にお越しください! corp.every.tv
アバター
データ&AIチームでデータエンジニアを担当している塚田です。 はじめに エブリーではデータ基板の活用の方法としてRedashとAmazon QuickSightを利用しています。 ビジネス職でもSQLを使ったデータ取得・分析は一定程度できる状況ではありますが、まだまだ利用するにあたって壁があることも事実です。 今回、Amazon QuickSightに搭載されている生成AIアシスタントである Amazon Q に新機能『シナリオ分析』がGAされました。 本記事では、このAmazon Q in QuickSightの シナリオ分析機能 にフォーカスし、弊社サービスのデリッシュキッチンの検索データを例にその可能性を検証します。 Amazon QuickSight とは? Amazon QuickSight は、AWSが提供するスケーラブルでサーバーレスなクラウドベースのビジネスインテリジェンス(BI)サービスです。 様々なデータソースに接続し、インタラクティブなダッシュボードやレポートを簡単に作成・共有できます。特徴として従量課金モデルで、インフラストラクチャの管理が不要な点にあります。 Amazon Q とは? (QuickSight 文脈で) Amazon Q は、AWSが開発した生成AIアシスタントです。ドキュメントの要約やコード生成など、様々な業務を支援する機能を持っています。QuickSightにおける Amazon Q は、この生成AI機能をデータ分析に応用したものです。 具体的には、QuickSight 上で「去年の売上トップ3の製品は?」「先月と比較してコンバージョン率はどう変化した?」といった自然言語で質問すると意図を理解し、適切なデータ分析を行い、グラフや表、あるいは文章によるサマリーとして結果を返してくれます。 シナリオ分析がなぜ必要か データを元にした分析は仮説を立てて必要なデータを集め分析し結果を出力し検証するという流れを何回も繰り返すことで実施していきます。 人間ではそれぞれのフェーズごとに時間をかけタスクを進めていきますが、 生成AIを活用した分析だと仮説を立てた時に「もし広告費をxx%増やしたら...」という曖昧な内容を「広告費というメトリクスを」「現在の値からxx%増加させる」という仮説の条件を特定し、その内容に基づいてデータやその関連性を判断し適切なデータの利用や集計を行っていきます。 また、集計したデータのシミュレーションや結果の視覚化まで幅広い対応を行うことができ、得たい結論までの道筋を自動的に出力することによって分析の幅は広がると思っています。 このような仕組みを活用することでアイデアを素早く検証したり既存プロセスの効率化が図れるようになってくると考えています。 実際に利用してみる ※今回ご紹介する機能は執筆時点で利用可能な権限やリージョンに制限がありますので、利用環境を確認した上で利用してください。 該当機能が利用可能な状態のときは左メニューに Scenarios のメニューが表示され、これを選択すると右上に NEW SCENARIO のボタンがあるのでそこから新しいシナリオ分析を開始できます。 その後利用するデータを選択します。すでにQuickSightを利用していればダッシュボードのデータを使うこともできますが、手元にあるCSVなどのデータを利用することも可能です。 今回は弊社アプリのデリッシュキッチンの検索ログの集計済みデータを用いて分析してみたいと思います。 このデータは日毎の検索ワード、検索回数、1日の検索回数などが入っているデータとなります。 簡単な集計の質問を投げてみる このように質問を投げることで英語での回答になりますが、質問に近しい分析メニューを提示してくれます。 今回は質問と同じ内容の Show the top 20 most searched words in the last 7 days を選択します。 選択するとベースのデータに対しての分析方法とともに結果が出力されます。 ※元となったデータを手動で集計したところ同様の結果となっています また、その結果に対して追加の分析も行うことが可能で 豚肉の日毎のトレンド を追加分析すると以下のような結果となりこちらも元データと一致しています。 ※データはサンプルとなるので、実際の傾向とは異なります 同じデータを用いた分析として 過去 1 週間で最も検索された上位 5 つの検索ボリュームの傾向 を出力しました。 ※データはサンプルとなるので、実際の傾向とは異なります このようにStep1からStep5の処理を経て結論まで出力されました。 分析スキルがない方でも利用したいデータが手元にあれば分析はもちろん関連した追加の示唆だしが行える状態になります。 まとめ 今回はQuickSightを例に手軽さと活用範囲の大きさを説明しましたが、これに限らず多数のツールが世の中には出ている状況です。 こういったツールにより、データ分析の専門家ではないビジネスユーザーでも、知りたい情報を迅速に、多角的に探索できるようになると考えています。 アイデアを素早く検証し、変化に即応するためのデータに基づいた意思決定が向き合いの事業の成長にも寄与できる可能性も広がります。 その中で何を使うか、どのように評価するかはそれぞれの状況によってメリット・デメリットがあるかとは思いますが、 データを安全な状態で利用することができる環境を作っていくということはデータエンジニアとして継続的に取り組んでいくべき重要な課題だと改めて認識しました。
アバター
sealed Class とは ディープリンクの制御を sealed Class を利用してリファクタリングしてみる リファクタリング前のコード リファクタリングの内容 使用例 責務を分離することにより得られた恩恵 sealed Class により得られる恩恵 終わりに エブリーで小売業界向き合いの開発を行っている @kosukeohmura です。私は普段はバックエンドの開発を行っていますが、数ヶ月ほど前から Flutter アプリの開発にも従事しております。エブリーではここ数年いくつかの Flutter アプリを開発/運用しています。 今回は Flutter アプリに機能追加する中で、 sealed Class を使ってリファクタリングを経て機能追加してみたので、その例を紹介します。 sealed Class とは Dart における sealed Class は そのクラス自体のインスタンスを生成できない点で abstract Class と似ています。 abstract Class と異なる点として、 Exhaustiveness checking (意訳: 網羅性チェック) により、 switch 文やパターンマッチングでサブクラスを網羅的に扱えることがあります。これにより、例えば switch 文でケースの書き漏れが存在するとコンパイルエラーとなり気づくことができます。 enum を使うことでも網羅性チェックを行えますが、列挙子それぞれにはフィールドやメソッドを持たせることはできません。一方 abstract Class ではフィールドやメソッドを持つことができますが、網羅性チェックは行えません。 sealed Class にはすべてのサブタイプは同じファイルに定義するという制約がありますが、それを受け入れることで網羅性チェックが可能となり、結果的に abstract Class と網羅性チェックそれぞれの恩恵を受けられると理解しています。 ディープリンクの制御を sealed Class を利用してリファクタリングしてみる アプリのディープリンク経由での起動のハンドリングを実装する際、URL パスごとに処理や遷移先画面が異なり、またそれぞれ特定のパスパラメータやクエリパラメータを期待するかと思います。 sealed Class の特徴を利用することで、URL パスごとに異なるパラメータをコード上に表現しながら、安全かつ見通しよくロジックを記述できると思い、実際にリファクタリングを行いました。 リファクタリング前のコード 以下はディープリンクに応じ、いくつかの条件を満たす場合に画面遷移を行うコードです。 // app.dart // リンクを受信した場合に呼ばれ、それを処理する関数 void openDeepLink(Uri uri) { final hostName = uri.host; if (hostName.isEmpty) return ; switch (hostName) { case 'news' : if (uri.pathSegments.length == 1) { final newsId = int .parse(uri.pathSegments[0]); // newsId を使って特定のお知らせの画面へ遷移 } break ; case 'carts' : // カート画面へ遷移 break ; default : // 未知のリンク break ; } } このコードには以下の問題があります: 責務が混在している URL に応じたバリデーション、値の取り出し、画面遷移の処理がすべて app.dart というアプリ全体の設定を書くファイル内の openDeepLink 関数に含まれています。単体テストは困難で、色々なパターンの URL を実際に踏むなどし動作確認をするしかなく、非効率です。 URL 構造の表現の欠落 それぞれの URL パスがどういう意味のパラメータの存在を期待しているか(いないか)が、コード上に明確に現れていません。 'news' のケースでは myapp://news/:id といった形式を期待しますが、 uri.pathSegments.length といったプリミティブな要素を扱うため、仕様と処理の一致が読み取りにくくなっています。 これらの課題を解決するため、リファクタリングをしてみます。 リファクタリングの内容 DeepLink という sealed Class を定義し、URL が特定の条件を満たしているかを確認し、値を取り出せるようにする責務を対象の関数から分離しました。結果下記のようになりました: // deep_link.dart sealed class DeepLink { final Uri uri; DeepLink._( this .uri); factory DeepLink.fromUri(Uri uri) { switch (uri.host) { case 'carts' : return CartDetailDeepLink.fromUri(uri); case 'news' : return NewsDetailDeepLink.fromUri(uri); } throw FormatException( 'Unknown URI pattern. URI: $uri' ); } } class NewsDetailDeepLink extends DeepLink { final int newsId; NewsDetailDeepLink(Uri uri, this .newsId) : super ._(uri); factory NewsDetailDeepLink.fromUri(Uri uri) { if (uri.pathSegments.length != 1) { throw FormatException( 'Invalid NewsDetailDeepLink URI: $uri' ); } return NewsDetailDeepLink(uri, int .parse(uri.pathSegments[0])); } } class CartDetailDeepLink extends DeepLink { CartDetailDeepLink(Uri uri) : super ._(uri); factory CartDetailDeepLink.fromUri(Uri uri) { if (uri.pathSegments.isNotEmpty) { throw FormatException( 'Invalid CartDetailDeepLink URI: $uri' ); } return CartDetailDeepLink(uri); } } 使用例 リファクタリング元の関数は以下のように書き換えました。 // app.dart void openDeepLink(Uri uri) { final DeepLink deepLink; try { deepLink = DeepLink.fromUri(uri); } catch (e) { // エラーハンドリング } switch (deepLink) { case NewsDetailDeepLink(: final newsId): // `:final newsId` の記述でオブジェクトのプロパティをローカル変数として利用 // newsId を使って特定のお知らせの画面へ遷移 break ; case CartDetailDeepLink: // カート画面へ遷移 break ; } } 責務を分離することにより得られた恩恵 URL 構造のコードでの表現 どの URL パスがどういう意味のパラメータの存在を期待しているか(いないか)、をコード上に明確に表現できるようになりました。 見通しの良さ、テスト可能性の向上 URL の解析と画面遷移のロジックを分離することで、コードの見通しが良くなりました。URL の解析ロジックを独立させたことで、以下のように単体テストを簡単に記述できます。なおテスト対象が単純になると、テストコードの記述を AI 任せにするのも楽になります。 group( 'DeepLink.fromUri' , () { test( 'should return NewsDetailDeepLink for valid news URI' , () { final uri = Uri.parse( 'example://news/123' ); final deepLink = DeepLink.fromUri(uri); expect(deepLink, isA<NewsDetailDeepLink>()); final newsDeepLink = deepLink as NewsDetailDeepLink; expect(newsDeepLink.newsId, 123); }); }); sealed Class により得られる恩恵 今回責務の分離に加え、 sealed Class の使用による恩恵も受けられるようになりました。 網羅性チェック switch 文で default ケースを記述する必要がなくなりました。かつ、case の漏れが生じた場合にはコンパイル時にエラーとなるため、漏れを防げます。 コードの一貫性 サブクラスを同じファイル内に記述する制約が生まれることで、関連するコードが一箇所にまとまり、メンテナンス性が向上します。 終わりに sealed Class を使って、実際にコードをリファクタリングした例をご紹介しました。 まだまだ未熟な Flutter エンジニアですが、Flutter / Dart の開発業務の中で知見を深めまたご紹介できればと思います。お読みいただきありがとうございました。
アバター
はじめに こんにちは!トモニテで開発を行っている吉田です。 今回は Serverless Framework で作成した Lambda 関数を lambroll に移行しようとしたことについて書きます! 移行検討の背景 昨年、Serverless Framework の v4 がリリースされました。 v4 からはライセンス形態が変更されて、収益の閾値を満たす、あるいは超える組織では有料でサブスクリプションを購入する必要があります。また v3 は 2024 年までのサポートで、クリティカルなセキュリティ問題やバグにしか対応しません。 他にもランタイムのアップデートにも対応していないため、今は大丈夫でもいずれ AWS Lambda のランタイムサポートが切れるため、v3 から v4 へのバージョンアップが必要になります。 www.serverless.com そこで Serverless Framework v3 を v4 に上げる以外の方法について検討しました。 Serverless Framework からの移行先候補について 1. Pulumi 概要 : Go や TypeScript などの汎用プログラミング言語で利用できるマルチクラウド対応の IaC ツール メリット: Go, TypeScript, Python などで記述できる ユニットテストが書ける 関連リソースもデプロイ可能 デメリット: 料金が発生する可能性あり。規模によっては無料枠に収まるが、料金体系が変更された場合には今回と同様に移行先の検討が必要になる 2. AWS CDK (AWS Cloud Development Kit) 概要 : Pulumi と同様に汎用プログラミング言語で利用できる AWS 向けの IaC ツール。AWS CloudFormation を通じてインフラストラクチャのリソースをデプロイ。Serverless Framework でデプロイしたスタックから移行できる cdk migrate というツールはあるが、懸念事項あり。 メリット: Go, TypeScript, Python などで記述できる 関連リソースもデプロイ可能 無償で利用可能 デメリット: cdk migrate で移行した場合、構造が最適化されない可能性がある。 上記のこともあり移行工数多め docs.aws.amazon.com 3. AWS SAM (AWS Serverless Application Model) 概要 : Serverless Framework と同様に yaml ベースでインフラ定義を記述して、CLI から操作するツール。AWS CloudFormation を通じてインフラストラクチャのリソースをデプロイする。 メリット: Serverless Framework と同じく yaml ベースでの定義設定 関連リソースもデプロイ可能 無償で利用可能 デメリット: Serverless Framework よりもプラグインは少なくなる 4. fujiwara/lambroll 概要 : Lambda 関数をデプロイすることに特化したツール。 メリット: tfstate を参照して連携できる。 CD の用意あり 無償で利用可能 社内での利用実績がある。 デメリット: 関連リソースは Terraform で管理する必要がある 5. Terraform + AWS CLI 概要: 既存の Terraform でのインフラ管理に便乗するプレーンな手段 メリット: インフラ管理ツールを 1 つにまとめられる(弊社的都合) 学習コストは低い 無償で利用可能 デメリット: 自動デプロイなどの CD に関してフローを整理する必要がある 学習コストや運用実績などを鑑み今回は fujiwara/lambroll(以下 lambroll)で移行が可能なのか調査を行いました。 (もし移行できたとしても CloudFormation で作成したものを途中で別のツールへ移行して不具合が起きないのか等と懸念事項はありますが今回はその点は考慮せず一旦ライトに実験しました) Serverless Framework と lambroll の違い 移行にあたり両者の違いは理解しておく必要があります。 Serverless Framework の特徴としては以下が挙げられます。 Lambda をメインとするインフラリソースをデプロイする デプロイは CloudFormation を使用して行われる 一方 lambroll は Lambda 関数のデプロイのみを行い Lambda 関数に関連したリソースをデプロイすることはできません。 lambroll does not, Manage resources related to the Lambda function. For example, IAM Role, function triggers, API Gateway, and etc. Only the function URLs can be managed by lambroll if you want. Build native binaries or extensions for Linux (AWS Lambda running environment). ( https://github.com/fujiwara/lambroll より) 移行できるか試してみる config ファイルを lambroll で作成しそれ以外は変更せずデプロイ 既存関数のダウンロード(LATEST で動作している zip をダウンロードできる) $ lambroll init --download ダウンロードすると Lambda 関数の定義となる function.json が作成されるので設定値を調整 デプロイ(ログを詳細に確認したかったので log-level オプションを設定) $ lambroll deploy --envfile=<環境別の設定ファイル> --log-level=debug デプロイ時のログを確認すると tag の設定に失敗していました。 2025 / 02 / 20 17:14:29 [ info ] lambroll v1. 1 . 3 2025 / 02 / 20 17:14:29 [ debug ] { " Src " : " . " , " Publish " :true, " AliasName " : " current " , " AliasToLatest " :false, " DryRun " :false, " SkipArchive " :false, " KeepVersions " :0, " Ignore " : "" , " FunctionURL " : "" , " SkipFunction " :false, " ExcludeFile " : " .Lambdaignore " , " KeepSymlink " :false } 2025 / 02 / 20 17:14:29 [ info ] starting deploy function < 関数名 > 2025 / 02 / 20 17:14:30 [ info ] creating zip archive from . (一部省略) 2025 / 02 / 20 17:14:30 [ info ] updating function configuration ... 2025 / 02 / 20 17:14:30 [ info ] State:Active LastUpdateStatus:Successful 2025 / 02 / 20 17:14:31 [ info ] updating function configuration accepted. waiting for LastUpdateStatus to be successful. 2025 / 02 / 20 17:14:31 [ info ] State:Active LastUpdateStatus:InProgress 2025 / 02 / 20 17:14:31 [ info ] waiting for LastUpdateStatus Successful 2025 / 02 / 20 17:14:32 [ info ] State:Active LastUpdateStatus:InProgress 2025 / 02 / 20 17:14:32 [ info ] waiting for LastUpdateStatus Successful 2025 / 02 / 20 17:14:34 [ info ] State:Active LastUpdateStatus:Successful 2025 / 02 / 20 17:14:34 [ info ] updating function configuration successfully 2025 / 02 / 20 17:14:34 [ debug ] 7 tags found 2025 / 02 / 20 17:14:34 [ debug ] remove tag cloudformation: < tag名 > 2025 / 02 / 20 17:14:34 [ debug ] remove tag cloudformation: < tag名 > 2025 / 02 / 20 17:14:34 [ debug ] remove tag cloudformation: < tag名 > 2025 / 02 / 20 17:14:34 [ debug ] remove tag aws:cloudformation: < tag名 > 2025 / 02 / 20 17:14:34 [ debug ] remove tag aws:cloudformation: < tag名 > 2025 / 02 / 20 17:14:34 [ debug ] remove tag aws:cloudformation: < tag名 > 2025 / 02 / 20 17:14:34 [ info ] removing 6 tags 2025 / 02 / 20 17:14:34 [ error ] FAILED. failed to untag resource: operation error Lambda: UntagResource, https response error StatusCode: 400 , RequestID: 6798028d-f699-4508-b000-0ba7b5eab222, InvalidParameterValueException: One or more of your tags had a problem, please ensure none of your tag names start with " aws: " , none are empty, and there are no duplicate tags ただ config の更新は成功していそうです。 2025 / 02 / 20 17:14:34 [ info ] updating function configuration successfully 実際にコンソールで確認すると、S3 に zip ファイルはアップロードされ、Lambda 関数が動作することも確認できました。 ただ、デプロイされたファイルのサイズは Serverless でアップロードしたものより大きくなってしまっていたので、この辺りはちゃんと見直す必要がありそうです。 上記で config ファイルがデプロイできるのが確認できたので試しにランタイムの更新ができるか試したところこちらも問題なくできました。 Lambda 関数の処理内容を変更 config は設定できたので処理内容を変更してみました。ログは先ほどと同様にタグの設定でエラーが出ました。 2025 / 02 / 20 18:04:02 [ error ] FAILED. failed to tag resource: operation error Lambda: TagResource, https response error StatusCode: 400 , RequestID: 7e16baf5-4b77-4248-a3c9-5f3d75d13fa0, InvalidParameterValueException: One or more of your tags had a problem, please ensure none of your tag names start with " aws: " , none are empty, and there are no duplicate tags コンソールで確認したみたところ確かに aws: で始まるタグがありました。タグに関わらず設定値は基本的に既存リソースの設定をダウンロードしたものだったので特に変更はしていませんでしたが aws: で始まるのがダメならそうじゃないようにすればいいかタグ名を変更した上デプロイしたりタグを設定せずにデプロイしてみましたが、変わらずタグ操作で失敗しデプロイできず...(すでに設定された aws:~ のタグを外すこともできませんでした) 一応 AWS CLI で untag-resource コマンドを実行してみましたがこちらでもタグの変更はできませんでした。 $ aws lambda untag-resource \ --resource arn:aws:Lambda: < region > : < アカウントID > :function: < 関数名 > \ --tag-keys aws:cloudformation:stack-name \ --tag-keys aws:cloudformation:stack-id \ --tag-keys aws:cloudformation:logical-id An error occurred ( InvalidParameterValueException ) when calling the UntagResource operation: One or more of your tags had a problem, please ensure none of your tag names start with " aws: " , none are empty, and there are no duplicate tags リソースの serverless.yml を見ても tag を明示的に指定しているところがなかったので調べたところ、 CloudFormation ではプレフィックス aws: が付いた以下のようなスタックレベルのタグを自動的に作成するとのことでした。 aws:cloudformation:logical-id aws:cloudformation:stack-id aws:cloudformation:stack-name https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-resource-tags.html さらに以下の記述もありました。 aws: プレフィックスは AWS 用に限定されています。このプレフィックスでは、大文字と小文字は区別されません。Key または Value プロパティでこのプレフィックスを使用すると、タグを更新または削除することはできません。 タグのエラーが出る原因が分かりました。 今回の調査では、CloudFormation によって自動的に付与された aws: プレフィックスのタグが原因で、lambroll からのタグ操作が失敗し、Serverless Framework で作成した Lambda 関数を lambroll に移行することは難しいという結論に至りました。 終わりに Serverless Framework で作成した Lambda 関数を lambroll に移行することはできませんでした。 もし Serverless Framework から lambroll へ乗り換える場合には以下の手順で作業を行う必要がありそうです。 既存関数のダウンロード( $ lambroll init --download ) 既存の関数と同等の関数ができるように function.json の設定値を調整 Serverless で作成したリソースを削除 lambroll で Lambda 関数デプロイ lambroll は既存の Lambda 関数をダウンロードすることができるので、移行する時のハードルはそこまで高くないのかなと感じました。また lambroll では関連リソースが作成されないため、これについては terraform に移行するなど別で検討が必要になります。 最後まで読んでいただきありがとうございました! 2025/5/15 追記 なんとこのブログを lambroll の作者である fujiwara さんに読んでいただけました! なるほど、aws: prefixが付いたタグを無視するようにしたら解決する? / “Serverless Framework で作成した Lambda 関数を lambroll に移行できるのか調査しました - every Tech Blog” https://t.co/VdaupqClp6 — fujiwara (@fujiwara) 2025年4月15日 そして 4/25 に更新された リリースノート の中に ignore AWS managed tags for tag/untag operation. という変更が!! 早速最新のバージョン v1.3.0 に上げてブログ内の操作を再トライしてみました! ※ログは一部省略しています $ lambroll deploy --envfile = .env.dev --log-level = debug 2025 / 05 / 14 21:09:41 [ info ] lambroll v1. 3 . 0 ... 2025 / 05 / 14 21:09:44 [ info ] updating function configuration ... 2025 / 05 / 14 21:09:44 [ info ] State:Active LastUpdateStatus:Successful 2025 / 05 / 14 21:09:45 [ info ] updating function configuration accepted. waiting for LastUpdateStatus to be successful. 2025 / 05 / 14 21:09:45 [ info ] State:Active LastUpdateStatus:InProgress 2025 / 05 / 14 21:09:45 [ info ] waiting for LastUpdateStatus Successful 2025 / 05 / 14 21:09:46 [ info ] State:Active LastUpdateStatus:Successful 2025 / 05 / 14 21:09:46 [ info ] updating function configuration successfully 2025 / 05 / 14 21:09:46 [ debug ] 4 tags found 2025 / 05 / 14 21:09:46 [ info ] ignoring AWS managed tag aws:cloudformation:stack-id 2025 / 05 / 14 21:09:46 [ info ] updating function code ... 2025 / 05 / 14 21:09:46 [ info ] State:Active LastUpdateStatus:Successful 2025 / 05 / 14 21:09:47 [ info ] updating function code accepted. waiting for LastUpdateStatus to be successful. 2025 / 05 / 14 21:09:47 [ info ] State:Active LastUpdateStatus:InProgress 2025 / 05 / 14 21:09:47 [ info ] waiting for LastUpdateStatus Successful 2025 / 05 / 14 21:09:48 [ info ] State:Active LastUpdateStatus:InProgress 2025 / 05 / 14 21:09:48 [ info ] waiting for LastUpdateStatus Successful 2025 / 05 / 14 21:09:50 [ info ] State:Active LastUpdateStatus:InProgress 2025 / 05 / 14 21:09:50 [ info ] waiting for LastUpdateStatus Successful 2025 / 05 / 14 21:09:55 [ info ] State:Active LastUpdateStatus:Successful 2025 / 05 / 14 21:09:55 [ info ] updating function code successfully 2025 / 05 / 14 21:09:55 [ info ] deployed version 30 2025 / 05 / 14 21:09:55 [ info ] updating alias set current to version 30 2025 / 05 / 14 21:09:55 [ info ] alias current is not found. creating alias 2025 / 05 / 14 21:09:55 [ info ] alias updated エラーは出ず無事にアップロードが完了していることをコンソールで確認できました。 fujiwara さんにこのブログが届くとは思っていなかったので、X(旧 Twitter)にポストしていただけただけでも驚きでしたが、ブログの内容を反映したものもアップデートに含んでいただけたことにはさらに驚きました。本当にありがとうございます!
アバター
はじめに ヘルシカについて モジュール化の利点 アーキテクチャと関心の分離の徹底 ビルド時間の短縮 テストの容易性 ヘルシカ Android アプリ既存のモジュール化戦略 既存のモジュール化戦略の問題点 改善後のモジュール化戦略 モジュールの分類 既存モジュールの命名変更 feature モジュールの新設 まとめ はじめに Android 開発エンジニアを担当している岡田です。 サービスの成長に伴い、コードの肥大化・複雑化は避けられないものだと思います。 しかしながらサービスの成長角度を下げないためには、持続可能で保守性の高いコードを保つ必要があります。 今回は弊社のサービスであるヘルシカにて、上記の問題を改善すべく Android アプリのモジュール化について再検討・実装しましたのでお話しさせていただきます。 ヘルシカについて ヘルシカは健康的な生活を送るためのヘルスケアアプリです。 この機能を使うことで、食事や体重、体脂肪率を記録できます。 食事の記録ではカロリー計算により健康的な食生活を目指せます。 ヘルシカは以下のリンクからダウンロード可能です!是非、利用してみてください。 ヘルシカ -健康管理のための食事記録・体重管理アプリ every, Inc. ヘルスケア/フィットネス 無料 apps.apple.com play.google.com モジュール化の利点 モジュール化が話題になって数年が経ちましたが、ここで改めてメリットについて簡単にまとめたいと思います。 以下に自分が特に大きいと感じているメリットを 3 つ選出しました。 アーキテクチャと関心の分離の徹底 アーキテクチャに従って機能や役割ごとにモジュールを分割することで、コードの可読性・保守性が向上します。 モジュール間の依存関係や設計意図をコードレベルで表現できるため、新規参入者にアーキテクチャを共有しやすくなります。 ビルド時間の短縮 モジュールごとに独立してビルドできるため、変更があったモジュールのみを再ビルドすることで、全体のビルド時間を短縮できます。 特に大規模なプロジェクトでは、差分ビルドによる恩恵が大きくなります。 テストの容易性 モジュールごとに独立してテストできるため、ユニットテストや結合テストを効率的に実行できます。 テストカバレッジを向上させ、品質の高いアプリケーションを開発できます。 他にも「コードの再利用性が向上する」、「 チームで開発が分割しやすくなる」などの利点があります。 詳しくは是非、Google Developers の公式ドキュメントを参照ください。 developer.android.com developer.android.com 自分の中では「 アーキテクチャと関心の分離の徹底 」を一番の利点に感じています。 他の利点も重要ですが、コードの構造的な強制力はモジュール化ならではの効果であるためです。 今回はこちらを重要視してモジュール化戦略を再考しました。 ヘルシカ Android アプリ既存のモジュール化戦略 ヘルシカ Android は、既にモジュール化されたプロジェクトでした。 モジュール分割は以下の通りになっており、 各モジュールは、 modules というディレクトリ配下に作成されていました。 モジュール名 説明 app アプリケーションのエントリーポイント。機能に関するコードも保持している api ネットワーク通信や外部APIとの連携を担う debug デバッグビルド専用のツールや機能を提供する core アプリケーション全体で共通して使用される機能やユーティリティを提供する repos repository の略で、データのリポジトリパターンを実装する resource アプリケーションで使用する共通のリソースを管理する usecase アプリケーションのビジネスロジックを実装する widget 再利用可能なUIコンポーネントを提供する model アプリ内で使用する共通のモデルクラスを実装 する 既存のモジュール化戦略の問題点 既存のモジュール化戦略には以下のように、いくつか問題点がありました。 api や repos 、 usecase に関して、パターン名を命名に使用しているため、アーキテクチャを意識しづらい widget や core に関して、初見で何が格納されているのか意識しづらい core と他モジュールの区分が曖昧 各機能に関するコードが app 配下に存在しているため、依存関係が曖昧 modules ディレクトリが不要である repos に関して、略称が使用されており分かりにくい 特に 1 ~ 4 に関して、モジュール化のメリットである「アーキテクチャと関心の分離の徹底」を、十分に享受できていないような戦略になっていることがもったない点でした。 改善後のモジュール化戦略 既存の問題点を改善するためのモジュール化戦略は以下の通りです。 モジュール化戦略を再考するにあたり、 Now In Android > Modularization learning journey を参考にしました。 モジュールの分類 大きな変更点として modules ディレクトリを削除し、すべてのモジュールを core モジュールと feature モジュールの 2種類に分類 しました。 ヘルシカでは現時点、機能に関するコードは app モジュール配下に格納されているため、既存のモジュールはすべて core モジュール配下に移動しました。 モジュール名 説明 core モジュール間で共有する必要があるコード feature 単一の責任を処理するようにスコープ設定されたコード この 2 つのモジュールについては、依存関係が重要です。 core モジュールは他のモジュールから依存されても良い feature モジュール は app モジュール以外からの依存を許さない 依存関係を強制することで、モジュール化のメリットである「アーキテクチャと関心の分離の徹底」を享受できます。 既存モジュールの命名変更 以下の通りにモジュールの命名を変更しました。 変更前 変更後 api network repos data usecase domain core common widget ui api 、 repos 、 usecase はパターン名でなく、具体的な役割を示す名前に変更しました core は共通機能を含むことをより明確にするために common に変更しました widget は UI に関連する要素を含むことを示すために ui に変更しました 命名変更することで、アーキテクチャについて意識しやすくなりました。 また Now In Android とモジュールの命名を似せたため、新規参入者に伝わりやすくなったと思います。 feature モジュールの新設 現時点では app 配下にすべて格納されている状態は、関心の分離の観点から好ましくないです。 従って、単一の機能を持つ feature モジュールを新設しました。 今後は機能ごとに feature モジュール配下へモジュールを作成し、実装を進めていきます。 まとめ ヘルシカ Android アプリのモジュール化戦略を再考し、以下の改善を実施しました。 モジュールの分類 既存モジュールの命名変更 feature モジュールの新設 これらの改善により、アーキテクチャと関心の分離の徹底を図りました。 現在ヘルシカはこのモジュール化戦略に従い、リファクタリング作業の真っ只中です。 今後も健全かつ人間にとって優しいコードを目指していきたいと思います!
アバター
目次 はじめに 前提 挑戦WEEK とは 実施にあたって気をつけていること 施策の流れ 実際に行われたもの 得られた効果 今見えている問題 まとめ 最後に はじめに こんにちは、トモニテ開発部ソフトウェアエンジニア兼、CTO 室 Dev Enable グループの rymiyamoto です。 4月で社会人 9 年目に突入し、もう若手とは言えない年齢になっており驚きを隠せません。 今回は弊社での組織活性施策の一環として行っている 挑戦WEEK の効果の振り返りを行います。 社内外からこの取り組みに伴う効果を聞かれることが多くなってきたので、実施した内容や効果を振り返ることで、今後の施策に活かせればと思います。 挑戦WEEK 自体に関する情報は過去に紹介しているのでぜひご覧ください。 tech.every.tv 運営に関しても過去の記事で紹介されています。 tech.every.tv 前提 エブリーでは「デリッシュキッチン」、「リテールハブ」、「トモニテ」、「TIMELINE」の複数のプロダクトを開発しています。 その中で、開発部は各プロダクトの開発を行うチームが存在し、各チームはそれぞれ異なる技術スタックや開発環境を持っています。 例えば私の所属する「トモニテ開発部」ではサーバーサイドとフロントエンドを一括りにしたチームと、クライアントアプリ専門のチームが存在します。 また、データや AI などより専門性が高い領域に対しては横断組織が存在し、各プロダクトの開発を支援しています。 この組織体制上だと他のチームでの出来事や技術的な挑戦を知る機会が少なく、各チームのメンバー同士の交流も少ないため、ナレッジ共有やコミュニケーションの活性化が課題となっています。 そのため行っている施策の一つに 挑戦WEEK があります。 挑戦WEEK とは 挑戦WEEK とは、普段の業務から離れ、1 週間かけて技術的な挑戦に取り組むイベントです。 技術的な挑戦を通して、エンジニア個人の成長と組織の活性化を促進することを目的としています。 通常業務で抱えている課題の解決(例:パフォーマンス改善)、新しい技術の検証(例:AI 技術の導入検証)、開発環境の改善(例:CI/CD パイプラインの構築)など、普段は時間を確保しにくいテーマに集中して取り組むことができます。 直近だと生成 AI 技術を活用した機能やツールの開発が行われました。 また、他チームのメンバーの交流を深める機会にもなっています。 実施にあたって気をつけていること まず「1 週間は事業部の施策や MTG には参加せず、集中して 挑戦WEEK に取り組む」ためにするべきは、各事業部の皆様にも協力していただく必要があります。 1 週間施策を止めることは、その期間プロダクト開発が止まり売上やサービスに影響が出てしまうため、多大な影響を与えることになります。 そのため実施する内容の精査は慎重に行っており、1 週間できる最大限の成果や知見が得られるような内容に調整しています。 これまでで 5 回実施してきましたが、ただ新しい技術を試したいだけではなく、将来組織やプロダクトに貢献できるようなものを選定しています。 もちろん、完璧に作り切るものを求めるのではなく、1 週間での成果物を作成することを目的としています。 施策の流れ 挑戦WEEK ではメンバーからの提案(プロポーザル)を募集し、選定されたものをチームで取り組む形で実施しています。 挑戦内容の募集 プロポーザル選定 投票 チーム分け 挑戦WEEK の実施(最終日成果発表会) 実施後アンケート 以前は各リーダーやマネージャーから提案してもらう形でしたが、参加するメンバー自ら意見を出すことで普段マネジメント層から見えてこない技術的な課題や興味を持っているテーマを知ることができ、より多くのメンバーの参加の意識が高まるようになりました。 実際に行われたもの 挑戦WEEK の実施内容は、毎回異なりますが、過去の実施内容を振り返ると大きく分けて以下のようなテーマがありました。 共通基盤の技術検討 新しい技術の検証 パフォーマンスやセキュリティの改善 AI やビッグデータ活用 異なるプロダクトのコラボレーション 普段であればコストを意識しすぎて出来なかったり、今のプロダクトのロードマップから離れてしまってタイミングが見つからなかったりするテーマに挑戦することができるため、メンバーのモチベーションも高く、普段の業務では手が出しづらいテーマを行ってきました。 実際に実施されたものに関しては以下で紹介されています。 tech.every.tv tech.every.tv tech.every.tv tech.every.tv 得られた効果 技術的な挑戦を通じて、メンバー同士の技術的な知見の共有が進み、普段の業務においても新しい技術やアプローチを取り入れる機会や姿勢が芽生えてきています。事業部側でも 挑戦WEEK でやってもらいたい技術検証を依頼されることもあり、 挑戦WEEK の成果をプロダクトに活かす機会が増えてきました。 実際に 挑戦WEEK で何度か生成 AI に対する取り組みが行われておりその中で得られた知見を基に、プロダクトに組み込むことができるようになってきました。 最初の頃は OpenAI を通して生成 AI を使ってみるところからで右も左もわからない状態ではありましたが、 挑戦WEEK を 2~3 回重ねるごとに 1 機能として実験的に作成するところまで到達できるようになってきました。 この知見を基に実際に動いているプロダクトで正式ロードマップの中で実装されるようにり、メインのプロダクトである「デリッシュキッチン」に生成 AI を活用した「デリッシュ AI」という機能が実装され、ユーザーのレシピ検索をサポートしています。 アーキテクチャに関しては以下の記事で紹介されています。 tech.every.tv また弊社の VP of Product からの AI/LLM によって、toC 向けサービスの在り方、プロダクトの作り方に関する紹介もありますので、ぜひご覧ください。 AI/LLMでtoC向けサービスはどう変わるのか?『DELISH KITCHEN』は、「レシピ動画アプリ」から「AI料理アシスタント」へ また開発組織のメンバー同士の交流は、初回実施した 2023 年 3 月と比較するとコミュニケーションが活発になってきており、普段の業務では関わることのないメンバー同士での交流が生まれています。一度同じチームで 1 週間過ごして開発を行うことで、戦友のような一体感が生まれ、気軽に相談しやすくなったり、他チームのメンバーと話す機会が増えています。 今見えている問題 挑戦WEEK の実施を通じて、メンバー同士の交流やナレッジ共有が進んできていますが、まだまだ問題も残っています。 出してもらったプロポーザルが精査や投票の中で、技術的な実現可能性やスケジュール、リソースなどの制約により実施できないものが多く、メンバーの意見を十分に反映しきれていない部分がありました。 実施後のアンケートから選考プロセスの見直しや、結果の伝え方を工夫することで、メンバー全員が納得できるようなプロポーザルの選定を行う必要があります。 また 挑戦WEEK で生まれたものがすべてそのままプロダクトに活かせるわけではありません。実際にプロダクトに組み込むためには、現在のロードマップや仕様要件を満たす必要があります。 作成したものがそのまま使えなくても、挑戦の過程で得られた知見はプロダクトに活かすことができます。どんな挑戦をしてどんな知見を得られたかを社内外に発信することで、1 週間の事業部の施策を止めてまで行ったことの意義を感じてもらえるようにしていきたいです。 まとめ 挑戦WEEK は、エンジニア個人の成長と組織の活性化を促進するための施策として、技術的な挑戦に取り組むイベントです。 普段の業務から離れ、集中して取り組むことで、メンバー同士の交流やナレッジ共有が進み、エンジニアリング文化の醸成に寄与しています。 また、 挑戦WEEK で得られた知見や技術をプロダクトに活かす機会も増えてきており、組織全体の成長にもつながっています。 大事なこととして各事業部側の皆さんに協力の上で成り立っています、改めて感謝申し上げます。 今後も 挑戦WEEK を通じて、エンジニアリング文化の醸成や組織の活性化を促進していきたいと思います。 最後に エブリーでは、ともに働く仲間を募集しています。 テックブログを読んで少しでもエブリーに興味を持っていただけた方は、ぜひ一度カジュアル面談にお越しください! corp.every.tv 最後までお読みいただき、ありがとうございました!
アバター
はじめに こんにちは、デリッシュキッチン開発部でソフトウェアエンジニアをしている新谷です。 エブリーの開発部では「挑戦week」という1週間の期間限定チャレンジを定期的に開催しています。これは日常業務から離れて、新しい技術やアイデアに挑戦する取り組みです。 今回は、この挑戦week期間中にデリッシュキッチンの検索基盤をElasticsearchからOpenSearchへ移行する挑戦を行いましたので、その内容を紹介します。 ※ 挑戦weekの詳細については過去の記事で紹介していますので、興味のある方は以下をご覧ください。 tech.every.tv 背景:なぜElasticsearchの移行が必要なのか デリッシュキッチンでは、レシピ検索とサジェスト機能のために長らくElasticsearch 5.5をECS上で運用してきましたが、以下のような課題がありました。 バージョンの古さ :Elasticsearch 5.5は既に数年前のバージョンであり、セキュリティ面や機能面で最新版に劣っています 検索周りの施策展開の困難さ :古いバージョンであるため、新しい検索機能の実装が難しくなっていました 運用負荷 :ECS上での自前運用による保守管理コストがかかっていました これらの課題を解決するために、ElasticsearchからAWSが主導開発しているOpenSearchへの移行を検討することになりました。 ElasticsearchとOpenSearchとは OpenSearchはElasticsearchからフォークする形で誕生したオープンソースの検索エンジンです。元々Elasticsearchは完全なオープンソースでしたが、Elastic社はバージョン7.11以降からライセンスをElastic License 2.0に変更しました。これに対しAWSは「完全にオープンソース」の状態を維持するためにOpenSearchというフォークプロジェクトを立ち上げました。 現在、それぞれの開発は並行して進められていますが、AWS上のマネージドサービスとしてはOpenSearchが優先的にサポートされています。 項目 Elasticsearch 7.11以降 OpenSearch ライセンス Elastic License 2.0 / SSPL Apache License 2.0 商用利用 制限あり(SaaS提供には制限) 完全に自由 ソースコードの変更と再配布 許可されるが制限あり 完全に自由 SaaS提供 制限あり 制限なし 開発主体 Elastic社 AWS主導のコミュニティ開発 挑戦weekでの取り組み 「挑戦week」というわずか1週間のプロジェクト期間と3人という少人数チームでの取り組みだったため、移行にあたっては現実的な範囲で以下のようなステップを設定しました。 ローカル環境でのOpenSearchの導入 ローカル環境での動作検証 (将来的に実施)AWS インフラ構築 (将来的に実施)CI 整備 当初は4つのステップすべてに挑戦する予定でしたが、実際に取り組んでみると想定以上に複雑な課題が見つかり、一週間では最初の2ステップである検証部分しか実施できませんでした。それでも、この限られた期間で多くの貴重な知見を得ることができました。 実施内容:ローカル環境での検証 ローカル環境でElasticsearchからOpenSearchへ切り替えるために、以下の作業を行いました。 Docker環境の整備 OpenSearchのDockerコンテナを使用するように設定を変更するため、Dockerfileの修正を行いました。具体的には、 elasticsearch/elasticsearch から opensearchproject/opensearch へDockerイメージを変更しています。また、docker-compose.ymlも対応する形で修正し、ローカルでOpenSearchを立ち上げられるようにしました。 クライアントライブラリの検証 デリッシュキッチンではGoのクライアントライブラリとして gopkg.in/olivere/elastic.v5 を使用していましたが、OpenSearchへの移行に伴い、 opensearch-project/opensearch-go/v4 への切り替えが必要になりました。 検証の結果、以下のような課題が見つかりました。 数十メソッドの書き換えが必要 新しいライブラリは比較的薄いため、リクエストボディを自前で組み立てる必要がある 当初の想定を大きく上回る作業量が必要であることが判明しました。一週間の限られた時間では全容を把握するだけでも大変でした。 データ投入スクリプトの整備 OpenSearchにテストデータを投入するために、スクリプト化してmakeコマンドから実行できるように改善しました。これにより、繰り返しテストを行いやすくなりました。 主な技術的課題 検証過程で明らかになった主な変更点と課題について解説します。 1. _typeパラメーターの廃止 Elasticsearch 5.5では _type パラメータを使用して、1つのインデックス内で異なる種類のドキュメントを区別していました。しかし、OpenSearch 2.19では _type は完全に廃止され、1インデックス1種類のドキュメントという設計に変更されています。 当初のマッピング定義では、ingredients、recipes、tagsの3つのドキュメントが1つのインデックスで定義されており、投入時に _type=recipes を指定することで対応していました。移行するには、「recipes」だけの定義に変更し、 _type 指定なしでも動作するように修正する必要があります。 これはデータ構造の見直しを意味し、単純な置き換えでは解決できない課題です。 2. インデックスマッピングの厳格化 OpenSearch 2.19ではマッピングの制約が強化されています。特にシノニム(同義語)の設定ファイルに関して、Elasticsearch 5.5では許容されていた一部不適切な定義がOpenSearchではエラーとなりました。 具体的には、以下のようなシノニム定義ファイルの問題が見つかりました。 形式が不正な同義語の定義 重複する定義 解析できない文字が含まれる定義 一週間で全て修正するには時間が足りないため、一時的な対応としてシノニム部分に "lenient": true 定義を追加し、問題のある行があっても無視するよう対応しました。本来は、シノニム定義を見直し、問題のある行を修正する必要があります。 3. クエリ仕様の変更 OpenSearch 2.19ではboolクエリのパフォーマンスが向上しているというメリットがありますが、以下のような変更点がありました。 _all フィールドの削除(元々falseで使用していなかったため、定義自体を削除) allow_leading_wildcard のデフォルト無効化 default_field の明示的指定必須化 その他、細かな仕様変更 これらの変更により、既存の検索クエリの多くを見直す必要があり、検索結果への影響を一つひとつ検証する必要があります。 4. xpackプラグイン xpackプラグインについては、OpenSearchではサポートされていないため、代替機能の検討が必要です。一週間という限られた時間内では、詳細な検討までは至りませんでした。 検索品質の検証結果 ローカル環境での検証後、実際の検索品質を評価するために比較テストを行いました。直近のアクセスログから1,000件の検索ワードをサンプリングし、ElasticsearchとOpenSearchの両方で検索して結果を比較しました。以下が比較した結果の一部です。 結果としては、どちらも一定の妥当性のある検索結果が得られましたが、複数の問題点が発覚しました。 1. 検索結果の品質低下 特定の検索ワードでOpenSearchのほうが劣っているケースが見つかりました。例えば、「麻薬たまご」という検索ワードでは、以下のような違いがありました。 Elasticsearch: 「麻薬たまご」が一つの検索語として扱われ、「麻薬卵」レシピがヒット OpenSearch: 「麻」「薬」「た」「ま」「ご」のように分かれてしまい、「麻薬卵」レシピがヒットしない このような違いは、トークナイザーやアナライザーの設定の違いによるものと考えられます。 2. その他の課題 検証結果から、ElasticsearchとOpenSearchにおいて明確に差異はありますが、なぜ差異がありどのようなパターンで差異が出るのかは明確になっていません。 「麻薬卵」は一例であり、その他の差異については検討が必要です。 今後の検討事項 挑戦weekの取り組みで明らかになった課題を踏まえ、今後取り組むべき検討事項は以下の通りです。 1. シノニム・辞書整備 現状の問題のある定義を洗い出して、Elasticsearchのシノニム定義をOpenSearchのシノニム定義に変更 日本語形態素解析の設定見直し、Elasticsearchと同等の設定にする 2. 段階的移行計画 ユーザー体験の急激な変化を避けるため、段階的な移行を立てる 一度にOpenSearchに移行するのではなく、ElasticsearchとOpenSearchを共存させ、カナリアリリースを行う等の方法を検討 3. 検索品質の評価方法の検討 実践投入に向け、0件ヒット率等の検索品質の評価方法を検討する まとめ 挑戦week前は、ElasticsearchとOpenSearchの知識がほぼない状態でスタートしましたが、シノニムの設定方法からそれぞれの仕様差分まで一週間で学び、良い経験になりました。 実際に取り組んでみて、単なるバージョンアップではなく、アーキテクチャの見直しを含む大規模なプロジェクトになることが分かりました。 短期間の検証ではありましたが、今後も機会を見つけてさらなる検証と課題解決を進め、最終的には検索機能の品質を向上させる形での移行を目指したいと思います。
アバター
はじめに 株式会社エブリーでCTOをしている imakei です。 本日から弊社では多くの新卒メンバーに入社していただきました。 これから彼ら・彼女らとともにより強い開発組織を作っていきたいと思います。 すでに新卒メンバーからは学ぶことも多く、特にAIに関しては自分以上に使いこなしているところを数多くみています。 そんな彼らに感化された部分もあり、生成AIを使いながら開発していく上でのちょっとしたTipsを紹介できればと思います。 弊社では今積極的に生成AIを活用した開発を行っています。 AIAgentを利用した開発も積極的に試しているのですが、そんな中で、 なかなか思っているコマンドを実行してくれないことにストレスを感じていたので、 個人的にModel Context Protocol (MCP) の理解も兼ねてちょっとしたツールを開発してみたので紹介できればと思います。 ※あくまで個人のローカルでの開発を想定したツールです。パブリックに公開するなどは一切考えていないので、その辺はご了承ください。 今回はMakefileを元にMCPサーバーを作成して、それをAIAgentに利用してみたいと思います。 MakefileをMCPサーバー化して開発効率を上げる方法 はじめに 開発プロジェクトでMakefileを使用している方も多いのではないでしょうか。ビルド、テスト、デプロイなど、様々なタスクを効率的に実行できる便利なツールですが、 せっかく整備していてもAgentがそれをうまく使ってくれないということが度々あります。 MCPとは Model Context Protocol (MCP) は、AIアシスタントがローカル環境のツールを実行するためのプロトコルです。これを利用することで、自然言語でMakefileのターゲットを実行できるようになります。 例えば: 「テストを実行して」 「開発サーバーを起動して」 といった指示をAIアシスタントに伝えるだけで、対応するmakeコマンドを実行できます。 実装方法 必要な環境 Deno // 自分は雑なツールを作るときにDenoが好きで使っていますが、正直なんでも良いと思います。 MCP SDK コアとなる実装 MCPサーバーの実装 まず、MCPサーバーの実装です。 SDKが提供されているのでそれほど難しくはなく立ち上げられると思います。 script.ts import { Server } from "@modelcontextprotocol/sdk/server/index.js" ; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" ; // MCPサーバーのセットアップ const server = new Server( { name : "local_makefile_mcp" , version : "0.0.1" , } , { capabilities : { resources : {} , tools : { ...makeTools, // Makefileのターゲットから生成したツール群 } , } , } ); await server.connect( new StdioServerTransport()); ツールの生成 Makefileを読み込んで、その中にあるターゲットをツールとして登録しています。 自分用に作ったので、matchesで愚直にやっていますが、より良い方法はありそうです。 script.ts function getMakeCommands ( makefilePath? : string ): Promise < string []> { const cmd = [ "make" , "-n" ] ; if (makefilePath) { cmd. push ( "-f" , makefilePath); } // Makefileの内容を直接読み取る const makefileContent = makefilePath ? await Deno.readTextFile(makefilePath) : await Deno.readTextFile( "Makefile" ); // ターゲットを抽出 const matches = [ ...makefileContent. matchAll ( /^([a-zA-Z0-9_\-\.]+):/gm ) ] ; const targets = matches. map ( m => m[ 1 ]); return [ ... new Set (targets) ] . filter ( target => !target. startsWith ( '.' ) && ! [ 'Makefile' , 'makefile' , 'GNUmakefile' , 'DEFAULT' , 'SUFFIXES' ] . includes (target) ); } function generateMakeTools ( targets : string []): Record < string , Tool > { const makeTools: Record < string , Tool > = {} ; for ( const target of targets) { makeTools[ `make_ ${ target } ` ] = { name : `make_ ${ target } ` , description : `Run 'make ${ target } '` , inputSchema : { type : "object" , properties : {} , required : [] , } , } ; } return makeTools; } // makefileから取得したcommandsを `make_<command>` という名前でツールとして登録している const commands = getMakeCommands( 'path/to/Makefile' ); const makeTools = generateMakeTools(commands); ツールの実行ハンドラー 今回はmakefileから取得したcommandsを make_<command> という名前でツールとして登録しているので、 それを実行するハンドラーを作成します。 script.ts // ツールの実行ハンドラー server.setRequestHandler(CallToolRequestSchema, async ( request : CallToolRequest ) => { const name = request.params. name ; if ( name . startsWith ( "make_" )) { const target = name . replace ( "make_" , "" ); const cmd = [ "make" ] ; if (makefilePath) { cmd. push ( "-f" , makefilePath); } cmd. push (target); // Makefileのディレクトリパスを取得 const makefileDir = new URL ( '.' , 'file://' + makefilePath). pathname ; const command = new Deno.Command( "make" , { args : cmd. slice ( 1 ), // "make"を除いた引数を渡す cwd : makefileDir, stdout : "piped" , stderr : "piped" , } ); const { stdout , stderr } = await command.output(); const output = new TextDecoder (). decode (stdout); const error = new TextDecoder (). decode (stderr); // ... return { content : [ { type : "text" , text : output, } , ] , isError : false , } ; } // 知らないコマンドきた時のハンドリング } ); このコアとなる実装では、以下の重要な機能を提供しています: MCPサーバーの初期化 サーバー名とバージョンの設定 利用可能なツール(makeターゲット)の登録 Makefileターゲットの解析 Makefileの内容を読み取り 有効なターゲットの抽出 特殊ターゲットのフィルタリング ツール定義の生成 各makeターゲットに対応するツールの定義 ツール名とコマンドのマッピング ツール説明の生成 ツール実行ハンドラー makeコマンドの実行 標準出力とエラー出力の取得 実行結果のフォーマット化 これにより、AIアシスタントはMakefileのターゲットを安全に実行し、その結果を適切にユーザーに返すことができます。 使用方法(Clineでの利用) ClineのパネルのMCPタブを開き、Configure MCP Serversを押下すると、 cline_mcp_settings.json というファイルが開くので設定していきます。 cline_mcp_settings.json { " mcpServers ": { " local_makefile_mcp ": { " command ": " path/to/deno/command ", " args ": [ " run ", " -A ", " path/to/script.ts ", " path/to/Makefile " ] , ... } } } これで MCP Servers に local という名前でサーバーが追加されます。 local_makefile_mcpが読み込まれている様子 MCPサーバーを起動後、AIアシスタントに実際にTestをお願いすると、mcpサーバーからコマンドを見つけてくれます。 mcpサーバーからテストコマンドを見つけてくれた様子 無事にコマンドを見つけられていますね。 ただ正直このままですと、自然言語でコマンドを見つけてくれないこともあるので、 Makefileにコメント等をつけてそれをディスクリプションに登録するといいかもしれません。 まとめ このように、MCPサーバーを利用することで、自然言語でMakefileのターゲットを実行できるようになります。 細かい部分にはなりますが、これにより開発におけるAgent利用のストレスが少し減ったように思います。 今後もこういった細かいことから生成AIをもっとストレスなく活用して開発効率を上げていきたいと思っています。 そんな弊社は現在積極採用中です! こんな開発を普段から行なっている弊社で働きたいなと思った方はぜひ一度お話ししましょう! https://twitter.com/imakei_ https://corp.every.tv/recruits/engineer 今井
アバター
はじめに エブリーでデータサイエンティストをしている山西です。 今回は、社内で継続的に実施している数学勉強会について紹介します。 勉強会を続けるうえで工夫したポイントや、取り組みを続けての所感をお伝えします。 概要 エブリーのデータ&AIチームでは、「数式に向き合う習慣を維持する」目的で週に1回のペースで数学の勉強会を実施しています。 記事執筆時点では、データサイエンティスト(MLエンジニアを含む)3名で、『機械学習スタートアップシリーズ ベイズ推論による機械学習入門』を輪読形式で進めています※ www.kspub.co.jp 勉強会の進め方は、各回で担当メンバーが前回の続きから読み進め、数式を咀嚼しながら書き出していくスタイルです。 iPad上の共有ノート(GoodNotes 6の共同作業機能を活用)にリアルタイムで式を書き出しつつ、他のメンバーが質問したり、時には並列で式展開したりして文殊の知恵的に理解を深めています。 ノートの例 2022年初頭にスタートし、教材や進行方法を途中で変えつつも、約3年間継続という比較的歴の長い取り組みになっております。 ※ 過去には『統計的機械学習の数理100問 with Python』を扱っていました。今は2冊目になります。 www.kyoritsu-pub.co.jp 実施の背景 この勉強会は、自分が言い出しっぺとなってスタートさせました。 その背景には、「事業主体であることを言い訳にして、数式と向き合う時間をおろそかにしたくない」という思いがありました。 データサイエンス組織のあらまし 前提として、弊社のデータ組織はR&D寄りではなく、事業に寄り添ったビジネス要求対応の比重が高い体制です。 さらに、少数組織であることから、データサイエンティストも一定のデータエンジニアリング業務を兼務する構図となっています。 データサイエンススキルの活かし方 このような体制下では、生のビジネス現場でデータを活用する多くの実務的な学びが得られます。 一方、意識せずに「事業並走」や「データ基盤の整備」などの比重が増しすぎると、統計や機械学習と向き合うための“可処分データサイエンス時間”が減ってしまう傾向があります。 これは、いわゆる「データサイエンティストだけど、データの整備や可視化、簡単な記述的分析に留まってしまう」状態です。 とはいえ、実際の業務でも「その先の活用」が求められるシーンは多く存在します。 たとえば、統計的な予測や効果検証、プロダクトへの機械学習技術の応用などです。 いざそうした要求が出てきたときに力を発揮するためには、基礎的な理解&実行力が不可欠です。 「ライブラリに頼り切り」は良くないという話 近年は、ライブラリやLLMの発展により、分析の実行そのものは圧倒的に手軽になりました。 しかし、「なぜその手法が使えるのか」「どのような前提で動いているのか」といった背景を理解せずに使うことは、意思決定や改善において、再現性等々の危うさを孕みます。 だからこそ、数学の原理に立ち返り、技術の背後にあるロジックを丁寧に理解しておくことが大切だと考えました。 とどのつまり つまり、日々のビジネス要求にしっかり応えながらも、ここぞというシーンでデータサイエンティストとしての専門性を発揮するためには、継続的で幅広い数学の素養がベースになるということです。 裏を返せば、この視点を疎かにして業務のルーチンが固定化しすぎると、新たな発見や創造的なアプローチが生まれにくくなるリスクにもつながると考えます。 こうした課題感を持つ中で、同じ想いを抱くメンバーとともに、日常業務とは異なる角度でスキルを磨く場として勉強会を企画する運びとなりました。互いに刺激を受けながら学び合い、知識の定着や応用力の向上につなげていきたいという狙いもあります。 運営の工夫 勉強会を継続するために、以下のような工夫を取り入れています。 参加のハードルを下げる 「予習はしない(しても良いけど義務ではない)」「みんなでその場で考える」ベストエフォート式で進行しています。 事前準備の負担を極力抑えることで、忙しい業務の合間でも無理なく参加できるようにしています。 「継続は力なり」の精神で、少しずつでも約3年続けられたのは、このような実施のハードルの低さの寄与が大きいのではないかと思います。 みんなで悩む、考える 心理的安全性の面では「わからないは恥ではない」「みんなで悩もう」という意識で臨むようにしています。 結果として行き詰まってほとんどページが進まないこともありますが、その過程を許容しながら進めることを大切にしています。 一人での学習に比べ、チームで議論することで多様な視点を得られています。 業務に関連するテーマを中心に意見交換を行うため、知識が定着しやすく、互いに学びを高め合うことができています。 行間含め、数式に向き合う 「数学の基礎体力をつける」をモットーに、数式の行間や詳細な導出過程を(書籍で省略されている部分も含めて)追える限り丁寧に追っています。 例えば現在取り扱っているベイズ本では、事前分布から共役事前分布を経て事後分布を導出する流れを、噛み砕きながら丁寧に追っています。 プログラミング上では結果の式さえあれば実装できる(何ならライブラリを使えば式すらラップされて利用できる)部分ですが、この勉強会の時間ではあえてその背後の理論に向き合うことを大切にしています。 個人的には、この習慣の継続によって行列計算の苦手意識が薄まってきたことが一定の成果です。 他の書籍や技術記事で統計数理をキャッチアップする際の壁も、徐々に乗り越えられるようになってきました。 電子媒体(iPad & GoodNotes)の活用 当初はオフィスのフリースペースでホワイトボードを使っていましたが、現在はGoodNotes6アプリを利用したデジタルノートに移行しました(たまたまメンバー全員がiPadでそれを行える環境だったため)。 この移行により、板書を自動記録し、全員で共有できるようになりました。 過去の内容を容易に検索・参照できたり、必要な式をコピー&ペーストで再利用できたりと、クラウド時代ならではの恩恵を感じています。 過去の章で扱った式を今の計算のために引っ張ってきたいとき、コピペの要領でできるのが大変便利(赤枠部分 継続する中での課題 「実施のハードルの低さ」や「数式の細部に向き合うこと」による良い側面がある一方、それならではの課題感もあります。 例えば、予習が義務でないことで「前回の内容を思い出すのが難しい」ことがあります。 間が空くことで細部は思い出せても、全体的な流れや体系的な理解が薄れてしまうケースがあります。 また、「行間を埋める」目的で目の前の数式や概念に集中するあまり、「そもそもこのテーマは何のために学んでいるのか?」という全体像を見失いがちになることもあります。 そして、読み進める速度もどうしても遅くなりがちです(今の本も、1年数ヶ月続けているものの、進捗としては70%程度です)。 さらに、「みんながiPadを持っている前提」で進めているため、新しいメンバーを受け入れる際のハードルがあるという細かな悩みもあります笑。 このあたりは、メンバーそれぞれと実施目的や状況を振り返りつつ、形式の一長一短を吟味しながら適宜アップデートしていけると良いと考えています。 今後の展望: 実務への還元 この勉強会は、「あえて日常業務から離れ、基礎力を向上させる」という趣旨の取り組みである一方、具体的な実務貢献という形でも成果を還元できるとより理想的です。 その一歩として、現在「ベイジアンA/Bテスト基盤」の実装にチャレンジしています。 勉強会を通じてベイズ推論の理論的な理解が深まり、実際のプロダクトへ応用する準備が整ってきたためです。 もともと輪読の教材としてベイズの教科書を選んだ背景には、メンバーの多くが明示的にベイズの文脈での学習/活用経験が少なかったという事情がありました。 その意味でも、この勉強会が新たな技術的創発のきっかけとなり、実践に結びついていくことを期待しています。 おわりに この記事では、エブリー社内で継続して行っている数学勉強会の取り組みについて紹介しました。 「数式と向き合う習慣を維持する」というシンプルながら重要な目的のもと、少人数・低ハードル・実務と地続きという特徴を持った学びの場を育ててきました。 これからも、事業貢献と専門性向上のバランスを意識しながら、実践と理論の往復を大事にした取り組みを続けていければと思います。 そして、同様の課題意識を持つ方々にとって、少しでも参考になる内容であれば幸いです。
アバター
はじめに Biomeとは 導入方法 使い方 lint format check 設定ファイル 複数の設定ファイル extends vcs まとめ はじめに こんにちは、TIMELINE 開発部 Service Development をしている hond です! 普段からLinterやFormatterにはとてもお世話になっているのですが、いざ導入するとなると細かい設定などめんどくさいな、と友人に相談したらほぼ設定いらずかつ爆速な Biome というツールを教えてもらったので触ってみた感想について紹介しようと思います! Biomeとは Web開発のためのたった1つのツールチェーン フォーマット、リントなどが一瞬で完了します! Prettierのようにコードをフォーマット、しかも高速 Biomeは JavaScript、TypeScript、JSX、JSON、CSS そして GraphQL のための高速なフォーマッタ であり、Prettier と97%の互換性を持ち、CIと開発者の時間を節約します。 問題を修正し、ベストプラクティスを学ぶ Biomeは JavaScript、TypeScript、JSX、CSS そして GraphQL のための高性能なリンタ であり、ESLint、typescript-eslint、その他のソースに由来する 200以上のルール を備えています。 https://biomejs.dev/ja/ Biomeについて公式サイトのトップでは上記のようにformatやlintが一瞬で完了することや豊富なルールが紹介されています。 新規導入の際には会社の秘伝のタレ化したものやネットのベストプラクティスを紐解いて対象のルールの比較検討を行っていましたが、Biomeを用いると推奨のルールが設定されているためこれらの作業を省くことができます! 新規導入だけでなく既存でeslint,prettierの設定がある場合は後述するコマンドを用いることでそれらの設定を引き継いだ上で一瞬で移行することが可能です。 Biomeのサポートされている言語一覧は下記になります。 ✅: 対応済み 🚫: 進行中ではない ⌛️: 進行中 ⚠️: 一部サポート(いくつかの注意点あり) Language Parsing Formatting Linting JavaScript ✅ ✅ ✅ TypeScript ✅ ✅ ✅ JSX ✅ ✅ ✅ TSX ✅ ✅ ✅ JSON ✅ ✅ ✅ JSONC ✅ ✅ ✅ HTML ⌛️ ⌛️ 🚫 Vue ⚠️ ⚠️ ⚠️ Svelte ⚠️ ⚠️ ⚠️ Astro ⚠️ ⚠️ ⚠️ CSS ✅️ ✅️ ✅️ YAML ⌛️ 🚫 🚫 GraphQL ✅️ ✅️ ✅️ Markdown ⌛️ 🚫 🚫 https://biomejs.dev/ja/internals/language-support/ 導入方法 インストールに関してはNode.js v14.18以降の環境で下記コマンドを実行することで完了します。 yarn add --dev --exact @biomejs/biome インストールに関しては先ほどのコマンドのみで可能ですが、プロジェクトごとの設定を行うために下記コマンドを実行して設定ファイルを作成します。 yarn biome init 作成された設定ファイルはこちらになります { " $schema ": " https://biomejs.dev/schemas/1.9.4/schema.json ", " vcs ": { " enabled ": false , " clientKind ": " git ", " useIgnoreFile ": false } , " files ": { " ignoreUnknown ": false , " ignore ": [] } , " formatter ": { " enabled ": true , " indentStyle ": " tab " } , " organizeImports ": { " enabled ": true } , " linter ": { " enabled ": true , " rules ": { " recommended ": true } } , " javascript ": { " formatter ": { " quoteStyle ": " double " } } } 新規の導入に関してはこれにて完了です。既にeslintやprettierが導入されている場合は下記のコマンドを実行することで移行ができます。 .eslintrc.json , .eslintignore や .prettierrc.js を参照して移行してくれます! Flat Config対応された設定ファイルも移行可能です。 biome migrate eslint --write biome migrate prettier --write 手動で設定する際にはESLintとBiomeのルール名は命名規則が異なるので こちら のページを参考に行う必要があります。 また、 CI やLeftHook、pre-commitをはじめとした Git Hooks への導入の仕方も公式サイトでは紹介されています。 使い方 Biomeには既に紹介した init と migrate を含め14個のコマンドが存在しますがここではよく使用する lint , format , check について説明します。 lint biome lint ${destination} Linterを実行するコマンドになります。デフォルトではBiomeの推奨のルールが実行されます。推奨のルールは こちら のサイトから確認することができます。執筆時点では約100のルールが推奨ルールとして設定されていました。 コードのセマンティクスを変更しないことが保証されているものをレビューなしに適応する --write やセマンティクスを変更する可能性がある変更を手動でレビューする --write --unsafe 、 --write のaliasとして --fix がオプションとしてあります。 ESLintの修正で --fix を使っていたので使い慣れたオプションがそのまま準備されているのは個人的に嬉しいポイントでした。 format biome format ${destination} Formatterを実行するコマンドになります。前述した通りPrettierからの移行が可能ですがあくまでPrettierと近い哲学をもつBiome独自のFormatterになります。デフォルトでは indentStyleSection 、 indentWidthSection 、 lineEndingSection 、 lineWidth が設定されています。 indentStyleSection はインデントのスタイルを tab 、 indentWidthSection はインデントサイズを 2 、 lineEndingSection は改行を \n 、 lineWidthSection は一行当たりの最大文字数を 80 にそれぞれ設定しています。 format は lint 同様に --write などのオプションがあります。 check biome check ${destination} FormatterとLinterに加えimportのソートを行うコマンドになります。importは自然順にソートされます。 設定ファイル Biomeはオプショナルの機能として設定ファイルを提供しています。ここではおすすめの設定についてピックアップして説明していきます。 複数の設定ファイル Biomeは複数の設定ファイルを作成することが可能です。複数の設定ファイルがある場合実行時に作業ディレクトリから最も近い設定ファイルが参照されます。 そのため、モノレポなどでバックエンドとフロントエンドそれぞれに設定ファイルを作成することで独立した設定をすることが可能です。 extends 次に設定の共有についてです。 extends を用いることで extends リストに含まれるファイルかのオプションを適用することが可能です。 例えば下記のように実装することで biome.json でも biome.base.json でlinterを有効化し推奨設定をしているオプション適用できます。 biome.base.json { "linter": { "enabled": true, "rules": { "recommended": true } } } biome.json { "extends": ["./biome.base.json"] } vcs VCS(バージョン管理システム)に関する設定です。 下記の例ではVCSをgitとして有効化し、変更ファイルの評価元としてプロジェクトの作業ブランチ( develop )を指定しています。このように設定することで develop から生やした作業ブランチでの差分をBiome実行時の対象とすることが可能です。 { " vcs ": { " enabled ": false , " clientKind ": " git ", " defaultBranch ": " develop " } } まとめ 新規導入だけでなく既にESLintやPrettierが導入されている場合でもコマンドを用いて簡単に移行できる点が、重い腰を上げる後押しをしてくれるのでとても良いなと感じました。 実行速度に関しても紹介されている通り高速で弊社のプロダクトに導入してみたところ下記図のようにlintに関しては既存のESLintと比較して約20倍、formatに関しては既存のPrettierの約10倍のスピードが出ました! 既存のプロダクトではパフォーマンスの観点で、新規プロダクトではスピード感を求める中で細かい設定を行わないで済むという点で十分導入を検討する価値のあるツールだと感じました。 name before after Lint 6.37s 0.34s Format 4.56s 0.44s
アバター
目次 はじめに 管理画面について 複雑なリレーションをロードする ユースケースと実装例 詰まったポイント 1. 多方向のリレーションのロード 2. 特定の外部キーに一致するレコードのリレーションのロード 複雑なリレーションのロードをテストする おわりに はじめに こんにちは。 トモニテ開発部ソフトウェアエンジニア兼、CTO室Dev Enableグループの庄司( ktanonymous )です。 みなさんは、Go の ORM ライブラリを使っていますか?どのようなライブラリを使っていますか? 弊社では、既存プロジェクト内での移行の容易さに惹かれ、 最近は sqlboiler という、"データベースファースト"な ORM ライブラリを使うプロジェクトが増えてきています。 プロジェクトは異なりますが、過去に sqlboiler を選定した際の記事も書いていますので、興味があれば是非ご覧ください。 現在、筆者はLP制作から応募データのETL、クライアントへのデータ送付までにまつわる処理群を非エンジニアの運用担当者が管理画面から自由に更新できるようにするための社内システムの開発を進めています。 今回の記事では、その中で複雑なリレーションに対して sqlboiler を有効に活用するために苦労した点について紹介したいと思います。 なお、本記事では2025年3月25日時点での情報を元に記述しています。 また、Go のバージョンは 1.24.0 、 sqlboiler のバージョンは v4.18.0 を使用しています。 管理画面について 最初に、現在進行している社内システム開発の背景についてお話ししたいと思います。 ちなみに、LPの制作に関しては、以前の記事でも紹介していますので是非見てみてください。 以前の記事でも説明をしていますが、弊社では、クライアントごとにカスタマイズされたLPを実装して公開しています。 また、LPから送信される案件に対する応募データのETLやクライアントへの送付フォーマットもクライアントごとに異なります。 そのため、都度クライアント-営業・運用チーム-エンジニアチーム間で擦り合わせの必要があります。 LP周辺の開発フロー概要 このように、1つのLPを制作するために何度もコミュニケーションが発生し、実動工数以上に時間を要してしまうという課題がありました。 そこで、クライアントごとのLPの制作や応募データのETL、クライアントへのデータ送付を非エンジニアの運用担当者が管理画面から自由に更新できるようにするプロジェクトを進めることとなりました。 複雑なリレーションをロードする sqlboiler では、外部キーに基づいて、リレーションを持つレコードに対して eager ローディングが可能となっています。 これによりパフォーマンス観点での恩恵を受けられます。 実装の記述も楽になるので積極的に使っていましたが、何度か使用感で詰まってしまったポイントを紹介したいと思います。 先に詰まったポイントを話してしまうと状況が見えにくくなってしまうので、 少々読みづらいですが、まずユースケースと実装例を紹介して、その後にどんなポイントで詰まったかを説明します。 ユースケースと実装例 初めに、以下のようなシンプルなテーブル構造を考えます。 シンプルなテーブル構造 この時、以下のような実装でユーザーのポストを取得することができます。 var mods []qm.QueryMod mods = append (mods, models.UserWhere.ID.EQ(userID)) mods = append (mods, qm.Load(models.UserRels.Posts)) user, err := models.Users(mods...).One(ctx, db) これで、特定のユーザーに紐づく全てのポストを取得することができます。 次に、以下のような少し複雑なテーブル構造の場合を考えます。 複雑なテーブル構造 この時、ユーザーに紐づく情報を全て取得しようとすると、以下のような実装になります。 var mods []qm.QueryMod mods = append (mods, models.UserWhere.ID.EQ(userID)) mods = append (mods, qm.Load(qm.Rels( models.UserRels.Likes, ))) mods = append (mods, qm.Load(qm.Rels( models.UserRels.Comments, ))) mods = append (mods, qm.Load(qm.Rels( models.UserRels.Posts, models.PostRels.Comments, ))) mods = append (mods, qm.Load(qm.Rels( models.UserRels.Posts, models.PostRels.Likes, ))) user, err := models.Users(mods...).One(ctx, db) これにより、特定のユーザーに紐づく全ての投稿/コメント/いいね、それらのそれぞれに紐づく全てのリレーションが取得できます。 先ほどと同じテーブル構造で、特定のユーザーの特定のポストに紐づく全ての情報を取得するケースを考えます。 この時、以下のような実装をすることになります。 var mods []qm.QueryMod mods = append (mods, models.UserWhere.ID.EQ(userID)) mods = append (mods, qm.Load( qm.Rels(models.UserRels.Posts), models.PostWhere.ID.EQ(postID), )) mods = append (mods, qm.Load(qm.Rels( models.UserRels.Posts, models.PostRels.Comments, ))) mods = append (mods, qm.Load(qm.Rels( models.UserRels.Posts, models.PostRels.Likes, ))) user, err := models.Users(mods...).One(ctx, db) こうすることで特定のユーザー、かつ、特定のポストに紐づく全ての情報を取得することができます。 詰まったポイント sqlboiler を使っていて、上記のような実装をしている中で、筆者は以下のようなポイントで詰まってしまいました。 多方向のリレーションのロード 特定の外部キーに一致するレコードのリレーションのロード それぞれについて、詰まったポイントを説明します。 1. 多方向のリレーションのロード ここでいう「多方向のリレーションのロード」とは、「ユーザーに紐づくコメント/いいねを取得する」というようなケースを指します。 つまり、 users -> posts -> comments / users -> posts -> likes のようにリレーション先が分岐する場合を指します。 (「1方向」は users -> posts -> comments のように、外部キーを辿ることで再帰的にリレーションを参照できる場合を指します) これを実現するために、初めに以下のような実装を試みました。 var mods []qm.QueryMod mods = append (mods, models.UserWhere.ID.EQ(userID)) mods = append (mods, qm.Load(qm.Rels( models.UserRels.Posts, models.PostRels.Comments, models.PostRels.Likes, ))) user, err := models.Users(mods...).One(ctx, db) 公式の GitHub リポジトリの Readme には、 Eager loading can be combined with other query mods, and it can also eager load recursively. と言及されています。 しかし、上記の実装では、ユーザーに紐づくポストからコメントといいねを並列で取得することが意図されています。 そのため、sqlboiler の想定される挙動と異なるため、正しくリレーションをロードすることができませんでした。 正しくリレーションをロードするためには、複雑なテーブル構造の例のように、以下のように実装する必要があります。 var mods []qm.QueryMod mods = append (mods, models.UserWhere.ID.EQ(userID)) mods = append (mods, qm.Load(qm.Rels( models.UserRels.Posts, models.PostRels.Comments, ))) mods = append (mods, qm.Load(qm.Rels( models.UserRels.Posts, models.PostRels.Likes, ))) user, err := models.Users(mods...).One(ctx, db) このように、リレーション先が分岐する場合は、1方向ずつ個別にロードの指定をする必要があります。 この時、具体的には以下のようなクエリが発行されます。 # `boil.DebugMode = true` でクエリログを出力 SELECT `users` .* FROM `users` WHERE ( `users`.`id` = ? ) ; [ 1 ] SELECT * FROM `posts` WHERE ( `posts`.`user_id` IN ( ? )) ; [ 1 ] SELECT * FROM `comments` WHERE ( `comments`.`post_id` IN ( ? )) ; [ 1 ] SELECT * FROM `likes` WHERE ( `likes`.`post_id` IN ( ? )) ; [ 1 ] 意図している取得ができない理由 sqlboiler では QueryMod というインターフェースでクエリの変更を管理します。 リレーションのロードでは loadQueryMod という構造体を利用してリレーションを管理しています。 // https://github.com/volatiletech/sqlboiler/blob/v4.18.0/queries/qm/query_mods.go#L10-L13 // QueryMod modifies a query object. type QueryMod interface { Apply(q *queries.Query) } // https://github.com/volatiletech/sqlboiler/blob/v4.18.0/queries/qm/query_mods.go#L63-L66 type loadQueryMod struct { relationship string mods []QueryMod } Load メソッドでは、ロードしたいリレーションを []string{"Relationship", "Relationship.NestedRelationship"} という形式の文字列で指定し、 1つの loadQueryMod 構造体の中で1連のリレーションを持ちます。 (e.g. Users.Posts.Comments ) リレーションを表現する文字列の . を起点として再帰的に外部キーカラムを辿ることでリレーションをロードしていきます。 そのため、1つの Load メソッドで並列したリレーションを指定してしまうと正しくリレーションを辿れなくなってしまうので、 リレーション先が分岐する場合は、1方向ずつ個別にロードの指定をする必要があります。 // https://github.com/volatiletech/sqlboiler/blob/master/queries/eager_load.go#L46-L68 // eagerLoad loads all of the model's relationships // // toLoad should look like: // []string{"Relationship", "Relationship.NestedRelationship"} ... etc // obj should be one of: // *[]*struct or *struct // bkind should reflect what kind of thing it is above func eagerLoad(ctx context.Context, exec boil.Executor, toLoad [] string , mods map [ string ]Applicator, obj interface {}, bkind bindKind) error { state := loadRelationshipState{ ctx: ctx, // defiant to the end, I know this is frowned upon exec: exec, loaded: map [ string ] struct {}{}, mods: mods, } for _, toLoad := range toLoad { state.toLoad = strings.Split(toLoad, "." ) if err := state.loadRelationships( 0 , obj, bkind); err != nil { return err } } return nil } 2. 特定の外部キーに一致するレコードのリレーションのロード ここでは、ユーザーAのあるポストPに紐づく全ての情報を抱き合わせてユーザーAの情報を取得するケースを考えます。 筆者の最初の実装は以下のようになっていました。 var mods []qm.QueryMod mods = append (mods, models.UserWhere.ID.EQ(userID)) mods = append (mods, qm.InnerJoin( "posts ON posts.user_id = users.id" )) mods = append (mods, models.PostWhere.ID.EQ(postID)) mods = append (mods, qm.Load(qm.Rels( models.UserRels.Posts, models.PostRels.Comments, ))) mods = append (mods, qm.Load(qm.Rels( models.UserRels.Posts, models.PostRels.Likes, ))) // その他のリレーションのロード user, err := models.Users(mods...).One(ctx, db) この実装をした時、筆者は「ジョインしてIDを指定しているからリレーションもその条件を見てくれるはず!」と思っていました。 しかし、実際には、リレーションをロードする際に改めてクエリが発行されるので、メインのクエリで指定する条件は反映されません。 そのため、正しくは以下のように実装する必要があります。 var mods []qm.QueryMod mods = append (mods, models.UserWhere.ID.EQ(userID)) mods = append (mods, qm.Load( qm.Rels(models.UserRels.Posts), models.PostWhere.ID.EQ(postID), )) mods = append (mods, qm.Load(qm.Rels( models.UserRels.Posts, models.PostRels.Comments, ))) mods = append (mods, qm.Load(qm.Rels( models.UserRels.Posts, models.PostRels.Likes, ))) // その他のリレーションのロード user, err := models.Users(mods...).One(ctx, db) このように、リレーションをロードする際に、リレーション先の条件を指定する必要があります。 ちなみに、ドキュメントには以下のような記述があります。 // the query mods passed in below only affect the query for Toys // to use query mods against Pets itself, you must declare it separately この言及通り、同一のリレーション先に対するクエリは重複実行されないため、 先の実装の models.PostRels.Comments や models.PostRels.Likes のロード時、 comment.post_id / like.post_id の条件は自動的に models.PostWhere.ID.EQ(postID) で指定した条件が適用されます。 というよりは、先にロードしているレコードが絞り込まれているため、除外されたレコードのリレーションのロードは発生しないことになります。 実際に、以下のようなクエリが発行されていることからも確認できます。 # `boil.DebugMode = true` でクエリログを出力 SELECT `users` .* FROM `users` WHERE ( `users`.`id` = ? ) ; [ 1 ] SELECT * FROM `posts` WHERE ( `posts`.`user_id` IN ( ? )) AND ( `posts`.`id` = ? ) ; [ 1 ] SELECT * FROM `comments` WHERE ( `comments`.`post_id` IN ( ? )) ; [ 1 ] SELECT * FROM `likes` WHERE ( `likes`.`post_id` IN ( ? )) ; [ 1 ] このように、sqlboiler を使って複雑なリレーションをロードする際には、 リレーション先が分岐するケースやリレーション先の条件を指定したいケースに注意する必要があることを学びました。 複雑なリレーションのロードをテストする DB アクセスしてデータを取得する実装する時、当然テストも書くかと思います。 その際、テストデータを定義して実際にテスト用の DB にデータをインサートする必要があります。 今回、実際にテストデータを定義している際、以下のようなポイントで詰まってしまいました。 テストデータのリレーションの定義 テストデータのインサート時のカラム指定 1. テストデータのリレーションの定義 については、テストデータのリレーションを定義する方法の理解が不十分で意図通りのリレーションを定義できなかった話になります。 sqlboiler がスキーマを Go の構造体にマッピングして生成されるモデル定義では、 <モデル>.R.<リレーション> というように、 R というフィールドを介してリレーションが定義されます。 このフィールドは、型がプライベートに定義されていて直接初期化することはできませんが、 NewStruct というメソッドから初期化することができます。 例えば、以下のようにリレーションを定義することができます。 // 例: ユーザーに紐づく投稿のテストデータの作成 var user = models.User user.R.= user.R.NewStruct() user.R.Posts[ 0 ] = &models.Post{ // ポストの情報を定義 } また、テストデータのリレーションを定義する方法として、 公式が推奨している boilingfactory というパッケージを利用する方法があります。 このパッケージを利用することで、以下のような実装で、リレーションを同時に持たせながらテストデータを初期化することができます。 (詳細な説明は割愛しますが、sqlboiler と連携させることで factories というパッケージが生成されます) user, err := factories.createUser( // ユーザーの情報を定義 factories.UserWithPosts( // ポストの情報を定義 factories.PostWithComments( // コメントの情報を定義 ), factories.PostWithLikes( // いいねの情報を定義 ), ), ) 筆者は初めから boilingfactory を使ってテストデータを定義していたため、いざテストデータを定義する際、 リレーション定義の仕組みをあまり理解できておらず、テストが通らずに悩んでしまっていました。 ただし、 boilingfactory 自体の更新が3年前から止まっていることや、 テストデータ定義の際にエラーハンドリングが必要になることなど、それぞれのやり方にメリット・デメリットがあるため、 ユースケースに応じた使い分けを考える必要はあるかと思います。 2. テストデータのインサート時のカラム指定 については、テストデータをインサートする際に、カラムを指定する方法に関する話になります。 ( sqlboiler の運用で作成するテンプレートファイルの作り次第でもあるので一概には言えないことをご容赦ください) テストデータを DB にインサートする際、 models.User などのモデルを使って以下のようにインサートすることができます。 user := models.User{ Name: "test" , } err := user.Insert(ctx, db, boil.Infer()) この時、 Insert メソッドの第3引数に指定しているもの ( boil.Infer() )がインサートするカラムを指定するためのものになります。 詳細に関しては sqlboiler で生成されたコードを見るとわかりますが、カラム名の指定では以下のようなメソッドが利用できます。 - boil.Infer() : 非ゼロ値のデフォルト値を持つフィールド以外をインサート - boil.Whitelist("name") : 指定したカラムのみインサート - boil.Blacklist("name") : 指定したカラム以外をインサート 実装時、 boil.Infer() でインサートする場面が多かったため、テストデータが想定とは異なっているケースがありました。 具体的には、nullable なカラムにデフォルト値が設定されている場合やデフォルト値が false でゼロ値として認識されてしまう場合などに、 期待されるテスト結果と実際のテスト結果が異なる状況になってしまっていました。 そのため、テストデータをインサートする際には、 boil.Whitelist() や boil.Blacklist() を使って、 明示的にインサートするカラムを指定することも重要な時があると感じました。 (なお、この話はインサート処理に限らず、 sqlboiler のメソッドを使う場面ではどこでも発生し得る問題です) おわりに 今回の記事では、現在進行中の社内システム開発において、複雑なリレーションを扱うために sqlboiler を有効活用しようとして苦労した点について紹介しました。 社内システム開発はまだ途中ですが、しっかりとやり切れるように頑張りたいと思います。 また、今回の記事が、少しでも皆さんのお役に立てれば幸いです。 最後まで読んでいただき、ありがとうございました。
アバター
iOSのウィジェットは、iOSのアップデートに伴い配置場所と機能が拡充されてきました。ウィジェットを開発する上で適切な技術を選択するための情報として、その変遷と各OSバージョンにおいて利用可能な機能を整理しました。 レガシーなウィジェット TodayExtension (iOS 8〜iOS 17) 初期のウィジェットは、ホーム画面ではなく、通知センター(ホーム画面を右にスワイプして表示される画面)に配置されていました。制約が比較的少なく、アプリに依存せずウィジェット内で機能を完結させることもできました。 iOS 14でWidgetKit が導入された後も引き続きサポートされていましたが、iOS 18でサポートが終了しました。そのため、アプリにToday Extensionが含まれていても、iOS 18以降では利用できません。 ホーム画面ウィジェット(iOS 14〜) iOS 14で WidgetKit が提供され、ホーム画面に配置するウィジェットを作れるようになりました。 ウィジェットのUIはSwiftUIで作成します。 WidgetKitはウィジェットの管理と表示更新の仕組みを提供します。 表示更新の仕組み ウィジェットは、システムリソースの過剰消費やバッテリーの消耗を抑制し、デバイスのパフォーマンスを維持するため、いくつかの制約が設けられています。 ウィジェットに表示するデータの取得と表示内容の決定は、メインアプリケーションまたは専用のバックグラウンドプロセスで実行する必要があります。時刻に応じてウィジェットの表示を切り替える必要がある場合は、事前に更新日時と表示内容を計画したタイムラインを作成します。 タイムラインは、 TimelineProvider によって生成され、 TimelineEntry オブジェクトの配列と TimelineReloadPolicy で構成されます。各 TimelineEntry は、 WidgetKit がウィジェットの表示を更新する date を指定し、表示に必要な追加データを含めることができます。これにより、ウィジェットをいつ、どのようなデータで更新するかを事前に計画します。 タイムラインの更新頻度はOSによって管理されており、開発者が直接制御することはできません。アプリから reloadTimelines() を使用して更新を要求できますが、即時実行は保証されません。更新頻度は、ユーザーのアクティビティ、バッテリー残量、ネットワーク接続などの要因によって変動します。 ウィジェットは静的なスナップショットとして表示されるため、継続的な更新には適していません。定期的に変化する情報の表示に適しており、リアルタイム更新が求められる場合はライブアクティビティの利用が推奨されます。 本体アプリとの連携 ウィジェットと本体アプリの間でデータを共有するためには App Groups を利用します。App Groups を作成し、ウィジェットと本体アプリの両方で共通の App Groups を有効にすることによって、User Defaults, File Manager, Core Data, Swift Dataでデータを共有できます。 UserDefaults UserDefaults は、ユーザー設定や小規模なデータセットの保存に適しています。 データは自動的にシリアライズされ、同期的にアクセスされます。そのため、UserDefaults はアプリケーションとそのウィジェット間で基本的な構成や状態情報を共有するのに便利です。 ただし、大規模なデータを扱うとパフォーマンス上の問題が生じる可能性があるため、そのような場合には CoreData や SwiftData の利用が推奨されます。また、機密性の高いデータには Keychain が適しています。 CoreDataとSwiftData より複雑なデータモデルと大規模なデータセットを扱う場合、 CoreData または SwiftData を使用して永続データを共有できます。 SwiftDataは、CoreDataよりも簡潔なコードで記述でき、モデルのマイグレーションも容易かつ安全に行えるため、iOS 17以降をターゲットとするのであれば SwiftData を使うのが良さそうです。 ロック画面ウィジェット(iOS 16〜) iOS 16では、ロック画面にウィジェットを配置できるようになり、これはホーム画面のウィジェットと同様の実装に基づいていますが、ロック画面特有の制約も存在します。 ロック画面のウィジェットは、基本的にモノクロ表示に限定されています。また、ウィジェットのサイズは accessoryInline (1行テキスト)、 accessoryCircular (円形)、 accessoryRectangular (長方形)の3種類がありますがいずれもホーム画面ウィジェットよりも領域が狭いです。これらの表示上の制限に合わせて情報の取捨選択やレイアウトを工夫する必要があります。 さらに、プライバシーに関する考慮が必要です。ロック画面はロック中でも情報が表示されるため、個人情報や機密性の高い情報を表示する場合は、ユーザーが設定で表示/非表示を切り替えられるようにするなど、適切な対応が求められます。 ライブアクティビティ(iOS 16.1〜) ウィジェットは静的あるいは定期的な情報更新に適しているのに対し、ライブアクティビティはリアルタイムかつ頻繁な情報更新に強みがあります。 ライブアクティビティは最大8時間のアクティブ表示が可能で、その後最長12時間ロック画面に残ります。この特性から、短中期的なイベントやタスクの追跡に最適です。一方、ウィジェットには表示時間の制約はありません。 両者はWidgetKitを基盤とし、UIはSwiftUIで構築されていますが、リアルタイム性やライフサイクル管理において異なる技術を採用しています。 ウィジェットはタイムラインプロバイダを通じて定期的にデータを更新します。対照的に、ライブアクティビティはActivityKitという専用フレームワークを使用し、アプリからリアルタイムに状態を更新します。バックグラウンドタスクの状態反映には、バックグラウンド動作中のアプリによる状態更新が必要です。 ライブアクティビティはiOSの複数の箇所に表示され、ロック画面ではバナー形式で詳細情報を提供します。iPhone 14 Pro以降ではDynamic Islandにも対応し、コンパクト、最小、拡張の3つの表示形式をサポートします。Dynamic Island非対応デバイスでは、更新時にロック画面上部に一時的なバナーが表示されます。 インタラクティブなウィジェット(iOS 17〜) インタラクティブウィジェットの導入により、ユーザーはアプリを開くことなく、ホーム画面ウィジェットとロック画面ウィジェット上で直接簡単な操作を実行できるようになりました。 利用可能なインタラクティブなUI要素は、 Button と Toggle の2つに限定されています。 このインタラクティブウィジェットを実現する上で重要なのが、 App Intents フレームワークです。 App Intents を活用することで、アプリがフォアグラウンドで動作していない状態でも、システムが実行可能なアクションを定義できます。 ユーザーがウィジェット内のボタンやトグルを操作すると、システムは対応する App Intent を起動します。App Intent の perform() 関数が実行され、アプリのデータモデルの更新といった必要なアクションが実行されます。 perform() 関数の実行完了後、システムは自動的にウィジェットのタイムラインを更新し、変更内容をウィジェットに反映させます。重要な点として、ウィジェット自体は状態を持たず、App Intent実行後の変更されたデータモデルに基づいてタイムラインが更新されることによってウィジェット表示内容が変化します。 ユーザー操作後、 App Intent の実行とそれに続くタイムラインの再読み込みにより、ウィジェットのUI更新にはわずかな遅延が生じる可能性があります。 コントロールウィジェット(iOS 18〜) iOS 18 で新たに導入されたコントロールウィジェットは、コントロールセンターやロック画面下部のウィジェット領域から、アプリケーションの機能を直接操作できるようにするものです。 従来のウィジェットがアプリ情報の視覚的な提供を主な目的とするのに対し、コントロールウィジェットは、シンプルで即座に実行できるタスクへのショートカットとしての役割に特化しています。 ボタンは瞬時の単一アクションに、トグルは明確なオン/オフを伴うアクションにそれぞれ用いられ、いずれもアクションの実行にはApp Intentsが活用されます。 コントロールウィジェットは、他のウィジェットと同様にWidgetKitフレームワークを基盤として構築されています。 おわりに 今回の記事ではiOSのウィジェットについて簡単にまとめました。参考になれば幸いです。
アバター