Closed15
Firebaseに入門してみる(勉強ログ)
2021/10/13(水)開始
基本的にこの流れに沿って勉強を進める。
Firestoreの動画視聴が終わったら下のように自分でアプリを考えながら作ってみる。
Firestoreの設計についても情報収集しながら進める。
- Cloud Firestore
- Cloud Functions
- Cloud Storage
- セキュリティルール(テストも必ずやる)
- エミュレータ
あたりを理解して使えるようになることを目指す。
2022/1/6追記
Cloud Firestoreの公式ドキュメントを大方読み終えた。
本業で中々まとまった時間が取れていないため、2月以降にNuxt.js + Firebaseで簡単なチャットアプリを作り始める予定。
Firestoreの公式動画
YouTubeのvideoIDが不正です
視聴を終えたらチェックを付けていく
- NoSQLデータベースとCloud Firestoreの構造を知ろう| Get to know Cloud Firestore #1
- How do queries work in Cloud Firestore? | Get to know Cloud Firestore #2
- Cloud Firestore Pricing | Get to know Cloud Firestore #3
- Maps, Arrays and Subcollections, Oh My! | Get to know Cloud Firestore #4
- How to Structure Your Data | Get to know Cloud Firestore #5
- Security Rules! 🔑 | Get to know Cloud Firestore #6
- How Do I Paginate My Data? | Get to know Cloud Firestore #7
- How do Transactions Work? | Get to know Cloud Firestore #8
- How do I Enable Offline Support? | Get to know Cloud Firestore #9
- リアルタイムを使うかどうか | Cloud Firestore を知ろう#10
- How do Cloud Functions work? | Get to know Cloud Firestore #11
- 5 uses for Cloud Functions | Get to know Cloud Firestore #12
NoSQLデータベースとCloud Firestoreの構造を知ろう| Get to know Cloud Firestore #1
- NoSQLとRDBとの違いについて
- RDBとは違い、NoSQLではデータを重複して持たすことをよくする
- WriteよりもReadの方が圧倒的に多い場合などはReadに最適化する方が合理的という考え
- NoSQLではドキュメント内に存在するデータが必ずしも同じとは限らない
- reviewというプロパティを持つドキュメントもあれば、持たないドキュメントもある
- 防御的プログラミングで対処する
- NoSQLの最大の利点は「データを複数のマシンに分散しやすいこと」
- より大きなデータセットにスケールアップする際、自動的にデータが複数のサーバーに分散される(水平方向スケーリング)
- Cloud Firestoreではドキュメントとコレクションで構成される
- キーと値のペアで構成され、フィールドと呼ぶ
- コレクションはドキュメントを集めたもの
- コレクション、ドキュメントにはルールがある
- コレクションにはドキュメントのみを格納できる
- ドキュメントサイズは1MB未満(それより大きいなら分割する)
- ドキュメントにドキュメントは格納できない
- Firestoreのツリー最上部に格納できるのはコレクションのみ
- コレクション内のドキュメントを取得すると、該当ドキュメントのみが取得され、サブコレクションは取得されない
How do queries work in Cloud Firestore? | Get to know Cloud Firestore #2
2本目から日本語字幕が付いていないのでかなり辛い...
かなり時間が掛かりそうだが、分からない箇所はDeepL翻訳を使いながら理解していく。
- データベースに格納されているデータを探すときにはクエリが使える
- クエリは高速で処理される
- 例えば、あるZipCodeにあるレストランコレクションの特定のドキュメントをクエリを使って探すことが可能
- 一方で、複数のサブコレクションにまたがるようなクエリの実行はできない
- 2019年から(?)複数コレクションにまたがるクエリは「コレクショングループクエリ」と呼ばれ、Cloud Firestoreでサポートされている
- コレクショングループクエリを有効にするにはFirebaseコンソールにアクセスしてどのコレクション名でどのフィールドを検索したいかを設定する必要がある
- 実行するクエリは、ドキュメント内の1つ以上のフィールドの等価比較、大小比較などが使える
- クエリに計算処理を含めることはできない(?)(~と~の平均が200以上の~を取得するといったようなクエリは不可能?)
- クエリを実行するのに掛かる時間は、取得される結果の数に比例する
- ex. 評価の高いレストランTOP5を取得したいとき、レストランの母数が60件であっても、6万件であっても同じ時間がかかる
- ドキュメントが更新される度にインデックスが更新される(?)
- インデックスでソートしておき、評価が4.5の箇所にジャンプしてから評価が4.7以上になるまで隣接するドキュメントを順に舐めていく
- 一方で店名のどこかに「Taqueria」という単語を含むレストランを探したい場合はFirestoreのネイティブ機能ではできない(このような機能を提供するインデックスはない)
- また、ネイティブのFirestoreではパターン検索や正規表現、OR検索ができない
- このような検索をおこないたい場合はサードパーティを使う
- 複合インデックス?いまいち理解できなかった...
- おそらく、複数条件のクエリを実行する際に必要となるインデックスっぽい?
- 住所がサンフランシスコ && 日本料理 && 評価が4以上のような条件
- 作成方法は二通りあって、1つ目はFirebaseコンソールから手動でインデックスを作成
- 2つ目は(これが推奨されている)、一度クエリを実行するとエラーが表示され、エラー文中にインデックスを作成するためのFirebaseコンソールへのURLが記載されているため、そこからアクセスして作成する
- おそらく、複数条件のクエリを実行する際に必要となるインデックスっぽい?
まだページネーションやデータ構造について、話したいことはあるが、今後のテーマにするらしい。
両方知っておきたいが、英語の動画は結構辛い。
Cloud Firestore Pricing | Get to know Cloud Firestore #3
- Cloud Firestoreでお金を使いすぎないようにするための仕組みについて
- shallow queriesのおかげで、複数のフィールドにまたがってクエリ実行が可能→論理的なデータ構造を構築できる
- RDBとNoSQLでは価格の決定方法が異なる
- RDBはほとんどのコストはデータ量に比例している(?)
- NoSQLではダウンロードしたデータ量よりも、実行した操作回数を重視している
- データベースへの書き込み、読み取り、削除の回数に応じて課金が発生する
- 1つのフィールドをtrue→falseに変更しても、30個の異なるフィールドを一度に変更しても1回の書き込みとしてカウントされる
- 読み取りは、ドキュメントからデータを取得するときに発生する
- 20個のドキュメントを読み取ったとき、読み取り回数は20とカウントされる
- Firestoreでは全ての検索をドキュメントそのものではなくインデックスを通して行われるため、日本食レストランのトップ20を検索したときに、検索対象が3000万件あったとしても読み取り回数は20とカウントされる(3000万Readとはならない)
- 利用料が高価になる例
- バックエンドにある全ての銘柄の価格を15秒ごとに更新していて、誰かがアプリを3分間開きっぱなしにしていた場合、12回の更新(12Read)を受ける
- この人が10種類の銘柄をフォローしていた場合、読み取り回数は120回となる
- もし1000人のユーザーがアプリを使っていた場合、その3分間だけで12万回の読み取りが発生する
- このペースだと1日平均5750万回読まれることになる
- 一般的にはリアルタイム性が高く、アクティブユーザーが多いアプリほど、Cloud Firestoreのコストは高くなる
- グループホワイトボードアプリがあるとして、リアルタイムに絵を描く感覚を得るために、1秒間に数回の更新を行っていたら、1秒間に多くのRead / Writeが発生する
- この場合はRDBの方が価格面では適しているかもしれない
- バックエンドにある全ての銘柄の価格を15秒ごとに更新していて、誰かがアプリを3分間開きっぱなしにしていた場合、12回の更新(12Read)を受ける
- データベース内の任意のドキュメントをチェックするには、
get()
やexists()
の呼び出しで行うが、この呼び出しは通常読み取りとしてカウントされる- しかしCloud Firestoreはできる限りこれらの値をキャッシュするようにしている
- 実際には、価格面ではCloud Functionsよりもセキュリティルールの方が心配は少ない
- データベースの使用状況はGoogle Cloudコンソールから確認できる
- Google CloudコンソールのApp Engine Settingsから1日の使用量の上限を設定することができる
- Google Cloud のオペレーション スイート(旧称 Stackdriver)機能を使えば、リクエスト数が多すぎたり少すぎたりする場合や、Cloud Functionsの実行に時間がかかりすぎている等を教えてくれる
Maps, Arrays and Subcollections, Oh My! | Get to know Cloud Firestore #4
- ドキュメントのフィールドとしてMapがあり、MapのMapを作れたりするのに、なぜサブコレクションが必要なのか、どちらか一方を使用するのが適切なのか?
- Cloud Firestoreでデータを構造化する最適な方法を決める際にいくつかルールを覚えておくといい
- ルール1. ドキュメントには制限がある
- 1ドキュメント1メガバイトまでしか格納できない
- 2万個以上のフィールドを持つことはできない
- これはマップ内のフィールドも1つのフィールドとしてカウントされる
- address: { street_number: '221B', city: 'London' }と保存したマップはフィールド数3とカウントされる
- この制限の理由はFirestoreがドキュメントのインデックスを作成するため
- 同一のドキュメントに対しては、通常1秒間に1回の書き込みに制限される
- 多くのクライアントが同じドキュメントに一斉に書き込みをしようとすると、書き込みに失敗することがある(コレクション内の異なるドキュメントに書き込むことは問題ない)
- これはマップ内のフィールドも1つのフィールドとしてカウントされる
- ルール2. 部分的なドキュメントを取得することはできない
- ドキュメントを読み取るときは、ドキュメント内の全てのフィールドを読み取ることになる
- 読み取る必要がない巨大なフィールドを格納していると、不要な読み取りが発生し、バッテリー容量の消費やパフォーマンスの低下につながる
- また、ドキュメントの一部にのみセキュリティルールを適用することはできない
- ルール3. クエリは浅い
- ドキュメントを取得するとき、サブコレクションの中のデータは取得されない
- つまり、データを階層的に保存することができる
- 本のタイトルをドキュメントのフィールとして保持し、内容を章ごとのサブコレクションにすると、本のタイトル一覧を取得できる、また内容は必要に応じて章ごとに取得できる
- ルール4. 課金は、読み書きの回数に応じて発生する
- もし、メインのドキュメントをサブコレクションに分けた場合、メインのドキュメントの内容を全て読み取るためのRead回数を増やしたことになる
- ルール5. クエリは1つのコレクション内のドキュメントを検索するためにのみ使用できる
- ちゃんとは理解できなかった...
- Collection Group Queryを使うためには、FirebaseコンソールでCollection Groupインデックスを設定する必要がある
- ルール6. 配列はちょっと変
- 伝統的な配列操作の多くは、Cloud Firestoreのようなサービスではうまく機能しない
- あるクライアントがインデックス2の値を編集しようとしていて、あるクライアントがインデックス2の値を削除しようとし、別のクライアントがインデックス0のどこかに値を挿入しようとした場合、全く異なる結果になるかも知れない
- これらの命令をどの順番で受け取るかによって、結果は大きく異なる
- ルール1. ドキュメントには制限がある
- Cloud Firestoreでデータを構造化する最適な方法を決める際にいくつかルールを覚えておくといい
- まとめ
- データを常に表示するのであれば、同じドキュメントにデータを入れる
- ドキュメントのサイズを必要以上に大きくしないこと
How to Structure Your Data | Get to know Cloud Firestore #5
- Cloud Firestoreのルールはデータをどのように構成するかに関わってくる
- レストランアプリを例に考える
- ユーザーがレストランをクリックするとその詳細をみることができる
- 詳細画面にはレストランの詳細情報とレビューの概要?が表示されており、レビューをクリックすると完全なレビューが表示される
- 一般的には、レストランは1つのコレクションに入っていて、全てのレビューはレストランごとにサブコレクションに入っている
- しかしこれは正しい設計なのか?代替案を見てみる
- 代替案1. これらのレビューを個別のドキュメントにせず、レストランドキュメントにMapやMapの配列として保持する
- この構造によってドキュメントを読み取る手間が省ける
- しかしこれには問題がある
- ユーザーが日本食レストランTOP20を検索しようとしたとき、レストランに関する情報だけでなくリストに載っている全てのレビューを取得してしまう
- また1ドキュメント1メガバイトの制限や2000フィールドの制限に直面するかも知れない
- さらに最新のレビュー10件だけをフィルタリングして取得といったこともできない
- そのため、レビューを個別のコレクションにしておくことは正しい方法である
- 代替案2. 各レストランのサブコレクションとしてレビューを保存するのではなく、完全に別のルートのコレクションにする
- Cloud Firestoreのクエリは高速
- ルートコレクションにすることで異なるレストランにまたがるレビューを検索することができるようになる
- セキュリティルールについては次のビデオで説明するが、セキュリティルールについて知っておくべきことは
- 特定のドキュメントを取得してその内容を読むことができる
- 特定のドキュメントの編集を許可されている全員を配列に入れて、ユーザーIDがこの配列に表示されている場合のみ、このドキュメントの編集を許可する
- ユーザーIDはかなり不透明で、これをリバースエンジニアリングしても本当のユーザーが誰なのか、解析することはできない
- またユーザーIDはアプリごとにユニークである
- Cloud Functionは別の動画で紹介する
- 代替案1. これらのレビューを個別のドキュメントにせず、レストランドキュメントにMapやMapの配列として保持する
- しかしこれは正しい設計なのか?代替案を見てみる
- Firestoreのデータ構造を決める際はこれらが大事
- ドキュメントを使うタイミング、マップを使うタイミング
- ドキュメントを使うべきか、マップを使うべきか
- サブコレクションを使う場合、配列を使う場合
- トップレベルのコレクションに入れるか
Security Rules! 🔑 | Get to know Cloud Firestore #6
- クライアントからのリクエストを信じてはいけない
- セキュリティルールはドキュメント単位で適用される
- トップレベルで追加したルールは下の階層まで連鎖しない
- あるドキュメントのサブコレクションは別にセキュリティルールを記述する必要がある
- =**と書くと再帰になる
- create: 新しいドキュメントを作成
- delete: ドキュメントを削除
- update: 既に存在するドキュメントを更新
- アクションの分割
- readはgetとlistに分割できる
- writeはcreateとupdate, deleteに分割できる
- write, readは使わずに分割したアクションとして使うのが一般的
- authオブジェクトにはサインインしたユーザーの情報が含まれている
- request.auth.uid
- resorceオブジェクトは既に存在するドキュメントを表す
- request.resource
- 任意のドキュメントが存在しているかをチェックするにはexists関数かget関数を使う
- Cloud Firestoreにはルールシミュレーターが備わっているため、どんな操作もセキュリティルールに照らし合わせてテストすることができる
- また、Cloud Firestoreのエミュレータを使えば、全てのセキュリティルールに対してユニットテストを実行できる
How Do I Paginate My Data? | Get to know Cloud Firestore #7
- ページネーションについて
- ページネーションは、データベースの結果を複数の小さな塊に分割して、一度に全て取得しないようにする方法のこと
- 一度に大量のドキュメント読み取りを行うとかなりの課金額になる
- Firestoreでは、クエリでドキュメントを読み取る時に制限をかけることができる
- limit(to: 20)
- では次の20件はどうやって取得する?
- start()が使える
- start(after: ["Tokyo", "tempura", 4.9])
- しかしクエリを書くたびにこれを追跡しないといけないのは面倒かつミスも起こる
- Firestoreのライブラリではafterパラメータとしてドキュメントを渡すことができる
- 特定のクエリの40行目から60行目を取得したい場合
- offset(40)
- のようなメソッドが使えるが、この場合(オフセットに到達するまでの?)60回分ドキュメント読み取りが発生するため推奨しない
- できるだけ上記のstartなどを使うのが良い
How do Transactions Work? | Get to know Cloud Firestore #8
- トランザクションについて
- ドキュメントへの書き込み中に他のユーザーからの書き込みをブロックしたり、複数のプロパティ間で整合性を保証したいときに使う
- トランザクションには知っておくべきいくつかのルールがある
- Rule #1 Reads Before Write(書き込み前に読み込みを行う)
- モバイルやWebクライアントでは、クエリではなく個々のドキュメントの読み取りでなければならない
- Rule #2 No Side Effects(副作用があってはならない)
- トランザクションの途中で変数をインクリメントしたりポップアップダイアログを表示してはいけない
- これらの作業は全て完了ハンドラで行う
- Rule #3 Don't Go Overboard(ドキュメントの数を増やしすぎるな)
- トランザクションは基本的に、いずれかのドキュメントが更新されるたびに自身を再実行しなければならない
- そのため同じトランザクションに関係のないドキュメントを追加することは避けるべき
- Rule #4 No Offline Support(オフラインではトランザクションは失敗する)
- トランザクションを書くことは、「この書き込みはデータベース上の最新の情報に依存しています」と言っていることと同じ
- 一方でオフライン時には書き込むことができない
- Rule #5 500 Documents(500ドキュメントまで)
- トランザクションで一度に書き込めるのは500ドキュメントまで
- このルールはトランザクションにもバッチライトにも適用される
- トランザクションで一度に書き込めるのは500ドキュメントまで
- データベースの値をインクリメント/デクリメントするのはトランザクションに頼らなくても可能(おそらくこれはFieldValue.incrementのこと)
- Rule #1 Reads Before Write(書き込み前に読み込みを行う)
How do I Enable Offline Support? | Get to know Cloud Firestore #9
- オフラインサポートについて
- iOSやAndroidではオフラインサポートがデフォルトで実装されているため、特に手動で対応する必要はない
- Webでは自分で使用可能な状態にする必要がある
- とは言っても2-3行のコードで対応可能
- Firebaseではクエリやドキュメント読み取りはローカルにキャッシュされ、オフライン時にはキャッシュから読み取りが可能
- オフラインでもドキュメントに書き込むことは可能
- その際、書き込みはローカルデバイスに保存され、オンラインに復帰時にサーバーに送られる
- サーバーへの書き込みは、書き込みが発生した順に解決され、同じドキュメントへの書き込みが発生していた場合は最も新しい内容で上書きされる
リアルタイムを使うかどうか | Cloud Firestore を知ろう#10
- Firestoreのリアルタイムデータ取得について
- リアルタイムの話をすると気乗りしない人が多い
- 理由1: リアルタイムは不気味で奇妙
- 理由2: 負荷(バッテリー消費や価格への懸念)
- Firestoreのリアルタイムリスナーでは複数ドキュメントをリッスン中にある1つのドキュメントが変更された場合、その1つだけドキュメント更新されるようになっている
- クライアント側では更新があったドキュメントと、残りのドキュメントのキャッシュデータを組み合わせて複数ドキュメントがリッスンされる
- つまり、この場合1ドキュメント分の読み取りにしかならない
- リアルタイムリスナーはバッテリー消費にはほとんど影響しない
- リアルタイムリスナーと単一のフェッチの使い分けは?
- 一般的にはリアルタイムがデフォルトであるべき
- リアルタイムを使用するとネットワークの弱い場所ではアプリのパフォーマンスが向上する
- リアルタイムリスナーは複数回起動するように設計されている
- そのため、キャッシュデータをすぐに取得してネットワークが回復した数百ミリ秒後に取得しなおせる
- フェッチは1度だけ呼ばれるように設計されている
- そのため、ネットワークが遅いとデータ取得まで時間がかかったりタイムアウトの後にキャッシュが取得される
- 一応、2回フェッチを行うようにし、1回はキャッシュから取得、もう1回はサーバーから取得するようにすれば対処可能だが、リアルタイムリスナーを使っていればこの必要はなくなる
- そのため、ネットワークが遅いとデータ取得まで時間がかかったりタイムアウトの後にキャッシュが取得される
How do Cloud Functions work? | Get to know Cloud Firestore #11
- Cloud Functionsについて
- Cloud Functionsを使うとバックエンドでロジックを実行することができる
- 全てのロジックをクライアントに配置するのではなく、バックエンドで実行することが重要な場合がある
- セキュリティ面
- クライアントからの変更を網羅するセキュリティルールが複雑すぎる場合
- Cloud FunctionsはGoogleCloudまたはFirebaseプロジェクトで特定のイベントが発生したときにアクティブになる
- Firestoreでドキュメントを作成、変更した時
- FirebaseAuthで新しいユーザーアカウントが作成された時
- Analyticsでコンバージョンイベントが発生した時など
- 他にも様々なものにトリガーできる
- Cloud Functionsの実行にはある程度時間が掛かる
- 他の呼び出しているサービスと通信するため
- Cloud Functionsには2つの種類がある
- Google Cloud Functions -> Google Cloud Platform製品
- Cloud Functions for Firebase -> Google Cloud Functionsのラッパーであり、Firebaseとの連携を簡単にする
- 欠点もあり、Google Cloud FunctionsはPythonやGoなど様々な言語に対応しているのに対して、Cloud Functions for FirebaseはTypeScriptとJavaScriptにしか対応していない
- TypeScriptで記述することが推奨されている
- Cloud Functions for Firebaseの癖
- QUIRK #1
- パフォーマンスは常に期待どおりには出ないかも知れない
- QUIRK #2
- グローバル変数は少し奇妙な場合がある
- Cloud Functions for Firebaseは関数ごとに独自の環境で実行されるため、関数間の状態を共有することができない
- グローバル変数は少し奇妙な場合がある
- QUIRK #3
- グローバル変数は常に読み込まれている
- ヘルパー関数やライブラリへの参照を含むグローバル変数は、使用するかどうかに関係なくコンテナに読み込まれる
- グローバル変数は常に読み込まれている
- QUIRK #4
- 関数が実行される順番は保証されない
- QUIRK #5
- 時々関数が複数回呼び出されることがある
- 関数呼び出し時にイベントIDを使用でき、複数回関数が呼ばれた時でもそれらのインスタンスは同じイベントIDになるため、2重決済を防いだりできる
- QUIRK #1
5 uses for Cloud Functions | Get to know Cloud Firestore #12
-
Cloud Functionsのユースケースを5つ紹介
-
#1 セキュリティルールを単純化する(セキュリティルールが複雑化したときの代替手法として使える)
- ゲームの情報をドキュメントに書き込む際に、各プレイヤーの手番や操作履歴を1ドキュメントに書き込む
- このとき更新されるフィールドは正しいものか?不正なものではないか?
- 対策
- ゲーム情報のコレクションは読み取り専用にしておき、代わりにプレイヤーの操作を書き込むコレクションを作成しておく
- そのコレクションに書き込みがあった時に、Cloud Functionsがその操作をゲーム情報コレクションに反映する
- 対策
- このとき更新されるフィールドは正しいものか?不正なものではないか?
- ゲームの情報をドキュメントに書き込む際に、各プレイヤーの手番や操作履歴を1ドキュメントに書き込む
-
#2 非正規化されたデータの同期
- レストランのレビューアプリを例に考える
- レビュードキュメントにユーザーのプロフィールも含める場合、自分のプロフィールを変更した場合には過去の全レビューに含まれるプロフィールにも変更を走らせないといけない
- これはクライアントで処理を行うのはかなり厳しい(主にバッテリー消費や通信の不安定さ、セキュリティルールの複雑性)
- ここでCloud Functionsの出番→レビュー者が自分であるドキュメント全てに対してコレクショングループクエリを実行
- 注意点としては、Cloud Functionsのタイムアウトは1分間のため、全て変更しきれない可能性がある
- 9分間に伸ばす方法もある
- レビュードキュメントにユーザーのプロフィールも含める場合、自分のプロフィールを変更した場合には過去の全レビューに含まれるプロフィールにも変更を走らせないといけない
- レストランのレビューアプリを例に考える
-
#3 Cloud Functionsでデータベースのメンテナンスを時折実行する
- 週ごとの集計レポートを作成したりもできる
- レビューの下書きがある場合、45日以上経過した下書きを削除することもできる
-
#4 レガシーなデーターベース上で稼働させる
- Cloud Firestoreは様々なサーバーSDKが提供されているため、レガシーのデータベース上にある情報をCloud Functionsを使ってFirestoreへコピーすることも可能
- また、メッセージパッシングサービスであるCloud Pub / Subと呼ばれるサービスもサポートしている
-
#5 カスタムAPIを構築する
- Cloud Functionsを利用して他に実行している可能性があるインフラストラクチャ上に独自のカスタムAPIを構築できる
- Cloud Functionsには呼び出し可能な関数と呼ばれるものが付属している
- HTTP呼び出しのラッパー
- Cloud Functionsと通信するために必要となる多くの作業が削減できる
- #1 であげた、プレイヤーの操作を書き込むコレクションを作成しておき、書き込み時にトリガーさせてCloud Functionsを実行する必要はなく、直接Cloud Functionsの呼び出し可能な関数に渡すことができる
- メリット: ドキュメント書き込みする必要がなくなったため、書き込み料金が節約できる
- デメリット: オフラインサポートが失われる
2月以降にNuxt.js + Firebaseで簡単なチャットアプリを作り始める予定。
と書いていたが、本業が多忙のためあまりまとまった時間が取れないのと、
業務でFlutter x Firebaseを使いつつ勉強も続けているため理解が進んできた。
という訳でこのスクラップはここまで。
このスクラップは2022/05/25にクローズされました