Yup.string().matches()でバリデーションを細分化した話

## はじめに
株式会社エアークローゼットのプロダクトグループでエンジニアをしている工藤と申します。
この記事は[エアークローゼット Advent Calendar 2022](https://qiita.com/advent-calendar/2022/air-closet) の5日目の記事になります

今回はReactのバリデーションライブラリである**Yup**を使ってバリデーションを細分化した話をします。
UXを最重要視するエアークローゼットらしい記事になっていますので、興味のある方はぜひご覧ください。

:::note info
この記事のまとめ
- Yupのバリデーションは`.matches()`を繋ぐことで細分化できる
- 細分化を目的にすると`.matches()` の仕様は多少ややこしいので注意が必要
:::

## やりたかったこと
今回は、エアークローゼットの[サービスサイト](https://www.air-closet.com/)における「無料登録導線」のUXを改善するプロジェクトにアサインされ、その実装を担当することになりました。
### ビジネス側の要望
- 無料会員登録のUXを改善するため、それまで1パターンだったバリデーションメッセージを**16パターン**に出し分けてほしい
- エラーメッセージは登録ボタン押下時ではなく、1文字入力する度に出してほしい

### エンジニア（私）の考え
1. 極力、既存のコードを破壊したくない（**かなり重要な導線**であり、バグが発生した際のダメージが大きい）
1. 該当箇所は[**Yup**](https://github.com/jquense/yup)で実装されていたため、実装の過程で苦手な正規表現を勉強したい

※ちなみに↓が実装前の動画です。
エラーがあっても全て同じメッセージで、何が足りないのか全くわかりません...
![9b8db476786f55a2abe837135ffb36e6.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/751808/731e7307-51af-d878-499b-6bf99ead5b60.gif)


## やったこと
`Yup.string.matches()`を数珠繋ぎにすることで、パスワードを細分化しました :pencil2:
実際のコードと実装後の動画を以下に貼っておきます。

### 実際のコード
```javascript: index.tsx
Yup.object().shape({
  // ...(一部省略)
  password: Yup.string()
    matches(/^((?=.*[a-z]).*|^[0-9]+$|^[A-Z]+$|.{8,})$/,ERROR_MESSAGE.INVALID_PASSWORD_REQUIRE_8_AND_LOWER_CASE)
    .matches(/^((?=.*[a-z]).*|^[0-9]+$|^[A-Z]+$|.{0,7})$/, ERROR_MESSAGE.INVALID_PASSWORD_REQUIRE_LOWER_CASE)
    .matches(/^((?=.*[A-Z]).*|^[0-9]+$|^[a-z]+$|.{8,})$/,ERROR_MESSAGE.INVALID_PASSWORD_REQUIRE_8_AND_UPPER_CASE)
    .matches(/^((?=.*[A-Z]).*|^[0-9]+$|^[a-z]+$|.{0,7})$/, ERROR_MESSAGE.INVALID_PASSWORD_REQUIRE_UPPER_CASE)
    .matches(/^((?=.*[0-9]).*|^[A-Z]+$|^[a-z]+$|.{8,})$/, ERROR_MESSAGE.INVALID_PASSWORD_REQUIRE_8_AND_NUM)
    .matches(/^((?=.*[0-9]).*|^[A-Z]+$|^[a-z]+$|.{0,7})$/, ERROR_MESSAGE.INVALID_PASSWORD_REQUIRE_NUM)
    .matches(/^([^0-9].*|(?=.*[a-z])(?=.*[0-9]).*|(?=.*[A-Z])(?=.*[0-9]).*|.{8,})$/,ERROR_MESSAGE.INVALID_PASSWORD_REQUIRE_8_AND_UPPER_CASE_AND_LOWER_CASE)
    .matches(/^([^0-9].*|(?=.*[a-z])(?=.*[0-9]).*|(?=.*[A-Z])(?=.*[0-9]).*|.{0,7})$/,ERROR_MESSAGE.INVALID_PASSWORD_REQUIRE_UPPER_CASE_AND_LOWER_CASE)
    .matches(/^([^a-z].*|(?=.*[0-9])(?=.*[a-z]).*|(?=.*[A-Z])(?=.*[a-z]).*|.{8,})$/,ERROR_MESSAGE.INVALID_PASSWORD_REQUIRE_8_AND_NUM_AND_UPPER_CASE)
    .matches(/^([^a-z].*|(?=.*[0-9])(?=.*[a-z]).*|(?=.*[A-Z])(?=.*[a-z]).*|.{0,7})$/,ERROR_MESSAGE.INVALID_PASSWORD_REQUIRE_NUM_AND_UPPER_CASE)
    .matches(/^([^A-Z].*|(?=.*[a-z])(?=.*[A-Z]).*|(?=.*[0-9])(?=.*[A-Z]).*|.{8,})$/,ERROR_MESSAGE.INVALID_PASSWORD_REQUIRE_8_AND_NUM_AND_LOWER_CASE)
    .matches(/^([^A-Z].*|(?=.*[a-z])(?=.*[A-Z]).*|(?=.*[0-9])(?=.*[A-Z]).*|.{0,7})$/,ERROR_MESSAGE.INVALID_PASSWORD_REQUIRE_NUM_AND_LOWER_CASE)
    .matches(/^(^[a-z]+$|^[A-Z]+$|^[0-9]+$|(?!.*[0-9]).*|(?!.*[a-z]).*|(?!.*[A-Z]).*|.{8,})$/,ERROR_MESSAGE.INVALID_PASSWORD_REQUIRE_8)
  // ...(一部省略)
})
```

### 実際の動画
文字を入力するたびにエラーメッセージが変化し、何が足りないのかが一目でわかるようになりました 🎉
![53578bcd3d629578e4f3f4fa5d2badfb.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/751808/29638e3a-90df-e0c1-8f82-967f8dece7e6.gif)

## 大変ポイント
### 1. Yupの仕様が(個人的に)直感的でない
Yupの`.matches()`は、第1引数に「**入力を許可する**」正規表現パターンを、第2引数に「第1引数のパターンに一致しなかった場合のメッセージ」をとります。このため、例えば`abc`のような英小文字しか入力されていないパターンのエラーメッセージを出す場合、第1引数には **「英小文字だけ」以外の全てにマッチする** という正規表現を書くする必要があるのです (下図)。

`.matches()`を数珠繋ぎにしてバリデーションを実装するにあたっては**許可しないパターン**を書けたら直感的だったのですが、生憎`.notMathes()`のような関数は存在しなかったため、その点で非常に苦労しました。
![Monosnap LT_2022_08_25 2022-12-04 19-46-11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/751808/a7128132-f0ff-df45-acef-ce99c603876b.png)

### 2. 正規表現の検証が難しい
正規表現が必要な実装をした経験がなかったため、初めは全て `1.コードを書く`→ `2.実際にパスワードを打ち込んでみる` の流れで実装をしていましたが、検証に時間がかかりすぎてものすごく非効率になっていました。

そこで、弊社エンジニアの@baochanh18が社内LT会で使用していた[regular expressions 101](https://regex101.com/)に必要なパターンを打ち込んで検証するようにしたところ、検証速度を圧倒的に早めることができました。
![Monosnap regex101: build, test, and debug regex 2022-12-04 20-09-25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/751808/25d5fdf6-250b-94d5-2d12-1707e85f0930.png)

## あとがき
最後までご覧いただきありがとうございました🙇‍♂️

[エアークローゼット Advent Calendar 2022](https://qiita.com/advent-calendar/2022/air-closet)はまだまだ続きますので、ぜひ他のエンジニア, デザイナー, PMの記事もご覧いただければと思います。

次回は今年の11月にエアクロにジョインしてくれた@victory1011さんが担当しますので、是非お楽しみに！