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

thumbnail

フルスタックエンジニアで活動してるドラレプです!

今はフリーランスで同時に複数の案件を抱えながら活動しているため面接の機会は多いのですが、なんとなく『フルスタックって役割が見えづらい』『週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が多用されていた

これは、次のような負のスパイラルに陥ってるためと考えました。

  1. エラーを直す場当たり的な対応が中心となってしまう
  2. 型の恩恵が受けられなくなっていく
  3. 型定義するモチベーションが湧かない

そのため、「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での型指定が不要になりました。

合わせてredux-tool-kitを採用し、ライブラリに頼れば型が付与されるようにました。

一緒に入ったredux-devtoolsもDX向上に大きく貢献します。

テスト/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を使いたいとのことで、導入を進めてくれました。

ちなみに課題感だけ見ると将来的にGlobal CSSを減らしていくために、ComponentのShadowDOM化を進めたい気持ちはありました。

その他

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テストを追加しました。

DDD

「DDDをしている」とのことだったのですが、戦略的DDDはしておらず、Aggregateパターン、Repositoryパターンなども採用されていませんでした。

そこで「教科書的にDDDをやらないか」と提案しましたが、あまりポジティブな反応は得られませんでした。

なのであまりここに対してアプローチするのは辞めようかと思いましたが、純粋に困った点があったので次の提案をしました。

ユビキタス言語の策定

複雑なドメインを扱っているが用語が統一されていないため、コードリーディングが困難でした。

  • 同じ概念をさす用語が複数ある
  • 複数の概念を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があります。

真の意味での正解はチームやメンバー次第なところもあるので、一緒により良い答えを探していければと思います。


@dorarep
小学生の頃からフリーゲーム作ってました。今はフリーランスでフルスタックエンジニアしてます。