アルパカ証券の裏側 - フロントエンド
今回は、アルパカ証券のフロントエンドについて述べます。
アルパカ証券ではReactを採用し、SPA(Single Page Application)として実装しています。また言語としてTypeScriptを初期から採用しています。
技術スタック
TypeScript
それなりに大きな規模のソフトウェアになるため、TypeScriptによって型レベルでの安全性を担保することで開発速度が出ると考えていたため採用しました。
特に型情報が有効なのはサーバーとのAPI部分です。JavaScriptではなにもないと勝手に変換されて不正な値になってしまうことがありますが、TypeScriptである程度保護されています。このAPI部分は前回述べたようにgRPCのprotoで定義されているのでこれを利用できます。
gRPCのproto定義からTypeScriptへの変換は、以下のように行います。
- 一度protoc-gen-swaggerを使用してswagger定義に1回吐き出す
- そこからさらにopenapi-generatorを使用してTypeScript定義を生成する
openapi-generatorからのTypeScript生成はいくつか種類がありますが、型定義が読みやすく使用しやすかったtypescript-fetch
を選択しました。
なお、現在のgRPC Gateway実装ではOpenAPIv2ですが、まだprotoc-gen-swaggerを使用しています。
フロントエンドからgRPCを使う方式として gRPC-webというブラウザから直接gRPCを投げる方式もあります。さすがにそれは現時点ではやりすぎなのでgRPC Gatewayを使用し、フロントエンドからはJSONを利用するようにしています。
React/Redux
描画部分では以下のようにContainerとComponentを分けています。いわゆるPresentational/Container Component Patternです。
- Container: Reduxなどとつなぎこむ
- Component: 画面を描画する
各画面ごとにReduxのreducerを作成しています。reducerごとにmoduleがあり、外部サービスとやりとりするのはmoduleの中だけ、という規約で実装しています。
ReactはCreate React Appを利用してbuildしています。融通が効かずどうしようと途方に暮れる時もありますが、フロントエンドの変化は激しいためCreate React Appを使っていればある程度乗り遅れないようにできるだろう、ということで使い続けています。今のところejectしていませんが、いつでも必要があればejectする体制にはあります。
React Hooks
フロントエンドの開発が始まったのはReact Hooksがリリースされて間もない頃でした。HooksになるとFunctional Componentが強制されることにより、多くの書き換えが必要になります。npm上にある多種多様なパッケージが実際にHooksに対応してくるか、当時は未知数だったためにHooksを採用しませんでした。
その後Hooksに移行していく様子が見られたため、2019年後半ぐらいから新規に実装する部分に関してはHooksを使うようにしていきました。書き始めて分かったのですが、HooksによりむしろFunctional Componentを強制され書き方が統一化されたり、Hooksとして処理を切り出せたりするので、表記が直感的になりました。もっと早くから使い始めておけばよかったかな、とあとから思ったりもしました。
Styled Components
CSS in JSのライブラリとしてStyled Componentsを利用しています。原則としてコンポーネントと1対1で作成し、そのコンポーネント内のスタイリングをすべて記述するようにしています。この場合、コンポーネントが大きくなりすぎると合わせてStyled Componentsも大きくなり、大変読みにくくなるため、コンポーネントを適切な粒度で分割するように心がけています。
デザイン
アルパカ証券では、3つの画面を持っています。
- 投資家向け
- IFA向け
- アルパカ社内向け
技術スタックとしてはすべて同じです。投資家向けにはブランドを構築するため下の例のようにアルパカ証券用のデザインセットを作成し使用しています。
投資家向け画面の例 ※“アルパカ化学“は、画面操作の説明にあたり表示している架空の銘柄です。銘柄コードや会社説明、現在値、更新時間、前日比、AI予報アイコン、決算情報・企業情報・株主優待情報等は実在のものではありません。
IFA向けおよびアルパカ社内向けについてはあまりデザインにこだわる必要がないため、Blueprintを使用しています。 Blueprintは多くのコンポーネントが実装されており、そのまま利用することで一貫した見た目になるため開発が楽になりました。
Figma
デザインは社内のデザイナーがFigmaを使ってデザインしており、開発者はそれを見ながら実装します。また、Figmaの段階から後述するAtomic Designを意識してデザインしています。
Atomic Design
ReactのコンポーネントはAtomic Designをベースに分類しています。ざっくりと、そのプロダクト特有のコンポーネントをorganisms、それ以外のプロダクトでも使えそうなものをmoleculesという分け方にしています。
テスト
Jest + Testing Library
テストにはJestとTesting Libraryを使っています。
Jestで全体のテストを実行しつつ、Reactの各コンポーネントはTesting Libraryでテストをしています。
ちなみに今さっき実行したカバレッジはこんな感じです。
File | % Stmts | % Branch | % Funcs | % Lines |
---|---|---|---|---|
All files | 88.65 | 86.06 | 83.09 | 88.7 |
元々カバレッジは100%を目指してはいませんし、テスト対象を絞っています。formatterやvalidationなどのロジック部分については原則テストを記述しています。一方、コンポーネントに関しては必要に応じて記述しています。
Storybook
コンポーネントの見た目がpropsの変更でどう変わるかなどはStorybookで確認しています。
弊社ではCircleCIを利用しています。pushするたびにStorybookを静的に生成し、自動的にそのビルドのURLをslackに通知しています。そのため、PRを出した後、すぐに見た目を全員で確認できます。 原則として、pageを除いたすべてのコンポーネントでStorybookを作成することにしています。導入のタイミングが良かったこともあり、Component Story Formatで書いているので、そのままTesting Libraryでも利用ができています。
E2Eテスト: Autifyを試用中
普段はチーム内でE2Eテストをしています。一部Seleniumを使って自動テストをしているところもありますが、実装コストが高くなかなか進まない状況でした。
最近Autifyを試しているところです。最終的に使うかどうかはまだ分かりませんが、今の所チーム内ではいい感触です。
WebAPIサーバー
TypeScriptはビルドして静的ファイルとしてWebサーバーから提供します。このWebAPIサーバーは、HTTP APIを受け付けてgRPCへ変換する役目も担っています。 静的ファイルの提供のためにnginxを挟むことも検討しました。しかし、gRPCへ変換するためにgRPC-Gatewayを使うため、必然的にGoで実装することになります。Goの性能は十分ですし、そもそもCloudFrontを挟んでいるためOriginサーバーに到達することも少ないだろうと判断し、Goで直接静的ファイルを提供しています。
認証
認証基盤にはAuth0を使用しています。ログインするとAuth0からJWTを受け取ります。すべてのHTTP API呼び出しにはAuthorizationヘッダーにこのJWTをつけて送信します。
リクエストを受け付けたWebAPIサーバーはJWTの期限などを検証した後にgRPCへ変換し、バックエンドサーバーに投げます。
JWTを使用する場合、即時無効化ができないという問題が生じますが、前回述べたようにバックエンド側でも無効なユーザーかどうかなどの権限チェックをしているため、問題は起きていません。
トレース
WebAPIサーバーからバックエンドへ投げる時に、OpenCensus形式でTraceIDを付与しています。具体的には以下のようにgRPCのmetadataに追加しています。
getTraceID()
内ではブラウザ側からTraceIDが付与されてきたらそれを、付与されていなかったら新規にTraceIDを作成しています。
func addDefaultMetadata(ctx context.Context) (context.Context, error) {
meta, ok := metadata.FromOutgoingContext(ctx)
if !ok {
st := newError(codes.Unauthenticated, msgUnauthorized, "could not get outgoing context")
return nil, st.Err()
}
return metadata.NewOutgoingContext(ctx,
metadata.Join(meta,
metadata.Pairs(
traceIDKey, getTraceID(meta),
),
),
), nil
}
これにより、フロントからバックエンドまで一貫したトレースができるようになります。
まとめ
今回はTypeScriptおよびWebAPIサーバーというフロントエンドに関連するコンポーネントについて説明しました。
おまけ
アルパカ証券では一緒に新しい証券システムを開発してくれる仲間を募集しています。ご興味がありましたら、こちらを御覧ください!