LIVESENSE ENGINEER BLOG

リブセンスエンジニアの活動や注目していることを発信しています

転生会議を支える技術「フロント・サーバー編」〜使ってみたいをふんだんに盛り込んでみた〜

はじめに

この記事は転生会議を支える技術「インフラ編」〜サイト爆速化への道〜の後編になります。
転職会議の2年ぶりのエイプリルフール企画として、「転生」をテーマに転職会議のエンジニアがそれぞれやりたいことを盛り込んだ内容になっています。

無料で豪華商品をゲット!今すぐ転生しろ!

このブログでは、前編と同じく「技術的挑戦をしながら転職会議システムのレンダリング最速を目指す。」について、詳しくご紹介します。

転生会議の技術要素

転生会議は次のような技術に支えられています.

  • GKE
  • Firebase
  • golang
  • Elixir
  • Phoenix
  • Vue
  • Nuxt
  • fastly
  • webp
  • nginx
  • Route 53

今回はフロント・サーバー編です! フロントを担当した山下と、APIを担当した中村、OGP画像生成を担当したse-yaの3人でお送りします。


やったこと1 Nuxt.js ✕ Atomic Design

フロントを担当した山下です。普段はサーバーサイドを中心に書いているのですが、Nuxtを使ってみたいとダダをこね、担当させていただきました。
今回は、本企画で使った技術と、実装する際に意識した点に関して書いていこうと思います。 今回は、Nuxt.jsを採用しAtomc Designを意識して実装を行いました。
どちらも業務で使ったことはなかったので、手探りの状態で使ってみたのですが、結果としてい非常に使いやすかったなという印象を受けたので、共有したいと思います。

Nuxt.js

Nuxt.jsとは、UIレンダリングをすることに焦点をあてたVue.jsアプリケーションを構築するためのフレームワークです。
公式サイトも非常に充実しており、create-nuxt-appという対話式でNuxt.jsのプロジェクトを構築するツールも提供されているので、難しい設定などはしなくても比較的容易にアプリケーション開発を始められるのではないかと思います。

ja.nuxtjs.org

また、デフォルトで状態管理を扱うvuexというライブラリがインストールされているので、ページ間をまたいだ状態管理が必要なアプリケーションも開発しやすいです。
本企画では、各ページでユーザーが選択した情報を保持しながらページ遷移する必要があったため、vuexを使ってみました。
Vuexでは、アプリケーションの状態をグローバルに管理することで、ページをまたいだ状態管理を比較的容易に行うことが出来ます。

app/store/answers.js

// 保持するデータを定義
export const state = () => ({
  answer: {
    story: null,
    avatar: null,
    body: null
  }
})

// 保持したデータに対してのgetter関数を定義
export const getters = {
  answer: state => state.answer
}
app/components/answer

import { mapGetters } from 'vuex'

export default {
  computed: {
    // 参照したいコンポーネント内で、定義したgetter関数を定義
    ...mapGetters('answers', ['answer'])
  },
  created() {
    // 定義したgetter関数でアプリケーションの状態を取得
    console.log(this.answer)
  }
}

また、外部との通信処理などをコンポーネントから切り出して書くことによって、コンポーネントではUIレンダリングのみに集中できるようになり、コードをシンプルに保つことが出来ます。

app/store/answers

export const actions = {
  async postAnswer() {
    // 通信処理を切り出して記述
    await this.$axios.post(‘https://hogehoge.com/api’, { params })
  }
}
app/components/form

import { mapActions } from 'vuex'

export default {
  methods: {
    post() {
      // 切り出した関数を実行
      this.postAnswer()
    },
    // コンポーネント内で、実行したい関数を定義
    …mapActions(‘answers’, [‘postAnswer’])
  }
}

vuexを使うことによって、処理を責務に合わせて分割することができ、アプリケーションをシンプルに保つことができます。
また、フロントにおける状態管理をUIと切り離して実装することができるので、デザイナーとエンジニアとの分担も比較的用意になります。

Atomic Design

Atomic Designとは、UI要素を最小の単位で分解し、一定のルールのもとに組合わせていくことによってページを構成していくデザイン手法です(と自分は解釈しています。)
実装する上で、以下を参考にしました。

design.dena.com

apbcss.com

UI要素を以下のようなルールで分割することによって、再利用性が高く変更に強いコンポーネントを作成していきます。

  • Atoms・・・UIの最小要素(ラベルボタンやフォームのパーツなど)
  • Molecules・・・複数のAtomsによって構成される要素
  • Organisms・・・データの取得などのドメインロジックを含んだ要素の集合
  • Templates・・・ページの枠組み
  • Pages・・・Templatesに必要な情報を流し込んだ、最終的なアウトプット

今回はNuxt.jsでAtomic Designを採用するために、以下のようなディレクトリ構成にしました。

- /app
  - components
    - atoms
    - molecules
    - organisms
  - layouts(Templatesにあたるディレクトリ)
  - pages(Pagesにあたるディレクトリ)

基本的に、organismsのコンポーネントで表示するのに必要なコンテンツを取得し、moleculesがatomsに情報を受け渡すという方針で作成しました。

app/components/organisms
<template lang="pug">
  .stories-content
    // データの子コンポーネントへの受け渡し
    Stories(:stories="stories")
</template>

<script>
// データの取得
import sample-data from '~/assets/jsons/sample-data.json'
import Stories from '~/components/molecules/Stories

export default {
  components: {
    Stories
  },
  data() {
    return {
      stories: sample-data
    }
  }
}
</script>
app/components/molecules/Stories
<template lang="pug">
  .stories
    // データの子コンポーネントへの受け渡し
    Story(v-for="story in stories" :key="story.name" :story="story")
</template>
app/components/molecules/Story
<template lang="pug">
  // 親コンポーネントから受け取った値を表示
  .story
    button
      img.story-icon(:src="require(`~/assets/images/${story.label}/stories/icon.png`)")
      span.name
        | {{ story.name }}
</template>

Atomic Designの手法を用いることによって、再利用性が高く変更しやすいコンポーネントを作れたかと思います。
また、それぞれのコンポーネントに対して責務が明確になるので、保守のしやすいアプリケーションを構築することが出きるのではないかと思います。

まとめ

NuxtもAtomic Designも、業務では初めて扱ったのですが非常に扱いやすく、メリットも多い印象を受けました。
何より、責務を分割することによってコードをシンプルに保つことが出き、書いていて気持ちがいいです!!


やったこと2 Elixir + Phoenixによるサーバーサイド実装

APIを担当した中村です。
普段はサーバサイド中心にやっており、Ruby on Railsや最近ではGolangを書くことが多いのですが今回初めてElixir + Phoenixという組み合わせに挑戦してみました。

APIについて

クライアントから受け取ったjsonをFirebaseのRealtime Databaseにpostするという簡単なREST APIです。
Nuxt + Firebaseでサーバーレスな構成が可能なのになぜAPIを間に挟むのか...? とお気付きの方もいることでしょう。
正直なところ今回このAPI自体は全く必要ありません。
今回は技術的挑戦+オーバーエンジニアリングがテーマということで、業務時間に堂々と遊んでみました(・∀・)

Elixirを書いてみた理由

新しいプログラミングパラダイムの言語に触れてみたかったからです。
今までJavaやRubyといったオブジェクト指向言語をメインで書いてきており、関数型言語をお仕事で書く機会はありませんでした。
関数型言語の中でも、ElixirはRubyとシンタックスが近く、Railsのような強力なMVCフレームワークのPhoenixがあるため、限られた時間で簡単に実装するのに向いていました。

実装にあたって

RailsでいうところのScaffoldのような機能があり、秒速でアプリケーションの初期構築が完了しました。
またライブラリ管理もmix.exsやmix.lockといったGemfileやGemfile.lockに近いものがあり、ほぼRailsに近い感覚で開発できました。
以下はコントローラのcreateメソッドです。
かなりRubyに近い見た目ですが、パターンマッチングやガード節を使ってみるなど関数型の特徴を取り入れてみました。

 def create(conn, params) do
    try do
      result = save_firebase(params)
      case result do
        {:ok, response} ->
          conn
          |> put_status(200)
          |> render("post.json", response: response.body)
        {:error, error} ->
          conn
          |> put_status(error.status_code)
          |> render("error.json", error: Poison.encode! error)
      end
    rescue
      exception ->
        Sentry.capture_exception(exception, [stacktrace: __STACKTRACE__])
        conn
        |> put_status(500)
        |> render("error.json", error: Poison.encode! exception)
    end
  end

苦労した点

今回は特に大きな苦労はなかったのですが...
FirebaseにはElixir用のSDKが存在しないため、Realtime Databaseの生成するREST APIを利用しました。
Firebaseのレスポンスを特に加工をするわけでもなく、そのままクライアントに横流しするコードを書いている時は(私は今、何をやっているんだろう...)という気持ちになりました。
また、転職会議で最速を目指すプロジェクトでありながら、Firebaseにpostしてレスポンスを待つ部分がどうしても遅く改善の余地が残りました。
今回はクライアントがAPIのレスポンスを待たないという形をとりましたが、休みの日にでもこっそり非同期でpostする処理を実装してみたいと思います。

まとめ

普段Railsを書く感覚で、短時間でアプリケーションを仕上げることができ楽しく開発ができました!
今回はRubyと比較したElixirのメリットを感じるところまではいきませんでしたが、要件に当てはまるプロダクトを作ることがあったら導入しやすいElixir + Phoenixの組み合わせを一つの選択肢に入れてみたいと思います。


やったこと3 Go言語によるTwitter OGP イメージ生成アーキテクチャ

転職会議で主にフロントからサーバーサイドを見てるse-yaと申します。
自分が担当したのは、前世の口コミをTwitter上に表示させるOGPイメージを生成するところです。
転生会議を担当したメンバーの一人から

  • 「se-yaさん空いてる?空いてるよね!」
  • 「前世の口コミをTwitterOGPで表示させる、レスポンスは早くしてね。」
  • 「よし、じゃあよろしく!」

と、怒涛のラッシュが来ましたのでやることになりました。
時間が殆どなかったので、ローコストで高いパフォーマンスを実現するためにざっと調べたところ Cloud Functionsにbeta版でgolangとpythonが使えると分かり、golangはパフォーマンスが良いとよく言いますし、 普段使わない技術を使うのもコンセプトだったのでgolangを採用しました。(他にGopherくんが可愛いのも理由ですかね(笑))

構成図

画像生成に時間がかかると、レスポンスが悪くなりユーザ体験が損なわれる可能性があったので、 処理時間を最小にするために、雛形をパターン別にCloud Storageに用意してそれをダウンロードし、合成した画像をアップロードする方法を取りました。
また、Cloud Funtionsはリクエスト数に応じてオートスケールするので、捌ききれなくなってもなんとかしてくれるだろうと思ってました。

よかったこと

1. Cloud Functionsで手軽にデプロイ・検証できること

サーバー不要でgcloudコマンド一発でデプロイからエンドポイント生成までやってくれて、ロジック作成だけに集中できました。
今回はhttpリクエストをトリガーにしたので、以下のようにResponseWriterとRequestを持つ関数だけ最低限用意すれば動くというわかりやすさも良かったですね。

package functions

import (
    "encoding/json"
    "log"
    "net/http"
)

type GenerateResponse struct {
    Status int64  `json:"status"`
    Result string `json:"result"`
}

func HelloFunction(w http.ResponseWriter, r *http.Request) {
    //logを使うとGCPのLoggingに、タイムスタンプ付きで書き込むことができる
    log.Println("Hello World!") 

    // JSONにする場合
    w.Header().Set("Content-Type", "application/json") 
    res, _ := json.Marshal(GenerateResponse{http.StatusOK, "Hello World!"})
    w.Write([]byte(res))
}

デプロイする場合は、goのモジュール機能を利用するので

export GO111MODULE=on

go mod init
go mod tidy
go mod vendor

gcloud functions deploy ${ENDPOINT_NAME} --runtime go111 --entry-point HelloFunction --trigger-http --region ${YOUR_REGION} --source . --project ${YOUR_PROJECT_ID}

と、runtimeにgo111を指定すれば、あとはよしなにやってくれます。
個人的にはAWS Lambdaより楽かと思います。

2. 早い!!

とにかく早かったですね。ざっと作った処理でも思った以上のパフォーマンスが出たのでそこにはびっくりしました。

3. エコシステムが優秀

golangだけでフォーマッタやテストツールなど揃ってるので安心して作ることができます。
今回はパッケージのインポートやフォーマット、linter周りで重宝しました。
そして、vim-goはいいぞ!(vim-go使ってる場合、GoImportsとすれば勝手にパッケージをインポートしてくれます)

_人人人人人人人人人人人_
> vim-goはいいぞ!! <
 ̄^Y^Y^Y^Y^Y^Y^Y^Y^Y ̄

苦労したところ

1. フォントとサイズと中央揃えの戦い

日本語フォントを読み込んで画像を生成したのですが、半角と全角が1:2の割合でなかったため、 中央揃えを行う際に単純に、「1行の文字数の半分 x フォントサイズ」で書き込み位置を決められなかったのは 辛かったですね。。。
ここは愚直にトライアンドエラーで係数を算出してバランスの良い値を見つける作業を行いました。

2. PNGイメージがdecodeできない

雛形のイメージがPNGファイルで作られていたのですが、なぜか error image: unknown format となって 読み込めない現象が起こってました。
拡張子が.pngになっていたので自分はPNGファイルだと思いこんでたのですが、実は中身はjpeg画像だったことが原因でした。。。
fileコマンドで確かめるとたしかにjpegになっていたので、image/jpegをimportしてimage.Decodeできるようになり解決。
たどり着くまでにそれなりに時間を要しました。

最後に

制約なしでいろいろと作れるのは楽しいですね。リリース直後は上手くいくかどうかハラハラしましたが なんとかなってよかったです。
golangはかなり久しぶりに書きましたが、書いていて楽しい言語ですね。
Cloud Functions + golang + Cloud Storageを本番導入しても問題なさそうな感じで、個人的にアリかと思います。
ぜひ、皆さんも試してみてください!
あ、最後にもう一つだけ。。。

_人人人人人人人人人人人_
> vim-goはいいぞ!! <
 ̄^Y^Y^Y^Y^Y^Y^Y^Y^Y ̄




転職会議ではマイクロサービス化を進めていることもあり、新技術を小さく試し、徐々に展開していく文化が根付いています。
今回は期間限定企画ということでかなり極端な例になりましたが、技術も楽しみながら社会のためになるサービスを作っていこうと日々奮闘しています。
少しでもご興味があれば、こちらLivesense Engineering Contact https://goo.gl/forms/jFO6b20jKck4zF0l2 からお気軽にお問い合わせください!

まだ間に合う!今すぐ転生しろ!