【登壇資料】製造現場を変えろ。ソフトウェアエンジニアが挑む爆速DX

f:id:kaminashi-developer:20210603133800p:plain

カミナシ・エンジニアの浦岡です。 先日、CADDiさんと共催で勉強会を開催しました。その際のカミナシ側の登壇資料を紹介します。

勉強会の概要

製造現場向けのサービスを開発しているCADDiさんと弊社カミナシが共催でオンライン勉強会を開催しました。

今回、製造現場向けのサービスを作る2社が、 ソフトウェアエンジニアが挑む爆速DXと題して開発エピソードをそれぞれ発表しました。

kaminashi.connpass.com

カミナシからは「紙のワークフローをDX化する際に行った多階層データ処理の高速化について」というタイトルで発表させていただきました。

speakerdeck.com

製造現場のDXとは

gyazo.com

製造現場のDXというと、無人化、高度化をイメージする方もいるかと思いますが、実際の製造現場ではまだまだ紙が主役です。

製造現場を変えるための、カミナシのアプローチ

gyazo.com

カミナシは、紙のワークフローをデジタル化することで製造現場のDXを加速しようしていますが、 ユーザー自身がノーコードでワークフローを作成・編集できます。

開発エピソード(性能低下の課題)

ノーコード(ロジカル)なデータを扱うことに起因した性能低下の課題が サーバー、クライアントそれぞれで発生し、それにどう対応したかを紹介せさていただきました。

サーバー側での性能低下

gyazo.com

サーバー側では、ボトルネックとなっていたDBクエリの発行回数を抑える施策を紹介しました。

クライアント側での性能低下

gyazo.com

クライアント側では、コンポーネントの無駄な描画による性能の低下に対して、memo化で対処した事例を紹介しました。

宣伝

今回、サーバー・クライアントのそれぞれでの性能低下の課題に対しての対処を開発エピソードとして発表しましたが、まだまだ小手先の対応しかできていないのが実情です。

今後のユーザー増加、機能拡張に耐えうる抜本的な改善を行うためにも、 EM/アプリエンジニア/SREと幅広く募集しています!

open.talentio.com

open.talentio.com

open.talentio.com

サービスの開発途中から、厳しめのESLintのルールを導入するためにしたこと

こんにちは、株式会社カミナシのエンジニア@tomiです。

今回は、開発途中からESLintのルールを見直す際にどうやって行ったかを書こうと思います。

開発途中からESLintを見直すとなると、膨大にあるコードがあるので一気に直すとなると、恐ろしい量の修正が必要です。

カミナシで理想的なESLintのルールに修正しようと思ったら、修正箇所が4000件を超えていました。。。

f:id:kaminashi-developer:20210524091458p:plain

とても一人で直すには無理な量です。更に修正中も開発は進むわけですから、コンフリクトも頻繁に発生するでしょう。

ESLintを見直すための道のり

では、一気に直すのは無理なので、はじめに考えたのは「段階的にルールを厳しくしていく」ことでした。

何個もルールをoffにしてあるので、それを一つずつerrorにしてプルリクを作成していく

そこで考えたのは、pre-commitを使ってコミットされるファイルに対して、新ルールのESLintを実行する方法です。

他の開発を行いながら徐々にコードがキレイになっていく仕組みを作ろうと考えました。

ただし、pre-commitでESLintを実行するだけでは駄目で、以下の要件をクリアする必要がありました。

  • 新ルールはガチガチの厳しいものにする
  • 開発環境の起動時は旧ルールで動く
  • ビルド時のlintチェックは旧ルールで動く
  • 編集したファイルのみ新ルールを適用する

既存の.eslintrc.jsonを厳しいルールに変更してしまうと、開発環境が起動しなくなったり、ビルドが失敗してしまったりと問題が起こるので、コミットされる時のみ新ルールが適用されるように書く必要があります。

新ルールだけをまとめた設定ファイルを作る

ESLintには設定ファイルを上書きして読み込める機能があるので、それを利用します。

例えば、.eslintrc.jsonは以下だとして、

{
  "env": {...},
  "parser": "@typescript-eslint/parser",
  "parserOptions": {...},
  "plugins": [...],
  "rules": {
    "no-unused-vars": "off",
    "no-empty": "off"
  }
}

ESLintを実行すると、自動で読み込まれます。

$ eslint --print-config src/App.tsx
...
  "rules": {
    "no-unused-vars": [
      "off"
    ],
    "no-empty": [
      "off"
    ]
  },
  "settings": {},
  "ignorePatterns": []
}

さらに、別ファイル.eslintrc-override.jsonを作成する

{
  "rules": {
    "no-unused-vars": "error"
  }
}

こちらには、.eslintrc.jsonでオフ設定したルールに対して、エラーになるように設定した。

.eslintrc-override.json-cオプションを使って読み込むと、

$ eslint -c .eslintrc-override.json --print-config src/App.tsx
...
  "rules": {
    "no-unused-vars": [
      "error"
    ],
    "no-empty": [
      "off"
    ]
  },
  "settings": {},
  "ignorePatterns": []
}

.eslintrc.jsonの上に.eslintrc-override.jsonが上書きされていることがわかる。

このオプションを使えば、pre-commit時だけ別ルールというのは比較的に簡単に実装できそう。

予期せぬ上書きが起こる

ただし、注意して設定を行わないと思っていたルールと違う設定になってしまう場合がある。

それが、上書きする側.eslintrc-override.jsonextendsを使うときである。

.eslintrc-override.jsonを以下のようにextendsを追加してみる。

{
  "extends": ["eslint:recommended"],
  "rules": {
    "no-unused-vars": "error"
  }
}

そして先程同様にESLintを実行すると、

$ eslint -c .eslintrc-override.json --print-config src/App.tsx

f:id:kaminashi-developer:20210524091546p:plain

**.eslintrc.jsonでoffにしていたはずが、errorになっている。**

eslint:recommendedでは内部では"no-empty": ["error"]が設定されているためである。

中身を把握してextendsを設定するなら問題ないが、意図しないルール変更が行われる匂いがプンプンする。

そこで、元々あったルールで変更したくないものがあるときは、更に別ファイルに分けて、.eslintrc.json.eslintrc-override.jsonに読み込ませるのが安全だろう。

最終的な形としては、以下の3ファイルを用意する。

// .eslintrc.json
{
  "env": {...},
  "parser": "@typescript-eslint/parser",
  "parserOptions": {...},
  "plugins": [...],
  "extends": [
    "./.eslintrc-rules.json"
  ],
  "rules": {
    "no-unused-vars": "off"
  }
}
// .eslintrc-override.json
{
  "extends": [
    "eslint:recommended",
    "./.eslintrc-rules.json"
  ],
  "rules": {
    "no-unused-vars": "error"
  }
}
// .eslintrc-rules.json
{
  "rules": {
    "no-empty": "off"
  }
}

extendsで変更したくないルールをそれぞれのファイルに読み込ませることで、上書きされる危険性がなくなる。

pre-commit時のみ新ルールを適用する

これはもう説明も不要かもしれませんが、記録に残して起きます。

CIで実行されるlintチェックは、yarn lintで実行させます。

pre-commitはhuskyを使って、.eslintrc-override.jsonを適用させます。

"scripts": {
  "lint": "eslint ./src"
}
"husky": {
  "hooks": {
    "pre-commit": "lint-staged"
  }
},
"lint-staged": {
  "./src/**/*.{js,jsx,ts,tsx}": [
    "prettier --config .prettierrc --write",
    "eslint -c ./.eslintrc-override.json"
  ]
},

これでコミットするファイルだけ厳しいルールが適用され、ルールを守らないとコミットできない仕組みができました。

4000件以上の修正はしないといけないのは変わらないですが、みんなで少しずつコードの整理をしていきましょう!!

将来的に

まだ実現できてないのですがやりたいことがあります。

それは残りのエラー件数を定期的に通知する仕組みです。

今回実装したものの課題は、このままだとずっとビルド時のlintチェックは緩いままで決して健全とは言えません。なのでいつかは新ルールがすべての場所に適用されるのが理想です。

そのためには、エラーがなくなったかを知る仕組みが必要かなと思いました。

いつか、yarn lint -c ./eslintrc-override.json srcが定期実行されて、Slackに通知される仕組みも作っておきたいなぁと思っています。

テスト駆動開発で文字列が日付か正規表現で調べる関数を実装してみた

f:id:kaminashi-developer:20210519192254j:plain

こんにちは、株式会社カミナシのエンジニア @imu です。

はじめに

テスト駆動開発(以下、TDD)は知っているけど、業務でTDD使ってみようと思ってもリリースに追われてしまい、時間が取れなかったりしてチャレンジ出来ていませんでした。今回は時間に余裕があったのでTDDを実践してみました。

環境構築

ExpoCLIのインストール
npm install -g expo-cli 

インストールが終わったらひな形を作成しましょう。 今回はManaged workflowのblank(TypeScript)を利用します。

expo init tdd_regexp
テストのパッケージをインストール

公式ドキュメントに従ってテストのパッケージをインストールします。

docs.expo.io

yarn add jest-expo @types/jest --dev

package.jsonに以下の設定を行います。

"scripts": {
  ...
  "test": "jest"
},
"jest": {
  "preset": "jest-expo"
}

TDD

目的
  • 「キレイ」で「動作する」コードを書くこと

乱雑、混乱、散らかっているコードがないように整然と保つことかなと思っています。どうしてもリリース日間に合わせるために、「コレでいいか」と動作する方向だけに力を入れないようにしないといけないですね。

テストが成功している内はその動作は保証されていると言えます。(正しい実装であるかの保証は出来ません。あくまでテストが通ることが保証されているだけです)

これによってエンジニアは自身を持って大胆かつ、積極的にリファクタリングがしやすくなると思います。

テストコード大事ですね!

3つのサイクル

基本となる開発サイクルは以下になります。

  • Red: 失敗するテストを書く
  • Green: できる限り早く、テストに通るような最小限のコードを書く
  • Refactor: コードの重複を除去する(リファクタリング

【Red】失敗するテストを書く

最初に必ず落ちるテストを書き、テストが失敗することから始めます。

【Green】できる限り早く、テストに通るような最小限のコードを書く

どんなコードでも良い(マジックナンバー、固定値等)のでテストを成功させるコードを書いていきます。

【Refactor】コードの重複を除去する(リファクタリング

意味のあるコードになっていれば終わりですが、Greenの段階で「とりあえず動いた」コードを放置してはいけません。

忙しいと思ってしまう「あとでキレイにします!」はやらないのと同義だと思うので、都度リファクタリングしていきましょう。

リファクタリングした後は、Red -> Green -> Refactorを繰り返していく手法となります。

speakerdeck.com

www.amazon.co.jp

TDDに沿ってやってみよう

今回はこのパターンだけ正となるようにします
yyyy/MM/dd
1回目

【Red】失敗するテストを書く

IsIncludesDate.ts

const isIncludesDate = (text: string): boolean => {
  const regexps: RegExp[] = []
  return regexps.some(reg => reg.test(text))
}

export default isIncludesDate

IsIncludesDate.test.ts

import isIncludesDate from './IsIncludesDate'

describe('文字列が日付か正規表現で調べる', () => {
  test('yyyy/MM/dd', () => {
    expect(isIncludesDate('2000/01/01')).toBe(true)
  })
})

テストを実行してみましょう!ここで落ちなきゃダメです笑 f:id:kaminashi-developer:20210512175750p:plain

【Green】動けば良いコードを実装

const isIncludesDate = (text: string): boolean => {
  const regexps: RegExp[] = [/^\d{4}\/\d{2}\/\d{2}$/]
  return regexps.some(reg => reg.test(text))
}

f:id:kaminashi-developer:20210512182257p:plain

【Refactor】コードの重複を除去する(リファクタリング

前のサイクルで正常になったので終わり…と言いたいところなんですが「yyyy/MM/dd」で日付じゃないパターンも想定しないといけないと思います。

それは次サイクルでやってみよう!

2回目

日付かどうか判定したい関数なので「0000/00/00」は日付と判定してほしくないことを確認します。

【Red】

describe('文字列が日付か正規表現で調べる', () => {
  test('yyyy/MM/dd - 異常値', () => {
    expect(isIncludesDate('0000/00/00')).toBe(false)
  })
})

テストを実行してみましょう! f:id:kaminashi-developer:20210512183201p:plain 日付と判定してほしくないのに関数の結果は日付を判定されていますね。 次サイクルでテストを成功させるようにしていきましょう。

【Green】

正規表現が「数値4桁/数値2桁/数値2桁」になっているので、日付の範囲になるように変更する

const isIncludesDate = (text: string): boolean => {
  const regexps: RegExp[] = [/^\d{4}\/(0?[1-9]|1[0-2])\/(0?[1-9]|[12][0-9]|3[01])$/]
  return regexps.some(reg => reg.test(text))
}

f:id:kaminashi-developer:20210512185139p:plain

【Refactor】

リファクタリングも必要ないので今回はこれで終わりです! 正規表現のパターンを追加した場合は再度1回目からやっていきましょう!

最終的なテストコード
import isIncludesDate from './IsIncludesDate'

describe('文字列が日付か正規表現で調べる', () => {
  test('yyyy/MM/dd - 正常値', () => {
    expect(isIncludesDate('2000/01/01')).toBe(true)
    expect(isIncludesDate('2000/1/1')).toBe(true)
    expect(isIncludesDate('2000/01/1')).toBe(true)
    expect(isIncludesDate('2000/1/01')).toBe(true)
  })

  test('yyyy/MM/dd - 異常値', () => {
    expect(isIncludesDate('0000/00/00')).toBe(false)
    expect(isIncludesDate('2000/13/01')).toBe(false)
    expect(isIncludesDate('2000/01/32')).toBe(false)
    expect(isIncludesDate('9999/99/99')).toBe(false)
    expect(isIncludesDate('')).toBe(false)
    expect(isIncludesDate(null)).toBe(false)
    expect(isIncludesDate(undefined)).toBe(false)
  })
})

f:id:kaminashi-developer:20210512192314p:plain

うるう年のチェックも出来れば正確になると思うので是非やってみてください!

おわりに

TDDのイメージはついたでしょうか?ロジカルな実装や修正が頻繁に入るところは、TDDを使うと良さそうです。すべてこの手法でするのは難しいですが、予めインプット、アウトプットが決まっている場合にはTDDを使って実装してきたいと思います!

正直なところ、この手順で合っているか不安ではありますが、まず第一歩が踏み出せた気がします笑

最後に弊社ではエンジニアを募集しております。 興味がある、話を聞いてみたい、応募したいという方はお気軽にご応募ください!

open.talentio.com

品質と新規開発のバランスというタイトルで登壇してきました

こんにちはカミナシでアプリケーションエンジニアをやっている沼田( @keinuma15 )です。 先日行われた Startup Issue Gym #1【開発プロセスのIssue】 にて「品質と新規開発のバランス」というタイトルで発表してきました。

概要

スタートアップのプロダクトは成長していくにつれて様々な課題にぶつかります。 その中でも0→1から1→10フェーズに変化すると既存機能を改善しつつ新機能を開発していく課題が出てきます。 今回はカミナシがこの課題どう立ち向かっているのか共有させていただきました。

speakerdeck.com

プロダクトの成長による課題の変化

Image from Gyazo

プロダクトはフェーズに応じて目指しているゴールと課題が変わってきます。

カミナシは2019年12月にピボットして今のプロダクトの開発を開始しました。 当時は0→1のフェーズだったため、顧客のどの課題を解くのかやソリューションを考え実現する必要がありました。 そこから2020年6月に正式リリースしたのが「カミナシ」です。

Image from Gyazo

カミナシは現場で働いてる全ての業界を対象としたアプリで記録業務をできるプロダクトです。 開発を開始して半年ほど経つと次第にユーザーが増え始め、既存機能だけではカバーできていない領域が増えてきました。 ユーザーにより価値を届けるためには新機能を開発しないといけないが、すでに提供している機能を改善する必要も出てきました。

狩野モデルとプロダクト開発

新機能と既存機能の開発のバランスをどうとってきたかを説明するために狩野モデルについて紹介させていただきます。

Image from Gyazo

狩野モデルは品質の種類とそれらの顧客の満足度の関係性を示したものです。 グラフには3種類の品質が書いてますが、元のモデルには5種類の品質が定義されています。

名前 充実した時の満足度 不足した時の満足度
魅力的品質 満足 不満足にはならない
一次元的品質 満足 不満足
当たり前品質 満足にはならない 不満足
無関心品質 満足にはならない 不満足にはならない
逆品質 不満足 満足

狩野モデルの品質をプロダクトに合わせて考えると魅力的品質が新機能、当たり前品質が既存機能と見ることができます。

カミナシにとって新機能は前例のプロダクトが少なく、ユーザーに提供してから業務に機能が組み込まれフィードバックが来るのに1,2ヶ月かかります。 一方で当たり前品質は、カミナシを導入する前は紙を利用して業務を行なっているため運用が止まらないことが前提になりやすいです。そのため高い当たり前品質を維持する必要があります。

これらの特徴を踏まえた上で、カミナシがそれぞれの品質にどうアプローチしてきたか紹介させていただきます。

カミナシの戦い方

カミナシは魅力品質と当たり前品質において以下の作戦を立てていました。

  • 魅力的品質:隣接した業務領域をカバーする
  • 当たり前品質:機能の再構築

魅力的品質

カミナシのメイン機能がカバーしている業務領域は作業後のチェック業務です。

Image from Gyazo

チェック業務はその後の集計、改善まで含めて効果を発揮します。 チェック業務だけカバーしていても紙の電子化に留まってしまい、ユーザーに価値を届けきれてないです。 そのため、チェック業務の次の工程にある集計業務を新機能として開発しました。

ここからは新機能を開発するフローと同じです。 カミナシでは最初にユーザーに現状の業務と課題をヒアリングし、どこに痛みがあるのかを把握します。 その結果、集計業務では分析・報告書・システム間連携の主に3種類あり、紙からEXCELファイルに転記しているため作業量が多い課題が存在することがわかりました。

次に3種類の業務をどういうソリューションで解決するか検討します。 当時はそれぞれの業務ごとに機能を開発することと業務に関係なくユーザーがカスタマイズできる機能が候補にありました。 魅力的品質は改革の性質を持っていますが、カミナシではなるべく元の業務とのギャップを小さくするようにしています。 そのため、集計業務として記録したデータを任意のEXCELで出力できるEXCEL変換機能をリリースしました。

当たり前品質

当たり前品質を下げる大きな要因として内部品質とUX品質があります。 UX品質はスタートアップのプロダクトの場合、作りっぱなしで放置されていると低くなりやすいです。

開発当初に想定していた仮説をもとに開発したものの、実際に使い初めてもらうと思っていたものと違うと感じる時があります。 カミナシでは当初想定していた仮説の間違いを正して、機能を再構築してきました。

具体例にダッシュボード機能を上げさせていただきます。

Image from Gyazo

カミナシのダッシュボードは予定されていた記録が行われているか確認する機能です。

当初の仮説では記録できていない数を知りたいと仮説をおいていたのですが、使いこなしてくれるユーザーが少ない状況でした。 そこでユーザーがどういう情報を知りたいのか声を集めたところ、どの現場でいつできてないのかを知りたいことがわかりました。 この学習をもとにダッシュボード機能を再構築し、どの現場でいつできてるかをOX表で表現してリリースしました。

当初はできてない現場が多くXが増えるのではないかという懸念がありましたが、現場の状況がわかりやすくなったというフィードバックをいただいてます。

次の課題

ここまで業務領域を広げて魅力品質を高めつつ、機能を再構築して当たり前品質を上げる例を紹介させていただきました。 今のカミナシはまた別な課題に向き合っています。

一つはプロダクトがカバーできる領域が1プロダクトの範囲を超えてきたことです。 カミナシは複数の業界にプロダクトを提供しているため、業界特化の業務に対応しきれていません。 これらを解決するために新プロダクトを開発していく予定です。

もう一つはプロダクトの内部品質の課題が増えてきたことです。 コードの自動テストを追加したりインフラの刷新をしていかないといけません。

これらの課題に興味がある方はEM/アプリエンジニア/SREと幅広く募集しているのでぜひお話しましょう。

open.talentio.com open.talentio.com open.talentio.com

react-native-svgで手書きアプリを作ろう

f:id:kaminashi-developer:20210428102834p:plain カミナシの浦岡です。弊社が開発している「カミナシ」には、 以下のような用途を想定して、手書きメモ機能を組み込んでいます。

  • カメラで撮影した写真の上に矢印マークやメモを追加したい
  • キーボード入力に不慣れなユーザーでも素早く簡単にメモを録りたい
  • 手書きで入力できる署名欄

ちょっとした機能ですが、弊社アプリのように現場作業で使われるケースにおいて何かと重宝されている機能です。

今回、react-native-svgを使った実装について紹介します。

手書き線の表現

手書きされた一筆、一筆をsvgのpathタグを使用して表現することにします。

pathタグはd属性に指定された座標群の情報を基に線を描画してくれます。

https://developer.mozilla.org/ja/docs/Web/SVG/Attribute/d

例えば、以下のように3点の座標を指定すると、それらを結ぶ線ができあがります。

gyazo.com

手書きイベントの取得

手書きの際のジェスチャーイベントの取得にはreact-nativeのPanResponderを使用します。

手書き対象のviewにPanResponderを設定することで以下の図のような順でイベントが取得できます。

gyazo.com

  1. 画面へのタッチ開始を起点に、pathタグを生成します。
  2. 指が移動している最中も、先ほどのd属性の値を指の軌跡に沿って更新することで表現できます。
  3. この時点で1つのpathタグを表現するための座標群が確定するので、確定情報として保持します。

コード

import React from 'react'
import { View, PanResponder, StyleSheet, PanResponderInstance, GestureResponderEvent } from 'react-native'
import Svg, { G, Path } from 'react-native-svg'

const pointsToSvg = (points: { x: number; y: number }[]) => {
  // 筆跳ね防止のための閾値
  const distanceThreshold = 40

  const filteredPoints = points.filter((point, index) => {
    if (!points[index - 1]) return true
    const distance = Math.sqrt(Math.pow(points[index - 1].x - point.x, 2) + Math.pow(points[index - 1].y - point.y, 2))
    return distance < distanceThreshold
  })

  if (!filteredPoints.length) {
    return ''
  }

  // svg-pathのd属性を生成
  let path = `M ${filteredPoints[0].x},${filteredPoints[0].y}`
  for (let point of filteredPoints) {
    path = `${path} L ${point.x},${point.y}`
  }

  return path
}

export default function App() {
  // 現在、一筆書きしている最中のパスの座標郡
  const [points, setPoints] = React.useState<{ x: number; y: number }[]>([])
  // 書き終わったパス情報の配列
  const [paths, setPaths] = React.useState<{ d: string }[]>([])

  const panResponder: PanResponderInstance = PanResponder.create({
    onStartShouldSetPanResponder: () => true,
    onMoveShouldSetPanResponder: () => true,
    onPanResponderGrant: (event: GestureResponderEvent) => {
      if (!event.nativeEvent.touches.length) {
        return
      }
      setPoints([...points, { x: event.nativeEvent.locationX, y: event.nativeEvent.locationY }])
    },
    onPanResponderMove: (event: GestureResponderEvent) => {
      if (!event.nativeEvent.touches.length) {
        return
      }
      setPoints([...points, { x: event.nativeEvent.locationX, y: event.nativeEvent.locationY }])
    },
    onPanResponderRelease: () => {
      setPoints([])
      setPaths([
        ...paths,
        {
          d: pointsToSvg(points),
        },
      ])
    },
  })

  return (
    <View style={styles.container} {...panResponder.panHandlers}>
      <Svg width="100%" height="100%" preserveAspectRatio="none">
        <G>
          {/** 書き終わったパス情報の描画 */}
          {paths.map((path, index) => (
            <Path
              key={index}
              d={path.d}
              stroke="black"
              strokeWidth="3"
              strokeLinecap="round"
              strokeLinejoin="round"
              fill="none"
            />
          ))}
          {/** 現在、一筆書きしている最中のパスの描画 */}
          <Path
            d={pointsToSvg(points)}
            stroke="red"
            strokeWidth="3"
            strokeLinecap="round"
            strokeLinejoin="round"
            strokeDasharray="4"
            fill="none"
          />
        </G>
      </Svg>
    </View>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'flex-start',
    width: '100%',
    height: '100%',
  },
})

動作イメージ

gyazo.com

最後に

以上、シンプルな手書きアプリができました。 もっと多機能にしてみたい!と興味持ってもらえた方がいたら、ぜひ自分向けの手書きアプリにカスタマイズしてみてください!

【Expo Go】アプリケーションがクラッシュして解決するまでの話

f:id:kaminashi-developer:20210426191004j:plain

こんにちは、株式会社カミナシのエンジニア @ImuKnskです。

はじめに

アプリケーションが急にクラッシュすることありますよね? 昨日まで動いていたのに何故かクラッシュ…。 アプリケーションがOSSの場合、特に切り分けが大変ですよね。

カミナシでは『Expo Go』を使ったアプリケーションの開発を行っており、急にアプリケーションがクラッシュしてしまい解決に至るまでの話をしてみようと思います。

突然クラッシュするアプリケーション

まずはこちらをご覧ください。

gyazo.com

ローカル環境でExpoを起動した後、iPadQRコード読み込んでアプリケーションが起動する間もなくクラッシュしています。始めは古いキャッシュでも残っているのかな?と思って、再起動やアプリケーションの削除を行いましたが、この事象は直りませんでした。

何かしら問題が起きていると思い調査を始めました!

どういう調査をしたか
  • シミュレーター等の起動確認

問題なく起動し、設定等は問題ないと思っていました。(最終的にはここに問題であることを知ります)Expo Publish(OTA Update)の確認や、developmentモードでの起動やリリースに問題ありませんでした。

  • 利用しているPackage問題

まっさらなExpoプロジェクトを作成して、『カミナシ』で使っている『package.json』を入れて起動してみると起動できる…。どうやら『package.json』が悪さをしているわけじゃない。

  • OSSGithub issueやアプリケーションのバージョンを確認

『Expo Go』側に問題があるのではと思って確認したところ、2日前に『Expo Go』がバージョンアップしていることを知り、これが影響しているのではないかと思い始めました。Github issueを検索したり、インターネットで『Expo Go Crash』等で検索してみたりしました。最終的には有益な情報はなく、ひたすらクラッシュしていたので開発者にクラッシュレポート届け!と思っていました。

  • カミナシのアプリケーションの修正履歴

アプリケーションがクラッシュしているので設定かなと思い、設定ファイルの履歴を洗いましたが直近での変更はなく、アプリケーションの問題じゃなさそう…と決めつけていました。早く『Expo Go』の変更リリースないかなと思う日々でした。

ある日舞い降りてきた解決の糸口

エンジニアメンバーにクラッシュするんですよね…早く変更リリースないですかねー。と会話していたとき、クラッシュした様子を見ると『Splash Screen』が表示される前にクラッシュしてないかと思いました。

『app.json』でファイルを参照しているところは『Splash ScreenとGoogleService-Info.plist』でした。

まずはファイルを読み込んでいるプロパティを削除したところ…

起動した!!!

ということは、ファイルがおかしいと思い1つずつ確認したところ『GoogleService-Info.plist』ファイルが悪さをしていることが分かりました。

『GoogleService-Info.plist』ファイルの確認

ローカル起動の場合、ファイルはダミー値を入れて利用していました。

  • developmentで使っているファイルに差し替える

当然ではありますが、起動しました。

  • localとdevelopmentの差分を確認

ローカルとdevelopmentでプロパティの不足があり、ダミー値を追加しましたが起動しませんでした。

よく見ると一部プロパティ名が…

f:id:kaminashi-developer:20210423091333p:plain

『痛恨のスペルミス』

ここにたどり着くまで時間が掛かりましたが、完全に思い込みでした。これまでは起動していたので設定ファイルのミスはない、と思っていたが実は最初から起きてもおかしくなかった事象でした。 (それなら『Expo Go』で最初からクラッシュして欲しかった気持ちはありますが、OSSなので利用する側の問題ですから何にも言えませんね…)

スペルミスを修正すればちゃんと起動しました(笑)

おわりに

思い込みから入る調査は時間が掛かることが身にしみて分かりました。まずは何事も疑うことから始めて調査に取り組みたいと思いました!個人的にはこういった調査は好きなので苦痛じゃなかったですが(笑)

解決してスッキリです!

最後に弊社ではエンジニアを募集しております。 興味がある、話を聞いてみたい、応募したいという方はお気軽にご応募ください!

open.talentio.com

StoryShotsの可能性を探る

こんにちは、カミナシの@tomiです。

前回は、Expo ReactNativeにStorybookの導入を行いました。

今回は、StoryShotsを入れてみたいと思います。

StoryShotsとは、自動スナップショットテストができるStorybookのアドオンです。 内部的には、JestのSnapshotが動作し、UI が予期せず変更されていないかを確かめるツールです。

なぜ入れてみようと思ったかというと、ただStorybookを導入しただけでは、メンテナンスされずに腐ってしまうのが目に見えているので、楽に忘れることなくstoryの更新ができる方法ないかなと調べていると、よくStoryShotsを目にしたので試してみることにしました。

StoryShotsの実際の動作を見て、今後の運用に必要かどういったときに役に立ちそうかを体験したいと思います。

StoryShotsを導入する

Storybookの導入は前回の記事で済んでいるので、StoryShotsのアドオンを入れます。

yarn add -D @storybook/addon-storyshots@^5.3
# monorepoの場合↓
# lerna add @storybook/addon-storyshots@^5.3 --scope=mobile --dev

ReactNativeに対応しているStorybookのバージョンに合わせて5.3をインストールしました。

/storybookディレクトリにStoryshots.test.tsファイルを作成。

// Storyshots.test.ts
import path from 'path';
import initStoryshots from '@storybook/addon-storyshots';

initStoryshots({ configPath: path.resolve(__dirname, '../storybook') });

ドキュメントでは、initStoryshots()だけで動くようでしたが、monorepo構成だからかconfigPathでStorybookの設定を行っている場所を指定してあげる必要がありました。

次に、StoryShotsの実行用のjest.configを設定します。

jest.config.jsと同じ場所にjest.config.storyshots.jsを作成。

// jest.config.storyshots.js
const path = require('path')
const baseConfig = require('./jest.config')

module.exports = {
  ...baseConfig,
  testMatch: ['**/packages/mobile/storybook/Storyshots.test.ts']
}

testMatchの部分を、StoryShots用のテストだけを読み込むように設定します。

こうすることで、ユニットテストと自動スナップショットテストを別で実行できるようにしています。

StoryShotsを実行するscriptを設定します。

// package.json
"scripts": {
  "storyshots": "jest -c jest.config.storyshots.js"
}

StoryShotsを実行する

では、実際にStoryShotsを実行するとどういう結果がもらえるのかを見てみます。

yarn storyshots

f:id:kaminashi-developer:20210422135244p:plain

*.stories.jsが存在する3件のSnapshotsが生成に成功しました。

そして、__snapshots__/Storyshots.test.ts.snapというファイルが作成されました。

中身は、Componentファイルがレンダリングされた形のようです

f:id:kaminashi-developer:20210422124254p:plain

差分がでるとどうなるのか

スナップショットが作成されたので、次にコンポーネントを更新して差分を出すとどういう結果がでるかを確認しようと思います。

Welcome/index.jsというコンポーネントに適当にテキストを追加して、再度SnapShotsを実行してみます。

f:id:kaminashi-developer:20210422124334p:plain

すると1件のスナップショットが失敗しました。

失敗した内容として今回変更した部分が表示されます。

f:id:kaminashi-developer:20210422124401p:plain

これが意図した変更の場合はスナップショットを更新しましょう。実行コマンドに-uオプションを付けてあげます。

yarn storyshots -u

1件が更新されました。

f:id:kaminashi-developer:20210422124435p:plain

なるほど、なるほど。 StoryShotsがどういうものか掴めてきました。(いやな予感がする・・・)

次に、Welcome/index.jsを使っているコンポーネントWelcome/Wrap.tsxを作成してみます。

// Welcome/Wrap.tsx
import React from 'react'
import Welcome from './index'

const Wrap: React.FC = () => {
  return <Welcome />
}

export default Wrap

Storyにもこのコンポーネントを追加します。

// Welcome/Welcome.stories.jsに追記
import Wrap from './Wrap'

storiesOf('Welcome', module).add('to Storybook Wrap', () => <Wrap showApp={linkTo('Button')} />);

そして、StoryShotsを実行します。

yarn storyshots

1件追加されました。

f:id:kaminashi-developer:20210422124518p:plain

では、ここでWelcome/index.jsを更新したらどうなるでしょうか?

以下が実行結果です。

f:id:kaminashi-developer:20210422124545p:plain

2件のスナップショットにエラーがでました。

失敗したスナップショットはWelcome/index.js自身とそれを使用しているWelcome/Wrap.tsxです。

StoryShotsによって、コードを変更した際の影響範囲がわかることで、予期せぬところで使われていて画面が崩れてしまったといったミスを防ぐことができそうです。

StoryShotsの効果

StoryShotsを実際に触ってみて、受けれる恩恵は大きく2点かなと感じました。

Storybookの更新漏れを防げる

ただのUI変更ではほぼStoryShotsは必要ないでしょう。

ですが、例えばButtonコンポーネントにcolorというプロパティを追加したとき、StoryShotsを入れておくことで、storyの更新もしなければと気づくことができます。

ただし、プロパティを追加してyarn storyshots -uで更新してみましたが、テストが通ってしまったので完全にStorybookの更新漏れを防ぐことができないかもしれません。

f:id:kaminashi-developer:20210422124602p:plain
Warningは出ているが、StoryShotsは更新される

予期せぬ更新を防げる

変更したコンポーネントが想定しているところ以外に使われていることに気づけるので、レイアウトの崩れや動作しないといった不具合を減らすことができそうです。

ただし、Storybookが入っているところだけが検知できるので、すべてのコンポーネントにStorybookが入っていないとあまり意味がなさそうです。

まとめ

StoryShotsを導入して、どういう効果があるかを確認してみました。

結果的にいまのフェーズでStoryShotsを入れるのは、あまり効果が期待できないと感じました。

Storybook導入の目的が新デザインガイドラインの運用・管理なので、基本となるコンポーネントにしかstoryを追加しないため、恩恵を受けづらそうです。

プロジェクトのはじめからStorybookを入れており、全コンポーネントのstoryを作成していたのであれば、非常に重要なツールかなと思います。