業務委託で入って3ヶ月でやったこと

フルスタックエンジニアで活動してるドラレプです!
今はフリーランスで同時に複数の案件を抱えながら活動しているため面接の機会は多いのですが、なんとなく『フルスタックって役割が見えづらい』『週2-3労働のイメージが湧かない』印象を受けました。
そのため自分自身の振り返りの意味も込めて、実際に契約を結んでからの3ヶ月に何を考えて何をしたのかをまとめていきたいと思います。
背景
今回お手伝いさせていただいたところは、ざっくり以下のような技術スタックでした。
- React/Redux/TypeScript
- Elixir/Phoenix
- Kotlin/Javalin
私自身React/Redux/TypeScriptはここ最近はずっと触っていて慣れ親しんだ技術で、実際にオファーとしてもここの改善を期待されることがほとんどです。
一方でElixirは初、Kotlinはちょっと触ったぐらいなので、バックエンドに関しては言語・フレームワークの特性をキャッチアップしながら進めていくことになりました。
立ち回り
まず全体的にコードを眺めて気になった点を都度質問し、対応方針を決めていく流れとなりました。
もちろんただタスクをこなす役割でも良いですが、先方としても「根元の改善を行う役割を期待したい」という話だったので、この3ヶ月はDX改善を中心に進めていくことになりました。
フロントエンド
まずは、フロントエンドを中心に改善です。
理由は、次のとおりです。
- バックエンドに比べ重篤な不具合に繋がりづらい。
- バックエンドに比べコードを捨てやすい。
- 技術背景的に良く知ってるので、改善点も見つかりやすい。
- 小手先の改善で大きなリターンが得られやすい。
linterの調整
linterはfix
かけるタイミングさえ用意できれば問題も起きづらい箇所なので、まずはlinterの設定から調整していきました。
linter周りの課題感としては、次のような点でした。
- deprecatedであるtslintを採用していた
- 環境に合わせた設定になっていなかった(たとえば
eslint-disable-nextline
ほぼ全てのファイルで行なっていた)
そのためeslint/prettierの構成にし、ルールのカスタマイズ、ReactPluginの追加などで基本はdisable
せずに開発できるようにしました。
TypeScript周り
TypeScriptを採用していたのですが、以下のような課題感がありました。
- 型定義されてない箇所が多かった
- 型エラーが放置されていた
- anyやas、non-null assertionが多用されていた
これは、次のような負のスパイラルに陥ってるためと考えました。
- エラーを直す場当たり的な対応が中心となってしまう
- 型の恩恵が受けられなくなっていく
- 型定義するモチベーションが湧かない
そのため、まずは強引にでも「useSelectorでちゃんと推論されて便利」みたいな体験を積み重ねていく必要があると感じました。
したがってまず型の恩恵を受けられるようにすることをゴールに置いていきます。
とはいえ大幅な変更はメンバーがついてこれないこともあるので、反応を見つつ少しずつ進めていきました。
型エラーの検知
既存の型エラーを全て修正し、CIでtsc
を回すようにしました。
TypeScriptのコンパイルをbabelからwebpackに移せばwatch時に型エラーを検知できるようになるのですが、Elixirからwebpackを呼び出していてlogが見づらい設計になっていたので、あまりワークしませんでした。
将来的にはElixirから切り離して自分でwatchする運用にしたい気持ちもありますが、課題自体は解決できたので深入りはやめました。
anyやas、non-null assertionが多用され、実質型が機能していなかった
これは地道に潰していき、直しきったあとstrictに変更する愚直な作業です。
ただNon-null assertionについてはネストの深いオブジェクトのnullチェックを避けるために使われているようで、そのままだと既存メンバーの開発効率が落ちてしまうことが危惧されました。
そのためTypeScriptのバージョンを上げ、Optional Chainingを使えるようにし、存在を周知しました。
Redux周りの型定義の充実
特にRedux周りの型が全然定義されておらず、Storeに何が入るのかコードを追わないとわからない状態でした。
例えばuseSelector
で以下のように呼び出し先で毎回定義されており、後から変更もしづらい状況でした。
useSelector((state: { user: { auth: { token: string }}}) => state.user.auth.token)
そのためまずは実装を見ながら定義し直し、DefaultRootState
等の設定をしました。
これによりRootStore
の型が全体に適用され、useSelector
での型指定が不要になりました。
ただ今後もStoreの型定義がなされないと意味がないため、redux-tool-kitを採用し、ライブラリに頼れば型が付与されるようにました。
テスト/storybook用のfactoryの作成
テストやStorybookで使うオブジェクトがそれぞれ個別に定義されていて、型定義が変更されると様々な箇所で型エラーになる状況にありました。
またパラメータが多かったりネストしてる場合にテスト用のオブジェクトの生成が面倒で、そもそもテストされないようなケースも見られました。
そのため簡単なfactoryメソッドを用意して、型定義通りのオブジェクトが簡単に生成できるようにしました。
export const makeUser = (user: Partial<User> = {}) => ({
name: "太郎",
age: 20,
company: makeCompany(),
...user
})
特にRedux Storeの挙動は今まで個別にMockされており、後からStoreの定義が変わった際にもテストがその変更に追随できない状況にありました。
しかしこれで目的に合わせたStoreの状態を簡単に生成できるようになり、Storeも含めたテストもやりやすくなりました。
Style周りの問題
Style周りの課題感としては以下の通りです。
- Global CSSが肥大かつCSS ModuleとComponentの対応関係が複雑で、UIが壊れやすかった。
- 歴史的に色々な外部のデザイナーが設計していたらしく、UIが統一されていなかった。
方針の策定
Mgr/現在の担当デザイナーも同様の問題意識を持っていたので、以下の要素をデザイナー中心に提案してもらう流れとなりました。
- Colorやマージンのルール
- px/remの方針
- ButtonやCardなどの基幹コンポーネントのデザイン
今後のUIに関してはその方針に従い、既存のものについては少しずつ統一させていく方針となりました。
Styled Component
CSS Modules自体は悪い技術ではないものの、今の運用では依存関係がぐじゃぐじゃになりやすい課題感がありました。
方針を相談したところ現フロントエンドメンバーはStyled Componentを使いたいとのことで、導入を進めてくれました。
その他
CIでのStorybookの生成
デザイナーにStorybookを共有する際にzipファイルを渡す運用をしていたので、CIでStorybookをgenerateしてartifactへ格納するようにしました。
Responsiveロジックの改善
sp/pcの判定ロジックがglobalに格納されていて、一度計算されたら再計算されない実装になっていました。
そのためまずStateに保持するCustom HookであるuseMedia
を作り、変更に合わせ再計算・再描画されるようにしました。
ただそれだとStyled Componentに渡すのが面倒とのことだったので、styled-media-queryっぽいものを作りました。
OpenAPI
フロントエンドの型の課題の延長として、バックエンドからの戻り値の型定義の問題がありました。
そのためOpenAPI Schemaを橋渡しにコードの自動生成をすることで一貫性を持たせるため方針を相談しました。
状況
今はバックエンドのOpenAPI Schemaを独自定義する運用になっていました。
ただあくまでコミュニケーションツールとして独立して運用されていたため、Schemaとコードの整合性の担保は取れていない状況でした。
バックエンドからSchemaの自動生成
スキーマファーストにするか、バックエンドから生成するか相談した結果、今の開発スタイル的にバックエンドからswagger.yamlを自動生成した方が良いとの結論に至りました。
そのためフレームワークのOpenAPI Pluginからバックエンドのコードから自動でswagger.yamlを出力するようにしました。
Schemaからクライアントの自動生成
フロントエンドに関しては、Swagger Codegenでバックエンドが生成したSchemaからAPI Clientを自動生成できるようにしました。
合わせて、クライアントからAPIを呼び出すCustom Hookを用意しました。
バックエンド
バックエンドに関しては、ざっくり全体の構成として以下のようになっていました。
- Auth: Elixir
- v1: Elixir
- v2: Kotlin
背景としては、Elixirを辞めるために新規機能をマイクロサービスとして切り出し(v2)、v1とv2でチームが分かれたとのことです。
そのためv1->v2->authと通信していたり、v2->v1と通信していたり、他サービスのDBを直接参照していたり、依存関係が複雑になっていました。
つまり歴史的背景からサービスの機能や役割が中途半端になってしまっている状況でした。
方針の策定
過去のドキュメントを見ると、Microservices vs Monolithでメンバーによって目指す方向性が違っているように感じられました。
今は次のような状況です。
- v1とv2の役割が実質被っているところがある
- v1の一部機能がなくなったため、v1の役割が減った
- Elixirは辞めたいという意向がある
そのため、まずはv1をv2に統合していき、その後必要に応じて機能を切り出すことを提案しました。
テストの充実化
大幅なリファクタリングをするためには、テストを充実させる必要があります。
ただバックエンドはActive Recordパターンで書かれており、DB操作を含んだテストが書きづらい状況にありました。
そのためほとんどがインテグレーションテストで書かれているのですが、実行時のDBの状態に依存しているため壊れやすいものでした。
Testcontainer
の採用
Repositoryパターンを採用してインフラ層はDIするのは変更が過剰になりすぎるとのことで、Testcontainer
を使いクリーンなテスト用DBを用意する方針にしました。
これによりUnitテストでDB操作を含んだテストが書けるようになりました。
E2Eテストの追加
v2でインテグレーションテストが書かれていたのですが、それだとv2を起点にしたテストしかできません。
v1をv2に移行するにあたり一番表側にあるフロントエンドから統合テストする必要があるため、E2Eテストを追加しました。
ユビキタス言語の策定
複雑なドメインを扱っているが用語が統一されていないため、他の人が書いた領域を触りづらいという課題感がありました。
- 同じ概念をさす用語が複数ある
- 複数の概念を1つのクラスで扱っている
- フロントエンドとバックエンドで用語が統一されていない
そのためまずエンジニア内だけでも、言語を統一することを提案しました。
ドメインモデリング
一度サービスの全体像を理解するために、一緒にゼロベースでドメインモデリングを行いました。
自分としてもドメイン知識が深まり、既存のコードとの関連性も見えてきました。
また元々のメンバーも曖昧だった点がクリアになったようでした。
DevOps
会社として自己組織化した組織を作ることが目標になっているとのことでした。
ただ実態としてエンジニア組織は正社員1人と業務委託のチームになっていて、正社員1名を中心に回っている形になっていました。
具体的にはコードレビュー・デプロイ・動作確認・他業種との調整・開発内容決定・開発方針決定などを全て1人で行っており、その他は割り振られた作業を行うだけでした。
自動化を進める
正社員の方の多くは確認の工数に取られているようで、特にデプロイのたびに人力で全体的な機能の動作確認を行ってるようでした。
背景としてテストへの信頼性の低さや、全システムの一貫したテストがないことが想定されました。
これもE2Eテストを提案した背景の1つとしてあります。
今後やりたいこと
DevOps
デプロイをより自動化したり、インフラ構成の最適化、datadogなどモニタリングツールの採用など、やれることはまだまだありそうのですが、DevOpsチームが担当してるとのことでした。
そのため直近としてはCI上でインテグレーションテストを回すなど、できる範囲での自動化を進めようとしています。
エラーハンドリング
バックエンドとしてはあまりハンドリングされておらず、多くは500で返却されていました。
フロントエンドはcatch
していないかsetError(“エラーが発生しました”)
をし、UIのどこかでerror
を表示してるだけでした。
そのためバックエンドでエラーメッセージを返すようにし、結果がエラーだあったらToastでメッセージを表示する処理を含めたCustom Hookを作ればよりユーザフレンドリーになりそうです。
おわりに
ということで、配属から3ヶ月で実際に考えやってきたことをまとめました。
基本方針として絶対的な答えはないと思ってるので、自分の考えは述べた上で意思決定は現場に委ねようと思っています。
たとえばMicroservices vs Monolithや、DDDを教科書通りに行うのかなど、結局どちらもPros/Consがあります。
真の意味での正解はチームやメンバー次第なところもあるので、一緒により良い答えを探していければと思います。