React/Redux約三年間書き続けたので知見を共有します

Enigmo Advent Calendar 2018の4日目の記事です。

この記事の目的

Enigmoが運営しているBUYMAでは古代から運用しているjQueryの他に、2016年頃から一部ページのフロントエンドをReact/Reduxで構築しています。 私自身もEnigmoに入社してからの約三年間でReact/Reduxアプリケーションの開発に多数携わってきましたので、そこで培った知見を共有したいと思います。

React/Reduxの利点

まずはじめに、ReactとReduxを使うメリットを再確認しておきたいと思います。 それぞれのメリットをしっかりと認識しておくことで、実装する際どう書くか迷ってしまった場合などにそのメリットを最大限活かす選択をすることができます。

Reactの利点

Reduxの利点

  • Reducer、ActionCreator、Componentといった利用側が書くべきモジュールはすべて純粋な関数で書ける
    • ほとんど関数の集合だけでアプリケーションが完成する
  • 副作用のある処理はすべて後述するMiddleware層に持っていける
  • 上記の理由からテストが非常に書きやすい

React/Reduxはこう書く!!

では実際に私がReact/Reduxアプリケーションを実装する際指標としているプラクティスを解説していきたいと思います。

Container Componentsの分割

Container Components とはRedux Storeconnect しているコンポーネントのことです。 詳細はReduxの公式Docをご確認ください。

Container Componentsを適切に分割することでコードの見通しを良くします。 Containerの中にContainerが存在する、 Container in Container も許容します。

分割されていない例

import { connect } from 'react-redux'
import * as React from 'react'
import ComponentA from '../components/ComponentA'
import ComponentB from '../components/ComponentB'
import * as actions from '../actions'

class BadContainer extends React.Component {
  componentDidMount() {
    this.props.fetch()
  }
  render() {
    return (
      <div>
         <ComponentA
          name={this.props.nameA}
          handler1={this.props.handlerA1}
          handler2={this.props.handlerA2}
        />
        <ComponentB
          name={this.props.nameB}
          handler1={this.props.handlerB1}
          handler2={this.props.handlerB2}
        />
     </div>
    )
  }
}

const mapStateToProps = state => {
  return {
    nameA: state.a.name,
    nameB: state.b.name
  }
}

const mapDispatchToProps = dispatch => {
  return {
    handlerA1() {
      dispatch(actions.actionA1())
    },
    handlerA2() {
      dispatch(actions.actionA2())
    },
    handlerB1() {
      dispatch(actions.actionA1())
    },
    handlerB2() {
      dispatch(actions.actionA2())
    },
    fetch() {
      dispatch(actions.fetchData())
    }
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(BadContainer)

この例では一つのContainerで2つのコンポーネント、ComponentAとComponentBを表示しようとしています。 今回の例では2つだけですが、今後3つ、4つと表示するコンポーネントが増えていくとその分必要なハンドラーやプロパティが増えていくため、 mapStateToPropsmapDispatchToProps が肥大化してしまい見通しが悪くなります。

分割されている例

// GoodContainer.jsx
import { connect } from 'react-redux'
import * as React from 'react'
import ContainerA from './ContainerA'
import ContainerB from './ContainerB'
import * as actions from '../actions'

class GoodContainer extends React.Component {
  componentDidMount() {
    this.props.fetch()
  }
  render() {
    return (
      <div>
        <ContainerA />
        <ContainerB />
      </div>
    )
  }
}

const mapDispatchToProps = dispatch => {
  return {
    fetch() {
      dispatch(actions.fetchData())
    }
  }
}

export default connect(()=> { return {} }, mapDispatchToProps)(GoodContainer)

// ContainerA.jsx

import { connect } from 'react-redux'
import * as React from 'react'
import ComponentA from '../components/ComponentA'
import * as actions from '../actions'

const mapStateToProps = state => {
  return {
    name: state.a.name,
  }
}

const mapDispatchToProps = dispatch => {
  return {
    handler1() {
      dispatch(actions.actionA1())
    },
    handler2() {
      dispatch(actions.actionA2())
    },
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(ComponentA)

// ContainerB.jsx

import { connect } from 'react-redux'
import * as React from 'react'
import ComponentB from '../components/ComponentB'
import * as actions from '../actions'

const mapStateToProps = state => {
  return {
    name: state.b.name,
  }
}

const mapDispatchToProps = dispatch => {
  return {
    handler1() {
      dispatch(actions.actionB1())
    },
    handler2() {
      dispatch(actions.actionB2())
    },
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(ComponentB)

分割されている例では、もともと componentDidMount でデータフェッチしていた箇所だけを元のContainerに残し、 ComponentAComponentB に関するプロパティとハンドラーをそれぞれ ContainerAContainerB に分割しています。 こうすることでコードの肥大化を防ぎ見通しがかなり良くなったと思います。 ただし、全てのコンポーネントconnected にするとやりすぎなので適切な単位で分割する必要がありますのでご注意ください。 例えばECサイトの場合、商品名商品コメント配送方法、のようなおおざっぱな単位で分割していき、それでもコードの見通しが悪いと感じたら更に分割するというような感じで実装しています。

またContainer Componentsのテストの仕方については以前書いた記事がありますので読んで頂けると幸いです。 https://qiita.com/hiyamamoto/items/281709cc2a98268fb6c2

Presentational Componentsの分割

前項に続きコンポーネントの分割についてです。 Presentational Components についての詳細はReduxの公式Docをご確認ください。

Presentational Components を分割する理由としては前項と同様に見通しを良くするため、テストのしやすさのためです。 また、Reactのメリットにも書いてある再利用性を高めるという利点もあります。

まずは分割されていない例を見ていきましょう。

適切に分割されていない例

function Page(props) {
  return (
    <div>
      <div>
        <h1>{props.headerTitle}</h1>
      </div>>
      <div>
        <button>進む</button>
        <button>戻る</button>
        {props.value}
      </div>
      <div>
        <a href="/path/1">Link1</a>
        <a href="/path/2">Link2</a>
      </div>
    </div>
  )
}

こちらの例では複数の divbuttona 要素が出てきています。 そんなに大きなコンポーネントではありませんが既にちょっと見通しが悪くなっていると思います。

また、このコンポーネントのテストは以下になります。 ※ コンポーネントのテストは enzyme というライブラリの使用を前提としています

describe('<Page />', () => {
  const wrapper = shallow(<Page />)

  it('render 進むButton', () => {
    // button が複数出てくるのでうまく特定出来ない
    expect(wrapper.find('button').fisrt().contains('進む')).to.be.true
  })
})

このように複数の同一要素がある場合は wrapper.find('button').first() のように表示順を意識してコンポーネントの取得をする必要が出てきてしまい、ただ表示順が変わっただけでテストが壊れてしまいます。

では分割されている例を見てみましょう。

適切に分割されている例

function Page(props) {
  return (
    <Container>
      <Header title={props.headerTitle} />
      <Body>
        <ForwardButton onClick={props.onClickForward} />
        <BackwardButton onClick={props.onClickBackward} />
        <BodyContent value={props.value} />
      </Body>
      <Footer>
        <Link1 />
        <Link2 />
      </Footer>
    </Container>
  )
}

分割されている例では分割されていない例で見られた div などが一切出てきていないため見通しが良くなっているのがわかると思います。 また、それぞれのコンポーネントの意味がコンポーネント名からひと目で分かるようになっています。

このコンポーネントのテストはこちらです。

describe('<Page />', () => {
  const wrapper = shallow(<Page />)

  it('render 進むButton', () => {
    // ForwardButton というコンポーネントがあるかテストするだけで良い
    expect(wrapper.find(ForwardButton)).to.have.length(1)
  })
})

進むボタンコンポーネント化されたため、戻るボタンとの表示順を気にする必要がなくなり、デザインの都合で表示順が変わったとしてもテストが壊れることがなくなりました。

ComponentをStatelessに保つ

Reactの利点として状態をDOMから分離できるということを前述しましたが、コンポーネントからも状態を分離することで本来のコンポーネントが持つViewとしての役割だけに専念させることができます。 Reactの基本として state というもので状態を管理することができますが、その state そのものを持たないコンポーネントStateless Functional Component と呼びます。

React/Reduxアプリケーションでは state は基本的に redux store で管理し、コンポーネントでは state を使わないようにすることで状態管理を一元化することができます。

ただし、ボタンクリック時にモーダルを表示したり、フォーカスしたときに値を加工するだけなど、そのコンポーネント内で完結するような state を持つことはさほど問題にはならず、全ての状態を redux store で管理しなければいけないというわけではないと個人的には考えています。 あるコンポーネントの状態を別のツリーのコンポーネントで参照したい場合や、ドメインロジックに関わる状態は redux store で管理し、コンポーネント内部で完結する状態はそのコンポーネントstate として管理するように柔軟に設計するのがベターです。

Stateless Functional Component の書き方

statelifecycleメソッド を持たない場合は class シンタックスではなく、関数を使います。 コンポーネント名として関数名が使われるため、 arrow function より通常の関数として書くとよいです。

function FooComponent(props)  {
  return (
    <div>
      名前: {props.name}
      年齢: {props.age}
    </div>
  )
}

利点

  • ただの関数なのでテストしやすい
  • stateを持ってないことが一発でわかる

Reducerの分割

Reduxでは combineReducers という関数を使って通常Reducerを分割すると思います。 Reduxの公式DocではReducerを分割する際、コンポーネントレンダリングツリーで分割するのではなくドメインデータごとに分割することを推奨しています。 また、 DomainStateAppStateUIState という3つのStateに分割することが提案されています。

DomainState

例えば、商品や取引などのドメイン特有のstateのことです。 前述したレンダリングツリーごとの分割とドメインデータごとの分割の比較をしてみます。

レンダリングツリーごとの分割

レンダリングツリーごとの分割は簡単に言うと画面ごとの分割ということです。 下記は 新規画面編集画面一覧画面 といった画面ごとに分割している例です。

reducers
 ├── newReducer.js
 ├── editReducer.js
 └── listReducer.js

ドメインデータごとの分割

変わってドメインデータごとの分割では、商品配送方法ブランド などのドメインデータで分割しています。

reducers
 ├── productReducer.js
 ├── shipphingReducer.js
 └── brandReducer.js

AppState

アプリケーション全体の stateドメインデータ用のReducerとは別のReducerとして用意すると、見通しが良くなります。 例えば、データをローディング中かどうかを管理する isLoading などの state はこちらに含めます。

UIState

モーダルの表示状態などのUI特有の state も別のReducerで管理します。 ただし、前述のようにコンポーネントstate として持つことも多いです。

stateはPOJO(Plain Old JavaScript Object)

state は基本的に immutable(不変) object として扱います。 下記のようなコードはNGです。

const defaultState = {
  foo: 'foo',
  bar: 'bar'
}
const reducer(state = defaultState, action) => {
  if (action.type === 'Foo Action') {
    state.foo = action.payload  // fooだけ変更したい
    return state
  }
}

immutable.js を使えばかんたんに不変オブジェクトを生成できます。

import { Map } from 'immutable'
const defaultState = Map({
  foo: 'foo',
  bar: 'bar'
})
const reducer(state = defaultState, action) => {
  if (action.type === 'Foo Action') {
    return state.set('foo', action.payload)  // fooだけ変更したい
  }
}

上記の Map.prototype.set は常に新しい Mapインスタンスを返します。

ただし、 Map は普通のオブジェクトのようにプロパティにアクセスできないので、コンポーネント内でアクセスする際には state.get('foo') のようにする必要があります。 また、APIなどから取得してきたJSONを毎回 Map オブジェクトに変換する必要があるため結構面倒です。

それ、ES2015+でできるよ

const defaultState = {
  foo: 'foo',
  bar: 'bar'
}
const reducer(state = defaultState, action) => {
  if (action.type === 'Foo Action') {
    return {
      ...state,
      foo: action.payload // fooだけ変更できる!
    }
  }
}

state は色々な場所(コンポーネントAPIクライアントなど)でアクセスするので、「immutableオブジェクトになってるか?」といちいち判定するのは面倒です。 常に POJO にしておくことでその手間を減らすことができます。

Reducerからドメインロジックを分離したい場合

ドメインデータ用のReducerにドメインロジックを書いてしまうとコードがどんどん肥大化していってしまうため、下記のようにドメインデータを models ディレクトリ配下に書きたくなると思います。

import MyClass from '../models/MyClass'
const defaultState = new MyClass({
  foo: 'foo',
  bar: 'bar'
})
const reducer(state = defaultState, action) => {
  if (action.type === 'Foo Action') {
    // ドメインロジックはドメインクラスに委譲
    return state.changeFoo(action.payload)
  }
}

このようにしたい場合は models 配下からクラスではなく関数をエクスポートすることで代替できます。

import { changeFoo } from '../models/MyDomainModel'
const defaultState = new MyClass({
  foo: 'foo',
  bar: 'bar'
})
const reducer(state = defaultState, action) => {
  if (action.type === 'Foo Action') {
    // ドメインロジックはドメインモデルの関数に委譲
    return changeFoo(state, action.payload)
  }
}
export const changeFoo = (model, value) => {
  // 複雑な処理
  return {
    ...model,
    foo
  }
}

他にも immutable.jsのRecordを使う方法などがありますが、前述の通り変換の手間などを考えると個人的にはあまりおすすめしません。

FSA(Flux Standard Action)を使う

Flux Standard Action とは

https://github.com/redux-utilities/flux-standard-action

actionの型が実装者によってまちまちになると読みづらいし不便だから標準化しましょうね、という話です。 割と界隈ではデファクトになってる気がします。

非同期処理の成功、失敗ごとに LOAD_SUCCESSLOAD_FAILURE のようにactionを分けるのではなく LOAD_FINISH のように一つにまとめることが出来ます。

{
  type: 'LOAD_FINISH',
  payload: {
    id: 1,
    text: 'Do something.'  
  },
  meta: {
    ... // metadata
  }
}
{
  type: 'LOAD_FINISH',
  payload: new Error(),
  error: true
}

アクションの型が統一されて書く方も読む方も負荷が減るのでおすすめです。

非同期処理

reduxの登場人物(component, reducer, actionCreator)は純粋な関数の集まりなので副作用のある処理を書く場所がありません。 一般的に副作用のある処理は Middleware層 を利用します。

非同期処理をする為のMiddlewareとしては redux-thunkredux-saga が有名です。

redux-thunk vs redux-saga

redux-thunk

Pros

  • APIがかんたんで学習コストがほぼ0
  • サッと導入してサッと書ける

Cons

  • actionCreatorにロジックが入り込む
  • actionCreatorから純粋さがなくなる
  • そのため actionCreatorのテストが複雑になる

redux-saga

Pros

Cons

  • 学習コストが高い
    • Generator関数はとっつきにくい
    • APIが多い

小さめのアプリケーションでは redux-thunk、大きくて複雑なアプリケーションでは redux-saga、 というように使い分けるといいでしょう。

まとめ

どのフレームワークにも言えることですが、見通しがよくメンテしやすいアプリケーションを書くためにはコードを適切な単位で分割することが非常に重要です。 React/Reduxアプリケーションではコンポーネントを分割し再利用性を高めたり、状態を適切に分割することで、それぞれのメリットを最大限に活かせると思います。

Reduxの公式Docでは今回書いたReduxアプリケーションを設計する上での考え方が詳細に書かれていて非常に参考になるのでぜひご一読ください。

株式会社エニグモ 正社員の求人一覧

hrmos.co

SQLでバスケット分析(信頼度・リフト値算出)を実行

はじめに

こんにちは。2018年9月入社でデータ分析担当の@Tawasshyです。 この記事はEnigmo Advent Calendar 2018の3日目です。

弊社ではファッションECサイトであるBUYMAを展開しております。 売り手となるバイヤー(パーソナルショッパー)は世界中に在住しており、多種多様な商品を買い付けして膨大なSKUが存在します。 一方で、買い手側の購買行動も多岐に渡るのも必然となります。 そのような複雑な関係性を分析する状況においても、今回紹介するバスケット分析のような、基本的とも言える分析手法は解釈もしやすく、効果的な手段の一つになり得ると思っております。

バスケット分析について

バスケット分析は何と何の商品が一緒に買われているかに着目する分析方法であり、マーケット・バスケット分析、アソシエーション分析とも呼ばれています。

一般的な話

有名な事例としては、「おむつとビール」が同時に買われやすい、というルールの発見があります。 また、Agrawal氏らが1994年に発表した論文「Fast algorithms for mining association rules」(IBM Almaden Research Center)が、この分析手法が普及したきっかけとなったことが有力な説のようです。

なぜSQLでやるのか

バスケット分析はR言語のarulesというライブラリを使うのが、もっとも手軽な方法であると思いますが、今回はSQLでの実行を紹介します。

弊社ではエンジニアだけではなく、ディレクターやマーケター、カスタマサポートといったビジネスサイドのメンバーもSQLを日常的に叩いており、SQLでの分析の幅を拡げることに貢献できたら良いなと思ったことがSQLでやる理由です

バスケット分析詳細

まず、二つの商品X、Yがあったとします。 商品Xが買われた場合、商品Yという商品を買う確率を扱います。その指標として以下の指標を扱います。

今回算出する指標

  • 信頼度(confidence) 商品Xが買われた場合に、商品Yを買う確率。

  • リフト値 商品Yが買う確率が、商品Xを買った場合に商品Yを買う確率がどれだけ変化したかの倍率。 (信頼度➗商品Yが買われる確率) Xを買ってYを買う確率がどれだけ持ち上がったかと解釈できる。

今回算出しない値

  • 支持値(support) XとYが同時に買われている確率を示します。バスケット分析においてはこれも重要な指標ですが、今回は説明を割愛します。

環境構築について

手元のローカルマシンで簡単に再現できるのでSQLiteでやってみます。 macOSの場合は以下のようにbrewで一発です。なお、Windowsでも手軽にインストールできますが、ここでは割愛します。

$ brew install sqlite3

データの準備について

SQLiteを起動します。カレントディレクトリにbasket.sqlite3というの名のDBが生成され、DBに接続した状態となります。

$ sqlite3 basket.sqlite3

無事に起動できたらこのようにterminalにこのように表示されます。

sqlite>

まず、にテーブルの定義を行います。

create table sales_history(
  item_category char(12),
  user_id char(8)
);

購買履歴を作成します。

insert into sales_history values
  ('トップス','UID100001'),
  ('財布','UID100001'),
  ('財布','UID100002'),
  ('トップス','UID100002'),
  ('','UID100002'),
  ('トップス','UID100002'),
  ('アウター','UID100002'),
  ('アウター','UID100102'),
  ('ワンピース','UID100103'),
  ('','UID100104'),
  ('財布','UID100104'),
  ('','UID100105'),
  ('','UID100106'),
  ('トップス','UID100107'),
  ('トップス','UID100108'),
  ('','UID100109'),
  ('財布','UID100110'),
  ('','UID100111'),
  ('トップス','UID100111'),
  ('','UID100112'),
  ('','UID100112'),
  ('トップス','UID100113'),
  ('財布','UID100113'),
  ('ワンピース','UID100114'),
  ('ワンピース','UID100114')
;

これでデータの準備はできました。

バスケット分析の実行

信頼度の算出

select
    combi_count.item_category,
    combi_count.item_category2,
    cast(combi_count.order_count as real)/item_count.order_count as confidence
from(
  select
      item_category,
      count(distinct user_id) as order_count
  from
    sales_history
  group by
    item_category
  )item_count
inner join(
  select
      X.item_category,
      Y.item_category as item_category2,
      count(distinct X.user_id) as order_count
  from
    sales_history as X
  inner join
    sales_history Y
       on X.user_id = Y.user_id
       and X.item_category  Y.item_category
  group by
    X.item_category,Y.item_category
)combi_count on combi_count.item_category = item_count.item_category
;

以下のように出力されます。 商品X、商品Y、信頼度という順番です。

商品X 商品Y 信頼度
アウター トップス 0.5
アウター 財布 0.5
アウター 0.5
トップス アウター 0.166666666666667
トップス 財布 0.5
トップス 0.333333333333333
財布 アウター 0.2
財布 トップス 0.6
財布 0.4
アウター 0.142857142857143
トップス 0.285714285714286
財布 0.285714285714286

財布を買ったユーザーがトップスを買う信頼度がもっとも高いことが分かります。

リフト値も追加して算出

上のsqlが書ければ、全体でYが買われている確率を算出し、joinして信頼度を割ればリフト値が算出できます。

select
    combi_count.item_category,
    combi_count.item_category2,
    cast(combi_count.order_count as real)/item_count.order_count as confidence,
    cast(combi_count.order_count as real)/item_count.order_count / AllBuyY.order_count as lift
from(
  select
      item_category,
      count(distinct user_id) as order_count
  from
    sales_history
  group by
    item_category
  )item_count
inner join(
  select
      X.item_category,
      Y.item_category as item_category2,
      count(distinct X.user_id) as order_count
  from
    sales_history as X
  inner join
    sales_history Y
       on X.user_id = Y.user_id
       and X.item_category  Y.item_category
  group by
    X.item_category,Y.item_category
)combi_count on combi_count.item_category = item_count.item_category
-- 全体でYが買われている確率を算出してjoinする
inner join
(
  select
      item_category,
      cast(count(distinct user_id) as real) /
      (select count(distinct user_id) from sales_history) as order_count
  from
    sales_history
  group by
    item_category
) AllBuyY on combi_count.item_category2 = AllBuyY.item_category
;

以下のように、出力されます。 商品X、商品Y、信頼度、リフト値を出しております。

商品X 商品Y 信頼度 リフト値
アウター トップス 0.5 1.25
アウター 財布 0.5 1.5
アウター 0.5 1.07142857142857
トップス アウター 0.166666666666667 1.25
トップス 財布 0.5 1.5
トップス 0.333333333333333 0.714285714285714
財布 アウター 0.2 1.5
財布 トップス 0.6 1.5
財布 0.4 0.857142857142857
アウター 0.142857142857143 1.07142857142857
トップス 0.285714285714286 0.714285714285714
財布 0.285714285714286 0.857142857142857

信頼度、リフト値について

ある商品を買う確率としての指標として、どちらが大事というのはケースバイケースとなりますが、 両方が高い値でないとユーザーの傾向を示す効果的な分析にならないことが言えます。

リフト値が高く、信頼度が低い場合

全体で商品X, あるいは商品Y自体を買われている確率が全体で低い場合に起きます。 商品Xが買われることが少ない為、リフト値が高い値を示してもYを買う確率は小さいままになります。 また、商品Y自体を買われている確率が全体で低い場合は、リフト値が掛けかれる元の確率が低いことになる為、Yを買われる確率は低いままということになります。

リフト値が低く、信頼度が高い場合

何もしなくてもYを買われる確率が高く、Xを買っても買わなくても、Yを買う確率の増減が小さい状態を示します。

そもそも購買履歴が少ない場合…

偶然、信頼度、リフトが高く出るということが起こり得るので、Xを買ったユーザーYの購買促進を行うキャンペーンなどの施策を行っても有効に働かない場合があります。

実際にDBに接続して行う場合は

同じテーブルへの問い合わせが散在しているので、PostgreSQL, MySQL, SQLServer等ではwith句は必須であると思います。

SQLでやるのがめんどくさいなと思ったら..

データを抽出する必要がありますが、R言語でarulesというライブラリを使えばもっと手軽にできますし、無料です。

さいごに

弊社ではデータ分析に取り組む一方、データ分析基盤を整え、データ活用の効率化とアクセスビリティを推進しております。 エンジニアはもちろん、データを活用しながら「世界を買える」ビジネスに挑戦してみたいメンバーのジョインをお待ちしております!

やんちゃであれ! ENIGMO7より

参考

https://www.albert2005.co.jp/knowledge/marketing/customer_product_analysis/abc_association

https://codezine.jp/article/detail/10284

AMP仕様の要点まとめ

こんにちは、Enigmo 新卒エンジニアの@sean0628_iです。 Enigmo Advent Calendar 2018 2日目の記事です。

初めに

Accelerated Mobile Pages の略であり、高速でスムーズにWeb ページを表示するためのライブラリー、或いはその仕組みのことです。 通常のHTML に比べて、制限が多く存在します。 今回はAMP 公式のドキュメントの仕様に関する箇所が英語だったので、読解し要点をまとめます。

AMP 制限

  • 特定の要素をマークアップに含める。
  • JavaScript は利用不可。
  • inline CSS のみ利用可。 -> 50KB の制限付き
  • 特定のHTML tag は利用不可。
  • HTML 内、コメント不可。
  • 特定のCSS properties は利用不可。

必要なマークアップ

  • <!doctype html> でHTML を書き始める。
  • <html ⚡> tag を含める。(<html amp> でも代用可能)
  • <head> tag と<body> tag を含める。(通常のHTML では任意)
  • <head> tag 内の一番初めに、<meta charset="utf-8"> を含める。
  • <head> tag 内に、<link rel="canonical" href="$SOME_URL"> を含める。($SOME_URL は通常版のHTML のURL、通常版のHTML が存在しない場合はAMP のURLを入れる。)
  • <head> tag 内に、<meta name="viewport" content="width=device-width"> を含める。minimum-scale=1 および、initial-scale=1 も含めることが勧められる。
  • <head> tag 内に、<script async src="https://cdn.ampproject.org/v0.js"></script> を含める。
  • <head> tag 内に、AMP Boilerplate Code(head > style[amp-boilerplate] と noscript > style[amp-boilerplate])を含める。

HTML tags

tag 適正
script ×(type がapplication/ld+json の場合のみ○)
noscript
base ×
img amp-img にて代用
video amp-video にて代用
audio amp-audio にて代用
iframe amp-iframe にて代用
frame ×
frameset ×
object ×
param ×
applet ×
embed ×
form ○(amp-form extension を含めることで利用可)
input elements 一部(<input[type=image]>、<input[type=button]>、<input[type=password]>、<input[type=file]>)を除いて○<fieldset>、<label> は○
button
style <head> tag 内に、一つのみ追加可。amp-custom attribute を持たせる必要あり。
link microformats.org に、登録されているrel は○
meta http-equiv attribute が特定の値に利用可。詳細: AMP validator specification
a href attribute をjavascript: から始めてはいけない。target を設定する場合、_blank を利用。
svg 殆どのSVG は利用可

Comments

  • HTML 内でコメントは利用できない。

CSS

  • 50,000 bytes の上限を超えてはいけない。
  • !important は利用できない。
  • transition および、animation 関連はopacitytransform properties のみ利用可。

新卒Rubyエンジニアがオススメする実務で役にたった技術書5選

こんにちは、18年新卒エンジニアの@sean0628_iです。 早いもので今年ももう12月、Enigmoにジョインしたのが今年の春なので、気がついたら入社してから半年ほどが経っていますね。。。

さて、12月といえばAdvent Calendar の季節ですねー笑

今年はEnigmoも初めてAdvent Calendar を公開することとなりました。 というのも、新卒の私が「面白そうだなぁー。」、とボソッと呟いたところ、部長殿から「やってみたらー。」、とOKをいただきましたので、 他のメンバーにもご協力をいただき、Enigmo Advent Calendar 2018 を公開することになりました。

記念すべきEnigmo Advent Calendar 2018の1日目は、文系学部から新卒でエンジニアになった私が今までに読んで、実際に業務で役に立った技術書5選をお届けします。

そもそもAdvent Calendarとは?

さっきから、「”Advent Calendar”とか、横文字多用しやがって。」、という方、大変失礼しました、、、ご説明します。

Advent Calendar とは、元々クリスマスまでの日数を数えるためのカレンダーで、日ごとに小さなポケットを設け、そこにお菓子を入れて楽しんだりするものだそうです。

数年前からWeb 業界の企業ではではそれに倣って、クリスマスまでの25日間記事を公開するようになりました。 つまり、我々が意味するAdvent Calendar とは、25日間記事を公開し続けるという言わば苦行のことです。 ただ、エンジニアとして新たな知識の習得、既知の知識の共有は避けられません。 ということで、今回は苦行を断行します。

* なお、苦行ゆえ時々お休みすることもあるかと思いますが、その点はご容赦いただければと思います。

オススメの技術書5選

1. プロを目指す人のためのRuby入門 言語仕様からテスト駆動開発・デバッグ技法まで (Software Design plusシリーズ)

こちらの本は、知人の勧めで入社する前に読みました。当時は本当にRuby初心者、というかプログラミング言語初心者でした。 そんな私でも途中で挫折せず、Rubyを学べたのはこの本のおかげかなぁと。

この本では実際に手を動かして学習することが出来ます。 ひたすら読むだけだと飽きてしまいますし、言語習得のためには実際に手を動かすのが一番かとも思います。 そういった意味でこの本はオススメです。

2. Ruby on Rails 5アプリケーションプログラミング

上記の本でRubyを一通り学んだ後、こちらでRails を学びました。

もし、アプリ作成の一連の流れを知りたいということであれば、Rails tutorial がオススメです。

一方で、 この本では、Rails 組み込みのメソッドや、Rails の仕組みが事細かに紹介されています。 Rails を体系的に学びたいという方や、Rails tutorial やったけどもう少し深くRails を知りたいという方にはこの本がオススメです 。

3. Everyday Rails - RSpecによるRailsテスト入門

この本は入社直後、研修の一環で読みました。

当時は、午前中に本を読み、午後に実務でアウトプットするという形式でした。右も左もわからず、テストを書くのが本当に嫌いでした笑 しかし、この本で基礎を学び、実務でのテストコードの実装を重ねた結果、今ではテストを書くことが苦ではなくなりました。 さらには、テストコードがないソースコードを見ると不安を覚える身体になってしまいました笑 とてもわかりやすいのでオススメです。

4. スッキリわかる SQL 入門 ドリル215問付き! (スッキリシリーズ)

実務では主にRuby on Rails を使用しているのですが、事あるごとにSQLの記述に迫られます。

ちなみに、Enigmoのメンバーは、エンジニアに限らず殆どの人がSQLを扱う事ができます。 もともとマーケティング業界でインターンシップをしていた私には、これがとても衝撃でした。

ともかくも、この本が良かった点はブラウザ上に環境が整備されているので事前の準備が不要で、すぐにSQLを学習できたということです。 実践問題も付いているので、この本一冊でSQLの基本をしっかりと身に付けることができると思います。

5. リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)

プログラミングを進める上で必要な命名規則、コード記述の作法に関して学びました。

正直に話すと、プログラミングを初めて1ヶ月くらいの時にこの本を読みました。 その時は、大した気づきもなく「レビュー高い割に、ふーん。」、くらいな感想しかありませんでした。 しかし、つい最近読み返してみると、当たり前だけど現状できていないことが多く記述されていることに気がつきました。

Enigmoのエンジニアの卓上でもちらほら見かけますし、一読の価値はあると思います。

所感

駆け足になりましたが、新卒の私がオススメする実務で役立った技術書5冊上げてみました。

これまでに読んだ本で他にも紹介したい良書はたくさんあります。オブジェクト指向の話とか、DB設計の話とかたくさん。 しかし、今回は日々Rubyを扱うエンジニアとして、言語系で実務に役立った本に的を絞り紹介してみました。 また機会があれば、他の良書も紹介したいと思います。この辺で、Enigmo Advent Calendar 2018 1日目終わりたいと思います。

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

RubyKaigi 2017 レポート

こんにちは。エンジニアの Steven です。 広島で9月18日から20日まで行われた RubyKaigi 2017 に山本さんとエンジニア二人で行ってきましたので、どうだったのかについてレポートさせていただきます。

RubyKaigi は毎年 Ruby 言語を中心に行われる会議です。 最近は日本を巡って毎回違う都市で行われてて、今回は広島で行われました。 日本語のセッションもあれば、英語のセッションもあって、外国人の参加者が少なくはないイベントです。 Ruby のカンファレンスの中でレベルが一番高いと言われています( Ruby の作者曰く)。

会議は月曜日から水曜日まで行われてて、事前の週末に広島まで行けば、東京に住んでいる私達にとっては、広島で少し観光をする機会でもあって、そうできるように準備していましたが、残念なことにこの週末はちょうど台風がその近くまで通ってきて、観光するどころか、新幹線ではそもそも行けるのかと、逆に心配することになりました。 ですが、会議の3日間自体は天気は極めてよくて、日焼け止めが必要になるのではないかという逆転にもなりました。

1日目

朝起きて、元安川(今調べました)を渡って、会議が開催されてる広島国際会議場まで行ってから、入場券を受けて、会議スタート!

(目をつぶっていてすみません。後から気づきました。)

初日で見てきたトーク

  • 基調講演、Nobuyoshi Nakada
  • Fiber in the 10th year、Koichi Sasada
  • How Close is Ruby 3x3 For Production Web Apps?、Noah Gibbs
  • Gemification for Ruby 2.5/3.0、Shibata Hiroshi
  • How to optimize Ruby internal、Shizuo Fujita
  • I quit my job to write my own language: Goby、Stan Lo
  • Ruby Committers vs the World

今見返してみると、毎日の仕事ですぐに活かせそうなトークは初日はそんなになかったかと思いますが、Ruby のコミュニティーはどう構成されているのか、どんな課題を抱えているのか、何をやっているのかはより具体的に理解できて、他の Rubyist の毎日に触れて勉強になりました。

この日の最後のセッションとして、Ruby のコミッターが壇上に上がって、全員で議論をしながら、聴衆の質問に答えてたというイベントがありましたが、この日の中で一番記憶に残ってるところです。 質問に答えるためにコミッターの皆さんが議論にのめり込んで、質問一つだけで1時間が立ってしまいそうな状況に数回なってしまいましたが、その議論を聞くのは単に面白くて、話題はなんであろうと、専門的な技術力を持ってる人が周りにいれば、それだけで面白くなると感しました。

初日のセッションが終わってから、晩御飯が目的で街に出ててたのですが、なぜか赤色のシャツを着ている人が多くて、最初はよくわかりませんでしたが、なるほど、広島のカープが日本一になった直後でした。 広島市は全体的に盛り上がっていて、東京に戻ってきたのではないかと、ところどころ人混みができてました。

その夜は広島風お好み焼きを食べる機会になったのですが、やはり美味しくて、2日目も3日目ももう一度食べようという衝動に耐える必要が出ました。

2日目

翌日は朝にスターバックスによってから、会議場に戻って、2日目スタート! 2日目で見てきたトーク

  • 基調講演、Yukihiro Matsumoto
  • The Ruby Module Builder Pattern、Chris Salzberg
  • Improve extension API: C++ as better language for extension、Kouhei Sutou
  • Automated Type Contracts Generation for Ruby、Valentin Fondaratov
  • Type Checking Ruby Programs with Annotations、Soutaro Matsumoto
  • Ruby Language Server、Fumiaki Matsushima
  • Write once, run on every boards: portable mruby、Yurie Yamane
  • Lightning Talks

基調講演は今回は Ruby の作者によって行われてて、やはりまつもとさんは本当に実在する人で、自分が普通の人間でも実際に会える方だとわかりました。 基調講演でも少し紹介されましたが、この日の注目は Ruby での型の導入のトークだったかと思います。

最近新しくできた言語の中では静的型付けの言語が多くあって、どんどん人気になってますが、動的型付けである Ruby でも型のいいところを適切な形で活かせるのかという考えがあって、今回はそれについてのトークは2つもありました。 Ruby には型が実際に入ってしまうのかどうかはまだわからないのですが、型のチェックをしてくれるツールを Ruby に入れることでどうなれそうなのか、Ruby の未来を少し見れました。

個人的には Lightning Talks はその日の一番面白いセッションでした。 1時間で5分だけのトークが12あって、何かをゆっくり学ぶという目的より、いろんな情報を短時間で伝えて、面白話を交えながら、聴衆を楽しませるのがそのライトニングトークの一つの印象でした。 ビデオは公開されましたので、技術の話で少し笑いたければ、そのトークをおすすめします。

その日は昼休みに原爆ドームを見に行く時間は少しあって、日本の世界遺産の一つを写真にとどめることができました。 一言でまとめさせていただきますと、感動しました。

その夜の晩御飯は海鮮料理で、少し高めな食事でしたが、広島のもうひとつの名物を食べれてよかったです。

3日目

いよいよの最終日! ホテルからチェックアウトして、会議場に戻って、最終日スタート! この日見てきたトーク

  • Compacting GC in MRI、Aaron Patterson
  • Ruby for Distributed Storage System、Satoshi Tagomori
  • Pattern Matching in RubyYuki Torii
  • Memory Fragmentation and Bloat in Ruby、Nate Berkopec
  • Busting Performance Bottlenecks: Improving Boot Time by 60%、Julian Nadeau
  • How to write synchronization mechanisms for Fiber、Masatoshi Seki
  • Towards Ruby 3x3 performance、Vladimir Makarov

この日は基調講演は特にありませんでしたが、注目はパフォーマンスに関するトークでした。

RubyGC をどう改善できるか、メモリー関連の問題をどう調査・解決できるか、Ruby をどう3倍早くに改善できるか、違う種類のパフォーマンスの問題についていろんなトークがあって、基礎知識としても実践力としても役に立ちました。

この前、Ruby の作者が設定した Ruby 3x3 という目的は、設定されただけで実際にそれを簡単に実現できるのかどうかは個人的には少し疑問に思ってたのですが、最後のトークでそれを実現できそうなプロジェクトを紹介してくださった方が現れて、そんなに遠い未来じゃないと思うようになりました。

面白い演説で聴衆の笑いを集めた閉会の辞の後、お土産を買って、無事に東京に帰りました。 今回会社で広島までこれたのは私達二人だけでしたが、得てきた知識を東京に残った他のエンジニアに共有する予定です。

終わりに

イベントが多い出張でしたが、RubyKaigi に参加できてよかったです。 新しい基礎知識と Ruby コミュニティーに関する知識を積めて、勉強になったカンファレンスでした。

Ruby 言語のカンファレンスに参加するのはこれで初めてでしたが、Ruby に限らずこれからも他に多くあるプログラミング言語を中心にしたカンファレンスに参加したいと思います。 今まで実際に参加してきたのは、大学で開催されたカンファレンスだけであって、それ以外のトークはすべてビデオで見てきましたが、前の私と同じく、まだカンファレンスに出席したことがない方に出席をおすすめします。一度やってみたら、中々やめられなくなってしまうかもしれません。

ヒカ☆ラボでReact導入の話と商品検索改善の話をしてきました

こんにちは。エンジニアの山本です。 5/23に開催された、【 ヒカ☆ラボ 】大規模サービスがリスクをとってまでモダンな開発環境にリプレイスした理由~ここだけの苦労話や手法を交えお話します~ というイベントに参加 & 登壇してまいりましたのでレポートします。

今回のイベントのテーマは レガシー改善 という、長年継続しているサービスでは避けては通れないものでした。

弊社のサービスである BUYMA もローンチから10年以上経ち、溜まりに溜まった技術的負債を日々返済しています。

Reactを導入した話

弊社からは2人登壇させて頂きまして、私、山本は "React導入時の苦労話とこれからについて" と題しまして、ある機能のリプレイスプロジェクトでReactを導入した話をしました。

既にデファクト・スタンダードになったと言えるReactですが、導入当時の一年前は大規模なサービスに導入したという事例もまだ少なく、如何にして非SPAなWebサービスにReact/Reduxをマッチさせるかという点でとても苦労しました。

当時の苦労話を共有することで、少しでもこれから大規模サービスにReactや他のJavaScriptフレームワークを導入しようとしている方々のお役に立てればと思います。

こちらが発表資料です。

商品検索を改善した話

木村の方は、"BUYMAの商品検索システムの改善の取り組み"というテーマでした。バイマの商品検索システムをSolrCloudへリプレースし、耐障害性を向上させた取り組みについてや、レガシーな検索ロジックを、BigQueryなどを駆使しながら数値で検索精度を計測しつつ、新しいロジックへと改善していく取り組みについてお話しました。

発表資料はこちらです。

当日の様子

木村 Image uploaded from iOS (1) 山本 Image uploaded from iOS 質疑応答時 Image uploaded from iOS (2)

まとめ

一緒に登壇させて頂いたJapanTaxiさん、一休さんのお話も非常に共感できるところが多く、また今回は最近開催されたヒカ☆ラボの中でも来場者数は多かったようで、"みんなレガシー改善に苦労しているんだなぁ"としみじみ感じました。 新しい技術がどんどん生まれる中、如何にして技術的負債を返済してくかはこれからますます重要になってくると思います。 みなさんもガンガン知見を共有して共にレガシーと戦っていきましょう。

AMP対応のススメ

エニグモでWEBエンジニアをやっております、大宮です。 今回は、先日英語版BUYMAで行った、AMP対応についてまとめた記事をお届けしたいと思います。

そもそもAMPとは?

Acceralated Mobile Pagesの略です。 その名称が示す通り、モバイル端末で高速なWebページを表示させるためのプロジェクト、またはそのためのフレームワーク(AMP HTML)の事です。 フレームワークGoogleTwitterにより共同開発されています。2016年の2月にローンチされて以降、AMP対応を行っている企業は増え続けています。

Googleからは今のところは明言されていませんが、メインの開発にGoogleが入っているということで、いずれSEOの上位表示に影響してくるのではないかという予測も立てられているようです。 現に、一部の記事ページではスマホで検索するとカルーセルで検索結果の上位に表示されます。

近年は新興国でもスマホ文化が普及する一方、回線速度の遅さが問題視されることもあり、AMPはこうした時代の流れにも対応したものと思われます。 国内を見ても格安SIMや通信容量制限による低速化などにより、WEBページの体感速度の向上はますます重要な要素になってきています。 ページが速く表示されるだけユーザーの満足度は向上するというのは、想像に難しくないでしょう。

実際に見てみましょう

実際に対応された英語版BUYMAのサイトを見てみましょう。

AMP未対応のページ(ベンチマーク計測:11.5秒) AMP対応のページ(ベンチマーク計測:3.5秒) AMP対応のページをキャッシュしたAMP Projectページ(ベンチマーク計測:0.76秒)

ベンチマークは低速な回線で撮ったものです。 体感でも表示速度が一番下のキャッシュ速度が速い事がわかるかと思います。

なお、上記AMPのページはPC版で見ると崩れているように見えますが、CSSをモバイルに最適化した(PC対応のCSSを排除した)結果です。後述する実装でAMPを別URLとした場合、AMPのページがPCからみられることはありませんのでOKとしています。

なぜ速いのか?

AMP対応のHTMLは読み込みが非常に速いです。その理由を見てみましょう。

- AMPでは、非同期のAMP専用JSしか使用出来ない。 - CSSのサイズ制限+外部ファイルに置く事を禁止している - 画像のLazy Load+幅高さ指定の強制でブラウザの負荷を軽減 特に一番大きいのは最後のLazy Loadでしょう。 通常Webブラウザはページの表示時でページ内のすべての画像を読み込もうとしますが、AMPでは表示領域のみ非同期で画像をロードしてきています。 それを可能にしているのが<amp-img />タグです。ampページでは通常の<img />タグは使用できず、すべてこちらに置き換える必要があります。

また、CSSのサイズ制限があるので、リッチなコンテンツの量は増やしづらく、AMPページとしては余分なモジュールを削りおとすことになり、全体として読み込むファイルサイズが軽減されやすいということです。

さらに、これらのHTMLは、AMP-ProjectのCDNサーバーにキャッシュされ、検索結果での表示時にはキャッシュされたページを表示するようになります。 このキャッシュ時に、画像もモバイル用に圧縮をし、ファイルサイズを軽減しています。

さらに検索結果の表示時には、ユーザーが見ている検索結果の部分で、裏側でPreconnect / Prerenderingが走っています。 ユーザーが検索結果からページにアクセスしようとしてタップした時には、すでに裏側でページが読み込まれているため、ユーザー体験としては爆速に感じるということです。

AMPの三大制約

AMPを実装するにあたって、設計の段階で留意すべき制約は3つあります。

- JSが使えない* - CSSは50KBまで - Cookieが使えない

ということ。言い換えれば

  • リッチなページは作れません。
  • ログインユーザーに応じて出しわけ...などの機能は使えません。

ということです。

JSが使えないというのは、自前のJSは一切使用不可ということで、JSでよく使用される機能について、ある程度はAMP-Projectの方で用意されています。 例えば、サイドバー、カルーセル、フォームのバリデーション等。詳細は公式のこちらのページに記載されています。

また、JSに関しては、2017年6月現在、amp-bindという機能が開発中です。 リリースされればJSで自前のスクリプトを書いた時ある程度は同じような挙動を実現できるようになります。 具体的には、イベントトリガーでclassやInnerText, attributesの値を動的に変更するということが可能になります。

が、それでも既存のJSの流用はできないため、amp-bind用に書き直す必要があります。

URL方式の設計

まずは上述のように、できることとできないことをただしく把握する必要があります。 基本的に上の項目を把握されていれば問題はありません。

次に、AMP用に新規URLを作るのか、既存ページをAMP化させるのかを決めます。 モジュールを分けるかわけないかという選択ですね。 可能であれば、メンテナンス性を考えて同一モジュール...つまり今あるページをAMP化し、新規では作らないというアプローチが望ましいです。ただ前述の通り、AMPには制限がありますので、オリジナルページを残したい場合もあるかと思います。 その辺りはページのボリュームと、JS書き直しの工数(既存機能への影響)を加味して判断する必要があります。

ちなみに新規でページを作る場合には、最初からAMPに対応したものと作成してしまえば、既存ページへの差し替えで悩むことはなくなります。

画面の設計

基本的にはオリジナルページから何を残して何を削るのかを判断する必要があります。 オリジナルページをそのままAMP化しても、CSSの容量超え+JSのリッチコンテンツで実現可能なものと不可能なものが出ると思います。 なのでAMPページの要件として絶対に必須なものを厳選して残し、CSS容量や工数に余裕があれば別の機能の対応も行うというアプローチが良いでしょう。 実際に開発してみてできること、できないことが分かるという事もあります。

なお、前述の弊社サイト(商品詳細ページ)では、商品情報+カート購入機能だけを残して、グローバルナビや検索バーといった要素は排除しています。これらの要素はAMPでも代替は可能ではありましたが、CSSの容量制限の関係で断念した要素でした。

実際にAMPページにしてみる

AMP専用の雛形がありますので、ご自身のサイトにあててみましょう。 bodyタグやheadタグやCSSは適宜調整していただければ良いかと思います。

これで、サイトはAMPとして扱われます。

なおAMP用に新規でURLを作る場合には、オリジナルページのHTMLのheadタグ内に下記を記述しましょう。 <link href="AMPページのURL(フルパス)" rel="amphtml"> これでGoogleにもAMPページが認識されます。

ただし、エラーが出る(多分)

Chromeの開発者モードを開きURLの末尾に#development=1とつけましょう。雛形をそのまま使っていない限り、このように大量のエラーとなるはずです。

f:id:enigmo777:20200415195952p:plain

弊社サイトの対応途中で怒られていた内容としては、主に下記のような内容です。

  • img -> amp-imgに変換+幅・高さ指定してください
  • scriptタグの使用してる所を全部削除してください
  • a href="javascript:void(0);"など
  • cssをインラインで読み込みしてください
  • cssのファイルサイズを削減してください

このエラーを解消しないと、GoogleからAMPページとして認識されませんので、すべて対応しなければいけません。 根気よく対応していけば、いずれは下記のように「AMP validation successful.」となりますので、頑張って対応しましょう。

WEBアプリケーションの場合はVIEWにIF文があることも多々あるので、当然ながらその条件分岐は全パターンみておく必要があります。

晴れてエラーがでなくなれば、本番環境にデプロイして、完了となります。 この時本番環境でしか動いてない監視用のJSなどがあると、またエラーになってしまいますので、注意深く観察しましょう。

終わりに

AMPはそれなりに工数もかかる上に、完成する画面としては真新しいものではありません。 むしろ既存の画面より機能が削ぎ落とされたものなので、パッとみた完成品は物足りなさを感じるかもしれません。

なかなか成果のわかりにくい改修ではありますが、実際にAMP対応によって数字の伸びた事例も挙がってきています。 何より低速回線のユーザーにも、快適な挙動のページをお届けするという意味で重要な要因となりますので、ぜひとも導入を検討していただければと思います。

本記事がその際の一助となれば幸いです。