🦁

同じプロダクトを初期から2年間触り続けて得られたこと

2021/11/19に公開

記事内容について

フロントエンドだと「最近だとこの技術が良さそう!」というのは色々巷で書かれている。
けど、「プロダクトを初期からずっと触っていて得られた結果や振り返り」も、世の中にもっと共有されても良いのでは?と思った。

エントロピーに打ち勝つために

エントロピーの法則はお馴染みの「秩序あるものはやがて無秩序になる」であるがソフトウェアもその例外ではない。プロダクトを愛し、磨き上げなければすぐに腐敗してしまう。
今回は2年の歳月に打ち勝つために自分は何をやったかを書いていく。

プロダクト基本情報

我が愛すべきプロダクトの大まかな内容

  • React(SPA)
  • TypeScript
  • styled-components
  • Jest
  • testing-library
  • Storybook
  • Cypress

常に心掛けたこと

設計ではなく易しい規約でいく

プロダクト開発には色々なレベルのエンジニアがいる。なので〇〇設計論でいくとかいうのは結構リスクである。そういうのは本当に骨の髄まで勉強し尽くした人しかわからない部分はわからない。
上手く浸透できるかも不明である。
それより規約を作った方が良い。
ここでいう規約というのは例えばフォルダの分け方、関数の書き方とかそのレベルである。
なので実行したことは規約をベースにした戦略になる。

完璧を目指さない

設計や開発だとどうしてもあれこれ考えるけど、スケジュールやら外部要因やらで、何から何までを完璧にするというには結構難しい。
だからと言って甘やかすと自分にツケが回ってくるのが世の習わし。
なので自分の中ではレベル別に少しずつ尖らせる手法にした。要するに目標を設定して「まずはここをしっかりやろう」と一つ設定する。
目標が達成したら「次はここをしっかりやろう」みたいな感じである。
以下書いていく。

実際のやったこと(レベル別目標で表示)

なお、lint系の導入は今回レベル0ということで省略する。

基本のロジック分離戦略(レベル1)

プロダクトの当初から「通信」と「データ整形」、「表示」をフォルダごと分けるというルールの下で徹底的に守るようにした。「表示」はTSXファイルであるが、関数コンポーネントでreturnの上にロジックをなるべく書かないようにしたし、周りのメンバーにも共有した。

例:
以下のように「表示」のファイルは「表示」に関心を集中させ、必要な初期処理はCustom Hooksでただ呼び出すだけにすることで、大体なんとかなる。


interface Props {
  defaultKeyword: string
}

export default function SearchInputArea({ defaultKeyword }: Props) {
  const { searchArticlesWithKeyword } = useSearchParams()
  const [keyword, setKeyword] = useState<string>(defaultKeyword)
  const input = useRef<HTMLInputElement>(null)

  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    setKeyword(e.target.value)
  }

  const handleClick = () => {
    searchArticlesWithKeyword(keyword)
    input.current?.blur()
  }

  return (
    <Section>
      <Title>Articles Search</Title>
      <DetailWrapper>
        <SearchInput
          inputRef={input}
          placeholder={'keyword'}
          onChange={handleChange}
          defaultKeyword={defaultKeyword}
        />
        <Button onClick={handleClick}>
          検索
        </Button>
      </DetailWrapper>
    </Section>
  )
}

一度「フォルダの型」を一人が決めてしまえば、逸脱したPRを出してくるメンバーはいなかったので、ここは本当に大事。

ユニットテストを書く(レベル2)

UIができてきて、開発もまぁまぁ順調だねーの時にUIに複雑なロジックをつけないといけない画面を作ることになった。複雑さに対抗するにはテストしかない。(今のところ)
ただ、何から何までテストを書くには時間がないよね、という話にもなり結局ここも細かく優先順位をつけて書くようにした。テストとして「ここは絶対に書こう!」という部分と「書いても書かなくても良いよー」という部分に分けた。
優先順位は以下の通りである。

1.ビジネスロジック
2.カスタムフック
3.UIコンポーネント

ビジネスロジックは不運にも激しく複雑だったので、ここをまずしっかりしないとね!という話になり、ここをまず攻めた。
1が落ち着いたら今度は2のカスタムフックである。UIを3番目にしたのは元々Storybookを入れてあったし、そもそもロジックが間違ってたらUIがしっかりしてても意味ないよねーということもあった(めんどいよねーという自己都合的な考えもあったが。)

成果

  • レベル1ではカバレッジ率はほぼゼロに近かったが最終的に70%まで上げられた。
  • 良い設計はテストコードが非常に書きやすい、が身に染みてわかった(その逆もあり)

E2Eテストを書く(レベル3)

単体テストを書いたのはいいけど、どうしてもそこだけだとカバーできない領域も出てきてしまう。
そういう場合は思い切ってE2Eで使われるCypressにお世話になるようにした。
ここは結構戦略的には反省点もある。

反省点

  • 必要以上にE2Eのテストファイルを量産していた(実際はそんなにいらなかった)
  • ファイルの数に対するテスト処理時間を考慮していなかった

E2Eの作りすぎは良くない、時間がかかる、と後々反省した。

カバレッジ率を測定する(レベル4)

テストのカバレッジ率を測定することで、どのようなテストが足りないかを確認することができる。要するに質の悪いテストコードを炙り出して改良する時期である。測定することで今度はレベル2では得られない、より良いテストを書くというのが身につくようになる。
ただし、カバレッジ率が低いからと言ってPRで弾くみたいなところまではしなかった。
これはプロダクトやメンバーの状況などを考えた上で検討した結果である。

得られたこと

他にもいっぱいあるが、すごくざっくりするとこんな感じである。
自分が得られたものは最終的には

  • 「小さく目標を設定して、どんどん育てていくことが大きな目標達成になる」
  • 「実装とテストコードを反復的に書いていくことで良い設計が何かを直感でも判断できるようになった」

長く一つのプロダクトを触ることで、自分にとってはかなり財産になった。
長く関わるって大事。

Discussion