新規事業をひとりで作るノウハウ

生存報告も兼ねて。
 
カリフォルニアに来てもう半年ぐらい経った感覚ですが、実はまだ4ヶ月ほどでした。非常に多くの素敵な方々との出会いがあり、妻も僕も子供もこの皆さまの助けがあってどうにか生きております。どう感謝してよいか言葉にできないほどです。
 
さて、ビジネス上の僕のミッションは次の3つです。

  1. 主に投資や連携目的の交渉(の技術面のサポート)
  2. 日本との連携
  3. 新規事業の開発

どれもなかなか難しいです。会ってアポぐらいなら応じてくれる会社も多いですが、投資や連携といってもバブル崩壊以後経済成長できていない我が国はもはや「商習慣だけめんどくさいのに今やカネも持ってないから相手にしてられない連中」というのは肌で感じます。ご存知の通り、サンフランシスコ・ベイエリアはIT企業会社員が年収5000万円もらうような場所です。なかなか同じ規模感で会話するのが難しいレベルに達しています。

さて、クヨクヨもしていられないので、僕はプログラマの本分たるプログラミングによって少しでも生きた証を残すより他ありません。渡米以来自分の持ち時間の30-40%ぐらいを使って新規事業のプライベートアルファを公開するに至りました。対象ユーザが弊社内記者ということもあり、現時点でみなさまにお使いいただけないのは残念でしょうがないのですが、たったひとりでもサービスを公開まで持っていけるというのは我々にとって明るいニュースなので書いておこうと思います。
 

TL; DR いま新規事業やるならFirebase1択

今回自分が作ったのはウェブサービスですが、とにかくFirebaseが強いです。僕がこれから起業するなら、まずはFirebaseで作って運良く流行りそうならお金がかかる部分だけ書き換えるみたいなアプローチを間違いなく採るでしょう。今回使った技術スタックは次のとおりです。

TypeScript

後述するCloud FunctionsがNodeランタイムで動くのでJavaScriptは好むと好まざるとにかかわらず使わざるをえませんでした。僕は強い静的型付けが好きなのと、FunctionsにTypeScriptの例がたまたま載ってたのでTypeScriptを選びました。Flowを選ばなかったのは「店頭に並んでなかった」ぐらいのことで深い理由はありません。

まったく初めてでしたが次の2冊を読んで各30分の計60分でとりあえず書けるようになったのでおすすめです。そもそもC言語の子孫の言語なんてどれも青森弁と山形弁ぐらいの違いしかありません。

比較的快適に開発していますが、関数オーバーロードがさながらC言語のプロトタイプ宣言であるところや、これらで引数に取るインタフェース型がtype erasureによりinstanceofが使えないことから少し持って回ったようなハックが必要な点などいくつかまだ慣れないところはありますが、許容範囲というところです。

tsc watchは文句なく素晴らしく、息をするようにトランスパイルしてくれます。JSをまったく意識することがありません。

ユニットテスト

ユニットテストchai, mocha, ts-nodeを使っています。このエントリを真似しました。

Unit testing node applications with TypeScript — using mocha and chai

async/awaitを多様しているのでプラスしてchai-as-promisedを入れています。次のように使います。

import * as chai from "chai";
import { expect } from 'chai';
import * as chaiAsPromised from "chai-as-promised";
import 'mocha';
import { NLP } from '../nlp';

before(() => {
    chai.use(chaiAsPromised);
    chai.should();
});

describe('NLP', () => {
    const nlp = new NLP();

    it('should return [Amazon Alexa]', async () => {
        const keyword = "Amazon Alexa の取材";
        const list: string[] = await nlp.analyzeEntities(keyword, defaultFilter);
        expect(list).is.not.empty;
    });
});

ts-nodeでテストケースを個別に実行する方法がよくわからんかったので

{
  "name": "functions",
  "scripts": {
    "testall": "mocha -r ts-node/register src/**/*.spec.ts",
    "test": "mocha -r ts-node/register src/spec/$spec.spec.ts"
  }
}

みたいにして全体実行はnpm run testall, 個別実行はspec=[クラス名] npm run testみたいにしています。
もっといい方法があったら是非コメントください。

namespaceがイマイチ使いこなせない

なにぶん全てひとりでやっており誰もコードレビューしてくれないので経験者からすると眉をひそめるような作法で書いている可能性が高いです。
たとえばnamespaceはKotlinのpackageの感覚でディレクトリを掘ってファイルも分けていますが、次のようにfoo.tsとbar.tsを別ファイルに分けて同じ名前空間の下にぶら下げた場合、

// my_service/foo.ts
export namespace MyService {
    export class Foo {}
}

// my_service/bar.ts
export namespace MyService {
    export class Bar {}
}

利用する側で名前空間がバッティングしてるとimport * as aliasするしかなくてなんか不格好です。これはこういうもんなんでしょうか🤔
たぶんTSの作法がある気がするのでTS Wayをどなたか教えてください。

// import {MyService.Foo} from './my_service/foo'  no good!!
// import {MyService.Bar} from './my_service/bar'  no good!!

import * as foo from './my_service/foo'
import * as bar from './my_service/bar'

export class Baz {
    qux() {
        const hoge: foo.MyService.Foo = new foo.MyService.Foo()
        const fuga: bar.MyService.Bar = new bar.MyService.Bar()
    }
}

Cloud Functions

いわゆるサーバレスにファンクション単位でデプロイできるやつです。これはとにかく素晴らしいです。

export const foo = functions.https.onRequest(async (request, response) => {
    try {
        const token: Credentials = await readToken(request.query.account)
        const resp = doSomething(token)
        response.send(resp);
    } catch (error) {
        sendError(response, error)
    }
});

こんなの書いておくだけで設定したエンドポイントに対応するファンクションが起動します。
HTTPトリガの他にバックエンドで各種イベントトリガに応答して処理を行うことができます。後述の通りPub/Subも使えます。
これの何がいいって、フレームワークだのミドルウェアだのつまらない知識をためなくても一筆書きでとりあえずサービスを提供できるんですよね。

サーバレスというと弊社ではAWS Lambdaを使っていますが僕はCloud Functions 1択でした。それはFirebaseとの連携性です。FirebaseのリアルタイムDBであるFirestoreのドキュメントへのイベントを検知してFunctionをトリガできるのは便利です。他にも認証トリガなんてのもあってアツイです。

Cron Job

単独でcron jobはできませんが、これはGAEと組み合わせればできます。次のエントリに詳しいです。

developers-jp.googleblog.com

端的にいうとGAEの持つCron機能をPub/Subで購読するというものです。GAEを一切使ったことがなくてもこのエントリの通りにすれば'hourly-tick'だの'minutely-tick'だのに反応するファンクションを書いておけばトリガされます。

export const batch = functions.pubsub.topic('hourly-tick').onPublish(async (event) => {
 // do whatever you like
});
Local Emulation

ファンクションはデプロイする前にローカルでいくらでも試せます。

Run functions locally  |  Firebase

リンクにほとんど書いてあるので特に補足はないのですが、firebase serve でローカルにまるっきりfirebase hostingとcloud funcionsのローカル版が立ち上げるのでcurlなりブラウザなりで試すことができます。
firebase functions:shellとかするとTCP/IPではなく対話的シェルのような感じでファンクションごとにエミュレートができます。ここでFirestoreの読み書きもテストできます。
個人的には前者で実際にFirestoreにつなぎに行ってInstrumentedにテストできるので後者はほとんど使ってませんが便利な使い方を今後発見するかもしれません。

それから、OAuth2のリダイレクトとかをテストするときにLocal Emulator環境なのかデプロイ後だったのか区別する必要があり、これはちょっと簡単にわからなかったので環境変数のようなやつを使いました。

Environment configuration  |  Firebase

これはコマンドラインから設定できる環境変数的なものです。

firebase functions:config:set my_service.is_local=true

みたいにして設定するとサーバに直接設定されるんですが、ファンクションのディレクトリに.runtimeconfig.jsonという形式で置いておいて適宜中身を書き換えるとローカルエミュレータではその値が使われます。アクセスするのはローカルもデプロイ後も同様です。

firebase functions:config:get > .runtimeconfig.json
const IS_LOCAL: boolean = JSON.parse(functions.config().my_service.is_local);
const HOST_NAME = IS_LOCAL
    ? `http://localhost:5001/${process.env.GCLOUD_PROJECT}/us-central1`
    : `https://${process.env.GCLOUD_PROJECT}.firebaseapp.com`;

Cloud Firestore

これはドキュメントベースのNoSQLなんですが、JSON状のデータ構造のあるパスを購読しておくとそこに対する読み/書き/削除/更新を全部通知してくれます。
オフライン対応してる、というか、利用者はすべてローカルコピーを真として読み書きする作りになっており、「オフラインになった!」という状態が存在しないと考えることができます。
結果、安全にデータの読み書きができます。
オンラインになるとこのローカルコピーは勝手にサーバのマスターデータともいうべきものと同期をかけます。同期後はそれが前述の更新イベントとして飛んでくるだけというわけです。

ここに敢えて貼りませんが、僕は2014-5年からFirebaseユーザでその頃はFirebaseといえばこのリアルタイムデータベース(いまのFirestoreはその後継)しかなく、僕はそれの連載記事を持っていたぐらい愛好者でした。
で、新規事業にあえてこのリアルタイムDBを使う必要もないんですが、別に使わない理由もなくて、割と雑になんでもデータを突っ込んでいます。ここも高くなったら置き換えるぐらいの気持ちです。早すぎる最適化は不要です。

Firebase Authentication

これは認証を楽ちんにしてくれるライブラリです。

Get Started with Firebase Authentication on Websites  |  Firebase

これもほとんどコメントの必要もないほど充実してるんですけど、認証時にFirebase内で一意なユーザ識別子を持って、あとからGoogleだのGitHubだの別の認証プロバイダで認証したときにそれらを全部ひもづけることができるので、複数の認証プロバイダに対応予定なら激つよです。

一点、この方法だとリフレッシュトークンがどうやらもらえない(リフレッシュ自体はできる)っぽくって、リフレッシュトークンがないと裏でCron Jobでトークンを更新しつつ他のAPIを叩くみたいなのができないので、僕は結局後述するGoogleのOAuthライブラリで手認証するのと組み合わせています。

Google APIs Node.js Client

これがとにかく素晴らしい。Google謹製のNodeライブラリ群ですけど、

  1. TypeScriptで書かれていて型情報がそのまま利用できるものがほとんど
  2. 非同期処理はほとんど余すところなくPromiseを返すようになっており、こちらが努力ゼロでasync/awaitできる

もう説明の必要もないですね。async/awaitできると同期的に待ち合わせるように非同期のコードが書けるし、他言語で同様の仕組みをもったプログラマが参入しやすいです。プログラムのバグと苦しみは非同期処理に根ざすものが非常に多いですからね。
一点、Listをmapしながら全てのasyncの完了を待ち合わせるような処理は書き方をすぐ忘れるのでメモしときます。

const tokens: [string, Credentials][] = await readTokens()
const resultAll = await Promise.all(
    tokens.map(async token => {
        const identifier = token[0]
        const credential = token[1]
        return result = await doSomethingWithToken(credential)
    })
)

あとは複数の非同期処理を一気にdispatchしてzipper関数で待ち合わせてまとめるzipみたいなのはちょっとまだ見つけてません。
まあそのものズバリのRxJSってのもあるしなんとでもなりそうです。

Cloud Natural Language

最後ですけどGoogle製の自然言語処理のライブラリを使いました。ノイズの多い文章から品詞ごとに取り出したりするのに非常に便利です。
この部分はサービスが大ヒットしたら自前のライブラリで置き換えても良いのだし、最初にNLP詳しくない僕みたいな者が曲がりなりにも実用に耐えうるデモを作れるのは、この分野では考えにくいことだったので、まったくありがたい話です。

まとめ

「ガチガチにロックインされてそうだけど大丈夫?」的な質問を飲み屋の会話(僕は一滴も飲めませんが)レベルでされたことがあったんですが、それが何か?という感じです。
言語、フレームワーク、思想とプログラマは色んなものにロックインされています。価値検証段階でそれらが何かにべったり依存していることは取るに足らないことだと思います。そもそも僕の稼働時間は僕一人が活動時間の3-4割で実質ひと月ぐらいで書いたので、本当にイケるとなったらいくらでも書き換えたらいいです。

最後にこの開発を開始して僕の人件費を仮にゼロと考えた場合の総コストは…!

f:id:fushiroyama:20180807042638p:plain

ジャーン!開発期間中0ドル、サービスインして本日までの合計は77セントでした!ありがとうございます!