Zeals TECH BLOG

チャットボットでネットにおもてなし革命を起こす、チャットコマース『Zeals』を開発する株式会社Zealsの技術やエンジニア文化について発信します。

fast_jsonapi gemを1.2から1.5へアップデートしたら躓いた点と対応策

f:id:zeals-engineer:20200124000233p:plain

こんにちは、分析基盤を担当しつつ Rails エンジニアのサポートもやっている鍵本です。

本日は Fast JSON API のバージョンアップをした時に突然テストが落ちて困った話をします。

github.com

  • 背景
  • テストが落ちる状態の再現
  • エラーの原因
  • 対応方法
  • まとめ
  • 最後に

背景

Zealsは、LINEおよびFacebook Messenger上で動作するチャットボットサービスで、WebのForm体験をチャット上で完結させることができます。この Zeals のアーキテクチャは同じくテックブログの記事『進化し続ける「Zeals」サービスを支えるアーキテクチャについて』でご紹介しているとおり、バックエンドを Ruby on Rails と React + Redux (& TypeScript) で実装しています。

tech.zeals.co.jp

Rails は DB から必要なデータを準備し JSON 形式に変換したものをフロント側の React に渡しています。その JSON 形式に変換する部分に Fast JSON API を用いています。アップデート前はバージョン 1.2 を用いておりテストコードも問題なく通っていたのですが、1.5 に gem のアップデートをしたところ突然テストが通らなくなりました。

テストが落ちる状態の再現

サンプルプログラムを使ってテストが落ちた状態を再現してみます。

属性として size, color を持つ Shirt モデルを作成します。

class Shirt < ApplicationRecord
  enum size: { 'XS': 0, 'S': 1, 'M': 2, 'L': 3, 'XL': 4 }
end

Shirt モデルのためのシリアライザを以下のように作成します。

class ShirtSerializer < ActiveModel::Serializer
  include FastJsonapi::ObjectSerializer

  attributes :size, :color
end

Shirt モデルをシリアライズして JSON を返すコントローラを作成します。

class ShirtController < ApplicationController
  def show
    @shirt = Shirt.find(params[:id])

    shirt_json = ShirtSerializer.new(@shirt).serializable_hash[:data][:attributes]

    render json: shirt_json
  end
end

ShirtController#show は {size: サイズ, color: 色} を返すので、例えば size: 'M' となる Shirt を生成して、その値が正しく得られることをテストするコードを用意します。

require 'rails_helper'

RSpec.describe ShirtController, type: :controller do
  describe '#show' do
    let(:shirt) { create(:shirt, size: 'M', color: 'black') }

    subject { JSON.parse(response.body, symbolize_names: true) }

    example 'response の size が M である' do
      get :show, params: { id: shirt.id }

      expect(subject[:size]).to eq shirt.size
    end
  end
end

fast_jsonapi のバージョンを 1.2 としてテストすると問題なく成功します。

$ bundle exec rspec spec/controllers/shirt_controller_spec.rb
.

Finished in 0.02659 seconds (files took 1.39 seconds to load)
1 example, 0 failures

ここで fast_jsonapi のバージョンを 1.5 にアップデートすると以下のような結果を得ます。

$ bundle exec rspec spec/controllers/shirt_controller_spec.rb
F

Failures:

  1) ShirtController#show response の size が M である
     Failure/Error: shirt_json = ShirtSerializer.new(@shirt).serializable_hash[:data][:attributes]

     NoMethodError:
       undefined method `each' for #<Shirt:0x00007f9478a3f318>
     # ./app/controllers/shirt_controller.rb:5:in `show'
     # ./spec/controllers/shirt_controller_spec.rb:10:in `block (3 levels) in <top (required)>'

Finished in 0.01942 seconds (files took 1.43 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/controllers/shirt_controller_spec.rb:9 # ShirtController#show response の size が M である

エラーの原因

fast_jsonapi の serializable_hash メソッドは ActiveRecord::Relation オブジェクト か否かで処理を分けているのですが、その判定方法が 1.2 以前とそれ以降で変わってしまったことが原因でした。具体的には 1.3 以降では以下のような条件で判定しています。

      resource.respond_to?(:size) && !resource.respond_to?(:each_pair)

我々の Shirt モデルには enum で size という属性を持っているため size メソッドが存在します。そのため ActiveRecord::Relation オブジェクトであると間違って判定され、

@shirt.each do |s|
  ...
end

のような処理が実行され、each メソッドが存在しない というエラーが出たということでした。因みに 1.2 以前では以下のような判定だったので問題なかったというわけです。

      resource.respond_to?(:each) && !resource.respond_to?(:each_pair)

対応方法

テーブルスキーマを変更して列名を size から別のものに変更すればいいのですが、影響範囲が大きいためシリアライズする際にオプションを渡すことで対応することとしました。すなわち、

class ShirtController < ApplicationController
  def show
    @shirt = Shirt.find(params[:id])
    options = { is_collection: false }

    shirt_json = ShirtSerializer.new(@shirt, options).serializable_hash[:data][:attributes]

    render json: shirt_json
  end
end

@shirt が ActiveRecord::Relation オブジェクトでないことがわかっているから採用できる方法です。これで先のテストコードが成功することがわかります。

続きを読む

TypeScriptでconditionベースのサブセットタイプを作成する

f:id:zeals-engineer:20191229220455p:plain Photo by Alvaro Reyes on Unsplash

究極の謎を解くためのタイピングシステムの詳細について

こちらの記事は翻訳記事となります。 原著者の許諾を得て翻訳・公開しております。

medium.com

続きを読む

会話分析におけるGraphDBの活用

f:id:newton30000:20191215174040p:plain

こんにちは。ZealsでCTOをしている佐藤です。
Zealsでは AdventCalendarを開催しており、そちらの15日目の記事となります!

  • はじめに
    • 会話分析とは?
    • 何がしたいか
  • Neo4j
    • 技術選定について
    • 環境構築・実行
  • 振り返り
  • さいごに

はじめに

会話分析とは?

私達が開発しているZealsは「ネットにおもてなし革命を!」をコンセプトにした、ユーザーと会話して商品を案内するチャットコマースのサービス です。

チャットコマースについて詳細が知りたい方は、同じくZeals AdventCalenarで公開された以下の記事をご覧ください!

qiita.com

このZealsの会話体験を向上させていく中で、ユーザーとチャットボットとの 会話データ を分析していく必要があります。

現在ではMySQLやBigQueryにデータを格納することで、 ETL分析や機械学習のためのマスターデータ を用意しています。 (機会があればこのテーマも記事にします)

www.ashisuto.co.jp

何がしたいか

f:id:newton30000:20191215173727p:plain

例えば会話構造の一部をサンキーチャートで表すと、上記のように表示されます。

ja.wikipedia.org

KPIの取得や特徴量の把握に関しては運用上の問題は特に発生していないのですが、機能が拡張する中で分析しきれない(あるいはしづらい)領域が出てきてしまいます。

例えば上記のサンキーチャートですが、再帰的な構造の会話には対応することができません。

  • ユーザー毎の会話の分岐を構造化
  • 会話が再帰的な構造に対するクエリ発行
  • ユーザーと会話の関係を表すデータが多様
  • 将来的にテーブルが肥大化することによって性能が悪化する

このような問題に対応するため、 RDB以外でデータ格納や分析を行うアプローチ を検討しました。

続きを読む

pytestのparametrizeの使い方とその有用性について

f:id:RyotaAraki:20191211175222j:plain

※こちらはZeals AdventCalendar 12日目の記事です。

こんにちは!

Pythonエンジニアの荒木です。
夏頃に社内の開発プロジェクトにPythonメンバーとしてアサインされ、本格的にPythonを業務で書くようになり、最近はほぼ毎日Pythonを書いています。

Pythonの業務としては主に新機能実装とDBリファクタリングをやっています。

今日はpytestのparametrizeの基本的な使い方
parametrizeでちょっと詰まったところがあったので、それらについて書きます。

目次は以下のようになります。

  • 前提
  • parametrizeについて
    • parametrize とは
    • parametrize の使い方
  • fixtureをparametrizeする
    • fixtureとは
    • fixtureのparametrize
  • 終わりに

前提

まず、以前にこちらの記事で紹介したように、Zealsは、LINEおよびFacebook Messenger上で動作するチャットボットサービスで、サービスは大きく分けると、

  1. チャットボットの管理用Webアプリケーション
  2. チャットボットへのメッセージ送受信サービス
  3. 1と2から共通処理を切り出したマイクロサービス群

の3つに分かれています。
詳細はこちらの記事をご覧ください。

tech.zeals.co.jp

その中の 2.メッセージ送受信機能フレームワークを採用しないピュアなPythonで実装しており、チャットボットに流入したエンドユーザーに対して、設定されたシナリオの応答や配信処理 を行います。
実際に、10万人を超えるユーザーに対する配信もここで行っています。

このようにZealsにおけるPythonコードは直接エンドユーザーとやりとりするので、誤った挙動をしてしまうとそのまま配信事故につながるリスクが大きいです。
例えば、想定していた配信先と異なる配信先にメッセージを送信して閉まった場合は、そのまま配信事故に繋がることになります。

そのためにこの配信機能の開発はより慎重さが求められ、当然テストコードの重要性も高くなります

parametrizeについて

parametrize とは

Zealsでは、Pythonのテストフレームワークとしてpytestを導入しています。

parametrizeとはpytestで使えるデコレータの1つで変数と結果をパラメータでテストコードに渡すことで、1つのテストメソッドで複数パターンのテストを行うことができます。

parametrizeを使ってテストコードを書くことで
テストコードをDRYにして改修コストを下げることができます。

parametrize の使い方

実際にparametrizeを使ってみます。

import pytest


numeric_datas = [
    (2, 5, 10),
    (3, 6, 18),
    (5, 9, 45)
]


@pytest.mark.parametrize("a, b, expect", numeric_datas)
def test_multiple(a, b, expect):
    assert a*b == expect

与えた2つの変数a, bの積がexpectであるかどうかを判別するテストです。 これを実行してみると、

============================ test session starts =============================
platform darwin -- Python 3.7.4, pytest-5.3.1, py-1.8.0, pluggy-0.13.1
rootdir: /Users/hogehoge/Workspace/myproject
collected 3 items

test_parametrize.py ...                                                [100%]

============================= 3 passed in 0.01s ==============================

となり、いずれの計算結果も正しかったことが分かりました。
このブログのコンソールのUIは白黒ですが、実際はテストが通ると1番下のラインが綺麗な緑色になります。

試しに与えるexpectを誤ったものに変えてみます。

numeric_datas = [
    (2, 5, 8),
    (3, 6, 18),
    (5, 9, 45)
]

この内容でテストを実行すると、

============================ test session starts =============================
platform darwin -- Python 3.7.4, pytest-5.3.1, py-1.8.0, pluggy-0.13.1
rootdir: /Users/hogehoge/Workspace/myproject
collected 3 items

test_parametrize.py F..                                                [100%]

================================== FAILURES ==================================
____________________________ test_multiple[2-5-8] ____________________________

a = 2, b = 5, expect = 8

    @pytest.mark.parametrize("a, b, expect", numeric_datas)
    def test_multiple(a, b, expect):
>       assert a*b == expect
E       assert (2 * 5) == 8

test_parametrize.py:16: AssertionError
======================== 1 failed, 2 passed in 0.05s =========================

このように、どのパラメータを与えた時にテストが落ちたのかが分かります。
あるパラメータでテストが通過しなくても、他のパラメータが別で独立してテストを続行してくれるところはありがたいですね。

parametrizeについてのドキュメントはこちらです。

docs.pytest.org

実際のZealsにおけるparametrizeのユースケースとしては
LINEとFacebook Messengerで共通した処理のテストをLINE, Facebook Messengerそれぞれの変数をパラメータで渡したり、個人情報に関するメッセージの情報の種類(郵便番号や住所など)をパラメータとして渡すことで活用しています。

fixtureをparametrizeする

さて、次はfixtureのparametrizeについてです。

fixtureとは

fixtureとはpytestで使い回すことができる、テスト用のインスタンスを定義しているメソッドのことです。

実際にpytestでテストメソッドを回す際には、fixtureをテストメソッドの引数に与えてテストを回すことが多いです。
引数として与えられたfixtureはテストメソッドが実行される前に必ず実行されるようになっています。

fixtureのparametrize

やりたかったことは「このfixtureをパラメータに渡してテストメソッドを回したい」ということでした。

そのため、↓のようにfixtureを定義してメソッドを実行してみます。

@pytest.fixture
def a():
    return 'A'

@pytest.fixture
def b():
    return 'B'

@pytest.fixture
def c():
    return 'C'


characters = [
    ('a', 'A'),
    ('b', 'B'),
    ('c', 'C')
]


@pytest.mark.parametrize("letter, expect", characters)
def test_capitalize(letter, expect):
    assert letter == expect

実際に実行すると、いずれのテストも失敗します。
テストケース3つともすべて同じ原因で失敗をするのですが、エラーメッセージは以下のように表示されます。

____________________________ test_capitalize[a-A] ____________________________

letter = 'a', expect = 'A'

    @pytest.mark.parametrize("letter, expect", characters)
    def test_capitalize(letter, expect):
>       assert letter == expect
E       AssertionError: assert 'a' == 'A'
E         - a
E         + A

test_parametrize.py:47: AssertionError

letterがfixtureとして認識されていないために、変数 'a' として返されていることが分かります。

そこで、与えた変数をfixtureに変換してあげる必要があります。
その変換のためのメソッドが getfixturevalue() です。

公式ドキュメントは↓です。

docs.pytest.org

これを用いて変数を変換してみましょう。

@pytest.fixture
def a():
    return 'A'

@pytest.fixture
def b():
    return 'B'

@pytest.fixture
def c():
    return 'C'


@pytest.fixture
def get_fixture_values(request):
    def _get_fixture(fixture):
        return request.getfixturevalue(fixture)
    return _get_fixture


characters = [
    ('a', 'A'),
    ('b', 'B'),
    ('c', 'C')
]


@pytest.mark.parametrize("letter, expect", characters)
def test_capitalize(letter, expect, get_fixture_values):
    letter = get_fixture_values(letter)
    assert letter == expect

これを実行すると、

============================ test session starts =============================
platform darwin -- Python 3.7.4, pytest-5.3.1, py-1.8.0, pluggy-0.13.1
rootdir: /Users/mbp028/Workspace/myproject
collected 3 items

test_parametrize.py ...                                                [100%]

============================= 3 passed in 0.02s ==============================

テストが問題なく通過しました。
ちなみに、Zealsではこのメソッドをpluginで実装しており、Pythonアプリケーション内の様々なファイルから呼び出すことができるように工夫しています。

終わりに

Pythonは日本語の情報が少なく、ましてやpytestやparametrizeと
より細かい領域の話であれば、情報を集めるのになおさらハードルが上がってしまうと感じます。

続きを読む

Mozilla Developer Roadshow 2019 - A short report!

f:id:aburd:20191118171719j:plain

I recently attended the 2019 Developer Roadshow Asia: Tokyo. I am a huge fan of Mozilla's work, not just simply for their Firefox browser, but for their efforts in trying to make the internet accessible to everyone. So when a friend told me that this conference was happening, I knew I had to go!

Obligatory, but here's some new Mozilla swag they were handing out at the door.

It was a relatively short affair, but the presentations were quite fun. Without further ado, here are three summations of my favorite presentations at the event.

1. Karl Dubost and Daisuke Akatsuka - Best Viewed With…Let’s Talk about WebCompatibility

f:id:aburd:20191118171140j:plain

I think there is this tendency to think that our websites work more or less. Short deadlines, ignorance, or just plain laziness can be a leading cause of our pages breaking on a multitude of devices. Karl Dubost and Daisuke Akatsuka to the rescue with some tips and tools to make your websites compatible with an ever complex web.

Karl works with Mozilla's WebCompatibility to fix broken sites, not only when displaying in Firefox, but to increase cross-browser compatibility among all major vendors. If you need a broken site, you can report it to Karl!

Daisuke Akatsuka went over how you can diagnose your own problems by using Firefox's dev-tools, especially surrounding CSS issues. Everybody knows you can change CSS rules in devtools, but did you know that Firefox will also give you hints when you write a busted CSS-rule? How about the ability to change fonts on your page in real time?

f:id:aburd:20191118173538p:plain

2. Hui Jing Chen - Making CSS from Good to Great: The Power of Subgrid

f:id:aburd:20191118171146j:plain

Ok, do you remember the days of float and clearfix? Thank goodness CSS has matured a bit since then. Yes, we have flexbox, but what about grid? Have you tried yet? I know I haven't given it the time it probably deserves...and Hui Jing Chen confirmed all my worst fears. I'm missing out!

Hui Jing Chen explained that CSS-Grid allows you to define that size of content in your layouts from the parent's point of view, rather than the child's point of view. This is an incredibly powerful tool and if you've ever wanted to go deep on flexbox, you can checkout her speaking yourself on youtube! www.youtube.com

3. Kathy Giori - Mozilla WebThings: Manage Your Own Private Smart Home Using FOSS and Web Standards

f:id:aburd:20191118171133j:plain

By far my favorite talk was by Kathy Giori, who turned on a bunch of stuff in her California office remotely from the presentation floor in Tokyo!

It's one thing to make stuff on your computer screen move around, but programming an IoT device to make ambulance sounds or a multi-colored light ball to spin around, all in a totally visual programming language? Nerds of the world unite! I am of course talk about the Mozilla IoT - WebThings Framework, which offers developers a way to interface with all your IoT devices so that you can focus on the fun part about IoT, making physical stuff do stuff.

Kathy was even handing out sim cards for FREE to everyone after the show that allowed you to get up and running raspberry pi (and I snagged one, heh). I was lucky enough to chat with her a bit and she let me know what actually, it's not as hard to adapt existing devices for IoT as I thought.

You can watch Kathy Giori talk about Mozilla IoT on youtube: www.youtube.com

続きを読む

Should you convert your React App from Javascript to Typescript?[React開発の言語をJavaScriptからTypeScriptに移行する理由]

f:id:zeals-engineer:20191123153958p:plain

(※画像はGeekbotさんの記事より引用)

ベトナム出身のフルスタックエンジニアNguyen(グエン)さんが、ReactにおけるJavaScriptとTypeScriptについての記事を書いてくれました!

Zealsでは、フロントエンド開発にReactフレームワークを導入しています。React環境には他にもJestなどのテスト環境や、TypeScrptの導入なども先行して行っています。

tech.zeals.co.jp

しかしTypeScriptについては、これまでは主にJavaScriptでフロントエンドの開発を進めてきたため、現在は両方の開発言語が混在している状態にあります。

そのような環境で、実際にフロントエンド開発に携わるエンジニアがどう考えて実装を進めているかについて、Nguyuenさんが記事を書いてくれました。ぜひ色んな方に読んでいただきたいので、後半では文章を日本語訳して書いております。英語が苦手な方はそちらをお読み頂けると嬉しいです!

それでは本編、お楽しみください。

  • Should you convert your React App from Javascript to Typescript?
  • Why we decided to integrate Typescript
    • Potential Issues
  • Let's convert some existing components from JS to TS and see the magic
    • Note
    • Note
  • Conclusion
  • References
  • [日本語訳]なぜReactをJSからTypeScriptに変換すべきなのか?
  • Typescriptを既存のReactに統合した理由
    • 潜在的な問題
  • 既存のJSコンポーネントをTSに変換する方法
    • 注意
    • 注意
  • 結論
  • リファレンス
  • さいごに(訳者あとがき)

Should you convert your React App from Javascript to Typescript?

Hi everyone, my name is Nguyen and I'm a front end engineer at ZEALS. Before, I only used Javascript and React to develop. But since I've learned Typescript and have enjoyed writing in TypeScript for more than a few months, now I feel I will struggle without it! Currently Angular has wonderful Typescript support, but how about Typescript with React? What are the advantages and disadvantages of doing so? I would like to share my throughts on why should we chose to integrate Typescript in our large React.

Why we decided to integrate Typescript

  1. Our team is getting bigger and there are many developers are working together. We think Typescript will help new developers understand our workflow and data structures used throughout the project.
  2. The front-end team wants to decrease small bugs while coding.
  3. We think new developers will find it slightly easier to maintain the existing codebase.
  4. We also think that the code will be easier to refactor in the future.

Potential Issues

  • It takes some time to get used to Typescript.

Let's convert some existing components from JS to TS and see the magic

Note

  • The following example is React <~ 16.8.0

I have a sample React component made in Javascript. This component has props, state and fetches data through an API.

  • The referencing repository are here

https://github.com/knguyen30111/diary

// This is mock API to fetch data from the Database
const todos = [
  {
    id: 1,
    text: "Learn React",
    done: true,
    priority: 1
  },
  {
    id: 2,
    text: "Learn JSX",
    done: false,
    priority: 3
  }
];


export function getTodoList() {
  return Promise.resolve(todos);
}

export function getTodo(id) {
  const todo = todos.find(todo => todo.id === parseInt(id));
  return Promise.resolve(todo);
}
import React, { Component } from 'react'
import PropTypes from 'prop-types
// mock fetch API
import { getTodo } from './mock-api'

class TodoDetailContainer extends Component {
  state = {
    id: null,
    text: '',
    done: null,
    priority: null,
  }

  componentDidMount() {
    const { match } = this.props
    // This is mock fetch API
    getTodo(match.params.id)
      .then(todo => this.setState({ ...todo }))
      .catch(error => console.error(error))
  }

  render() {
    return (
      <div>
        <pre>{JSON.stringify(this.state, null, 2)}</pre>
      </div>
    )
  }
}

TodoDetailContainer.propTypes = {
  match: PropTypes.object
}

export default TodoDetailContainer

When you look at both of the files above, it has some potential problems:

     1. The writer of this component was not clear about the types in the component
     2. I have to guess what the types of the state will be
     3. It's not clear what the type of the API call should return

I applied Typescript to both of files above:

const todos: Array<TodoData> = [
  {
    id: 1,
    text: "Learn React",
    done: true,
    priority: 1
  },
  {
    id: 2,
    text: "Learn JSX",
    done: false,
    priority: 3
  }
];

//This is one object with datatype in array response must have id, text, priority and optional is done
export interface TodoData {
  id: number,
  text: string,
  done: boolean,
  priority: number,
}

//This is mock API to response Promise with Array TodoData
export function getTodoList(): Promise<Array<TodoData>> {
  return Promise.resolve(todos);
}

//This is a mock API that responds with todoData
export function getTodo(id: string): Promise<TodoData> {
  const todo = todos.find(todo => todo.id === Number(id));
  return Promise.resolve(todo);
}
import React, { Component } from 'react'
import { TodoData, getTodo } from './mock-api'


//Define Props dataType
interface Props {
  match: {
    params: {
      id: string
    }
  }
}

type State = TodoData

class TodoDetailContainer extends Component<Props, State> {
  state = {
    id: 0,
    text: '',
    done: false,
    priority: 0,
  }

  componentDidMount() {
    const { match } = this.props
    // This is mock fetch API
    getTodo(match.params.id)
      // Typescript tells us the result of this function is `Promise<TodoData>`, which ensures that setState is being used correctly
      .then((todo) => this.setState({ ...todo })) 
      .catch((error) => console.error(error))
  }

  render() {
    return (
      <div>
        <pre>{JSON.stringify(this.state, null, 2)}</pre>
      </div>
    )
  }
}

export default TodoDetailContainer

After converting to Typescript, the component with me is totally understandable:

  1. First thing I want to talk about Fetch API:

・Through Typescript, I know getTodo(id: string) needs argument id with datatype is string and response data is Promise<TodoData>. We don't need to worry about if the API requires the ID as a number or string. In addition, we know that we are setting the state of the component correctly with TodoData.
・Frontend and backend members know the request and response data. This kind of information can be maintained through documentation, but maintaining documentation about many APIs can be difficult. But when you see above, It's clear to all members that the component works correctly, even if they haven't read the API functions module.
・Typescript's static analysis will let us know when we've made a mistake. If the API changes, simply update the interface and Typescript will tell you if you're missing something or setting something incorrectly.

  1. The second thing to notice is the component is easier to understand:

・There's no need to use the PropType library! We can simply use Typescript's typing system instead of setting propTypes on the component.
・And we can use the same type of system for the state as well! We were able to define the components State as an alias of TodoData, which means that if someone updates that interface, this component's state will automatically be updated. In addition, the compiler will complain that this component's initial state is not being set correctly if there is a change to that interface and it is lacking something necessary.

If you prefer React Hooks, over class components, I have converted the previous example to a React Hooks version and put it below:

Note

  • The following example is React 16.8.0 above
import React, { useState, useEffect } from 'react'
import { getTodo } from '../mock-api'


const TodoDetailContainer = props => {

  const { match } = props;

  const [todo, setTodo] = useState(null);

  useEffect(() => {
    const runEffect = async () => {
      const id = match.params.id
      try {
        const todo = await getTodo(id)
        setTodo(todo)
      } catch (e) {
         console.error(e)
      }
    }
    runEffect()
  }, [match.params.id, setTodo])

  return (
    <div>
      <pre>{JSON.stringify(todo, null, 2)}</pre>
    </div>
  )
}

export default TodoDetailContainer
import React, { useState, useEffect } from 'react'
import { TodoData, getTodo } from './mock-api'

//Define Props interface
interface Props {
  match: {
    params: {
      id: string
    }
  }
}

type State = TodoData

const TodoDetailContainer: React.FC<Props> = props => {
  const { match } = props;

  const [todo, setTodo] = useState<State>({ id: 0, done: false, priority: 0, text: '' });

  useEffect(() => {
    const runEffect = async () => {
      const id: string = match.params.id
      try {
        const todo = await getTodo(id)
        setTodo(todo)
      } catch (e) {
        console.error(e)
      }
    }
    runEffect()
  }, [match.params.id, setTodo])

  return (
    <div>
      <pre>{JSON.stringify(todo, null, 2)}</pre>
    </div>
  )
}

export default TodoDetailContainer

Conclusion

After converting JS to TS, I found the experience mostly positive:

  • Small bugs are gone while coding (or caught by the compiler).
  • The workflow is clear with every single function or component. The types of inputs and outputs of functions/components are defined and communicated to anybody reading the code.
  • New members can immediately maintain our codebase if we have TypeScript components.
  • In the future, if somebody wants to refactor a component or function. It's easier to do because we know for sure the data inside components and functions. In addition, Typescript will tell us if we've forgot to return something, or carry out an operation that is incompatible with the types.

But that's not to say that it was all positive, there are some potential problems with converting an existing JS codebase to TS:

  • I think if the codebase is small, we don't need to apply Typescript. In fact, when applying Typescript to a small codebase you may find that adding types takes more time than is worth the benefit. However, if you are working in a big team with a large codebase, the extra typing will save you hours of hunting down small bugs and increase understanding among your team members.
  • I don't recommend you convert all existing components to Typescript. It takes time to do this and you may not receive much benefit. But when creating the new components applying TypeScript will pay dividends.

References

itnext.io

dev.to

[日本語訳]なぜReactをJSからTypeScriptに変換すべきなのか?

みなさん、こんにちは!グエンです。ZEALSでフロントエンドエンジニアをやっています。

私はこれまで、Zealsのフロントエンド開発でJavascriptしか使ってきませんでした。しかしTypescriptを勉強してからはTypeScriptばかりを使っていて、もはやTypeScriptなしでは生きられない身体になってしまいました。笑

現在、AngularではTypescriptを十分にサポートしていますが、TypeScriptとReactの組み合わせについてはどうでしょうか?大規模なReactにTypescriptを統合することを選択した理由について、メリットやデメリットなど、私の考えを皆さんに共有したいと思います。

Typescriptを既存のReactに統合した理由

  1. 開発チームが大きくなってきたため。 Typescriptであれば、プロジェクト全体で使用されるワークフローとデータ構造を、新しく入ってきたエンジニアがキャッチアップしやすい 2.フロントエンドチームは、開発中に起こり得るバグを事前に少しでも減らしたい 3.新しく入ってきたエンジニアは、新しいコードベースをイチから作るより既存のコードベースを使う方が簡単なはず 4.TypeScriptコードは将来的にリファクタリングしやすくなる(と考えられる)ため

潜在的な問題

  • Typescriptに慣れるには時間がかかる

既存のJSコンポーネントをTSに変換する方法

注意

  • 以降では、Reactは16.8.0以下のバージョンの利用を前提としています

Javascriptで作成したサンプルのReactコンポーネントがあります。このコンポーネントには、APIを介してStateとFetchデータを保持しています。

  • サンプルリポジトリはこちら

https://github.com/knguyen30111/diary

// This is mock API to fetch data from the Database
const todos = [
  {
    id: 1,
    text: "Learn React",
    done: true,
    priority: 1
  },
  {
    id: 2,
    text: "Learn JSX",
    done: false,
    priority: 3
  }
];


export function getTodoList() {
  return Promise.resolve(todos);
}

export function getTodo(id) {
  const todo = todos.find(todo => todo.id === parseInt(id));
  return Promise.resolve(todo);
}
import React, { Component } from 'react'
import PropTypes from 'prop-types
// mock fetch API
import { getTodo } from './mock-api'

class TodoDetailContainer extends Component {
  state = {
    id: null,
    text: '',
    done: null,
    priority: null,
  }

  componentDidMount() {
    const { match } = this.props
    // This is mock fetch API
    getTodo(match.params.id)
      .then(todo => this.setState({ ...todo }))
      .catch(error => console.error(error))
  }

  render() {
    return (
      <div>
        <pre>{JSON.stringify(this.state, null, 2)}</pre>
      </div>
    )
  }
}

TodoDetailContainer.propTypes = {
  match: PropTypes.object
}

export default TodoDetailContainer

上記のファイルには、いくつか問題があります。

  1. このコンポーネントの作成者は、コンポーネントのTypeを明確に示していない
  2. コールドリーディングの際、StateのTypeを予測しなければならない
  3. APIを叩く時に、どのようなTypeが返ってくることを期待しているかがよくわからない

そこで、上記のファイルにTypescriptを適用してみましょう。

const todos: Array<TodoData> = [
  {
    id: 1,
    text: "Learn React",
    done: true,
    priority: 1
  },
  {
    id: 2,
    text: "Learn JSX",
    done: false,
    priority: 3
  }
];

//This is one object with datatype in array response must have id, text, priority and optional is done
export interface TodoData {
  id: number,
  text: string,
  done: boolean,
  priority: number,
}

//This is mock API to response Promise with Array TodoData
export function getTodoList(): Promise<Array<TodoData>> {
  return Promise.resolve(todos);
}

//This is a mock API that responds with todoData
export function getTodo(id: string): Promise<TodoData> {
  const todo = todos.find(todo => todo.id === Number(id));
  return Promise.resolve(todo);
}
import React, { Component } from 'react'
import { TodoData, getTodo } from './mock-api'


//Define Props dataType
interface Props {
  match: {
    params: {
      id: string
    }
  }
}

type State = TodoData

class TodoDetailContainer extends Component<Props, State> {
  state = {
    id: 0,
    text: '',
    done: false,
    priority: 0,
  }

  componentDidMount() {
    const { match } = this.props
    // This is mock fetch API
    getTodo(match.params.id)
      // Typescript tells us the result of this function is `Promise<TodoData>`, which ensures that setState is being used correctly
      .then((todo) => this.setState({ ...todo })) 
      .catch((error) => console.error(error))
  }

  render() {
    return (
      <div>
        <pre>{JSON.stringify(this.state, null, 2)}</pre>
      </div>
    )
  }
}

export default TodoDetailContainer

Typescriptに変換すれば、コンポーネントの中身を完全に理解することができるのではないでしょうか?

  1. Fetch APIの前提

getTodo(id:string) は引数idがデータ型であり、応答データが Promise <TodoData> である必要があるとわかります。 IDを「数値」または「文字列」として渡す必要があるかどうかを気にしなくてよくなりますね。さらに、 TodoDataを使用してコンポーネントのStateを正しく設定していることがわかります。
・フロントエンドとバックエンドのエンジニアはリクエストとレスポンスのデータの内容を理解できます。この種のノウハウはドキュメントを残すことで共有できますが、ドキュメントをうまく残して運用することは簡単ではありません。ただし、上記を見ると、API関数モジュールを読み取っていなくても、コンポーネントが正しく機能することを読み取れると思います。
・Typescriptの静的解析は、コードのミスを教えてくれます。 APIが変更された場合はインターフェースを更新するだけで、Typescriptが間違っている内容を知らせてくれます。

  1. 2番目に注意することは、コンポーネントが理解しやすいことです。

・「PropType」ライブラリを使用する必要はありません!コンポーネントに「propTypes」を設定する代わりに、Typescriptの型システムを使用できます。
・また、Stateにも同じく型システムを使用できます!コンポーネント StateTodoDataのエイリアスとして定義できました。つまり、誰かがインターフェースを更新すると、このコンポーネントのStateの型が動的に変更されます。さらに、コンパイラはそのインターフェースの変更に間違いがあれば、コンポーネントのStateが正しく設定されていないことを教えてくれます。

クラスコンポーネントよりもReact Hooksを使いたい場合は、前の例をReact Hooksバージョンに変換したものを以下に示します。

注意

  • 以降では、Reactは16.8.0以下のバージョンの利用を前提としています
import React, { useState, useEffect } from 'react'
import { getTodo } from '../mock-api'


const TodoDetailContainer = props => {

  const { match } = props;

  const [todo, setTodo] = useState(null);

  useEffect(() => {
    const runEffect = async () => {
      const id = match.params.id
      try {
        const todo = await getTodo(id)
        setTodo(todo)
      } catch (e) {
         console.error(e)
      }
    }
    runEffect()
  }, [match.params.id, setTodo])

  return (
    <div>
      <pre>{JSON.stringify(todo, null, 2)}</pre>
    </div>
  )
}

export default TodoDetailContainer
import React, { useState, useEffect } from 'react'
import { TodoData, getTodo } from './mock-api'

//Define Props interface
interface Props {
  match: {
    params: {
      id: string
    }
  }
}

type State = TodoData

const TodoDetailContainer: React.FC<Props> = props => {
  const { match } = props;

  const [todo, setTodo] = useState<State>({ id: 0, done: false, priority: 0, text: '' });

  useEffect(() => {
    const runEffect = async () => {
      const id: string = match.params.id
      try {
        const todo = await getTodo(id)
        setTodo(todo)
      } catch (e) {
        console.error(e)
      }
    }
    runEffect()
  }, [match.params.id, setTodo])

  return (
    <div>
      <pre>{JSON.stringify(todo, null, 2)}</pre>
    </div>
  )
}

export default TodoDetailContainer
続きを読む

Zealsの新たな開発拠点『Zeals Garage』はなぜ生まれたのか?

f:id:neuneu39:20191122112053j:plain こんにちは。Zeals開発チームのPMで、今回のオフィス増床の責任者を務めた阿久津です。

去る11月6日、Zealsの新しい開発拠点『Zeals Garage(以下Garage)』がオープンしました。

zeals.co.jp

本日はこちらのGarage増築の背景や中の様子、実際の使い勝手などをエンジニア目線で紹介したいと思います。

  • Garageを作るに至った背景
  • エンジニアが考えるエンジニアのためのオフィス
    • 開発チームの課題洗い出し
    • 開発チームのあるべき姿の議論
      • 全ては熱意の「共有」からはじめ、アウトプットも「共有」する
      • 失敗を恐れず「挑戦」し、集中してやりきる
      • 志を同じくする仲間と共に「腕を磨き」、達成感を味わう
    • テクノロジードリブンでZealsのバリューを実現するためのオフィス
      • あらゆることを共有できる空間
      • 集中かコミュニケーションかを選べる執務環境
      • メンバー同士で学び合うことができる環境
    • 名前に込められた想い
  • エンジニアの想いが形になったオフィス
    • 全体の図面
      • ペアプロ専用スペース
      • 集中できるスペース
      • 広くなった執務デスク
  • 開発メンバーからの声
  • 最後に

Garageを作るに至った背景

2019年9月13日に、Zealsはロゴおよびサービス名の変更を行い、新たな一歩を踏み出しました。

zeals.co.jp

事業の拡大を加速させていく過程で、Zealsが抱えるプロダクトの課題レベルは徐々に高くなっています。例えば大規模なトラフィックを捌いたり、ログをRDB外に切り出していく、といったことが必要になっています。

tech.zeals.co.jp

これらの課題を解決し事業をより前に進めていくために、Zealsの開発チームはより高い技術力とプロフェッショナル精神を持った開発者集団に進化していく必要があります。エンジニアの人数も順調に増えオフィスが手狭になったことと重なり、エンジニアのための開発拠点オープンPJが動き始めました。

エンジニアが考えるエンジニアのためのオフィス

今まで何度かオフィス移転を経験してきたZealsですが、エンジニア主導のオフィス移転はもちろん初めて。

以前からオフィス移転の際にお世話になっているヒトカラメディア様協力の元、Garageの構成やデザインなどの検討を開始しました。

オフィスの方向性を大きく決めることになったトピックスを中心にご紹介します。まずは、「開発チームの在り方」から検討をはじめました。

  • 開発チームの課題洗い出し
  • 開発チームのあるべき姿の洗い出し
  • テクノロジードリブンでZealsのバリューを実現するためのオフィス

開発チームの課題洗い出し

f:id:neuneu39:20191122110350j:plainf:id:neuneu39:20191122110356j:plain

20人弱のエンジニア全員で貸し会議室を3時間貸し切り、開発チームの継続するべき点や改善するべき点、今後の開発チームの理想像についてプレスト行いました。 開発チームのマインドやプロダクトへの思い、開発フローまでいろいろな角度から、改めてZealsの開発チームについて振り返る貴重な時間となりました。

振り返りを通して出てきた課題として、エンジニアが開発に集中できる時間が限られ、コードを書く時間が短くなりがちであるという課題意識が多くのメンバーから出ました。

開発チームのあるべき姿の議論

現状の開発チーム課題が棚卸しできたので、次に「開発チームは今後どのような組織であるべきか?」を改めて考えました。

抽象的な議論になるため様々な意見が出ましたが、Zealsとして大切にしている3つのバリューFROM ZEAL, BET ON PARADOX, UNITED WILLを体現できる組織であることが第一。そして、そこにテクノロジーで事業を引っ張っていくという意志を込めるために、技術が先導する テクノロジードリブン な開発を行える組織を目指すのが良いのではないかという結論に至りました。

f:id:neuneu39:20191122120402p:plain

大まかな方向性が決まったので、開発チームがテクノロジードリブンでバリューを実現できるためには、どういった環境が必要か? ということを改めてチームで議論し、その結果以下の3つに集約しました。

全ては熱意の「共有」からはじめ、アウトプットも「共有」する

失敗を恐れず「挑戦」し、集中してやりきる

志を同じくする仲間と共に「腕を磨き」、達成感を味わう

テクノロジードリブンでZealsのバリューを実現するためのオフィス

理想とするチームの像や環境が絞り込まれましたので、その後はエンジニア全員で、 理想とするオフィスを画像イメージやモノで当てはめることで形にしていきました。

各メンバーが思う理想のオフィスに必要な機能を、実際の写真や言葉で形にしていくこの過程は楽しく、あっという間に時間は過ぎました。

f:id:neuneu39:20191122120525p:plain

この議論を経て、オフィスに必要な機能としては、以下の3つに集約しました。

あらゆることを共有できる空間

集中かコミュニケーションかを選べる執務環境

メンバー同士で学び合うことができる環境

ここからは、数ヶ月のうちに開発チームの理想のオフィスが図面になり、着工となりました。

名前に込められた想い

Zealsの新しい開発拠点「Zeals Garage」という名前には開発チームの想いが込められています。

アップルやグーグルと行った名だたるIT企業も最初はガレージからのスタートでした。そんな事実から、新しい開発拠点を「イノベーションを生み出し、ワクワクするモノづくりが行われる空間」にしていこう、というメンバー達の意気込みが込められています。

blog.btrax.com

エンジニアの想いが形になったオフィス

11/6(水)ついにGarageがオープンしました。エンジニアの視点でGarageを紹介したいと思います。

全体の図面

GarageはMTG、執務室等、一般的なスペースからペアプロや集中専用のスペース等、エンジニアの思いが込められたスペースで構成されています。 簡単にですが、特徴的なスペースやポイントを紹介したいと思います。 f:id:neuneu39:20191124220504p:plain

ペアプロ専用スペース

こちらは、ペアプロ専用のスペースです。

従来はスペースの関係上なかなか行われてこなかったペアプロですが、今では毎日の様に誰かがペアプロを行っていて、日々学びを実感してくれています。

f:id:neuneu39:20191125154552j:plain

集中できるスペース

一人で集中したい時のための空間です。集中スベースの角は、あっという間に人気の仕事場になりました。 f:id:neuneu39:20191125154432j:plain

広くなった執務デスク

一人あたり横幅1400mmの執務デスクです。モニターを2台配置しても十分なスペースが確保できます。 f:id:neuneu39:20191125154520j:plain

開発メンバーからの声

f:id:neuneu39:20191122112454j:plain

エンジニアからは、 開発に集中できる時間が増え、生産性が上がった という声を多数上がりました。

実際に、スクラムを導入しているチームではベロシティが1.5倍近く向上しているチームもあります。休憩時間になると自然と入口のMTGスペースに人が集まり、コミュニケーションが始まる姿を見ると、これから何かが起こりそうというワクワクを感じます。

続きを読む

第3回、Zeals開発合宿に行ってきました!@湯河原

f:id:zeals_kody:20191121213718j:plain

どうも!味わい深いエンジニア kody です!Zealsでは第3回となる開発合宿に行ってきました!

Zealsでは、過去に2度の開発合宿を行っております。

tech.zeals.co.jp

tech.zeals.co.jp

3度目の実施となった今回の合宿はこれまでと異なり、Zealsの社員メンバーだけでなく、普段お世話になっている業務委託や社外の方なども巻き込んで(任意参加)参加してもらう、新しいスタイルで合宿を企画してみました。

このような企画にした意図として、普段リモートのみで稼働してる方や、Slackではよく見かけるけど(出社の頻度が少なく)実際に話したことがない方も混ぜて行うことで、より有意義なコミュニケーションが生まれ結果チーム力が上がるとのではないか、という考えがありました。

それでは、当日の模様をお伝えして参ります。

  • 合宿のしおり(スケジュール)
    • 合宿の会場
  • 開幕!
    • テーマ一覧
  • 開発中の様子
    • 1日目
    • 夜ご飯
    • 2日目
    • 最終プレゼンテーション
  • 反省
    • よかったこと
    • 改善したいこと
  • さいごに

合宿のしおり(スケジュール)

実際の合宿は以下のようなスケジュールで進みました。特に変わったことをするわけではなく、ひたすらに集中して黙々と作業する時間が続きます。笑

f:id:zeals_kody:20191121203418p:plain

合宿の会場

今回お世話になったのは、開発合宿の宿といえば...でおなじみ「おんやど恵」さん。 開発合宿用にプランが用意されており、設備も開発用に整えられているので特に準備せずに気軽に合宿を始められるのでおすすめです!

以下のリンクから予約できるので、開発合宿の開催を検討中の方はぜひ。

www.onyadomegumi.co.jp

(※人数次第では早めに予約しないと取れない可能性があるのでご注意ください)

開幕!

f:id:zeals_kody:20191121212206j:plain

今回の合宿では、各自で自由にテーマを決めて開発を進めていく形を取りました。

参加者の開発テーマ一覧はこちら!機械学習からインフラまで様々なテーマに取り組みました!

テーマ一覧

- Quattro Luncher(”Quattro Lunch”という、社内のシャッフルランチをいい感じに仕分けるアプリケーション)
- PWAを使ったwebサービスの開発
- TwitterAPIとJavaScript(Node.jsやReac)を用いたアプリケーション開発
- Golangの勉強(とりあえずTour of Go)
- CLI化ツールの習得&コードスコア自動スクレイピングのCLIツール化
- KPI管理シート自動更新スクリプト開発
- CI/CDを修正してのDXの向上
- k8sを使ったイベント駆動マイクロサービス作成
- automlでCV予測
- 自然言語処理を用いたbot開発
- React Native with Expo で スマホアプリ作成
- Face detection
- Golang × LINE bot
- Prophetを用いたデータ量増加予測
- Pythonコードに Annotation を追加

開発中の様子

1日目

スタート直後の様子です。諸事情あり、遅れて来る方もいたので、開始時点では10名程度のメンバーが集まっていました。 f:id:zeals_kody:20191121212421j:plain

開発の息抜きに、近くに公園があるので体も動かせます。(これで運動不足も解消👍)

f:id:zeals_kody:20191121212709j:plain

なんと、地方にいるインターン生も遅れて参加してくれました!!(写真左) f:id:zeals_kody:20191121212825j:plain

15時を過ぎ、部屋のチェックイン可能になったので各々好きなところで自由に開発します。

部屋でゴロゴロする人やリラクゼーションスペースを活用する人も・・・ f:id:zeals_kody:20191121222857j:plain

開発できる場所が会議室だけではないので、気分転換できて良いですね。 ほぼ全員集合できたのが、 18:00 を過ぎたあたりでした。

夜ご飯

f:id:zeals_kody:20191121212937j:plain

さて、お楽しみの夜ご飯(お酒)の時間です。笑

夜ご飯(お酒)をほどほどに楽しんだ後、各々自由に過ごします。

f:id:zeals_kody:20191121213025j:plain

夜ご飯の後も黙々と開発する人が予想より多くいて感激しました。 メンバーによっては、かなり夜遅くまで開発をしていた人もいましたね。 f:id:zeals_kody:20191121213222j:plain

2日目

あっという間に、2日目の朝です。

旅館の朝ごはん、量が多くてもなぜだか食べれちゃいますよね(?)。

f:id:zeals_kody:20191121213301j:plain

僕はコーヒー片手に、一人寂しく朝のお散歩を。 自然が隣にある環境は控えめにいって最高でした。

f:id:zeals_kody:20191121222454j:plain

最終プレゼンテーション

15時からは最終プレゼンテーションでした。 各々がこれまで開発した内容を、まとめて発表してもらいました!

f:id:zeals_kody:20191121213559j:plain

各自が熱の入ったプレゼンテーションを実施してくれたので、盛り上がりました!

また、普段業務で使わない技術を使うメンバーも多かったので、知らない・使ったことのない技術について知る良いきっかけとなりました。

反省

よかったこと

  • 普段関わりの少ないメンバーを巻き込むことで、チームの一体感が増した
  • 技術力の高い業務委託のメンバーからの学びがあった
  • 様々な技術領域での発表があったので、ノウハウ共有の機会となった

改善したいこと

  • 人数が増えてきたので、最終発表の時間が伸びてしまう
    • チーム開発にする等の工夫をしていきたい
  • 場所やメンバーによっては、wi-fi の速度が遅くなることがあった

さいごに

開発合宿も3回目の開催となりましたが、改めて技術への投資としてサポートをしてくれるエンジニア以外のメンバーに感謝ですね。今回の合宿で得た経験をプロダクト開発に活かして、さらに事業を加速させていきたいと思いました。

1泊2日で普段とは違った場所で開発に集中できることで良い成長に機会になったのと、普段業務だと関わらないメンバーと顔合わせて時間を過ごすことで結束力も高まったと思います。

ということで、Zealsでは文字通り老若男女様々な個性のあるメンバーが集まり、チーム一丸となって開発していることがご理解いただけたかと思います(伝わってなければすいません)。一緒に日本をぶち上げたい、熱い思いを持っているエンジニアの方お待ちしておりマウス

hrmos.co

続きを読む