超絶手軽に多言語対応を行う事が出来る「shutto翻訳」概要と、Nuxt.jsに導入する上での躓いたところ備忘録

はじめに

お久しぶりです! 普段は広島に住んでいるのですが、色々予定を組んで東京に着いてすぐのところで滑って転けて大腿骨の上らへんを折り入院中です!

フロント領域を主に担当しています。クロスマート株式会社のナイスガイ福留(@fal_engineer)です。

今回は弊社プロダクトにも最近導入する運びとなりました、shutto翻訳についての記事となります。

以下から弊社のエンジニアチームによって投稿している記事一覧が閲覧可能です。

xmart-techblog.hatenablog.com

ぜひ読者になるもクリックいただけると嬉しいです!

書くこと

  • shutto翻訳の概要
  • Nuxt.jsにshutto翻訳を導入する上で詰まったこと、原因と対策

書かないこと

  • Nuxt.js、vue.jsの基本的な書き方、構造

shutto翻訳とは?

shutto翻訳|さぶみっと!|株式会社イー・エージェンシー

shutto翻訳とは株式会社イー・エージェンシー によってリリースされた、JavaScript導入タグを埋め込むだけで導入可能なUI翻訳ツールです。

管理画面よりURLを登録し、設定を行うことで、サービスにアクセスしたユーザーに対しAIによって自動的に翻訳されたページを提供する事が可能となります。

またユーザーは、ページにデフォルトで表示される言語切り替えバーによって、英語・韓国語・タイ語・中国語を始めとした様々な言語への翻訳も可能です。

他に提供されている機能として、辞書登録によって意図に沿っていない翻訳がされている箇所を修正・カスタマイズ可能な「翻訳機能」、翻訳をshutto管理画面上でプロの翻訳者に依頼出来る「プロ翻訳機能」があります。

ランタイムを読み込むよう一行追加するだけでサービスを多言語対応出来る手軽さと、カスタマイズ性の高さ、管理画面の見やすさがとても魅力的なツールです。

導入方法

メールアドレスによる無料のアカウント登録を行い、管理画面より翻訳したいサービスのURLを登録します。

サイト設定画面より表示可能な導入タグをコピーし、

サービスのHTMLに貼り付けます。

  <head>
    ...
    <script src="https://d.shutto-translation.com/trans.js?id=xxxxxx"></script>
    ...
  </head>

その後、言語切り替えバーの表示設定、どの言語への翻訳を有効とするか、等の設定を必要に応じて変更することで、導入が完了します。

めっちゃ簡単でビビります

料金形態

※ 2022/11時点でのプラン表です。

www.submit.ne.jp

サイトの自動翻訳

shutto翻訳を導入している場合、管理画面からデフォルトでどの言語を表示するかの設定が可能です。

例えば以下のように設定をしている場合、

  • ブラウザや、本体の言語設定が日本語、英語、韓国語、タイ語、中国語(簡体)、中国語(繁体)の場合は自動的に翻訳して表示する
  • それ以外の場合は日本語で表示する

といった設定となります。

翻訳精度・辞書機能

基本的に翻訳精度が気になることはありませんが、おや?となることは当然有り得ます。

また、特定の文章を文ままではなく、意訳したい場合等です。

その場合は辞書機能を使い、予め設定しておいた文章を表示可能です。

その他

特定の画面に対し、画面を直接書き換えるような感覚で翻訳の辞書設定が可能だったり、

同様に、翻訳時の画像の挿し替えの設定を行う事が可能です。(英語で表示されているときには、英語用のロゴ画像に表示を差し替える、といった設定が可能です)

Nuxt.jsでshutto翻訳を利用する際の注意点

今回Nuxt.jsによって実装されている弊社プロダクト、クロスオーダーに対しshutto翻訳を導入するにあたり、躓く箇所が数点あったのでピックアップして紹介したいと思います。

言語切り替えバーのデザインをカスタムする場合、ページ遷移した時に翻訳リンクが無効になってしまう

現象

言語切り替えバーはカスタマイズが可能です。

www.submit.ne.jp

この通りに独自に言語切り替えバーをコンポーネントとして実装し、layoutファイルに配置しました。

結果、リロード後独自に作成した言語切り替えバーは期待通りの場所に表示されており、リンクをクリックすることで「日本語」⇨「英語」の翻訳が実行されました。

しかし、別の画面にページ遷移したのち、再度リンクをクリックすると、翻訳が機能しませんでした。

activeはクリックした行が有効となっているため、HTMLやCSS、js等、実装した箇所によるエラーではない事が伺えます。

原因

どうやら、shutto翻訳を利用する上で言語切り替えバーをデザインカスタマイズしてHTML上に配置する場合、画面が読み込まれたのちにdata-stt-changelang属性を持っている要素がHTML上から削除された場合動作が無効になる、という仕様があるみたいでした。 (公式ドキュメントからは見つかりませんでした。どこに上記の仕様についての記載があるのか知っている方がいらっしゃいましたらページリンクいただきたいです。)

弊社プロダクトはレンダリングSSRを採用しています。

独自実装の言語切り替えバーを配置したlayoutファイルではは<client-only>タグ内にありましたので、それ以下はCSRによってレンダリングが行われていることになります。

SPAでは画面が切り替わった際に仮想DOMを比較し、差分だけを更新するため、同一のlayout上で以下のみが変化しているはずなのに何故一度それがHTML上から消えていることになっているのか気になりました。

しかしこれは私の根本的な理解不足からなる問題でした。

Nuxt.jsの2系では、nuxt-router, Vue.routerを使ったページ遷移は全てCSRによって行われます。

CSRでは

以下が空のHTMLをキャッシュするため、layoutファイルが同一のページ間を遷移したとしてもlayoutはページ遷移中に一度完全に削除される形になります。

Nuxt.jsの排出するHTMLの構造

<html>
  <head> ... </head>
  <body>
    <div id="__nuxt">
      <div id="__layout">  <-- layoutファイルの内容はここ以下 -->
        ...
          <-- <Nuxt /> が入る -->
        ...
      </div>
    </div>
  </body>
</html>

以上の事柄から、layoutファイルに独自実装した言語切り替えバーを配置したことが原因となっていることが判明しました。

対策

対策としては二つあり、

  • ページ遷移をnuxt-router, Vue.routerでは無くaタグを使用する
  • と同階層に言語切り替えバーを配置する

となります。

弊社プロダクトでは前者はシステムの仕様上制約があるため、後者を採用しました。

Nuxt.jsではapp.htmlをプロジェクトルート(srcDirを採用している場合はsrc直下)に配置することで、Nuxtが排出するHTMLを上書きする事が可能です。

nuxtjs.org

こちらの機能を利用し、全ページ共通で独自実装の言語切り替えバーを有効化させることに成功しました。

プログラムによって制御の文字列が翻訳中に、制御不能となる

現象

<template>
  <div>
    <button @click="buttonClick()"> increment </button>
    {{ num }}
  </div>
</template>
<script>
export default {
  auth: false,
  name: 'Health',
  data() {
    return {
      num: 1,
    }
  },
  methods: {
    buttonClick: () {
      alert('increment click')
      this.num ++
    }
  }
}
</script>

雑ですが、この数値を表示している部分が、翻訳中はうまく動かない、というものです。

原因

こちらも公式ドキュメントから原因と思しき記述を見つける事が出来ませんでした。

しかしこちらについては、num変数をwatchしたところ、ボタンが押下され表示が切り替わっていなくとも、numは加算されている事がわかりました。

  watch: {
    num(n) {
      // console.log(`ちくわ大明神スレ part.${n}`)
    }
  }

よって、shuttoの翻訳APIの結果を描画している箇所の評価タイミングによるものと仮定し、dev toolsからネットワークを監視したところ、翻訳APIがいちいち飛んでいない事がわかりました。

対策

www.submit.ne.jp

公式ドキュメントにあるやり方で、jsによって表示制御される箇所を翻訳除外しました。

  <div>
    <button @click="buttonClick()"> increment </button>
    <p data-stt-ignore> {{ num }} </p>
  </div>

DBデータ構造の改修タスクを通して得た学び

 

はじめに

業務委託で参画している伏田大貴と申します。

クロスマートではバックエンドを主に担当しており、
今回はDBテーブル構造の改修タスクを通して得た学びについてお話します。

 

DBテーブル構造を変更する開発業務自体は良くあると思いますが、

クロスマートでは、スタートアップというフェーズであることもあり、

大規模なデータ構造変更が求められる機会が多いと感じています。

 

クロスマートにjoinしてから、データ構造改修系の開発案件を何件か担当させていただいたので、そこで得た学びを共有します。

前提

  • 当時の筆者のステータス
    • 大規模なDBデータ構造変更の開発業務経験なし
    • 業務でがっつりdjangoを触るのは初
    • toCwebサービス開発経験が約4年。バックエンドがメイン。フロントも少しいじれる。
  • このレベル感の筆者による主観的な内容になります。
  • 今回お話しするデータ構造改修タスクについては、設計はメインではなく、実態としては大規模リファクタタスクに近いです。

携わったデータ構造改修タスクの例

クロスマートでは、卸と飲食店の受発注のサービスを提供しており、

代表的なマスタテーブルとしては、卸、取引先(飲食店)、 商品テーブルなどがあります。

それらのテーブルについて、例えば以下のようなデータ構造改修を行いました。

 

取引先マスタに関するテーブルのリレーション変更
  • 卸と取引先(飲食店)テーブルのリレーションについて、多:多から1:多に変更
  • 取引先(飲食店)に付随する複数のテーブルについても同様に、リレーション変更や別テーブル移行
 商品テーブルの単位カラムに関するデータの持ち方変更
  • 1レコードに複数の単位が詰め込まれた非正規な形を、レコードごとに一単位ずつ持つように正規化する変更

データ構造改修系タスクの難しさ

影響箇所が多い

商品や取引先といった主要なマスタテーブルを変更するとなると、それらを参照している全機能に影響し得ます。

各機能や実装に関する理解が求められ、把握するだけでも時間がかかります。

 PJが長期間におよぶ

影響箇所が多いため、PJの期間が長めになりがちです。一度始めたら途中で引き返したりやめたりすることはできないので、根気強さが必要になります。

 リリース時に不具合を出してはいけない

顧客の業務に支障がでると致命的であるため、エラーや不具合を出さないように慎重に進める必要があります。

また、不具合が出てしまうと、対応作業の工数もとられるため、開発効率向上という点でも、慎重に進める方が好ましいです。

経験して学んだ進め方のコツ

進め方を定期的に見直す

前提として、作業内容を一度に全て洗い出すのが困難という課題があります。全て洗い出した上で進められると理想ですが、影響箇所が多すぎるため難しいです。

 

そのため、作業内容の洗い出しはある程度で切り上げて、着手可能な所から実装を進めていく流れが現実的と考えています。

この進め方には以下のメリットがあります。

  • 手を動かすことで解像度があがり、副次的に作業内容の把握が進めやすくなる。

  • 作業内容への理解が深まることで、進め方自体をより効率的なものに見直し・改善ができる。

とはいえ、やみくもに手を動かすだけだと、手戻りのリスクが高くなるデメリットもあります。

 

そこで、調査 <-> 実装 を交互に取り組んで、最適な作業の進め方を臨機応変に調整する方法にたどり着きました。

「広く浅く」と「狭く深く」を切り替えながら進めるイメージです。

プロトタイピング的に動くものをざっくり高速に作って、それを元に詳細を詰める進め方も活用できます。

 

このような工夫によって、大規模で長期間要するPJでも、着実に進捗を出しやすくなりました。

ただ、現時点では我流すぎるため、もう少しアジャイル開発について勉強したいと課題感を持っています。

 

漏れなく影響箇所を洗い出す方法

一か所でも漏れるとリリース時に不具合がでるので、漏れを生まないことが重要です。

そのために以下の点を工夫しました。

 

静的解析で辿れない部分が厄介で注意が必要

影響箇所調査で漏れがちなのは、静的解析で辿れず、手動で調査しなければならない箇所です。

例えば、

  • リレーションの逆参照の変数
  • grepしにくいキーワード
    • 一般名詞のような検索結果にノイズが多く含まれるようなキーワード
  • コード外の影響箇所
    • レコードにjson文字列で持っていて、影響を受けてしまう箇所等

のような箇所に注意を払いました。

 

複数レポジトリを横断した調査の効率化

フロントとAPIリポジトリが分かれているのですが、影響箇所調査の際には、横断的に見れると作業がはかどります。

エディタの設定で、複数リポジトリを同時に見れるようにして、効率化しました。

シンプルですがかなり効果的でした。

 

調査方法を記録して残す

後で調査を再現できるようにしておくと、実装後に再度セルフレビューする際の効率が上がります。

また、こうすると漏れている箇所がないか不安に思うことが軽減されるので、自信や集中力の向上にもつながります。

 

段階リリースを活用

まず、段階リリースしやすくするための仕組みとして、フィーチャートグル機能という仕組みを簡単に構築しました。

これにより、影響が大きな変更も対象ユーザーを絞ってリリースできるようになり、リスク軽減できます。

 

また、PRやブランチを細かめに切り、細かく段階的にリリースするといった開発手法を意識しました。

リスキーなPRは他と切り分けてリリースすることで、全体的なリスク軽減につなげられました。

 

コードだけからではよくわからない背景については、詳しい人に必ず相談する

実装上の細かすぎる疑問点については、心理的に相談するのがはばかられる場合もありましたが、遠慮せずに相談することを意識しました。

気軽に相談できる環境に恵まれていたというのもあります。

 

確実にタスクを完了させに行くメンタリティーを養う

精神論になりますが、重要な部分としてメンタル管理があります。

 

圧をかけるぐらいの勢いで積極的に動く

作業の手、コミュニケーションの両面で”圧”をかけに行くようにしました。

ただ、コミュニケーションにおける礼儀は忘れないようにだけは注意ですw

 

フルリモートで業務委託という働き方が逆風にならないようにする

フルリモートで業務委託という自由な働き方をさせていただいていますが、

 

物理的にも心理的にも距離が生まれがちなので、

気を抜くと、仕事の進みが遅れてしまうことが時々おきていました。

 

作業環境を見直したり、意志力を鍛えたり(w)など、

モチベを自己管理することが大切だと感じています。

まとめ

データ構造改修タスクとして、どんな開発業務に取り組んでいるか紹介しました。

また、その難しさと経験して得た学びをまとめて共有しました。

 

業務委託という立場ですがデータ構造改修という大きな仕事にチャレンジできたり、

日々様々な学びが得られる刺激的な環境が魅力的だと感じています。

 

クロスマートでは絶賛エンジニア募集中です。

このお話を読んでちょっとでも気になった方がいたら

「話を聞きに行きたい」を押してみてください!

www.wantedly.com

【Django】第二回:DjangoでN+1を防いで高速化を行うテクニック

バックエンド担当の山田です。

前回の予告通り引き続き処理負荷軽減テクニックを紹介します。

 

xmart-techblog.hatenablog.com

 

前回はN+1の箇所の見つけ方を紹介したので、今回は具体的に問題箇所に対応する方法… select_related, prefetch_relatedについてです。

 

今回は以下のようなデータと構造を持つModelsがある場合を例とします。

# Model構造
from django.db import models

class Company(models.Model):
    name = models.CharField(max_length=255)

class Staff(models.Model):
    first_name = models.CharField(max_length=255)
    last_name = models.CharField(max_length=255)
    company = models.ForeignKey(Company, related_name='staffs', on_delete=models.DO_NOTHING)
# 投入されているデータ
INSERT INTO `company` (`id`, `name`)
VALUES
	(1, 'クロスマート_本店'),
	(2, 'クロスマート_分店'),
	(3, '肉屋');

INSERT INTO `staff` (`id`, `first_name`, `last_name`, `company_id`)
VALUES
	(1, '一郎', '山田', 1),
	(2, '次郎', '山田', 1),
	(3, '三郎', '山田', 1),
	(4, '千代子', '鈴木', 1),
	(5, '花子', '山田', 2),
	(6, '智子', '佐藤', 2)
	(7, '十兵衛', '山田', 3);

このデータから「companyに”クロスマート”を含む」かつ「staffのlast_nameが”山田”と一致」するものを抽出して、「会社名とフルネームを出力する」という処理を行う場合で実装例を交えて説明します。

何も考えずに取ってくる場合

# from your/models/pass import Company, Staff

staffs = Staff.objects.filter(last_name='山田')
for staff in staffs:
    if 'クロスマート' in staff.company.name:
        print(f'所属:{staff.company.name}, 氏名:{staff.last_name}{staff.first_name}')

おそらく最も良くない書き方の1つです。 この書き方ですとループごとにクエリを発行してしまいます。この場合に発行するクエリは以下のようになります。

# 4行目 staffs の行。
SELECT `staff`.`id`,
       `staff`.`first_name`,
       `staff`.`last_name`,
       `staff`.`company_id`
FROM `staff`
WHERE `staff`.`last_name` = '山田'

# 5行目 ループ内で staff.company を利用している行。
# 上記でヒットした分だけクエリが発行されるため、staffが「山田」のレコードの数だけ同じクエリを実行します。
SELECT `company`.`id`,
       `company`.`name`
FROM `company`
WHERE `company`.`id` = 1

この例の場合、全体で6回クエリが実行されます。

親のテーブルを最初に検索してから子を探す場合

# from your/models/pass import Company, Staff

companies = Company.objects.filter(name__contains='クロスマート')
for c in companies:
    for s in c.staffs.all():
        if s.last_name == '山田':
            print(f'所属:{c.name}, 氏名:{s.last_name}{s.first_name}')
# 4行目 companies の行
SELECT `company`.`id`,
       `company`.`name`
FROM `company`
WHERE `company`.`name` LIKE BINARY '%クロスマート%'

# 5行目 ループ内で c.staffs.all() を実行した行。
# 上記でヒットした分だけクエリがループするのでcompany.nameに"クロスマート"を含むレコードの数だけ同じクエリを実行します。
SELECT `staff`.`id`,
       `staff`.`first_name`,
       `staff`.`last_name`,
       `staff`.`company_id`
FROM `staff`
WHERE `staff`.`company_id` = 1

この場合、クエリの実行数は3回になります。 最も悪い例と比較するとほぼ半分になりました。

そしてDjango始めたての人でよくあるのが、

# from your/models/pass import Company, Staff

companies = Company.objects.filter(name__contains='クロスマート', staffs__last_name='山田')
for c in companies:
    for s in c.staffs.all():
        print(f'所属:{c.name}, 氏名:{s.last_name}{s.first_name}')

という書き方です。

パッと見で「companyに”クロスマート”を含む」AND 「staffのlast_nameが”山田”と一致」となっているように見えますが、これは完全にトラップです。 これを実行すると以下のようなクエリを実行します。

# 4行目 companies の行
SELECT `company`.`id`,
       `company`.`name`
FROM `company`
INNER JOIN `staff` ON (`company`.`id` = `staff`.`company_id`)
WHERE (`company`.`name` LIKE BINARY '%クロスマート%'
       AND `staff`.`last_name` = '山田')

# 5行目 ループ内で c.staffs.all() を実行した行。
SELECT `staff`.`id`,
       `staff`.`first_name`,
       `staff`.`last_name`,
       `staff`.`company_id`
FROM `staff`
WHERE `staff`.`company_id` = 1

先程と同じような回数のクエリが発行されると思われますが、2回目移行のクエリで無駄なクエリを連続で実行してしまい、今回の例だと合計5回もクエリを発行します。

「ループに入る前に条件を絞り込んだのにどうして…!?」って考えてしまうのですが、これはあくまでも「1つ目のループを行うための条件における絞っただけ」となっています。

「実はStaffの情報は一切取得していない」ばかりか「下手な絞り込みを行った結果、”山田”以外のスタッフを出力してしまう」という前提条件すら満たしていない状態です。

 

こんなに難しいなら生SQL

SELECT * FROM xxx INNER JOIN xxx

といった生クエリを発行して結果を取得して、それをいい感じに使い倒す。

ということをしたくなりますが、これをDjangoで実施する場合はselect_related, prefetch_relatedを使います。

というわけで本題です。

select_relatedを使う場合

長々やっていますが、結果から言うと最速は select_related が使えるならそれを使うのが最速かつ最も発行回数を少なく出来ます。

# from your/models/pass import Company, Staff

staffs = Staff.objects.select_related('company').filter(last_name='山田')
for staff in staffs:
    if 'クロスマート' in staff.company.name:
        print(f'所属:{staff.company.name}, 氏名:{staff.last_name}{staff.first_name}')

このように記載すると…

# 4行目 staffs の行で1回クエリが実行される。
SELECT `staff`.`id`,
       `staff`.`first_name`,
       `staff`.`last_name`,
       `staff`.`company_id`,
       `company`.`id`,
       `company`.`name`
FROM `staff`
INNER JOIN `company` ON (`staff`.`company_id` = `company`.`id`)
WHERE `staff`.`last_name` = '山田'

なんとわずか1回のクエリ発行しか行いません\(・ω・)/イェーイ

似たような形で親から子を参照する場合にはprefetch_related を用います。 これはprefetchするテーブルの数だけクエリが発行されてしまうので、使った時点で2回以上のクエリ実行が確約されますが、prefetch対象のデータはキャッシュされるため、上手くやればクエリの実行回数をprefetchした回数までに抑えることが出来ます。

prefetch_relatedを使う場合

# from your/models/pass import Company, Staff
from django.db.models import Prefetch

companies = Company.objects.prefetch_related(Prefetch('staffs', queryset=Staff.objects.filter(last_name='山田'))).filter(name__contains='クロスマート')
for c in companies:
    for s in c.staffs.all():
        if s.last_name == '山田':
            print(f'所属:{c.name}, 氏名:{s.last_name}{s.first_name}')
# 5行目 companies で2回のクエリが実行される。
SELECT `company`.`id`,
       `company`.`name`
FROM `company`
WHERE `company`.`name` LIKE BINARY '%クロスマート%';

SELECT `staff`.`id`,
       `staff`.`first_name`,
       `staff`.`last_name`,
       `staff`.`company_id`
FROM `staff`
WHERE (`staff`.`last_name` = '山田'
       AND `staff`.`company_id` IN (1,
                                    2))

と行った具合に、今回の実行回数は2回となりました。

Djangoを使っていてN+1問題に悩まされている方、まずはselect_relatedprefetch_related を使って見てはいかがでしょうか?

 

最後に

クロスマートでは絶賛エンジニア(バックエンド・フロントエンド)募集中です。

このお話を読んでちょっとでも気になった方がいたら

「話を聞きに行きたい」を押してみてください!

www.wantedly.com

FastAPI x Pydantic による入出力定義とValidation

クロスマート バックエンドエンジニアの武(タケ)(@pouhiroshi)です。

前回は5分で作れる!FastAPI入門編 ということで、簡単なFastAPIのご紹介をしました。

今回はWebフレームワークには欠かせない「入出力定義とValidation」をFastAPIではどのようにやるかをご紹介します。

FastAPIでこれらを実装する際に推奨されているライブラリが Pydantic です。
https://pydantic-docs.helpmanual.io/

Pydanticの読み方ですが、パッと検索した感じ見当たらなかったので勝手に「パイダンティック」と読んでいます。
もし正解をご存知の方がいらっしゃいましたら、ぜひ弊社のカジュアル面談にお越しいただき、教えてください・・・

使い方ですが、JavaのBeanValidatonに近い感じです。
フィールドに型ヒントをつければそれに沿ってバリデーションなどをおこなってくれます。

インストール方法

pip install pydantic

基本的な使い方

まず、人間(Person)データをidで1件抽出するエンドポイントを作ってみたいと思います。
出力データの定義はPersonクラスに定義し、APIにてインスタンスを作成して返そうと思います。
response_modelに返すpydanticのクラス型を指定できます。Personインスタンスを返すように定義します。

from fastapi import FastAPI

from person import Person

app = FastAPI()

@app.get("/person/{person_id:int}",
         response_model=Person)
def get_person(person_id: int):
    """
    idによる人間データ取得
    """
    return Person(
        id=person_id,
        name="たけじい",
        job="会社員")

PersonクラスはPydanticのBaseModelを継承して作成します。
Field定義はなくても動作しますが、後ほどバリデーションの定義などに使用します。
Fieldのtitleに説明を書くと、swaggerに出力されますので便利です。

from pydantic import BaseModel, Field


class Person(BaseModel):
    """
    人間クラス
    """
    id: int = Field(title="id")
    """id"""
    name: str = Field(title="名前")
    """名前"""
    job: str | None = Field(title="職業")
    """職業"""

こちらでFastAPIを実行します。

swaggerのAPIレスポンス内 Schemaの欄に先ほど定義したtitleが表示されていますね。わかりやすくなりました。

実際にperson_idに値をいれて実行してみましょう。

Personインスタンスのデータがjsonで取得できましたね。idはintで定義しているので、ちゃんと数字型で表現されているようです。
出力に関してはこのような流れです。

Pydanticでフォームバリデーションを行う

次に、Validationについてやっていきたいと思います。
Personデータを登録するエンドポイントを例にしてやってみたいと思います。

idはAPI側で自動発番するものとして、入力としては要求しません。
なので、idフィールドは定義していません。
nameを必須とし、jobは無いことがありえるので任意入力とします。(str | None にあたるOptional表記です)

データ登録用のPersonクラスをPersonCreateクラスとして定義します。

from pydantic import BaseModel, Field


class PersonCreate(BaseModel):
    """
    人間作成クラス
    """
    name: str = Field(title="名前")
    """名前"""
    job: str | None = Field(title="職業")
    """職業"""

登録用エンドポイントは以下のようにします。
引数にPOSTされるPersonCreateのインスタンスを受け取ります。
本来はデータベースに登録して、idはデータベースに発番させますが、仮で999番をいれています。
発番したidをつかって先ほどGET /person/{person_id} で使っていたPersonインスタンスの形で返すようにしています。

@app.post("/person",
          response_model=Person)
def create_person(person_create: PersonCreate):
    return Person(
        id=999,
        name=person_create.name,
        job=person_create.job)


swaggerを見てみましょう。

POST /person が作成されました。Request bodyの Schemaを見ると、赤の*がnameについていて、jobにはついていません。
ちゃんと定義した通り、nameが必須、jobが任意のフィールドになりました。

では実際に動かしてみましょう。
まずはサッカー選手のたけじいを登録してみます。

pycharmのデバッガーで、nameとjobが渡ってきているのを確認できました!

swaggerのレスポンスもちゃんとidが999で出力されましたね!

では、バリデーションの確認をしたいと思います。
nameが必須なので、nameをnullで登録してみます。

実行結果は以下のようになります。

http statusは422 で、response bodyにエラー内容が出力されます。

{
  "detail": [
    {
      "loc": [
        "body",
        "name"
      ],
      "msg": "none is not an allowed value",
      "type": "type_error.none.not_allowed"
    }
  ]
}


必須だけではなく、文字列長の制限もいれてみましょう。jobのほうに最大文字長10を設定してみましょう。

class PersonCreate(BaseModel):
    """
    人間作成クラス
    """
    name: str = Field(title="名前")
    """名前"""
    job: str | None = Field(title="職業", max_length=10)
    """職業"""

Field属性のmax_lengthに最大文字数を定義するだけです。
swaggerを見てみましょう。

maxLength:10 が表示されていますね。

実際に11文字いれてみましょう。

12345678901と11文字いれたら、maxLengthのエラーになりました!!

複雑なvalidationを行うときは
@validator(プロパティ名) をつけて実装します。

ためしに名前が「たけじい」の場合はエラーにしてしまいましょう。値はvalue引数で受け取れます。
エラーにするにはValueError(エラーメッセージ)をraiseし、エラーがなければvalueをそのままreturnします。

from pydantic import BaseModel, Field, validator


class PersonCreate(BaseModel):
    """
    人間作成クラス
    """
    name: str = Field(title="名前")
    """名前"""
    job: str | None = Field(title="職業", max_length=10)
    """職業"""

    @validator("name")
    def validate_name(cls, value):
        if value == "たけじい":
            raise ValueError("たけじいは入力できません")
        return value

これで試してみます。

ちゃんとバリデーションエラーになりました!(ちょっと悲しい・・・・


以上、FastAPIとPydanticを連携して、入出力関連の定義やバリデーションをスマートに実装できることが
実感できたと思います。

今後は、FastAPIのデータベース連携(SQLAlchemy) について記事を書きたいと思っています。

最後に

クロスマートでは絶賛エンジニア(バックエンド・フロントエンド)募集中です。

このお話を読んでちょっとでも気になった方がいたら

「話を聞きに行きたい」を押してみてください!

www.wantedly.com

フルリモート+入社4ヶ月で新卒のメンターに!? やったこと全部

サーバーサイドエンジニアの小久保(@yhei_hei)です。

実は小久保、Radiotalkでクロスマートのことを さんざん喋ってはいるものの、
入社して1年経ってません。

 

入社は2022年の1月で、クロスマート2人目の社員エンジニアでした。

 

そんな中、2022年の4月。
クロスマートは創業以来初の新卒エンジニアを採用しました。

 

この規模感の会社が新卒エンジニアを取るのは、
かなりチャレンジングと言われてるらしく、
XTechグループの方たちがざわついていたのを覚えています。

 

で、のほほんと開発していたある日。

突然PdMの杉原さんからオンラインミーティングに呼ばれ第一声、

 

「小久保さん、新卒のメンターお願いできますか?」

 

と言われました。

 

フルリモートで新卒メンターはやばい

打診を受けた率直な感想としては「やばい」でした。

新卒の子は関東勤務です。しかるに僕は札幌からフルリモート。

物理的な距離すご! って感じでした。

 

当時入社4ヶ月。

ようやく部分的に業務仕様を理解し、このチームの開発の勘所も掴めてきたところ。

このタイミングで新卒メンター。。。しかもフルリモート。。。

 

ああ、とてもスタートアップっぽい。。。と目眩のする思いでしたw

 

本記事の対象読者

本記事ではフルリモートの制約がある中、新卒の方とどうやって関わっていったか。

うまくいったこと、いかなかったことを赤裸々に綴っていきます。

このコロナ禍。フルリモートでメンターをやる同志達の参考になれば幸いです。

 

フルリモートの不利な点

まず、フルリモートの不利な点は下記。

  • 顔が見えないため、人となりが見えにくい
  • 「ちょっといいですか」が気軽にできない。質問のハードルが高い
  • 手元のエディターが見えないため、ペアプロ等がしにくい。画面共有がだるい

これらをいかに解消するかがポイントでした。

 

「ちょっといいですか」を促進する。Slackのハドルで常時接続

まずやったこととしては、質問のハードルをとにかく下げる、ということ。

 

最初は聞きたいことがあったら、
空いてるところにGoogleカレンダーで会議入れてくれ〜みたいな体制でした。
これがまずかった。

 

まず自分が新卒になった気持ちで考えてみましょう。

 

腐っても会議です。先輩との会議です。

そう、準備が必要です。

 

僕が新卒だったら

  • 要点まとめないと怒られるんじゃないか?
  • 最低限自分がわかったこと、わからなかったことをまとめなければ。。。
  • 色々チケット持ってて忙しそうだな。。。ここに会議入れても大丈夫かな。。。

みたいに、ごちゃごちゃ考えちゃうと思います。

会議っていう体をとると大袈裟になっちゃうんですよね。

 

結果、問題を抱え込みがちになることが多かったです。

 

会社であれば、「ちょっといいですか?」で済むことが、
リモートになると途端にハードルが上がっちゃう。

 

この高いハードルをいかに下げるかが課題でした。

 

そこで使ったのがSlackのハドル

ハドルとはいわゆるボイスチャットです。

 

Slackの分報チャンネル(#times_kokubo)で常時ハドルをONにし
質問があれば音声で話しかけて、というルールにしました。

 

時々#kintai_allでハドル を開き、赤っ恥をかいておりました



ハドルであれば、ワンクリックで繋がれ、お手軽です。

画面共有もできるため、ほとんどの悩みを
ハドル起動+画面共有で解決することができました。

 

もちろん、Zoomでも同じことができるのですが、
URLを発行して〜メンションして〜マイク聞こえてますか〜
画面共有開始して〜見えてますか〜?みたいな一連のやりとりが億劫です。

 

その点、ハドルであれば向こうから勝手に入ってきて、
10秒もかからずコミュニケーションが開始できます。

 

オフィスにいるのと同じぐらいの手軽さになりました。

 

最近では新卒の方も、「今いいですか??」って入ってきて、
「ここなんですけど。。。」と、いきなり画面共有をするまでになってくれました。

 

「まだ良いって言ってないよ!?」って思いながら画面共有を受ける小久保。
このぐらい食い気味で来るのがちょうど良い

 

なんでも相談会を週1で実施

人って顔が見えないといろいろ邪推するんですよね。。。

なんかあのテキスト冷たかったな。。。

怒ってる声してる。。。?

メンションしてんのに全然スタンプつかないな。。。無視されてる。。。!?

 

みたいな。

で、実際顔を合わせて話してみると、そんなつもりは一切なくて〜。って。あるあるですよね。

 

率直に言うと最初の方は何を考えているかわからない場面が多々あり、

ストレスを溜めていましたw

 

多分お互いそうだったかもしれません。

 

ということで、コミュニケーションの場を増やすべく、

新卒研修定例という名のなんでも相談会を週1で開催しました。

 

アジェンダ。この他にもいろいろざっくばらんに聞いてた

 

相談会では、新卒研修の報告とともに、今困ってることをざっくばらんに相談する場を設けました。

 

相談事項は本当になんでも良くて、

例えばチケットのここがわからないとか。

migrationが変になっちゃって。。。どうしよう。。。とか。

ビジネスロジックってどこに書けばいいんですか?? などなど。多岐に渡りました。

 

人間顔を合わせて話しているとなんとかなるもので、ここでグッとお互いの距離を縮め、オープンになることができました。

 

また、ここでペアプロを実施することでノウハウの伝授も行え、技術力アップの場としても最高でした。

 

VSCode Live Shareでペアプロの効率促進

前述しましたが、頻繁にペアプロを行うようにしています。

フルリモートでペアプロって。。。大変そう。。。と思われるかもしれませんが、

逆です。めちゃくちゃやりやすいです。

 

秘訣はVSCode Live Shareを使うことです。

 

VSCode Live ShareはVisual Studio Codeの拡張です。

リモートでVScodeの開発環境を共有することができます。

 

主なメリットは3点

  • 新卒の方の環境を手元で動かせる
  • 自分が動かしている様子をフォローしてもらうこともできる
  • コードに直接コメントを入れながら、一緒に開発ができる

 

これが最高で、下記のような感じで一つのファイルを一緒に見ながら開発ができました。

 

小久保が手元でコメントを書きつつ、そこに処理を書いてもらうスタイル

僕がどういう風にコードを調べているかも見せられるため、
新卒の方の解像度が上がりました。

 

むしろ会社で1つのディスプレイ見ながらやるよりこっちの方が効率いいです。

 

新卒研修を一緒にやった(笑)

これは賛否両論あるかと思いますが、
メンターの僕も新卒研修のカリキュラムを一緒にやりましたw

 

WBSを引き、毎週進捗を報告する形式で行っています。

 

もちろん小久保もこのWBSに従い、同じ教材を同じペースでやっていきました。

 

これには理由があり

  • エンジニアは学習習慣がないと伸びない。学習習慣の形成が必須
  • 学習習慣は仲間がいるとつきやすい。今回新卒が1人だったので、仲間役が必要だった
  • メンターが進捗を守っているとメンティーがサボるわけにはいかなくなる

のようなことを狙っていました。

 

実際これできちんと進捗を守ることができ、業務的な力をつけることができたと思います。

副次的な効果として、僕も知らないことが結構あったのでためになりましたw

 

まとめ

以上、フルリモートでメンターになる際にやったことでした。

ポイントは

  • 質問のハードルを下げるため、Slackのハドルで常時接続を実施
  • なんでも相談会を定例化。コミュニケーション頻度を増やす
  • VSCode Live Shareで頻繁にペアプロを実施
  • 新卒が一人であれば、研修の仲間役を買って出る

です。

 

とまあ、ぐだぐだ言ってきましたが、結局は「会話する」に尽きると思います。

 

最近では新卒の方、技術的な成長を感じるのはもちろんですが、
単純に面白い人であることがわかりコミュニケーションをとるのがとても楽しいです。

 

コミュニケーションを取り続け、1個良いところを見つけたら、
好きやで〜、信じてるで〜って応援し続けるのが重要なのかなと。

 

これからも新卒メンバーの成長を
遠くにいながらもガッツリと近場にいるかのように見守っていければなと、切に思います。

本番相当のデータ環境を作ってみる

インフラを担当しているエンジニアの川口(@kawasystem)です。

今回はクロスマートでの本番相当のデータ環境をどのように用意しているか紹介したいと思います。

なぜ本番相当のデータ環境が必要なのか?

  • ステージングだとデータ量が少なくてパフォーマンスを測定しずらい
  • 本番だと意図しないデータパターンが存在する
  • リリース前に本番と同じ、または近いデータでテストしたい

といったことは、どのチームでもあると思います。

 

どのように環境を作っているのか?

構成図を書くと以下のようになっています。

本番相当のデータ環境を作るときの構成図

ポイント

  • 本番環境とステージング環境はAWSアカウントが異なるので、共有スナップショットを利用しています。
  • ストアパラメータでアプリの接続先のDBホストを管理しているので、ダウンタイムなしでDBを切り替えることが可能です。
  • ステージングで作ったデータは消えるので、注意が必要です。

 

最後に

初めに作るところがまでが大変ですが、一回作ると構成を変更しない限り使いまわせるので、オススメです。

なお、StepFunctionsの中身はPythonで作っています。ついつい、楽してBashAWS CLIで実装したくなりますが、後から変更が大変になるので、PythonでもRubyでも何でも良いと思いますが、きちんとした言語で書くのがオススメです。

 

クロスマートが気になってきた方はこちら

クロスマートでは絶賛エンジニア募集中です。

このお話を読んでちょっとでも気になった方がいたら

「話を聞きに行きたい」を押してみてください!

www.wantedly.com

クロスマートの開発フローを就任2ヶ月のPMが赤裸々に書いてみた



前置き

自己紹介

はじめまして。

紆余曲折を経てクロスマート株式会社に入社し、インサイドセールスのチームを経て、4月からプロダクトマネージャーを兼任しております、森(@monroo12)です。

最近このテックブログが立ち上がり『みんないい話書くなぁ。まぁまだ2ヶ月だし”テック”ブログはなぁ』と気を抜いていたらそんなこと有無を言わさず白羽の矢が立ったので、旅行で遊びに来た北海道でPC開いてます。

今回は旅行中のちょっとした空き時間ですが、クロスマート株式会社はフルリモートも歓迎です。フルリモートの感じは弊社エンジニア(@yhei_hei)のラジオトークでも語られているのでお時間あればぜひ聞いてみて下さい。

PM2ヶ月のペーペーが”テック"ブログという土俵で語れることは多くないので、今回は「クロスオーダーのPMとして就任後2ヶ月間を赤裸々に。」というテーマで記事を書かせていただければと思います。

大体5分ほどで読み流せる軽い内容かと思いますので、お昼ごはんついでに見ていただけると嬉しいです。

 

全体感こんな話をします。

 

 

想定読者

  • 事業ドメインもそうだけど開発環境、開発チームの雰囲気も大事だよねっていう転職活動のエンジニアの方
  • 自社サービス・SaaSでどんなプロダクト開発のプロセスをしているか、その実態に興味がある方

この文章に関する免責

  • 私自身はプログラムを書いた開発経験もなく、開発チームにがっつり入るのはほぼ初めての新人PMです、言葉や概念間違っていたらすみません
  • 特殊な内容はないかもしれませんが、生のお話をできればと思っています
  • 現在クロスマートはプロダクトチーム13名(業務委託含む)なのでそれくらいの規模のチームの話と思って聞いていただけると幸いです

 

突然のPMジョインの難易度は?

クロスマート株式会社は食品流通のDXを実現する、バーティカルSaaSを提供する会社です。一般的にバーティカルSaaSの開発では、業界解像度の高さ(粗業界特有の商習慣、業務フロー、現場の声のリアルさ)が必要と言われています。

新しい業務フローを生み出すというよりは、現時点で誰が、どのような業務を、なんのツールを使って、どの様に成果を生み出しているのか?

を理解しないと、通常業務とハレーションが起きて逆に複雑化する。ということが起きるからかな?と今では理解しています。

もともと、入社から半年はインサイドセールスという営業チームに近い部署にいたため一定の業界解像度はあると自負しています。

それでも現場の、個人の業務フローまで理解しているかと言うとまだまだだったなと思います。

そういった意味では、クロスマートでは『ドキュメントの重要性』が浸透しており、クロスオーダーが生まれた経緯や、過去のお客様インタビューのログ、お客様の作業の現場にお邪魔して見学させていただいたログ、社内の業界勉強会の動画など、開発における業界解像度の重要な部分を追体験をするには十分すぎるほどの情報がありました。

開発チームのNotion。オンボーディングから、過去の出来事まで残してくれています。

また、もうひとりのPM杉原が前回の記事で書いている通り、Flyleというお客様の声を吸い上げて開発に生かす。そういったログもあるためキャッチアップにはすごく役立ちました。

※参考:杉原のテックブログ記事

xmart-techblog.hatenablog.com

 

PMとしてのスタートで意識したこと

『過去の情報は積極的にインプットする』
  • 前段にも書いたドキュメントや、現場の業務動画、Flyleに溜まったお客様の声など既に過去話に上がったであろう、情報や知識については積極的にインプットをする。
  • 一般的に変だと思う仕様があっても業界の業務フローを考えると必要な仕様だったりするので検討された議事録なども読むようにしました
『今の現場の声を集めに行く』
  • フィールドセールスや、カスタマーサクセスのメンバーにご協力いただき、今検討頂いているお客様や、既にクロスオーダーを活用頂いているお客様の商談にたくさん同席させていただきました。ドキュメントでは伝わらない、細かい業務への機微や、現在の業務フローへの課題などつぶさに伺うことが出来ました
  • また、セールスメンバーによる卸売市場の見学ツアーや、お客様の業務現場への訪問など様々な方法で現場の温度感をキャッチアップできる仕組みがクロスマートにはあります
『開発メンバーの人となりを知る』
  • 開発知識や、業界知識をつけることは前提ながら、何よりもチームで開発するものなので、事前に開発メンバー13人の皆様と15分ずつ面談をお願いしました。直接業務ではないにも関わらず快く受けてくださる空気感、チーム感があります
  • ①業務の得意領域(技術的な)、②業務の苦手領域(技術的な)、③業務の得意なコミュニケーション方法(タイミングやツール)、④好きな仕事の進め方、⑤仕事において嬉しい、いやな、人、こと、雰囲気、組織、仕事、などなど。SlackやMTGだけではつかめない人柄もわかってとてもいい時間でした

 

どのようにスプリントを回しているのか?

開発の全体の流れは杉原の記事にもかかれている、これがわかりやすいかもしれないです。

 

日々のバックログ作成編

事業を見通した大型案件の実行
  • 日々の営業活動、市場を見た必要機能、新規事業など今後のクロスオーダーの大きな成長に必要な機能を考えます
  • 既存のリソースや新規採用の必要性なども踏まえて、『ロードマップを揉む会』として大きな視点で開発計画のスケジュールを揉んでいます
  • 決まった方向性はお客様のヒアリング、ストーリー作成などを行い、ユースケースを元にチケットを切り、アサインしバックログを作成します
日々の改善案、追加機能案の実行
  • 営業活動や、カスタマーサクセスの活動の中で生まれた要望や課題はリアルタイムに報告され、Flyleにためられます
  • Flyleにためられた案件は要望を上げたメンバーおよび、エンジニアの集まる『Flyle定例』で話し合い優先度や背景の確認、簡単な仕様を確認してチケットを発行します
  • 通常こういう改善案を揉む会はめんどくさいMTGになりがちだと思うのですが、一定のゲーム性を持ちながらエンジニア、営業メンバー双方楽しみながらも冷静に案件判断が出来ています

 

全体計画編

スプリントプランニング(2週間に1回)
  • クロスマートでは2週間1スプリントで開発スプリントが回されています
  • 開発メンバーが全員集まりスプリントプランニングを実行しています
  • プランニングポーカーを活用しフロントエンド、バックエンド、インフラ双方向から工数を確認しながらポイントを見積もっています

開発メンバーが13人とスクラムが大きくなってきたこともありスプリントプランニングも重くなってきたため、チームを分けてみる。

自己見積もり可能な小さいチケットは事前に見積もって挑む、などスプリント関連についてもPDCAを即座に回しながら負荷のない開発が実行できています

デイリースクラム(毎朝15分)
  • 毎朝15分オンラインでデイリースクラムを行っています
  • PMからKPIの共有、ベロシティ進捗の確認、各メンバーのチケット進捗の確認、ほうれんそうを行う場となっています
  • 『デイリースクラムっている?』みたいな文脈はたまにあるかもですが、クロスマートはフルリモートメンバーが居ることもあり日々の状況確認だけでなく、開発メンバーの調子の確認など実行する意味は大きいなと感じています

仕様設計編

大型案件の場合
  • 背景把握、ストーリー設計、要求整理の段階から必要に応じてセールスメンバー、カスタマーサクセスメンバー、エンジニア含めて、お客様にとって正しい課題解決ができる要求を整理していきます
  • その後は小さくても毎週プロジェクト定例を行い仕様を叩き上げていきます。ログとして議事録、PRDを整理、可視化しながら認識の齟齬がないように進めていきます
小型案件の場合
  • PMとしては、叩くことができる目に見えるものをとにかく早く作り上げ、関係者とともに細かい仕様の詰めを行っています
  • Figma、スライド、Canvaなどを活用しながら少しでも目に見えるものを大事に進めています
  • 必要に応じて、Slack、Slackハドル、Zoom,対面などを使い分けスピードを持ちながらも認識の齟齬が生まれない開発進行を意識しています

これだけだと日々の開発案件のことしか進まないので、技術定例という、今のクロスマートにたいして、解決しないといけない技術的な話のみを切り出してガッツリ話す会議を設定し、技術アプローチの課題解決にも向き合っています。

 

リリースからの振り返り編

リリース後の機能デリバリーについて
  • リリースされた機能はお客様に届いてこそ価値がでるものなので、様々な方法でリリースの連絡をしています
    • 『Slackリリースチャンネルによる連絡』
      • 機能詳細や、動画キャプチャ、課題報告者へのメンションを行います
    • 『朝会による報告』
      • クロスマートでは月水金に全体の朝会があり、その場でのリリース報告が行われています

リリース共有チャンネルの投稿
数字やユーザーの声での振り返りについて
  • お客様の利用状況に係るものに関してはしっかりと数値でも振り返ります
  • 事業の目標と開発の目標を紐付けすぎるとやりにくい、なんてこともあるかもしれませんが
  • 目標設定や、会社の数値の伸びなどしっかりと開発案件が事業を伸ばしている雰囲気を大事にしているなと感じています
  • SlackにはUserVoiceというチャンネルがあり、セールスチームやカスタマーサクセスチームからお客様の声が日々届きます。喜びの声を目にしながら働けるのは本当に楽しい瞬間です

お客様からの喜びの声の共有
スプリントの振り返りについて
  • 2週間のスプリントが完了し、次のスプリントの開始前にレトロスペクティブとしてKPTで2週間のスプリントを振り返り次のスプリントに活かす動きをしています
  • ファシリテーターは持ち回りで行い、雑談もベースにしながら、ファシリテーターの好みの音楽を流すなど振り返りやすい空気の中行われています
  • Keepではちょっと雑談しながら、Problemはしっかりと再発防止をするなど程よい空気感があり、この振り返りが好きだったりします

 

 

まとめ

  • クロスマートはお客様の声起点で開発を行っています
  • そのため業界知識のインプットなど会社のメンバーが全力でサポートしてくれます
  • そのためのドキュメント文化、ログ文化も根付き始めているのでキャッチアップがやりやすいです
  • ”教科書的なスクラム開発”と言われたぐらい安定した開発フローを実行しています
  • お客様に届いた喜びの声もリアルタイムに届く環境です
  • クロスマートでは一緒に働く仲間を募集中です!!!

 

以上です。

基本的な話が多くなってしまいましたが、

このお話を読んでちょっとでも気になった方がいたら
「話を聞きに行きたい」を押してみてください!

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

www.wantedly.com

顧客の声や現場の要望をどのように開発に反映させているか

 

 

ご覧いただきありがとうございます。
クロスマート株式会社でプロダクトマネージャー(以下、PM)を
務めております杉原(@sugihara_xmart)です。

最近話題のスプラ3を購入しましたがエイムが下手すぎて絶望しているこの頃です。
エイム力を上げないことにはうまくなれないとYouTubeの数多の動画に触発され
イカバルーン相手に基礎トレを実施しようかと画策中です。
もう少し世間に顔向けできる実力になりましたらぜひ対戦お願いします。

営業出身のPMでコードを書いた経験がほとんどありません

社会人的な自己紹介としては、ずっと営業周りのキャリアでしたが、
紆余曲折あってプロダクトマネージャーに転向し、2年ほど経過しました。

今回テックブログではありますが、営業出身PM目線で開発に関連する発信ができないかと考え、
「顧客の声や現場の要望をどのように開発に反映させているか」というテーマで記事を書かせていただこうと思います。

想定読者

  • 上流から要求だけ降ってきて顧客の顔が見えづらい環境で業務をされている転職活動中のエンジニアの方
  • 自社サービス・SaaSどんなプロダクト開発のプロセスをしているか、その生々しい実態に興味がある方
  • クロスマートの選考プロセス中で、どんな雰囲気で日頃やりとりをしているかの手触り感を掴みたい方

免責事項

  • 就活時代のインターン以外でほとんどコードを書いたことのない人間がテックブログを書いています。
    今までの記事とテイストがかなり異なることを何卒ご容赦ください。
  • 本記事では「Flyle(フライル)」というツールをめちゃめちゃポジティブに語ってますが、PR記事ではありません
    ただ杉原個人がロイヤルユーザーで、好きなプロダクトというのみです。この手のツールをめちゃくちゃ比較検討したというわけでもないので、その点ご容赦ください。
  • また本記事の内容は6月に行われた「Flyle」のユーザー会で話した内容を利活用しています。一層PR感がすごく感じるかもしれませんが、あくまで伝えたいことは「顧客の声や現場の声をどう開発に活かしているか」なのでその点もご承知おきください。
  • 現在クロスマートはプロダクトチーム13名(業務委託含む)なのでそれくらいの規模のチームの話と思って聞いていただけると幸いです。

事業全体で約30名弱の組織

抱えていた課題と導入した「Flyle」というツール

当初はGoogleSpreadSheetにまとめていましたが、形骸化していました

1年半ほど前なのでまだチームが上記の半分(6~7名)くらいのときでしたが、
徐々に顧客の声が遠のいていくのを課題に感じていました。

GoogleSpreadsheet(以下、GSS)に要望を記入してもらっていましたが、
そもそもGSSを開いて記入する作業そのものが営業・CSメンバーからするとハードルがあったり記入する粒度感がばらばらだったり、記入している改善策も扱われる機会は月に一回あればいい方で、たまに時間が空いたときに着手するという使い方で鮮度が下がったりと、運用が形骸化していました。

PJT化して改めて企画を進めていくにも、そもそも顧客要望が可視化・整理されていない状態からなのでせっかくのGSSの情報も有効活用されない、非常に微妙な顧客の声の扱われ方でした。そのような中で「Flyle」という顧客と製品のフィードバックループを構築するツールと出会い、導入するに至りました。

現在の機能開発の流れ

「Flyle」を顧客の生声DBにし、要望を説明する会を起点に

そういった機能開発の状況から1年半経った今は、概ね上記のような流れで開発を進めています。左から右に流れていくような形ですが、スライド右上に「心のバックログ」なる「実はこう改善したい」という思いや、自身がプロダクトを利用していての気付き、日々お客様とやりとりする中で生まれてくる改善要望などがあり、これらを「Flyle」に収集します。

現在は週2回、ビジネス・プロダクト双方のメンバーが参加する「Flyle定例」があり、そこでビジネスメンバーが要望の背景を説明し、やるべきか様子見をするか、やるべきな場合PJT化する(粒度が大きいもの)か、カイゼンタスクにする(粒度に小さいもの)かを大枠割り振りを実施しています。

PJT化するべきものに関しては、経営陣と月一回開催している「ロードマップ揉む会」という会でPJT優先度のチューニングを実施し、経営戦略・プロダクト戦略の接続を実施します。

直近扱うべきPJTに関してはPJT発足と定例を設置し、仕様詰め・チケット作成をし、以降はスクラム開発(現状2週間のスプリントで実施中)の流れで推進をしていく、そんな進め方になっています。

もちろん(株主との共同事業など)経営戦略・プロダクト戦略が比重を多く占めるPJTもありますが、大半のPJTはこうした「Flyle」に蓄積された「顧客の生声DB」を存分に参考にしつつ企画が進むのが現状のプロダクト開発の特徴かな、と考えています。

具体的な1週間の流れ

Slackに直接記入し、Flyleに連携しています

具体的な1週間の流れをチラ見せすると、上記のような形でいろんな職種・役割の人がSlackの指定チャンネルに要望を書き込みます。

現状のフォーマットは

■ 対象
■ 事実
■ 解釈

という非常にシンプルなフォーマットで最低限、顧客が言ったこと・事実として発生していることと、それを聞いた投稿者の解釈を分けて記入してもらっています。

記入してもらった情報はSlack連携でできるので、現場メンバーは「Flyle」にログインすることなく、要望をツールに蓄積することが出来ます。

この連携時に温度感を高・中・低 の3つに分けて投稿してもらうようにして、要望をさばく優先度の参考にしています。

起票者の温度感と内容を見て大枠の優先度をつけます

その後、週2回の定例に向け事前の確認をします。
主に定例では温度感高を確実に扱い、温度感中以降は余った時間に可能な限り見るような形で扱っています。

事前に起票者に準備してもらうためにも「お品書き」と称して事前に扱う一覧のキャプチャを展開するようにしています。

ビジネス・プロダクトチームが同じ場で要望を説明

そして30分×週2でのFlyle定例です。
要望は、よほどのバグでなければ差し込み対応することは稀なので、

  • (現状進んでいるPJTがあれば)その進捗共有
  • 類似機能の開発状況や運用回避のTIPS
  • 開発者目線での難易度共有や工数の所感
  • 開発を進める上でのユースケースや考慮事項のヒアリングすべき項目

などを共有することで顧客への一次回答や追加ヒアリングの初速を早めるようにしています。

要望はチケットに紐付けてJIRAへ連携

会議が終了すると定期的に、ソリューションに紐付けを実施します。
すぐに対応せず、様子見にすると決めた要望も、要望の数によっては優先度を変更することもあるため原則的にはすべてのチケットをソリューションに紐づけて管理をしています。

またこのソリューションをJIRAの新規チケット作成 または 既存チケットとの紐付けをすることができるので、JIRA上でのステータスがどうなっているかも一覧で確認することができるようになっています。

リリース完了したものはSlackにて起票者に連絡

そうしてチケットが進んでいき、リリースされたあとには本番環境での動作確認後、起票されたSlack投稿に「リリースされましたよ〜」と連絡をするようにしています。

2桁を超える起票だとかなりの通知数になるのですが、それだけ期待が大きかった機能だということでやや過剰めでも伝えることを優先して通知しています。

自分が思っている以上に伝えているつもりのものも伝わっていないことがあるので、キアコン(気合と根性)が必要な作業ではありますが、意識的に実施しています。

投稿数ランキングとFlyle起点の改善チケットの消化具合を定例で報告

そして週次の定例での共有です。
顧客ドリブンなプロダクト開発を推進していく上でも、要望を起票することは非常に重要な文化です。
その労力を割いていただけることが素晴らしいことだ!というメッセージが届くよう、
投稿数のランキングと頂いた要望の中でその週時点での着手中とリリース済のチケットの一覧を全社に共有するようにしています。

運用してみての

顧客に向き合う時間としてプロダクトチームからも好評な場に

このような取り組みを経て導入半年時点での定例メンバーからの声の抜粋です。

・要望をサクッと記載できること(手間が少ない)
・↑のおかげで細かい要望を出すときも心理的なハードルが低い
・flyle MTGのおかげで、エンジニア視点での意見やシステムの構造について知る機会になる

by CSメンバー

エンジニア視点だと「どうしてこのタスクを作業しているんだっけ?」という背景を知れるという点で良い。 何か問題がありそうだと感じた際に、相談や改善案の提示、アラートを上げる為の事前情報として「とりあえずFlyleを見よう」という形で活用出来る。 こういう場が無いとエンジニアは「全てのSlackメッセージから該当の要望を探す」為の工数を割かれたり、「言われた物を言われた通りに作る」だけになってしまうので、それが起こりづらくなったのは大進歩

by エンジニアメンバー

slackを追わなくても顧客やCSの要望をまとめてみれる。
普段は目先の開発タスクでいっぱいなので、
定期的なflyle MTGが、顧客のことを考えるいい機会になっている。

by エンジニアメンバー

と概ね前向きな声をいただいています。

こうした顧客の声を開発に活かす体制が魅力的に感じ、クロスマートへの入社を決めてくださったメンバーもいて、採用にもポジティブサプライズがあったなあ〜という気持ちです。

まとめ

  • Flyle運用はPMの「キアコン(気合と根性)」が必要な部分も多い🔥
  • 要望を説明し、背景がわかることでエンジニアが納得感をもって開発に従事できる(採用にも好影響🙆)
  • 運用は会議体とセットで⏰
    Bizチームとプロダクトチームが顧客に向き合う時間になる
  • 起票者が損しないポジティブフィードバックが廻る運用を🎉

 

以上です。

まだまだこれが完成形とは思っていませんが、
これからも顧客の声や現場の要望を活かしながらプロダクト開発を推進していく姿勢は変えずに実施して行きたく、そのためのメンバーを募集しています。

このお話を読んでちょっとでも気になった方がいたら
「話を聞きに行きたい」を押してみてください!

www.wantedly.com

 

おまけ

発表当時の資料も展開しておきますので、よければご参考に。

speakerdeck.com

 

 

VSCodeでDjangoをデバッグする環境を整えた

こんにちは。22年度新卒でバックエンドエンジニアとして入社しました、mobojisan (mobojisan) · GitHubと申します。

コーヒー中毒かつ2次元イケメンが大好きな腐夢兼任系オタク男性です。現推しはA3!の佐久間咲也くん。ナポリタン毎日食べてほしい。

今回は、VSCodeでのDjangoデバッグ環境をセットアップする方法について書いていきます。

続きを読む

【Django】DjangoでN+1を防いで高速化を行うテクニック

こんにちはバックエンド担当の山田です。

自分の担当回では処理負荷軽減のテクニックを数回に渡って記述していこうと思います。

弊社のサービス「クロスオーダー」ではPythonフレームワークであるDjangoを利用しています。 Djangoに限らずORMの機能が充実しているフレームワークでは注意を払わないと、簡単にN+1問題が発生してパフォーマンスに影響が生じてしまうケースがあります。

今回は以下の点に絞って記載します

  • Django環境でN+1発生箇所の見つけ方
    • django-debug-toolbarを利用する
    • debugsqlshellを利用する

N+1発生箇所の見つけ方

「いやいや、発生箇所を見つけるってことは、もうN+1が発生しているって事だよね?最初からN+1を発生させないように書こうよ…!」というツッコミ。ありがとうございます。

結論から言うと僕は一発ではそのようなコーディングをすることは出来ないです。はい。

しかし、そんなソースコードもネットの海に公開しなければ問題ありません。

「またN+1を書いてしまったぜ、ガハハ」と笑っていられる内に発生箇所を特定してしまいましょう。

django-debug-toolbarを利用する

django-debug-toolbarはSQLの実行だけではなく、各種ログ、負荷状況が確認出来るデバッグ用のツールです。

使い方は簡単。

  1. パッケージをインストール
$ pip install django-debug-toolbar
  1. settings.pyにINSTALLED_APPSに”debug_toolbar”を追加。 MIDDLEWAREに”debug_toolbar.middleware.DebugToolbarMiddleware”を追加。 INTERNAL_IPSに”REMOTE_ADDR”の値を追加(各環境に合わせて値を設定。今回の場合は’127.0.0.1’を指定)
INSTALLED_APPS = ['debug_toolbar']

MIDDLEWARE = ['debug_toolbar.middleware.DebugToolbarMiddleware']

INTERNAL_IPS = ['127.0.0.1']
  1. config URLに定義を追加
if settings.DEBUG:
    import debug_toolbar
    urlpatterns = [path("__debug__/", include(debug_toolbar.urls))]

これらを設定した後、Djangoをrunserver で起動すると画面右側にパネルが出てきます。

殺伐とした画面にこのようなメニューが!

 

その後は実際に画面を操作し、「SQL」のパネルを開いて内容を確認するだけです。

(あー!これはいけません!612回もクエリが実行されています!!)

 

N+1が発生している箇所には”xxx similar queries.” や “Duplicated xxx times.”と回数付きで教えてくれます。

プラスアイコンを押下するとクエリ実行箇所とtracebackまで丁寧に表示してくれるので、問題箇所を修正します。

 

”xxx similar queries.” や “Duplicated xxx times.”と表示されている箇所を全て修正することで…



実行回数を14回に減らすことが出来ました!

debugsqlshellを利用する

django-debug-toolbarを導入すると’debugsqlshell’コマンドが使えるようになります。

$ python manage.py debugsqlshell

このコマンドを実行すると対話型シェルが起動するので、後はDjangoの記述をそのままシェルで実行すると、このようにクエリを実行するタイミングに合わせて実際のクエリが表示されます。

>>> from xmart.webapi.models import Product
>>> Product.objects.values('id').all()
SELECT `product`.`id`
FROM `product`
WHERE `product`.`is_active`
LIMIT 21 [2.01ms]
<QuerySet [{'id': 1}, {'id': 2}, {'id': 3}, {'id': 4}, {'id': 5}, {'id': 6}, {'id': 7}, {'id': 8}, {'id': 9}, {'id': 10}, {'id': 11}, {'id': 12}, {'id': 13}, {'id': 14}, {'id': 15}, {'id': 16}, {'id': 17}, {'id': 18}, {'id': 19}, {'id': 20}, '...(remaining elements truncated)...']>
>>>

例えばN+1になっている場合は…。

>>> products = Product.objects.filter(id__in=[1,2,3])
>>> for p in products:
...   print('カテゴリ : ' + p.category.name)
...
SELECT `product`.`id`,
# 省略
FROM `product`
WHERE (`product`.`id` IN (1,
                          2,
                          3)) [1.98ms]

SELECT `product_category`.`id`,
# 省略
FROM `product_category`
WHERE `product_category`.`id` = 1
LIMIT 21 [1.37ms]

カテゴリ : 野菜

SELECT `product_category`.`id`,
# 省略
FROM `product_category`
WHERE `product_category`.`id` = 1
LIMIT 21 [1.16ms]

カテゴリ : 野菜

SELECT `product_category`.`id`,
# 省略
FROM `product_category`
WHERE `product_category`.`id` = 2
LIMIT 21 [1.02ms]

カテゴリ: 肉

このように同じようなクエリが何度も実行されている事がわかります。

簡単に実行させ確認が出来るので、開発中に気になる記述があればすぐに叩いて確認することが出来るので、気軽に確認を行いましょう。

【Nuxt.js + Jest】初学者向けにフロントエンドの単体テスト実装解説と、中々実装方法が見つからなかった点の備忘録

初めまして。弊社エンジニアでは現在一番新参です!

フロントエンジニアの福留(@fal_engineer)です。

ビール派です!

以下は先輩方の大変素晴らしい記事です。

とてもわかりやすく楽しく読むことの出来る構成となっております。まだ読まれていない方はぜひご覧ください!

xmart-techblog.hatenablog.com

xmart-techblog.hatenablog.com

Jestについて

JestとはMeta(旧Facebook)社が開発しているJavaScriptのテストフレームワークで、TypeScript, Babel, React, Vue, Angularをはじめとした様々なソフトウェアで利用されています。

パブリックだけでも640万リポジトリにタグ付けされており、独立したテストを並列で実行できること、ドキュメントの豊富さやナレッジの蓄積度の高さからデファクトスタンダードとなっています。

jestjs.io

ユーティリティをパッケージとして読み込むことで仮想DOMを仮想のブラウザ上で再現しテストを実行可能なため、Vue, React, Solid, Angularのコンポーネント単体テストも可能です。

フロントエンドの単体テストを実装するメリット

  • 設計通りに実装が行われているかを確認することが出来る
  • 変更時、既存の動作に支障を来していないかを確認することが出来る
  • テストコードがあることで、仕様を読み解きやすくなる

Jestの書き方

例えば、以下のようなVueコンポーネントです。

<template>
  <div>
    <p>{{ count }}</p>
    <button @click="$emit('click')"> + 1 </button>
  </div>
</template>
<script lang="ts">
import Vue from 'vue'

export default Vue.extend({
  name: 'count',
  props: {
    count: {
      type: Number,
      default: 0
    }
  }
})
</script>

当該コンポーネントの要件をまとめると、 - propsとしてcountを受け取り、pタグ内で表示している - button要素クリック時にclickイベントをemitする

となります。

こちらのUTをJestで実装すると、以下のようになります。

import { mount } from '@vue/test-utils'
import Count from '~/components/Count.vue'

describe('<Count />', () => {
  const propsData = {
    count: 1,
  }
  const count = mount(Count, {
    propsData,
  })

  it('props.countがpタグ内で表示されていること', () => {
    expect(count.find('p').text()).toBe(props.count.toString())
  })

  it('button要素クリック時、clickイベントがemitされること', () => {
    count.find('button').trigger('click')
    expect(count.emitted()).toHaveProperty('click')
  })
})

vue/test-utilsは読んでの通り、vueコンポーネントをテストで使用する上でのユーティリティです。

vue/test-utilsからはmountをインポートしています。

describe文では第一引数にテストスイート名をテキストで渡します。

テスト結果が以下のように一覧で表示されていくため、今回ではわかりやすいようにコンポーネント名を記述しています。

読んでみる

  const propsData = {
    count: 1,
  }
  const count = mount(Count, {
    propsData,
  })

の部分についてです。

propsDataという名前でCountコンポーネントに渡すプロパティを定義しています。

countという定数にはCountコンポーネントをユーティリティからインポートしたmount関数を使い、コンポーネントのラッパーを代入します。

またmount関数の第二引数はマウントのオプションです。 v1.test-utils.vuejs.org

今回はpropsのモックを準備してあげたかったのでpropsDataを渡しています({ propsData } は { propsData: propsData }の糖衣構文です)。

mount関数についてですが、shallowMountというものもあります。子コンポーネントをスタブとしてHTMLに描画するか、展開してHTMLに描画するかが違いとしてありますが、今回はコンポーネントを使用しているコンポーネントではないため、どちらを使っても問題ありません。

通常はshallowMountの方がテスト実行が早くなります。


  it('props.countがpタグ内で表示されていること', () => {
    expect(count.find('p').text()).toBe(propsData.count.toString())
  })

まず、it('',()=>{})についてですが、こちらはdescribeと同様にテストの名前付けです。(test('',()=>{})と同義です

通常、itが一つのテストケースであり、目的は一つであるべきと考えられます。

it内の全てのexpect関数の結果がfalseでない時にCLIに正常完了のチェックマークが表示されます。

expectはマッチャと呼ばれる、引数に対してプロパティの値を比較して正となるかをチェックする関数です。 jestjs.io

expect(true).toBe(false)

のように書くと、(args) === falseをチェックするのでfalseとなり、テストは失敗します。

また、

expect(1).toBeTruthy()

と書くと !!(args) === true をチェックするので、テスト結果は正となります。

今回はcount(コンポーネントのラッパー)に対してpタグをfindを使ってエレメントを取得しています。

findは条件に当てはまるDOM要素を取得します。

findではセレクタを使って要素を検索することもでき、classNameから取得したい場合はfind(.class-name)、idからはfind(#id-name)となります。

取得したDOMのラッパーにはtext()プロパティがあり、要素内のテキストを取得します。

今回はpタグにprops.countが表示されていることを確かめる必要があったので、props.countを文字列として変換し比較しています。


  it('button要素クリック時、clickイベントがemitされること', () => {
    count.find('button').trigger('click')
    expect(count.emitted()).toHaveProperty('click')
  })

コンポーネントや画面要素に対してイベントをシミュレートしたい場合はDOM要素のラッパーに対してtriggerを発火させます。

今回はbutton要素のclickイベントを発火させたかったので、文字列としてclickを渡しました。(カスタムイベントも同様です。) またイベント発火時にPromiseを返す場合(非同期処理の場合)はasync-awaitを使用します。

その後、コンポーネントのラッパーのプロパティであるemitted()をexpect関数に渡し、toHavePropertyと比較を行います。

第一引数としてプロパティ名(この場合は発生したイベント名)、第二引数としてvalueを渡しexpect内との比較が可能です。 Expect · Jest

環境

jestを実行する上で以下が必要だったのでインストールしました。

    "@types/jest": "^29.0.0",
    "@vue/test-utils": "^1.2.2",
    "babel-core": "^6.26.3",
    "jest": "^28.1.3",
    "jest-environment-jsdom": "^29.0.2",
    "jsdom": "^20.0.0",
    "ts-jest": "^28.0.8",
    "ts-node": "^10.9.1",
    "typescript": "^4.8.2",
    "vue-jest": "^3.0.7",
    "vue-loader": "^15.10.0"

その他: - nuxt: 2.15.8 - vue: 2.6.14

jestのバージョンについてですが、通常にインストールすると現在(2022/9)は29.x.x系がインストールされます。 29系はvue-3用のバージョンなので、28系をインストールする必要があります。

jest.config.js

module.exports = {
  roots: ['.'],
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/$1',
    '^~/(.*)$': '<rootDir>/$1',
    '^vue$': 'vue/dist/vue.common.js',
  },
  moduleFileExtensions: ['ts', 'js', 'vue', 'json', 'svg'],
  transform: {
    '.*\\.(vue)$': 'vue-jest',
  },
  collectCoverage: true,
  collectCoverageFrom: [
    '<rootDir>/components/**/*.vue',
    '<rootDir>/pages/**/*.vue',
  ],
  testEnvironment: 'jsdom',
  testPathIgnorePatterns: ['/node_modules/'],
}

tsconfig.json

paths: ["jest"] <- 追加

vue.shim.d.ts

declare module '*.vue' {
  import Vue from 'vue'
  export default Vue
}

中々実装方法が見つからなかったもの

モジュール分割されたVuexのモック作成

Vuexがモジュール分割されている場合のモック作成です。

本来ならばこのようにしてStoreのモックを作ってあげるのですが、

import Vuex from 'vuex'
const localVue = createLocalVue()
localVue.use(Vuex)

describe('', () => {
  const store = new Vuex.Store({
    state: {},
    actions: {}
  })
  const wrapper = shallowMount(component, { store, localVue })

Vuexのモジュールは名前解決で呼び出されるため、コンポーネントがAModuleとBModuleを呼び出している場合はStore連携がうまく行きません。

そういった場合は、このような実装になります。

import Vuex from 'vuex'
const localVue = createLocalVue()
localVue.use(Vuex)

describe('', () => {
  const AModule = {
    state: {},
    actions: {},
    namespaced: true
  }

  const BModule = {
    state: {},
    actions: {},
    namespaced: true
  }

  const store = new Vuex.Store({
    modules: { AModule, BModule }
  })
  const wrapper = shallowMount(component, { store, localVue })

Storeのモックをそのままオブジェクトとして渡すのではなく、namespaced = trueとしてmodulesにkey-valueで渡してあげると名前解決が実行され、上手く動きます。

@nuxt/deviceのモック作成

@nuxt/deviceを使ったコンポーネントでは、this.$xxxのようにして呼び出しているオブジェクトがundefinedとなるためテスト実行が上手くいきません。そのため、モックとしてコンポーネントに渡してあげる必要があります。

$device等が挙げられます。

使うオプションはマウント(mount, shallowMount)関数のmocksプロパティです。

const localVue = createLocalVue()

describe('', () => {
  const mocks = {
    $device: {
      isMobile: true,
      isDesktop: false,
    }
  }
  const wrapper = shallowMount(component, { mocks, localVue })

localForageのモック作成

nuxt/deiceと同様で、mocksに格納した状態でコンポーネントオプションとして渡してあげます。

this.$route.query, this.$router.push等も同様です。

まとめ

最後まで読んでいただいて、ありがとうございました!

フロントエンドのテストは「どこまでやるか」等がハッキリとアーキテクチャとしてスタンダードになっておらず、まだまだ黎明期だと思われます。

しかしプロダクトの品質管理として、「画面の主要機能が動作するか」、「JSONを渡した時の状態の網羅」はしていきたいと思っています。

弊社のフロントエンドのテスト実装はまだまだ導入を行い始めたところですが、お客さまに安心して使って貰うために、品質向上を目指しテストのカバレッジを上げていきたいです。

次回はバックエンドのエンジニアの山田さんの記事です!楽しみです!!


弊社ではバックエンド、フロントエンドエンジニアの方を募集しています。

社員を第一に考える、とても働きやすくチャレンジしやすい・スキルを上げながら働くことの出来る会社です。

クロスマート株式会社について気になった方がいらっしゃいましたら、以下のリンクから「話を聞きに行きたい」をお願いします!

www.wantedly.com

5分で作れる!FastAPI 入門編

クロスマート バックエンドエンジニアの武(タケ)(@pouhiroshi)です。
社内ではおそらく最高齢なため「たけじい」と呼ばれております。(けど入社して半年なので気持ちは若いです♪)これからよろしくお願いいたします。


前回は 小久保さんの「テストコード好きがテストを布教し続けた結果プロダクトや個人がどう変わったか」についての記事でした。
わたしも小久保さんに見習ってテストを頑張って書くようにしています!まだ、見ていない方はぜひこちらの記事もご覧ください。
xmart-techblog.hatenablog.com


さて、弊社のクロスオーダー 飲食店と卸をつなぐ受発注システムのバックエンドは、サービスイン当初からPythonフレームワークDjango+DjangoRestFramework(DRF)が採用されております。

今回、新しいサービス開発プロジェクトが立ち上がることになり、メインデベロッパーとして言語やフレームワークを選定したのですが、最近話題にあがっていて評価も高そうなFastAPIを使うこととしました。
fastapi.tiangolo.com


Pythonフレームワークgithubスター数はDjango, Flaskに次ぐ第3位!初版リリース2018年にして、上位を追い越しそうな勢いです!

選定した理由はいくつかあるのですが、
ポジティブな理由としては、

  • わかりやすい・かきやすい・自由がある
  • 使っている人が多くドキュメントや参考となる記事が多い
  • レスポンス速度が速い(らしい)
  • Javaで慣れ親しんだSwaggerが自動で生成されるらしいぜ!!!

ネガティブめな理由としては

  • 既存利用のDjango+DjangoRestFramework(DRF)が(私は)わかりにくい・・・!!

というものがありました。

特にDjango+DjangoRestFramework(DRF)がわかりにくいというのは、私がもともとJavaエンジニアでPythonDjangoをほぼ初めて使ったという理由も多分にあるとは思います。調べてみると私と同様「DRFはわかりにくい」と挫折(とまではいきませんが)した人は多いようです。

そんな中発見したのが、FastAPIです。
理由にも挙げた通り、わかりやすくシンプルな形で開発を行うことができます。
コンポーネント間のつながりも直感的でJavaでいうSpringBootと同じような感覚で開発を進めることができます。
さらにREST APIを開発する上でとても便利なSwaggerも自動生成されます。

今回は、「5分で作れる!FastAPI」と題して、FastAPIにおける開発を入門編としてご紹介したいと思います。
なお、開発環境はPyCharmを使っていますが、VSCodeなどでももちろん開発を行うことができます。ぜひ軽率にチャレンジしてみてください!

入門編目次

プロジェクト作成

PyCharmのプロジェクト作成で、作成するディレクトリを選んでFastAPIを選択します。

FastAPIとuvicorn(アプリケーションサーバ)がインストールされます。*1

すると、main.pyが作成されていると思います。


FastAPI Webアプリケーションの起動

いったん、右上のfastApiProject1(プロジェクト名)が出ている状態で再生ボタンを押しましょう。
アプリケーションが起動できます。

コンソールに Application startup complete.
と出たら起動成功です。
コンソールに出ている http://127.0.0.1:8000 をブラウザで表示してみましょう。

ブラウザに async def root(): に書かれている{"message": "Hello World"}
が表示されていますね。

何もしてないのにAPIができあがってしまいました!

アプリケーションは実行タブで Ctrl+Cで終了することができますし、実行ボタンの右のほうにある停止ボタンでも停止することができます。

ブレークポイントによるデバッグ実行

また、ブレークポイントをつけて、デバッグを行うこともできます。アプリケーションを停止したら、再生ボタンのよこにあるデバッグボタンで同じように起動してみましょう。

同じく、http://127.0.0.1:8000 をブラウザで表示してみましょう。
ブレークポイントで 実行が止まりましたね!

こちらで実行中の変数などをみてステップ実行することができるようになります。
これがなくては開発が捗りませんね!

Router(Controller) の定義

main.pyにはAPIのパスとhttp methodを定義する @app.get("/") という表記がありますね。

@app.get("/") は ルートパスをにGETアクセスした際の処理がかかれています。

@app.get("/hello/{name}") の{name}部分はパス変数と呼ばれており、対応するアドレスは http://127.0.0.1:8000/hello/{name変数に入れる値}となります。

main.py

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
async def root():
    return {"message": "Hello World"}


@app.get("/hello/{name}")
async def say_hello(name: str):
    return {"message": f"Hello {name}"}

ブラウザで http://127.0.0.1:8000/hello/FastAPI にアクセスしてみましょう。

ブラウザに {"message":"Hello FastAPI"} と表示されましたね!
URLのパスであたえたFastAPIという文字列がnameという変数に入り、say_hello(name: str)の引数nameで受け取れたことが確認できたと思います。

デバッグでも確認してみましょう。

ソースコード上やマウスオーバーで変数が表示されますし、下のデバッグコンソールにも値が表示されていますね。

デバッグが捗りそうです。

Swagger (APIドキュメント) 自動生成

FastAPIはAPI開発に特化しており、エンドポイントを作成すると同時にSwaggerというAPIドキュメントが生成されます。
アプリケーションを起動すると見れるようになります。
アプリケーションを起動し、http://127.0.0.1:8000/docs へアクセスしてみましょう。

このように@app.get で定義されている2つのエンドポイントが出ていますね。
アコーディオンを開くと詳細を確認することができます。

このようにパラメータやレスポンスの仕様について確認することができます。

SwaggerによるAPI実行(Try it out)

右上についているTry it out ボタンを押すと、実際にこのAPIにパラメータなどを与えて実行することができます。

今回は、わたしの社内でのニックネーム「たけじい」を入力して、Executeを押してみます。

すると、このようにCurlでリクエストした場合とURLリクエストした場合の表記がでてきて、
さらにhttp status codeとResonse body が表示されます。
正しくjson形式で
{
"message": "Hello たけじい"
}
と返ってきているようですね!

まとめ

いかがでしたでしょうか。
FastAPIをつかってAPIサーバーサイドを、素早く、かつシンプルな形で実装することができることを実感いただけたかと思います。

説明しきれていない部分もたくさんありますので、今後もFastAPIの情報をお伝えしていこうと思いますのでご期待ください。

FastAPIを使いたいとエンジニアチームの技術定例で提案したとき、「使いやすそう」「おもしろそうじゃんやってみなよ」というメンバーからの反応があったように、弊社には新しいことにチャレンジすることへの前向きな雰囲気があります。

開発メンバーをバックエンド、フロントエンド、共に絶賛募集中です!

ぜひ新しいことにチャレンジできる環境の中で一緒に楽しくお仕事しましょう!

www.wantedly.com

*1:Pycharmを使わない場合はプロジェクトディレクトリ作って、その中で pip install fastapi pip install uvicorn でインストールしてもOKです

テストコード好きがテストを布教し続けた結果プロダクトや個人がどう変わったか

サーバーサイドエンジニアの小久保(@yhei_hei)です。

本日からクロスマートに関連した技術的なお話を書いていきます。

皆さん、テストコード書いてますか?

突然ですが、小久保はテストコードを書くのが大好きです。

本業はもちろん、個人開発でもテストコードを書いています。

時折、ロジックを書きたくてプログラミングしているのか、テストコードを書きたくてプログラミングしているのか、よくわからない感じになりますw

日々Twitterでテストコード書け書けと啓蒙中です。

↑最近流行のブロックチェーン関連の開発でさえテスト環境を整え始める小久保

そんなテスト狂の僕なので、クロスマートの面接も

「テスト書けます!」

「テスト書かせてください!」

の一点張りで入社しました。(超意訳)

入社当初はAPIのテストコードがなかった

で、2022年の1月におっかなビックリ人生初のスタートアップにフルリモートエンジニアとして乗り込んだわけです。

そこでクロスマートのプロダクトコードを見ることになるのですが、

「む? APIのテストがないぞ??」

と言うのが第一印象でした。

当時のカバレッジは41%と低め。

重要なバッチにはテストコードが書かれていましたが、APIにはテストコードがありません。

慣らしタスクのついでにテストを書きまくった

入社して1ヶ月くらいは慣らしタスクとして、簡単なAPIの改修を任されてました。

なんとかテストコードを増やしたかった僕は、慣らしタスクのついでに、APIのテストを勝手に書いていったのです。

例えば商品一覧取得のAPIの改修があれば、改修と同時に正常系のテストを書く、みたいな。

どんどん正常系のテストが追加されていき、改修がやりやすくなっていきました。

勉強会でAPIのテストの書き方講座をやった

ただ、1人でテストを書いていても限界があります。

これから改修の入る機能、新規で作られる機能、全てにおいてテストが書かれる必要があります。

チームメンバーの協力が不可欠です。

そこで毎週木曜の10分勉強会で、APIのテストの書き方講座を実施しました。

モックを使う派と実DB使う派の論争から始める小久保

うちはDjango+Django Rest Framework(以下DRF)を使ってAPIを作ってます。

基本的にJWT認証ありきのAPIのため、テストの書き方にちょっとした癖があります。

あんまりJWT認証つきのテストコードの解説ってないんですよね。。。

その辺りの書き方の基本を解説し、これを真似していけば他のAPIのテストも書けちゃうよみたいな状態に持っていきました。

ちなみにDRF+JWT認証の具体的なテストコードの書き方は小久保のブログにまとめてあります。興味のある方は是非↓

django.yhei-web-design.com

皆がテストコードを自主的に書くようになった

すると、気づけばほぼ全エンジニアがテストコードを自主的に書いてくれるようになりました。

なかには

テスト駆動開発...だと...?

こんなことを言ってくれる方もおり、 ですよね〜〜〜〜!!! 僕が言いたかったやつこれなの〜〜〜〜〜〜!!!!!!! って感じ。

テストコードが加速度的に増えていきました。

バグを拾ってくれるのはもちろん、レビューしやすくなった

Joinした初期の頃はレビューが来ても既存仕様を読み解くのに時間がかかっていました。

が、最近ではテストコードで仕様がある程度読み解けます。

assertを見て仕様を読み解く

結果、圧倒的にレビューしやすくなりました。

まさに「動くドキュメント」としてテストコードが機能しており、僕の心の中の@t_wadaさんも思わずニッコリ。

もちろん、CI上で動いてバグ検出もしてくれるのでコード修正の負担がかなり減りました。

理想は全仕様がテストコードで表現されている状態

勉強会を終えて数ヶ月。複数人でテストコードを書いて行った結果、

カバレッジは41% → 58%まで上昇しました。

Googleの指標によると60%から許容可能になるとのことで、まだギリダメですw

Google we offer the general guidelines of 60% as “acceptable”, 75% as “commendable” and 90% as “exemplary.” However we like to stay away from broad top-down mandates and encourage every team to select the value that makes sense for their business needs.
Google Testing Blog: Code Coverage Best Practices

とは言え鬼門の50%を超え、体感的にはようやく少し安心して開発ができるようになりました。

また、メンバーからも「仕方なくテストを書いている」というよりは「有用だから書いている」というのがひしひしと伝わってきます。

これがマジで嬉しいです。

そうなんです。テストって便利なんですって。それが伝わればもう本望です。

とは言いつつ、さらなる改善はやっていきます。

次なるゴールは

  • 全ての重要な仕様がテストコードに書かれている
  • そしてそれが読みやすく、ドキュメントとして機能する

と言ったところでしょうか。

単にカバレッジをあげることを目的とはしません。

例えば3年後。

現メンバーが別プロジェクトに配属されバラバラになり、当初の仕様を誰も知らない状態になったとしても。

残った人がテストコードをもとに仕様を読み解き、99%のデグレはテストコードで検出できる、という状態。

これが理想です。

そのためにはさらにメンテナブルなテストコードの書き方、読みやすいテストコードの書き方をチーム全体で習熟していく必要があります。

やること満載です。

これからも快適なテストライフを送るために、ガンガン啓蒙活動を続けていきます。

時々、具体的なテストコードの書き方も載せていく予定なのでお楽しみに。

クロスマートが気になってきた方はこちら

クロスマートでは絶賛エンジニア募集中です。

このお話を読んでちょっとでも気になった方がいたら

「話を聞きに行きたい」を押してみてください!

www.wantedly.com