Next.js + Material UI v5 でフロントエンドアプリケーションを作成する

Next.js + Material UI v5 でフロントエンドアプリケーションを作成する

なぜこの記事を書いたのか

こんにちは。エニグモでサーバサイドエンジニアをしております、寺田(@mterada1228)です。

この記事は Enigmo Advent Calendar 2021 の 20 日目の記事です。

業務では主に Ruby on Rails を使っているのですが、最近新しいチャレンジとして、フロントエンドの勉強をしています。

そこで、Next.js + Material UI(以降 MUI)を使った Web アプリケーションの開発にチャレンジすることにしました。

ただ、この2つは全く異なる開発形態のフレームワークですので、一緒に使うためにはちょっとしたセットアップが必要になります。

参考にするため色々と検索してみましたが、なかなか求めている検索結果が得られなかったことと、なぜこのようなセットアップが必要になるかまで詳しくまとめられているものがなかったので、今回自分で調べて一つの記事を書いてみることにしました。

なお、本記事では Next.js で MUI を使うためのセットアップに焦点を絞っているので、Next.js のプロジェクトを立ち上げる部分は説明しません。

その部分を詳しく知りたい方は、公式チュートリアルを参考にしてもらえると良いかと思います。

Material UI を使うために必要なパッケージをインストールする

MUI v5 のパッケージをインストールしていきます。

ひとまず、コアな機能をインストールすれば十分かと思いますので、 @mui/material というパッケージをインストールしていきます。他のパッケージについては、必要に応じてインストールして下さい。

また、MUI ではスタイリングのために内部で CSS in JS のためのライブラリを使用しています。 こちらは、emotion もしくは styled-component のいずれかを入れる必要があります。

  • emotion を使う場合

    // with npm
    $ npm install @mui/material @emotion/react @emotion/styled
    
    // with yarn
    $ yarn add @mui/material @emotion/react @emotion/styled
    
  • styled-component を使う場合

    // with npm
    npm install @mui/material @mui/styled-engine-sc styled-components
    
    // with yarn
    yarn add @mui/material @mui/styled-engine-sc styled-components
    

next/link, @mui/material/Link の統合

Next.js からは、 next/link、MUI からは @mui/material/Link というコンポーネントが提供されており、名前からお察しの通りどちらもカスタマイズされたリンク(a タグ)を生成します。

この2つには競合する部分もありますが、ある場面では next/link を、また他の場面では @mui/material/Link を用いた方が便利というケースがあります。(どういった場面でどちらを使うべきか、という理由はサンプルコードの後で説明します。)

なので、状況に応じてどちらかのコンポーネントを返してくれるように、独自の Link コンポーネントを作成します。

src/Link.js

ここで重要になってくるのは65行目から86行目の部分で、href に設定された URL が、internalexternal のどちらであるかを判定して、 return するコンポーネントを分けている、ということです。

URL が internal な場合は、Next.js が提供する、next/link を返します。

これは Link がアプリケーション内のページ遷移であるときは、以下のような、Next.js が提供する各種機能を使用することができるからです。

  • Client-Side Navigation
  • Code Splitting and Prefetching

これらの説明は本記事の趣旨から逸れますので省きますが、詳しくはこちらをみて頂くと理解することができると思います。

URL が external な場合は、next/link である理由はあまりないので、豊富なスタイリング用の Props が使用できる @mui/material/Link を使うようにします。

実際にリンクを作成する時は、ここで作成した、src/Link をインポートする形になります。

pages/Index.js

独自 Theme を作成する

こちらは作成せずとも、MUI の default Theme を利用できますが、多くの場合アプリケーション独自の Theme を設定していくことになると思いますので、本記事でも取り上げていきたいと思います。

今回は src 配下に Theme ファイルを配置していきます。例は簡単のために、Typographyfont-size だけを設定したものになります。

src/Theme.js

独自で定義した Theme は、ThemeProvider を使って、アプリケーションに適用させることが可能です。

具体的な実装方法としては、 _app.js にて、Component より外側のコンポーネントThemeProvider を設定してあげれば OK です。

pages/_app.js

app.js, document.js の修正

最後に、Next.js で MUI を使用するために、_app.js_document.js の設定方法についてお話ししていきます。

はじめに、これから行う設定が何のためのものかというのを説明します。

MUI は元々、サーバサイドレンダリングされるものという制約のもと開発されています。 なので Next.js をはじめとした、クライアントサイドでレンダリングする可能性があるフレームワークを使用した場合に不具合が発生してしまいます。

具体的にどのようなことが起きるかというと、いわゆる FOUC(Flash Of Unstyled Content)というもので、画面上に CSS の当たっていないページが一瞬表示されてしまいます。

これはクライアントサイドレンダリング時に、HTML だけ表示されて、後から CSS が注入されるような流れになるため発生します。

なので、大まかに以下のような手順を踏むことでこの問題を回避していきます。

  1. サーバサイドレンダリング時に一度、コンポーネントツリーのレンダリングを行う
  2. そこから CSS を抜き出す(<style> を抜き出す)
  3. 抜き出した CSS をクライアントに渡してあげる

この手順では以下のパッケージが必要となりますので、事前にインストールしていきます。

// with npm
$ npm install @emotion/server @emotion/cache

// with yarn
$ yarn add @emotion/server @emotion/cache

まず、_document.js から説明していきます。_document.js は Next.js においてあらゆるコンポーネントの初期化時に実行されるコンポーネントですが、その特徴として、サーバサードレンダリング時に、サーバサイドで実行されるという特徴があります。

要するに前述した、1, 2 の手順をここで実行していくわけです。

サンプルコードは以下になります。

pages/_document.js

30行目からの処理に注目していきます。

getInitalProps はサーバサイドレンダリング時に、ページにあらかじめサーバから取得した情報を埋め混むことができるメソッドです。

getInitialProps 内の60行目から66行目までの部分で、一度ページコンポーネントレンダリングを行なっていきます(前述した 1. の手順に相当)

以降の処理で、レンダリングされたコンポーネントから CSS<style>)を抜き出し、emotion/cache を使ってキャッシュに保存します。(前述した 2. の手順に相当)

次にクライアントサイドの処理について見ていきます。

_app.js_document.js 同様に、あらゆるコンポーネントの初期化時に実行されるコンポーネントですが、クライアントサイドレンダリング時には、 _document.js は実行されず、_app.js しか実行されないという特徴があります。

ここでは、_app.js でサーバサイドレンダリング時にキャッシュに保存した CSS を受け取るといった方法で、クライアントサイドレンダリング時に発生する FOUC を回避します。以下がサンプルコードです。

11行目でサーバサイドレンダリング時に保存した CSS を取得します。

このキャッシュに保存された CSS は17行目のように、<Component> コンポーネントの外側を <CacheProvider> で囲ってあげることで、各ページコンポーネントに渡されます。

ここでお話しした内容は、MUI の公式ドキュメントでも詳しく説明されていますので、お時間ある方は合わせて見て頂けると理解が深まるかと思います。

おわりに

今回紹介したサンプルコードですが、https://github.com/mui-org/material-ui/tree/master/examples/nextjs で MUI が公式に提供しているものです。今後変更がある可能性もありますので、サクッとコピペして使いたい方は、紹介したリンクから取得いていただくのが確実と思います。

明日の記事の担当は データテクノロジーグループの堀部さんです。お楽しみに。


株式会社エニグモ すべての求人一覧

hrmos.co

GASとスプレッドシートを使ったOkta管理の一例

こんにちは。Corporate IT 所属の田中です。この記事は、Enigmo Advent Calendar 2021 の19日目の記事です。本記事では、Google Apps Script(以下、「GAS」といいます。)を使用して統合認証基盤サービスであるOktaの管理業務を効率化したことについて紹介したいと思います。 Oktaのグループにユーザーを追加する際、ユーザー数が多いと管理コンソール上で行うのに手間がかかり大変かと思います。
また、作業自体は簡単なものであるため業務アシスタントの方にお願いしたいと思いつつ、ロールの仕様上グループへのメンバー追加のみ権限を付与することも難しく、必要以上の権限付与を防ぐため結局自分で作業してしまうということもあるのではないでしょうか。
上記を改善するため、GASを用いてGoogle スプレッドシート上で簡単にメンバーのグループ追加を行えるようにしました。
更に、DB化されたスプレッドシートを利用して、Okta ユーザーのステータス情報を毎月コラボレーションツールのSlackに通知することで、月次のアカウント棚卸を効率化しました。

構成

仕組みはざっくり下図のとおりです。f:id:enigmo7:20211214002140p:plain *1*2

ユーザー及びグループ一覧の取得

OktaのAPIを使用してグループへユーザーを追加するためには、グループとユーザーそれぞれ一意に付与されているIDが必要です。 そのため、プロセスの前段としてGASでOkta APIからユーザー情報一覧とグループ情報一覧をスプレッドシートに同期しています。同期は、GASのトリガーを定期的に実行することで実現しています。
下図が、スプレッドシートへ反映されたユーザー一覧及びグループ一覧です。 「users-list」シートでは、ステータス、氏名、ローマ表記、email、ユーザーID、最後にログインした日時を取得しています。

f:id:enigmo7:20211214003056p:plain

また、group-listでは、グループ名、グループID、グループの説明を取得しています。 f:id:enigmo7:20211214003127p:plain

グループ追加

プロセスの後段として、上記取得した情報を基に、スプレッドシート上でOktaユーザーのグループ追加を行います。 下図左表「users-list」に追加する人の氏名を、右表「group-list」に追加先グループ名をそれぞれ入力すると、スプレッドシートのvlookup関数を利用してid等が自動入力されますので、後は下図右側の画像をクリックすると、GASでOkta APIを使用してグループへのユーザー追加プログラムが実行されます。 f:id:enigmo7:20211214003159p:plain

実行後、下図のように正常に追加されたことを確認しました。今後は、Slackの関連ページから本スプレッドシートを開いて簡単にグループへ多数ユーザーの追加ができそうです。 f:id:enigmo7:20211214003220p:plain

Oktaユーザーのステータスを月次棚卸

Oktaユーザーに関するステータスやログイン状況を利用して、スプレッドシート上で集計し月末にSlackに通知することにより、棚卸作業がやりやすくなりました。 f:id:enigmo7:20211214003254p:plain

最後に

いかがでしたでしょうか。私自身は非エンジニアで公開できる程綺麗なコードは書けませんためここでは非掲載とさせていただきますが、そんなレベルでも上記のような実装ができるぐらいGASは社内ITの連携に活用しやすく親和性のあるサービスかと思います。本記事の実装内容についてご興味ある方はご連絡くださいませ~。
また、enigmo Corporate ITでは各種クラウドサービス間の連携・自動化を実装できるようなコーポレートエンジニア及び部門を横断的にセキュリティ整備を行うセキュリティエンジニアを募集しております!ご興味ある方は下記求人からご応募お待ちしております!

明日の記事の担当は、サービスエンジニアリング本部の寺田さんです。お楽しみに。


株式会社エニグモ すべての求人一覧

hrmos.co

*1:Oktaのロゴは、次の利用規約に基づき使用しております。 https://www.okta.com/terms-of-use-for-okta-content/

*2:Google 製品のロゴは、次の利用規約に基づき使用しております。 https://about.google/brand-resource-center/brand-elements/#product-icons

あんしんしてお買い物してもらうためのカートUIリニューアルの裏側

f:id:enigmo7:20211210121849p:plain

こんにちは、BUYMAデザイナーの本田です。 BUYMAでは今年の2月〜7月にかけてカートの大幅なリニューアルを行いました。

PHPからRailsへの洗い替えや、パフォーマンスの改善がメインの施策だったのですが、UXの改善として、 上部に追従する購入ボタンよくある質問エリア を新しく追加しています。

ユーザーからよくある実際に届いている声の中から、

  1. 購入ボタンが遠くて見づらい
  2. キャンセルや返品がわかりにくくて不安
  3. 配送が遅い
  4. エラーがわかりにくい

上記4つの課題を解決するために、どのような想いのもとでデザインに落とし込んだのかについて紹介します。

比較

1. 購入ボタンが遠くて見づらい

BUYMAはカート画面でユーザーに確認していただきたいことが多く、1商品あたりの高さができてしまい、購入ボタンまでスクロール回数が多いことが課題となっていました。

複数商品をカートに入れていても、購入ボタンを見失わないでもらうために

  • 要素の見せ方整理をして、1商品あたりの高さを低くする
  • 購入ボタンを上部へ固定し、スクロール時も追従するようにする

の2つを行いました。

1商品あたりの高さを低く調節

2. キャンセルや返品がわかりにくくて不安

海外で購入した商品を日本へ送るCtoCショッピングサイトという特性上、

  • 偽物ブランドはないか
  • 返品はできるか
  • 送料や関税の負担はだれがするのか

などの質問をユーザーからいただくことが多く、専用の説明LPやガイドラインなどで説明しているのですが、それでも購入前になかなか気づけない・ユーザーの不安を拭えないことが、課題となっていました。

解決策として、LPへ遷移しなくてもカートページ内で補償サービスの内容を理解できるようにしたりユーザーからよくある質問エリアをまとめたエリア設置しました。

LPバナーをテキストへ変更、よくある質問を新設

3. 配送が遅い

海外配送となるため、送料の1番安いプランだと、繁忙シーズンには2週間以上かかってしまうこともあります。一方で、オプションメニューで、DHLやEMSなど海外の速達サービスを利用すると、+1,000円ほどで配送日数は大幅に短縮され、数日でお手元に届きます。

今までのUIですと、オプションメニューを選ぶことで、数日で到着することに気づかないまま、ご購入されるケースも多くありました。そのため、最短のプランがある時は、オプションメニューを選びやすくするUIパーツを追加しました。

最短プランがあるときはオプションメニューを選びやすくするUIパーツを追加

4. エラーがわかりにくい

今回のプロジェクトを機にすべてのエラー文言を洗い出し、どの状況下でエラーが発生してしまうのかを洗い出して整理し、提示するようにしました。

また、ユーザーは次に何をすればいいのか、ネクストアクションをきちんと提示することで、離脱率の低下を狙いました。

エラーパターン

検証

1. 購入ボタン

購入ボタンのクリックの割合は埋め込み:追従=8:2くらいで予想以上に利用されていました。 以前に他のプロジェクトで、商品詳細ページでも追従ボタンを設置してABテストをしていたので、あまり期待はしていなかったのですが、購入をある程度決めているカート画面には適切だったということがわかりました。

2. キャンセルや返品についてわからなくて不安

よくある質問エリアのクリックが全体の約2%で、割合としてはそこまで高くないですが、あんしんしてお買い物をするための後押しに少しでも貢献できたと思うと、デザイナーとして手応えがあったなと思います。

3. 配送が遅い

「最短の配送方法あり」のツールチップが表示されているユーザーの中で、さらに配送選択をしたユーザーの約45%が最短を選択していました。 サービスの特性などにもよるとは思いますが、追加料金を払っても早く商品が届いてほしいユーザーが多いことがわかりました。

まとめ

今回のカートリニューアルはユーザーの今の課題を解決する に加えて、あんしんしてBUYMAでお買い物を楽しんでもらうというビジョンを持ってサービス開発ができたと思ってます。

プロトタイプを定例で関係者全員に見せてフィードバックをいただけたり、プロジェクトチームの方全員と一丸になれたのもいい思い出です。来年も今年学んだことをベースにさらにいろんなことに挑戦していきたいです。

最後まで読んでいただきありがとうございました。 明日の記事の担当は 情シス の 田中さんです。お楽しみに。

株式会社エニグモ すべての求人一覧

hrmos.co

Googleスプレッドシートのオリジナル関数作ってみた

こんにちは、エニグモ嘉松です。

この記事はEnigmo Advent Calendar 2021の17日目の記事です。

はじめに

毎日毎日、それこそ仕事で使わない日は無いくらい、いつもお世話になっている表計算ソフト。 昔はみんなExcelを使っていましたが、最近は社内でもGoogleスプレッドシートを使うことが増えているように感じます。 (因みに私はLotus 1-2-3やNumbersは使ったこと無いです。)

さて、皆さんいろいろな関数を駆使して作業を効率化していることかと思います。

代表的な関数をいくつか紹介すると、

  • SUM

    • 表計算ソフトの関数の代表格といえば何と言ってもSUMですかね。
    • 指定した範囲の合計値を求めてくれます。
    • 「=SUM(A1:A10)」といった形で、合計したいセルの範囲を指定してあげます。
  • COUNTIF

    • 条件に一致するデータの個数を求めてくれます。
    • シンタックスCOUNTIF(範囲, 検索条件)です。
    • 「=COUNTIF(A1:A10, "<100")」といった形で、検索の対象とする範囲と、検索する条件を指定します。
    • 結果としては条件に合致したセルの数を返します。
    • 実は私、ほとんど使ったことないですが、よく聞くので紹介しました。
  • VLOOKUP

    • VLOOKUPを使えると表計算ソフトのスキルは中級くらいのレベルになりますかね。
    • この関数はホントに便利です。もはや無いと生きていけないレベル。
    • シンタックスVLOOKUP(検索値, 範囲, 列番号, 検索方法)です。
    • 範囲で指定した先頭の列を上から順に検索して検索値に一致する値を探します。見つかったセルと同じ行の列番号にあるセルの値を結果として返します。
    • 検索方法FALSEしか使ったことないです。

ちなみに、Googleスプレッドシートの関数の一覧はこちら。

Google スプレッドシートの関数リスト - ドキュメント エディタ ヘルプ

何十、何百の関数が用意されていて、使ったことも無い関数が殆どなのですが、自作のオリジナル関数を作ることも出来ます。

えっ、関数を自分で作る?そんな事できるの?と思われた方も多いかとも思いますが、私も最近知りました(笑)

では、これからオリジナル関数を自作する方法を紹介します。

消費税を求めるオリジナル関数を作ってみる

オリジナル関数、どう作るか?

そのためにはGoogle Apps Scriptでスクリプトを書く必要があります。

Google Apps Script(GAS)とは、Googleが開発・提供しているプログラミング言語です。

JavaScriptをベースに開発されたそうです。

スクリプトエディタを起動する

Googleスプレッドシートを開いたら、タブから「拡張機能」>「Apps Script」を押下します。

f:id:enigmo7:20211210112651p:plain
スクリプトエディタを起動

新しいタブが追加されて、以下のようなスクリプトエディタが起動されます。

f:id:enigmo7:20211210113818p:plain
スクリプトエディタが起動

スクリプトを書く

スクリプトエディタが起動されたら、ここにオリジナル関数のスクリプトを書きます。

ここでは消費税を求める関数myTAX関数を作ってみます。

/* 消費税を返す関数myTAX */
function myTAX(price){
  const taxRate = 0.1; //消費税率
  return price * taxRate;
}

functionのあとのmyTAXは関数の名前です。

priceは、関数の引数です。この関数の引数は1つなので、このように指定します。

const taxRate = 0.1; //消費税率taxRateという定数を作成して、税率の0.1(10%)を代入してあげます。

そしてreturn price * taxRate;で引数で指定されたpriceに消費税率taxRateを掛けて、その結果をreturnで返してあげます。

スクリプトができたら、フロッピーディスクのアイコンをクリックして保存してあげます。 (いまの若い方はフロッピーディスクなんて知らないと思いますが、これでイメージ付くのかな?)

オリジナル関数の使い方

では、作った関数を使ってみましょう。

使い方は簡単で、標準の関数と同様に=の後に関数の名前と引数を指定するだけです。

f:id:enigmo7:20211210120142p:plain
オリジナル関数の使い方

はい、このように消費税の金額が求められました。

f:id:enigmo7:20211210120700p:plain
オリジナル関数の結果

B2のセルの右下の小さな四角を下に引っ張れば、B2のセルがコピーされて、下の行の消費税が算出されます。

f:id:enigmo7:20211210121320p:plain
関数のコピー

消費税の算出だけであれば、セルに直接=A2 * 0.1とすればできます。

ただ、オリジナル関数を作って消費税を算出するメリットは、消費税の税率が変わった時にスクリプトの消費税率を変えてあげるだけで、変更後の消費税が取得できます。

  const taxRate = 0.12; //消費税率

こんな感じですね。

ABテストの信頼度を算出するオリジナル関数を作ってみた

実は、というか当然ですが私は消費税を算出するオリジナル関数を作りたかったわけではありません。

我々は日常的にABテストを実施しています。

例えばクーポンを配布した時に、配布したグループと配布しなかったグループで注文率に差があるが、どの程度差がでるか、クーポンを配布することで注文は増えるけど利益はどうなのか?といったようなことです。

この時、以下のような結果が出たとしましょう。

ABテストではクーポンの配布を実施したグループをトリートメントグループ、クーポンの効果を検証するために敢えてクーポンを配布しないグループをコントロールグループと呼びます。

対象者 注文者 注文率
トリートメントグループ 179,120 1,677 0.94%
コントロールグループ 11,800 98 0.83%

この結果からクーポンを配布したトリートメントグループの注文率が0.94%とコントロールグループの0.83%を上回っているので「クーポンの効果があった」と判断したくなります。

ただ、世の中、誤差は付き物です。 サイコロを10回振った時に1が10回連続で出たとしても、必ずしもそのサイコロがイカサマとは決めつけられません。偶然10回連続で1が出る確率は少なからずあります。

同様にクーポンのABテストでも偶然トリートメントグループの注文率が高かった、ということは無くはないです。

そんな事言ったらABテストを行う意味がないではないか、というとそうではありません。 今回の結果が偶然なのか、それとも偶然ではないのかを統計的な手法を使って判断することが可能です。 その方法を統計の用語で検定と呼びます。

検定の方法はいくつかあるのですが、今回はカイ二乗検定という検定方法を使って検定を行います。

詳細は省略しますがカイ二乗検定の方法は、以下の順序で行います。

①実績値を集計

注文あり 注文なし 合計
トリートメントグループ 1,677 177,443 179,120
コントロールグループ 98 11,702 11,800
合計 1,775 189,145 190,920

②期待値を算出

注文あり 注文なし 合計
トリートメントグループ 1,665 177,455 179,120
コントールグループ 110 11,690 11,800
合計 1,775 189,145 190,920

③実績値と期待度数の乖離値を求める

④乖離値を合算してカイ二乗値を求める

カイ二乗値からp値を求める

⑥p値から有意差を求める

一般的に有意差が95%を超えると有意な差がある、つまり偶然ではないと判断します。

このように有意差を求めるには多くの手順が必要となります。

そこでオリジナル関数の出番です。

今回は①〜④のカイ二乗検値を求めるところまでをオリジナル関数で行い、⑤〜⑥は標準で用意されている関数を使いました。

function myGETCHISQ(a, b, c, d){

  // 実績値
  var o = [
    a - b,
    b,
    c - d,
    d,
  ];
  
  var s = o[0]+o[1]+o[2]+o[3];

  // 期待値
  var e = [
    (o[0]+o[1]) * (o[0]+o[2]) / s,
      (o[0]+o[1]) * (o[1]+o[3]) / s,
        (o[2]+o[3]) * (o[0]+o[2]) / s,
          (o[2]+o[3]) * (o[1]+o[3]) / s,
            ];
  
  // カイ二乗検値
  var chisq = 0;
  for (i=0; i<=3; i++){
    chisq += (o[i] - e[i]) * (o[i] - e[i]) / e[i];
  }

  return chisq;
}

この関数のシンタックスは、以下のとおりです。

myGETCHISQ(試行回数A, 注文数A, 試行回数B, 注文数B)

上記の例だと、

=myGETCHISQ(179120, 1665, 11800, 98)

となります。もちろん引数にはセルの値を参照させることもできますよ。

この関数で求められたカイ二乗検値を標準で用意されているCHISQ.DIST.RT関数に与えてあげることでp値が求められます。

CHISQ.DIST.RT(カイ二乗値, 自由度)

そして、1からp値を引いて100を掛けてあげれば有意差が求まります。

因みにこの例の有意差は75.36%で95%を下回っているので、有意な差が出ているとは言えない、となります。

最後に

今回はGoogle Apps Script(GAS)を使ってGoogleスプレッドシートのオリジナル関数を作る方法を紹介しました。

また、より実践的な例としてABテストの信頼度を算出するオリジナル関数を作ってみました。

GASを使うと日頃の作業を上手に、そして簡単に効率化することが出来ます。

是非参考にしていただければと思います。

明日の記事の担当はデザイナーの本田さんです。お楽しみに。


株式会社エニグモ すべての求人一覧

hrmos.co

受注リストRails化プロジェクトについて

こんにちは、Webエンジニアの平井です。 この記事は Enigmo Advent Calendar 2021 の16日目の記事です。

現在、私はBUYMAのSellチームに所属していて出品者関連システムの実装を担当しています。 今季最も注力した受注リストページのRails化プロジェクトについて、プロジェクトの概要、プロジェクトを進める上で工夫した点、失敗した点について書きたいと思います。

Rails化プロジェクトとは

もともとBUYMAPHPで実装されたシステムで、その実装をRuby on Railsで書き換えるのを弊社ではRails化と呼んでいます。 Rails化する目的は以下になります。

受注リストページについて

受注リストページはBUYMAで発生した受注が一覧で表示されていて、出品者がその受注情報を確認したり、受注に対して発送通知などの アクションを実施するために用意されたページです。

出品者にとって利用頻度が高いページで、施策で修正する頻度が高いもののPHPの古いフレームワークで実装されているため修正のコストがかかるのがネックでした。

また、絞り込み機能が乏しく、出品者が目的に沿った取引を絞り込めずに各アクションを実施するのが大変という企画サイドの要望も有りました。 ですので、今回はRails化に加えて、検索機能の充実化も開発スコープに含めてプロジェクトを進めました。

システム概要

クライアントサイド

クライアントサイドは、実装スピードを上げるため大部分はerbで実装しました。ただ、検索フォームに関しては共通コンポーネントを利用したかったため Reactを使いました。

サーバーサイド

すべてRuby on Railsです。また、BUYMAでは検索サーバーにSolrを利用していて、この受注リストでも例外なく 受注の検索はSolrを使っています。ただ、旧受注リストではSolrを直接利用していたためSolr特有のパラメーター生成処理など も実装されていました。そこで、検索エンジニアの方にAPIを開発してもらい新受注リストからはそのAPIを利用して検索処理を実装しています。 APIを利用したため、Solrの専門的な知識が不要でアプリケーション側の実装の負担がかなり減りました。

工夫した点

ドキュメントの作成

Rails化プロジェクトの難しいところは、PHPで書かれたソースコードを読み正しく仕様を理解することです。 旧受注リストには、出品者が利用する様々な機能が実装されていますがまとまったドキュメントがなかったのでソースコードを 呼んで仕様を理解する必要があります。 ドキュメントがあれば非エンジニアとのコニュニケーションがスムーズに進むのでこのタイミングでドキュメントを作成しました。 また、実装とドキュメント作成を別々で行うとどうしてもドキュメント作成が後回しになってしまうため、実装しながらドキュメントも同時に作成するように 工夫しました。

ドキュメントを作成した事で、デザイナーやQA担当の方とのコニュニケーションがスムーズに進んだと思います。

フィーチャーフラグの利用

リリースの負担を減らすために、フィーチャーフラグを利用しました。 リリースブランチに受注リストRails化に関わる修正をマージし、該当ブランチを毎週リリースすることで新受注リスト公開時のリリース負担を減らすことが出来ました。

追加機能の優先度付け

リリーススケジュールに関しては、12月のセール時期より前に新受注リストをリリースしたいという企画側の要望が有りました。 そこで、企画サイドとコニュニケーションを取り追加機能の優先度を決めて、リリース目標日までに実装すべき機能とそうでない機能を切り分けました。 優先度を決めたのが功を奏し、目標としていたスケジュール日通りに基本的な機能をリリースすることが出来ました。

ユーザーインタビュー

企画的な内容になりますが、企画サイドの担当者の方が、新しく実装する機能やデザインについて多くの出品者に対してインタビューを実施してくれました。 リリース後に追加機能に関してネガティブな意見がほとんどなかったのもこのインタビューのおかげだと思います。

失敗した点

テスト環境でのパフォーマンス監視

そもそもRails化プロジェクトの目的としてレスポンスタイムの向上があるのですが、テスト環境でのパフォーマンス監視が不十分だっため 旧受注リストよりも、新受注リストのほうがレスポンスタイムが遅くなってしまいました。 原因としては, N+1とerb上でのrenderメソッドの多用です。 現在、絶賛修正中です。

最後に

いかがでしたでしょうか。現在弊社で進められているRails化プロジェクトについて書かせて頂きました。 今回プロジェクトで学んだ点を活かし、今後もBUYMAの改善を進めていきたいと思います。

明日の記事の担当は。データ活用推進室の嘉松さんです。


株式会社エニグモ すべての求人一覧

hrmos.co

MLOpsはじめました

この記事は Enigmo Advent Calendar 2021 の15日目の記事です。

はじめに

寒さが身にしみる今日この頃、みなさん如何お過ごしでしょうか。
最近、○○エンジニアという肩書きがよく分からなくなってきたエンジニアの伊藤です。

アドベントカレンダーの時期になると年末になったんだなという実感が湧きますね。
今回は今年一番注力してやってきたMLOpsについて書いていこうかと思います。

MLOpsとは?については他にも色々と記事があると思うのでここでは触れずに、 導入に向けてどういった手順でどのような取り組みを行ってきたかを中心にご紹介できればと思います。

なぜMLOpsなのか?

弊社でも機械学習を活用したプロジェクトがここ最近増え始めています。
そういった中で課題も色々と見えてきました。

  • 属人化
    • 実装内容を把握しているのが個人に依存している
    • レビューがされていない
    • 使用されているバージョンやライブラリがプロジェクトによってバラバラ
  • 開発効率の低下
    • 同じような処理が色々なところにある
    • テストコードがない
  • 心温まる運用
    • モデルの作成、デプロイは手動
  • オブザーバビリティの欠如

こういった状況を踏まえつつ、 今後も増えてくるであろう機械学習プロジェクトを見据えて、MLOpsを導入することになりました。

全体的な流れ

MLOps導入まではざっと下記のような流れで進めました。

  1. 調査
  2. MLOpsの定義、方針/目標の設定
  3. アーキテクチャの設計
  4. ML基盤の構築
  5. 開発体制やルール等の策定
  6. 実装
  7. CI/CD/CTの検討、設計、実装
  8. 監視周りの検討、設定

4以降についてはきっちり分けて進めていた訳ではなく、走りながら(実装、構築しながら)並行で進めていくことが多かったです。 以降で各項目からいくつかピックアップして詳細については記載します。

調査

私自身、MLOpsについては何となく分かっていたつもりでしたが、先人たち(先行他社)の取り組みや関連資料を読み漁りました。 ここで特に気を付けていたのは下記の点です。

  • 機械学習プロジェクトにまつわる課題としてはどういったものがあるのか?
    • 現時点で弊社としての課題感はないが、今後課題になりうるものがないか?
  • 先人たちの取り組みを真似るのではなく参考にする
    • 各課題に対してどういった考えで、どのようなアプローチをとっているのか
    • 個人的な考えですが、組織の規模感や文化といったものが違うのに同じような取り組みをしても失敗すると思っています

MLOpsの定義、目標の設定

MLOpsはバズワードだなんて意見もあります。 MLOpsという単語自体が一人歩きしている感もあるかなと思います。 なので一通り調査が終わったあとは弊社におけるMLOpsを定義することにしました。

今更ながら弊社のメンバー構成を紹介すると下記の通りで、スモールスタートにはちょうど良い人数かなという印象です。

  • データサイエンティスト(以降、DS) ✕ 2名
  • エンジニア(以降、Ops) ✕ 2名

エニグモにおけるMLOps

「DS(ML)とエンジニア(Ops)がお互いの役割を理解し、強調し合うことで、 機械学習モデルの実装から運用までのライフサイクルを円滑に進めるための仕組みづくりやその考え方」

f:id:pma1013:20211208132316p:plain

方針

続いて方針についてです。3つ挙げました。

  1. 小さく始めて、大きく育てる

  2. 同じ言葉で話せるようにする

  3. マネージドサービスの活用

小さく始めて、大きく育てる

  • 最初からあれも、これもと色々なことを詰め込みすぎない
    • いつまで立っても設計が固まらず、システムの構築や運用が始まらないというのは一番避けたい
  • MLOpsを取り巻く環境はまだまだ成熟しておらず、ノウハウや技術も発展途上の段階
    • 柔軟性が高くスケールする基盤であることを第一としてシステムを設計、構築する

同じ言葉で話せるようにする

  • スキルセットやメンタルモデルが異なるメンバー同士で作業を進めていく上ではどうしてもコニュニケーションコストは高くなる
  • エンジニア同士では暗黙知としているようなルールやお作法についてもきちんと明文化し、使用するツールやフレームワークなども共通化
    • ガチガチのルールで縛るのは好きではないので、バランスには注意

マネージドサービスの活用

  • 限られた人的リソースの中で対応していくにはどうしても人がボトルネックになりがち&人的コストはスケールさせるのが難しい
    • お金で解決できるようなところは積極的に利用する
  • 何でもかんでもマネージドという訳ではなくバランスが大事
    • コスト(人とお金) X システムとしての柔軟性は意識する

目標

f:id:pma1013:20211208141457p:plain

最後に具体的に何をしていくか、どこを目指すかの目標についてです。 ひとまず今期(2021年)としては下記を目標として挙げました。

DSが開発しやすい環境作り

  • UIベースでの実験管理
  • 再現性の確保(今期はデータではなく環境面にフォーカス)
  • 各種ルール化(明文化)
    • コーディング規約、レビュー規則、ログ仕様、開発フロー
  • 実験段階から本番環境へのデプロイがシームレスに可能
  • ドキュメントの充実化

開発コスト削減

データの可視化と監視

  • モデルの性能指標など、可視化して確認したいデータが見たい時に見られる状態
  • 過不足のない監視、アラート

変化に強い基盤構築

  • 汎用性が高く、スケール可能な基盤であること

チームとして1つの目標を目指す上で、言葉の定義や、目標の明文化というのはとても大事だと思っているので、 この辺については特に丁寧に行いました。

また目標としてやるべきことを明確化することと合わせて、やらないこともあえて決めておきました。
どうしても作業を進めていく中で「こんなこともできるとうれしいよね」みたいなことは出てきます。
方針にも挙げていますが、まずは小さく始めることを第一としていたので、こういった迷うケースでも チームとしてブレずに進めたのは良かったなと思います。

アーキテクチャの設計

続いてアーキテクチャの設計についてです。

ツール選定

アーキテクチャを設計する上ではまずはMLワークフローの中心となるツール選定を行いました。 MLOpsを取り巻くツールはMLOps Toysに集約されるようにとても多岐に渡ります。
この1つ1つを調査、検証していてはとても時間がかかるのでツール選定については時間をかけずに、 下記の観点で一番相性の良さそうなKubeflowを選択しました。

  • 利用事例が多い
  • 開発が活発
  • 柔軟性がある
    • まずはPipelinesのみ導入し、その後必要に応じて他のコンポーネント導入が可能
  • Kubernetesの構築、運用実績がある

アーキテクチャ

分析等に扱うデータはBigQueryに集約している関係で、そのデータを扱うML系のプロジェクトもGCP上で構築することが多いです。 そのため今回新たに構築するML基盤についてもGCP上で構築しています。 またKubeflow PipelinesについてはそのマネージドサービスであるAI Platform Pipelinesを利用することとしました。

f:id:pma1013:20211208143338p:plain

  • ML基盤としてGKEを利用
    • 3パターンのnodeプールを用意
    • ML nodeプール
      • MLパイプラインでの処理を実行するpod用のnodeプール
        • 0 to Nのノードスケール
        • 使用するマシンタイプは短時間で高速で処理が完了するようにCPU,メモリともに潤沢なものを選択
    • API nodeプール
      • 予測結果のサービング用API向け
        • 1 to Nのノードスケール
        • 使用するマシンタイプはCPUメインの比較的軽めのものを選択
  • GCPマネージドサービスとして下記を利用
    • AI Platform Pipelines
    • BigQuery
    • Cloud Storage
    • Cloud Datastore
    • Container Registry
  • 各種メトリクスの可視化、監視にはDatadogを利用

ML基盤の構築

ML基盤としては上記の通り、GKEをインフラとしつつ、AI Platform Pipelinesを利用しています。 基本的には全てIaCとしてterraformで管理、運用していますが、AI Platform Pipelinesは未対応だったため対象外として扱っています。 また、システムに名前があった方が良いよねということで、 メンバーそれぞれで候補となる名前をいくつか挙げて、投票で一番多かったCapellaという名前に決まりました。

CI/CD/CTの検討、設計、実装

CI、CD、CTともに全てGitlab CIを利用するようにしました。
Cloud Buildなども検討しましたが、Gitlab CIの方がより柔軟性が高かったのと、元々別のシステムでもGitlab CIを利用していたというのが理由です。

CI/CD

  • テスト環境

f:id:pma1013:20211208190442p:plain

  • 本番環境

f:id:pma1013:20211208190522p:plain

  • ポイント
    各工程の細かい処理については置いておいて、ポイントについてここでは記載します。
    • 成果物はテスト、本番環境で共通
      • Kubeflow Pipelinesにおける成果物してはパイプライン(コンパイル済みファイル)とそこで実行されるコンポーネントのコンテナイメージとなります。
      • 本番環境ではテスト環境で作成された成果物を使用してパイプラインのデプロイを行います。
    • integrationテストとして実際にKubeflow Pipelines上でコンポーネントの動作検証を自動テスト
      • 作業を進める中でコンテナイメージ単体としては問題なく動作するが、Kubeflow Pipeline上でパイプラインとして実行した際に問題となるケースがいくつかあったため組み込みました。

CT

パイプラインの実行トリガーをどこに持たせるかは正直一番悩んだところです。 Kubeflow Pipelines側に標準実装されているスケジュール機能を利用するのが良いのかなとも思いましたが、下記の理由からGitlabパイプラインスケジュールを利用して定期実行するようにしています。

  • CI/CD用に作成したツールを使い回せる
  • gitブランチと紐付けてスケジュール実行が可能
    • master → テスト環境
    • production → 本番環境
  • CI/CDジョブとシームレスに扱うことができる
  • CI/CD/CTがすべてGitlabで管理されているという分かりやすさ
  • Kubeflow Pipelines側のスケジュール機能で実行した場合に、MLパイプラインの実行ステータスの通知をどうするかの検討が別途必要だった
    • Gitlab側であればパイプラインステータスを判断してSlack連携が可能なことは確認済みだった

なお、下記がGitlabパイプラインスケジュールのUIになります。 内容としても直感的に分かりやすく、cronジョブベースでの定期実行と、設定した変数で処理が分岐できるようにしています。

f:id:pma1013:20211209093618p:plain

おわりに

ここまで調査/検討から始まり導入までについて、ざっとご紹介させていただきました。 書ききれていない内容もありますが、少しでもどういった取り組みをしているのかが伝われば幸いです。

また今回構築したシステムについてはまだまだ未熟で、これからどう成長させていくかが大事かなと思っております。 そんな訳でエニグモではMLプロジェクトを一緒に盛り上げていくメンバーについても募集中です! この記事を見て少しでも興味を持っていただけたら、まずは雑談からでもOKですので気軽にご相談ください。

明日の記事はエンジニアの平井くんです!きっとワクワクするような内容だと思うのでめちゃくちゃ楽しみですね!!!

株式会社エニグモ すべての求人一覧

hrmos.co

シェルスクリプトの実装に潜む 4 つの罠

こんにちは。サーバーサイドエンジニアの伊藤です。

この記事は Enigmo Advent Calendar 2021 の 14 日目の記事です。

みなさんはシェルスクリプトを実装する機会はどのくらいの頻度でありますでしょうか?

私は社内ツールや個人で利用するちょっとしたツールを作成する際に、シェルスクリプトを実装することがあります。 とはいえ、普段の業務では Ruby on Rails を用いて実装をすることが多いので、 シェルスクリプトを実装する機会自体はそれほど多いものではありません。

その為、シェルスクリプト実装時に割と基本的な罠に陥ってしまうことがありました。 

そこでこの記事ではシェルスクリプトの実装に潜む 4 つの罠を共有したいと思います。

1. shebang の記載漏れ

# bad

echo "FOO"

# good

#!/usr/bin/env bash
echo "FOO"

ファイルの 1 行目に shebang を記載することで、プログラムを実行するシェルを明示的に指定することが推奨されます。 というのも、シェルによってはサポートされている機能が異なることがあるため、 スクリプトを実行するシェルを明示的に指定することで意図しない挙動を避けることができます。

ここで 1 つの疑問が発生しました。

シェルスクリプトにおいてshebang の記述は下記の通り複数ありますが、好ましい記述方はどれなのでしょうか?

#!/bin/bash
#!/usr/bin/env bash

一般的には#!/usr/bin/env bash の形式を利用することが推奨されるようです。

絶対パス等で指定をした場合、実行環境によっては指定の場所に実行可能なコマンドが存在しない可能性が考えられます。 上記の方法で記載すると、 $PATH から実行可能なコマンド(この場合 bash)を捜査してくれるのでポータビリティという点でメリットがあります。

さて、少し話が逸れてしまったので閑話休題、 2 つ目の罠に話を移します。

2. 適切な set options を指定しない

# bad

#!/usr/bin/env bash
echo "FOO"

# good

#!/usr/bin/env bash
set -eu
echo "FOO"

-e (errexit)

こちらのオプションを指定することで、シェルスクリプトの途中でエラーが発生した場合その場で直ちにスクリプトを終了します。

大抵のケースではエラーが発生した場合、その場でスクリプトを終了したいことが多いと思いますので有効にしておくことが推奨されます。

ちなみに、エラーを許容したい場合には下記のように記述することで該当のコマンドのみエラーを許容することが可能です。

command1 || true
echo "This line is executed anyway"

-u (nounset)

こちらは変数宣言に関するオプションです。

デフォルトでは宣言していない変数を利用した場合、空文字として扱われてしまいます。 その為、typo 等で未宣言の変数を利用したとしても特にエラーにはならないので、意図しない挙動を招くことがあります。 そういったケースを避けるためこちらのオプションを有効にしておくことをお勧めします。

3. 1行で宣言と代入を行う

# bad
export BAR="$(command2)"

# good
BAR="$(command2)"

export BAR

上記のスクリプトでは command2 の結果が無視されてしまいます。 set -e を指定している場合、本来エラーが発生した箇所でスクリプトは終了します。 ただ、上記のケースでは command2 の結果に関わらず後続の処理が実行されてしまいます。

その為、代入と宣言の箇所を分けることが推奨されます。

4. Double quotes で変数を囲わない

# bad
file="sample file.txt"
ls $file

DIR="$(dirname $0)/.."

# good
file="sample file.txt"
ls "$file"

DIR="$(dirname "$0")/.."

double quotes を利用しないと該当の文字列中にスペースやタブ等を含むケースで、意図しない結果を招くことがあります。 例えば、上記のようにファイル名にスペースを含むケースだと、double quotes で該当の変数を囲わない場合スペースによって samplefile.txt という 2 つの単語に分割されてしまいます。 結果として、 sample file.txt というファイルではなく samplefile.txt を引数に ls コマンドが実行されてしまいます。

このような、意図しない挙動を避ける為、シェルスクリプトでは変数を double quotes で囲むことが推奨されます。

さて、ここまでシェルスクリプトの実装に潜む 4 つの罠について記載してきました。 シェルスクリプトを実装したことがある開発者にとっては、これらのどれもが一度は経験してきた問題なのではないでしょうか?

これらの問題が一般的ではある一方で、全てをマニュアルで人間が気をつけることは難しく労力を要する作業かと思います。 ということで、 shellcheck というツールを利用することをお勧めします。

shellcheck とは?

シェルスクリプト用の静的 Lint ツールです。 こちらを利用することで、上記で説明してきたエラー(罠)を簡単に検知することが可能です。

コマンドを実行すると、下記の通り問題のある箇所を明瞭に指摘してくれます。

>>> shellcheck ./tmp.sh

In ./tmp.sh line 1:
set -eu
^-- SC2148 (error): Tips depend on target shell and yours is unknown. Add a shebang or a 'shell' directive.


In ./tmp.sh line 5:
echo ${baz}
     ^----^ SC2086 (info): Double quote to prevent globbing and word splitting.

Did you mean: 
echo "${baz}"

For more information:
  https://www.shellcheck.net/wiki/SC2148 -- Tips depend on target shell and y...
  https://www.shellcheck.net/wiki/SC2086 -- Double quote to prevent globbing ...

さて、これだけでも記憶を頼りにマニュアルで全てを確認することに比べ、 簡便になったとは思います。

しかし、こちらのコマンドを修正の度に毎度手動実行することは非常に骨の折れる作業であり、非人間的です。 ということで、CI として設定して自動で実行しましょう。

今回はサンプルとして昨年の Advent Calendar 2020 の記事に記載した自作の OATHTOOL ラッパーコマンドshellcheck を CI として追加してみます。

こちらのレポジトリは GitHub 上に存在するので、今回は GitHub Actions を用いて設定していきます。

設定ファイルは下記。

# .github/workflows/main.yml
name: CI

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  shellcheck:
    name: Shellcheck
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - name: Run ShellCheck
      uses: ludeeus/action-shellcheck@master
      with:
       scandir: './bin'

shellcheck は既に GitHub Action としてマーケットプレースに存在するので、設定自体は簡単です。 上記のように設定ファイルを 1 つ追加するだけで設定は完了です。

github.com

無事に CI として shellcheck が走るようになりました。

最後まで読んでいただきありがとうございました。

明日の記事の担当は検索エンジニアの伊藤 明大さんです。お楽しみに。

参考


株式会社エニグモ すべての求人一覧

hrmos.co