DELISH KITCHEN WEBについて
はじめに
はじめまして。DELISH KITCHENバックエンドチームの梅木です。 DELISH KITCHENのバックエンドチームはアプリ向き合いとWEB向き合いのチームとで別れており、自分はWEB向き合いのチームに配属されています。 担当業務としては、DELISH KITCHENのWEBフロントの開発はもちろん、APIサーバーやインフラと、WEBサービスに関しての改修では境界を設けずに、開発・運用・監視を日々行なっています。
本日はDELISH KITCHENのWEBサービスのシステムについて、ご紹介できればと思います。
DELISH KITCHEN WEBで使われている技術スタック
DELISH KICTEHEN WEBで使用されている技術スタックはこちらになります。
- フロント
- Nuxt.js(Universal mode): 2.14
- アプリケーションサーバー & BFF: express 4.x
- ランタイム: node 12.x
- コードフォーマッター: prettier / stylelint / eslint
- バリデーション: vee-validate 3.x
- テストフレームワーク: jest
- エラー監視: sentry
- ビデオプレイヤー: Videojs
- 状態管理: vuex
- ページ遷移: vue-router
- ページのメタ情報生成: vue-meta
- SSRレンダリング: vue-server-renderer
- Nuxt.js(Universal mode): 2.14
- APIサーバー
- Golang
- echo: 1.x
- Golang
- インフラ
- AWS ECS
- AWS Route53
- AWS Cognito
- AWS API Gateway
- AWS s3
- AWS Lambda
- AWS CloudFront
- Elasticsearch
- ansible
- docker
- パッケージ依存関係解消ツール
- renovate
- ログ/分析
- TreasureData
- redash
- 開発/運用ツール
- Github
- CircleCI
- Datadog
- terraform
DELISH KITCHEN WEBでは、基本的にNuxt.jsのフレームワークのルールに従って開発しています。 vuex、vue-router、vue-metaやvue-server-rendererなどのライブラリも、最初からNuxt.jsの中に組み込まれているものを使っています。 Universal modeのNuxt.jsはBackend for Frontendとして機能するため、GolangのAPIサーバーとは別々で管理されており、Nuxt.jsアプリケーションだけを考えて開発を行うことができます。
Nuxt.jsとGolangのAPIサーバーをそれぞれdockerコンテナ化し、そのコンテナ化したマイクロサービスをAWS ECSでec2インスタンスにデプロイするという構成で運用しています。 他にも、メールアドレスログインでAWS Cognito、検索や絞り込みシステムでElasticsearch、レシピのサムネイルを縮小化しwebp拡張子で配信するシステムでAPI Gateway x Lambdaを組み合わせたサーバーレスアプリケーションを利用するなど、WEBチームでは、WEBフロント開発だけでは終わらずクラウドインフラを多く使い、裁量が広い範囲でWEBサービスの開発を行なっています。
Nuxt.jsを採用した理由
現在、DELISH KITCHEN WEBはNuxt.jsで構成されておりますが、Nuxt.jsで運用しているのは下記のような背景があります。
背景
DELISH KITCHEN WEBの1stリリースでは、Riot.jsというSPAライブラリで構築されていました。普通にブラウザで動かすSPAアプリケーションとしては何も問題なかったのですが、DELISH KITCHEN WEBはメディアサイトであるため、ユーザーだけではなくSEO対策としてクローラーのことも意識して開発する必要があります。クローラーがページにアクセスしたときは、ブラウザからアクセスされたときと同じjsが返却されるわけですが、当時のクローラーのレンダリングエンジンはjs解析にそこまで強くなく、クローラーにページの内容を読み取ってもらうことができませんでした。その為クローラーからのアクセスだった場合は静的なhtmlのページを返す必要があり、Express(nodejsのアプリケーションサーバー)がhtmlを生成してクローラーに読み取ってもらうという方針を取っていました。
問題
上記の背景により、Riot.js + Expressの構成でプロジェクト運用していましたが、ユーザー向けのブラウザで実行するコード(SPA用のコード)とクローラーに向けのコード(SSR用のコード)の2重管理をしていたので、特に新規ページや新規機能の開発、確認や運用コストが大きいという問題がありました。クローラーからのアクセスを考慮すると、クローラとユーザーごとに返却するHTMLを変えることが、SEO効果に悪い影響があるのか未知でした。そのような問題があるなか、事業側から新しい施策に打ち出したいと話があり、要件としてWEBサービスにメールアドレスログイン機能や課金機能が必要でした。コードが2重管理されているプロダクトでそのような大きい機能を無事に実装し運用できるかという不安がチーム内にありました。
他にも当時の構成について、下記のような意見もありました。
Riot.jsを本番運用しているプロダクトが少なく、開発で困ったときに参考にできる知見もあまりない。
当時のDELISH KITCHENは、ユーザーからのインタラクションでdomを表示/非表示の切り替えが発生する機会が少なく、リッチなアニメーションもないため、WEBサービス自体がブラウザでレンダリングさせるSPAアプリケーションで構成する必要がない。
ユーザーへはSPAである必要がなく、クローラーを考えるとSSRが必要ということで、ユーザー側にもクローラーにも最初から同じコードからSSRで生成されたhtmlを返したほうがいいのではないかと考えました。大きな機能をWEBサービスに入れる前に、Universalアプリケーション(SSR + SPA)の開発ができる技術を使ってシステムリプレースをすることになりました。
検討
フレームワークレベルでUniversalアプリケーション開発が担保されている技術を検討したところ、Angular Universal / Next.js / Nuxt.jsの選択肢が出てきました。その選択肢の中からNuxt.jsを選びましたが、理由は下記となります。
- 当時のバックエンドチームには、バックエンドに強いメンバーがほとんどでwebフロント開発に長けているメンバーは多くなかったため、Nuxt.jsのような薄いフレームワークが取り掛かりやすいのではないかと考えた。
- バックエンドチームのメンバー内でVue.jsの開発経験者が一名在籍していた。
- Riot.jsのSFC(Single File Component)の作りが、Vue.jsでのコンポーネントと同じであるため、システムリプレースもしやすいのではないか。(template / script / styleとブロックを分ける構成がほぼ同じ。)
下はレシピのサムネイルとタイトルを表示するコンポーネントです。
Riot.jsのSFC
<delish-recipe-item> <a href="/recipes/{ opts.recipe.id_str }"> <img riot-src="{ utils.resizeImg(opts.recipe.square_video.poster_url, opts.size) }" /> <div class="item__title-wrap"> <p class="item__title">{ getTitle() }</p> </div> </a> <style type="scss"> .item__title-wrap { text-align: left; flex: 1; margin-left: .75em; padding: 0 1em .5em 0; border-bottom: 1px solid @color5; } </style> <script> import utils from '../../misc/utils'; this.utils = utils; this.getTitle = () => { let str = this.opts.recipe.lead + this.opts.recipe.title; if (str.length < 29) { return str; } return str.slice(0, 27) + '...'; }; </script> </delish-recipe-item>
Vue.jsのSFC
<template> <div class="delish-recipe-item"> <nuxt-link :to="`/recipes/${recipe.id_str}`"> <img :src="utils.resizeImg(recipe.square_video.poster_url)" /> <div class="item__title-wrap"> <p class="item__title">{{ recipeTitle }}</p> </div> </nuxt-link> </div> </template> <script> import utils from '../../misc/utils'; export default { props: { recipe: { type: Object, required: true, } }, computed: { recipeTitle() { const str = this.recipe.lead + this.recipe.title; if (str.length < 29) { return str; } return str.slice(0, 27) + '...'; } } } </script> <style type="scss" scoped> .delish-recipe-item { .item__title-wrap { text-align: left; flex: 1; margin-left: .75em; padding: 0 1em .5em 0; border-bottom: 1px solid @color5; } } </style>
実際に移行してみてどうか?
- はじめに課題として上げた、ユーザ向けとクローラ向けのコード二重管理がなくなり、同じコンポーネントで一元管理できるようになったため、開発やQAのコスト削減を実現しました。 Nuxt.jsにリプレースしてから、SEO対策向けに多くの機能リリースを行ったことから、これらのコストを下げられたのは良かったと思っています。
- Nuxt.js(Vue.js)の豊富なドキュメント、ライブラリや活発なコミュニティの恩恵を受けられ、開発して困る問題も大半は解決されました。多くの事例を参考にしながら開発をスムーズに行うことが出来ています。
- 追加で、webpackもNuxt.jsに組み込まれているため、ページ単位のjs分割やコンポーネントのdynamic importも可能となり、パフォーマンス対策もできています。
- Nuxt.jsはフレームワークであり一定のルールに沿って開発するため、UIライブラリのRiot.jsと違い、チームメンバーの間で認識を揃えながら開発もできます。
最後に
今回はDELISH KITCHEN WEBで使われている技術スタックと弊社でNuxt.jsを採用した理由をまとめてみました。
DELISH KITCHEN バックエンドチームでは、WEBフロント開発だけではなく、APIサーバーやクラウドインフラの運用も行なっていきます。プロダクトを良くするのに必要であれば、自ら提案し新しい技術に触れられる環境です。
現在運用が安定してきた中、これからも新規機能をリリースしたり、テストやtypescript導入するなど開発改善を行うことも考えています。
このブログでDELISH KITCHEN WEBについて少しでも知っていただけたら幸いです。最後までお読みいただき、ありがとうございました。