情報系同人誌プラットフォーム「ジンフォ」を支える技術
はじめに
こんにちは! はじめましての人ばかりだと思います。はっぱです。
普段は 同人音声についての同人誌 を作ったりなどしています。
いきなりですが、「同人誌」といえば一般的にはマンガやイラスト、小説が主体の物語創作を思い浮かべる人が多いでしょう。ですが、Zennの読者の皆さんはおそらくご存知の通り、技術系同人誌や評論系同人誌など、個人で本を作るという文化は物語創作以外にも多々存在します。
私はこれら「創作とは違った形で何かを『語る』同人誌」のことを 情報系同人誌 と定義し、その告知や情報集積を担うプラットフォーム ジンフォ を立ち上げました。
ジンフォの設立理念や機能など、細かい情報は是非公式の紹介記事をご覧ください。
さて、この記事ではジンフォを支える技術やその選定理由等についてご紹介します。
システム構成
本サービスはおおよそ以下のような構成になっています。
基本的にはCloudflareのサービスを使いつつ、補助的にGoogle Cloudを活用するような構成です。
それぞれ詳しく見ていきましょう。
Cloudflare周り
今回用いたCloudflareサービスは主に以下のものです。
- Cloudflare Pages:ウェブサイトホスティング
- Cloudflare Workers:エッジコンピューティング
- Cloudflare D1:データベース
- Cloudflare Workers KV:Key-Value型ストレージ
- Cloudflare R2:S3互換のストレージ
- Cloudflare Register:ドメイン管理
このうち、R2とRegister以外はCloudflare Workersプラットフォームとして、一つの料金体系としてまとまっています。
詳細は上記の公式紹介を参照いただきたいのですが、料金体系としてはWorkers Free
と Workers Paid
の2プランがあります。 Workers Paid
では$5/moで料金がかかる一方、ある程度の量の資源が含まれており、加えてそれ以上の資源消費に関しては従量課金でスケーリングされるような料金体系となっています。
ジンフォはWorkers Paidを採用しているため、Cloudflare周りのランニングコストとしては$5~となります。(もっとも、Cloudflareはジンフォ以外にも個人で色々と活用しているので、お財布的にはほぼノーダメージと行っても過言ではありません)
ホスティング
主軸となるフレームワークはRemixであるため、これをホスティングするサービスが必要です。現在、Cloudflare上でRemixをホスティングするにはウェブサイトホスティングサービスである Cloudflare Pages を使う方法と、エッジコンピューティングサービスである Cloudflare Workers を使う方法の二通りがあります。
ジンフォの技術選定を行った2024年春頃の時点ではCloudflare上にRemixを展開する際はCloudflare Pages一択でしたが、2024年秋ごろにCloudflare Workersでの静的ファイル配信が可能になったために、Cloudflare WorkersでRemixを動かす方法も十分選択肢に入るようになってきました。
一方、Cloudflare Pagesも、pages functionsというほぼWorkersのような機能を使うことでRemixの動作を可能としているため、内部的にはおおよそ変わりはありません。
ただ、Workersで動かしたほうがより詳細なプロキシ制御が行えるというのは一つの利点でしょう。
ジンフォも少し開発時期が遅ければ、Workersで動かす方法を取っていたかもしれません。
余談:環境の切り分けとwrangler.tomlについて
Cloudflareの各種サービスでは、wrangler.tomlという設定ファイルをプロジェクトのルートディレクトリに設置することで、環境変数やCloudflare内のサービスとの連携などをIaCチックに管理することができます。
また、Cloudflare Pages/WorkersにはEnvironmentという、環境そのものの切り分けをする機能もあります。一つの wrangler.toml
に複数のEnvironmentの設定を記載することで、複数環境を管理できます。便利ですね。
このEnvironment機能ですが、Workersでは無制限に作ることができるのに比べて、Pagesではproductionとdevelopmentの二種類しか作ることができません。簡単なウェブサイトだとこれで十分かもしれませんが、例えば本番環境、ステージング環境、開発環境と3つの環境を建てたい場合、Pagesでは簡単に実現することができません。
加えて、Cloudflare側に wrangler.toml
以外の設定ファイルを読んでもらうようにすることも2025年1月現在は不可能です。
後述しますが、ジンフォの開発では大きく4環境を用意しました。とはいえいちいちダッシュボード上から設定を管理したくなかったため、 wrangler.toml
を複数作り、デプロイ時に解決させるという脳筋解決策を取ることにしました。
つまり、以下のような作戦です。
- ルートディレクトリに
wrangler-dev.toml
とwrangelr-prod.toml
を作る -
wrangler.toml
自体は.gitignore
にいれて管理から外す - ダッシュボード上で2つのPagesプロジェクトを作る
- ビルドコマンドの設定を
pnpm build
からcp wrangler-dev.toml wrangler.toml && pnpm build
とする(prod用のプロジェクトも同様)
これによって、毎回ビルド前に該当するwrangler.tomlが生成され、一つのリポジトリで3つ以上の環境が用意できる、という算段です。
ただあまりスマートな解決方法ではないので、環境を複数切り分けたい場合はPagesを使わずにWorkersを使ったほうが良いでしょう。
アセット配信
ジンフォはユーザ投稿型のウェブサービスであるので、アセット配信用のサーバを用意する必要があります。ストレージにはS3互換のクラウドストレージサービス Cloudflare R2 を採用しました。
アセット配信サーバとしてはRemixとは別にCloudflare Workersを建てており、画像の圧縮やキャッシュなどを行っています。
画像変換については空雲さん開発の wasm-image-optimization
を活用させていただいています。next.js互換のクエリパラメータで画像変換が行えるめちゃくちゃ便利なライブラリです!
メインデータベース
個人開発のコストはDBのコストに左右されるという話もあるように、DB選定はコストに直結する要素です。
ジンフォについても、なるべくランニングコストを抑えるというのを一つの方針として技術選定を行っていたため、無料枠があるDBサービスを中心に検討していました。
検討初期段階ではplanetscaleが有力候補でしたが、その作業の最中、planetscaleが無料枠を撤廃するとの報が流れてきました。
しかし、時期を同じくしてCloudflare D1のGAが発表されました。実体はSQLiteということで、機能面にやや不安要素もありましたが、CloudflareのCDN網を活用した素早いリードレプリカの配信や、Cloudflare Pages/Workersとの連携のしやすさ、そして何と言っても破格の料金体系に惹かれ、Cloudflare D1を採用するに至りました。
さて、改めて紹介ですが、Cloudflare D1はCloudflareが提供するフルマネージドRDBサービスです。DBシステムとしてはSQLiteを採用しており、オリジンのDBが変更されると、そのRead ReplicaをCDN上の各地に配信するという、Cloudflareの強みを活かしまくった強力なDBサービスです。任意の時間のバックアップを呼び出せるTime travel機能やダッシュボード上での簡易的なテーブル閲覧機能もあり、フルマネージドDBとしても必要十分な機能を備えています。
しかし細かいところで制約も多いです。その一つが容量制限です。おそらくRead Replicaをばらまくという性質上、Workers PaidプランですらDBの最大容量が10GBに制限されています。まぁ十分といえば十分ですが。。
キャッシュ
RemixはSSRであるために、リクエストに応じて都度ページがサーバサイドでレンダリングされ、ユーザへと返却されます。ここで、大きくボトルネックとなる箇所はSSRそのものとDBの呼び出しでしょう。この部分のキャッシュに、Cloudflare Cache及びCloudflare Workers KVを用いています。
Cloudflare Cache は、CloudflareのCDNのオプションです。CDN上にキャッシュをばらまくことでキャッシュさせる代物です。自動で適用させることもできる他、Cache APIを経由することで特定のHTTPレスポンスをキャッシュさせておくこともできます。
Cloudflare Workers KV (以下Workers KV)はCloudflareが提供するKey-Value型のストレージサービスです。いわゆるNoSQLの一種ですね。
KVサービスと言うだけあって、読み出しの速度が非常に高速であるために、キャッシュ用途としても用いることができます。
ジンフォでは、SSRのキャッシュにはCache APIを、DBのレスポンスのキャッシュにはWorkers KVを用いています。
先述の通り、Cloudflare Pages上でWorkersを動かすPages Functionsという機能を使うことでCloudflare Pages上でのRemix動作が実現されています。この呼び出しはソース内で明示的に行われており、Remixを呼び出す前段階でCache APIを呼び出してやることで、Remixを起動せずにキャッシュをレスポンスとして返却するのがSSRのキャッシュです。
一方、Cloudflare Cacheは無料プランにおいては詳細な制御を効かせづらいという欠点があります。特にキャッシュをパージしたい際、無料プランでは基本的に全パージしか方法がありません。
そこで、Workers KVをキャッシュ領域として使うことで、詳細な制御が可能となります。
DBのキャッシュは検索パラメータや他様々なものをキーとしつつ、一部パージタイミングをシステムとして組み込みたい箇所もあったため、Cache APIではなくWorkers KVをキャッシュ領域として採用しました。
Workers KVにはTTLが設定でき、指定した時間を超えた場合は自動パージすることが可能です。また、その他にも独自のフィールドを定義し、カスタムメタデータを付与することも可能です。
ジンフォではこれらを活用し、SWR(Stale-While-Revalidate)パターンを用いたキャッシュ機構を実装しました。実装の自由度が高く、その一方サービス間の連携も容易であるのはCloudflareスタックのいいところです。
↓参考にした記事
Google Cloud周り
基本的なインフラはCloudflareで完結させていますが、一部補助的にGoogle Cloudを用いています。現時点で利用しているのは以下のサービスです。
- Indentity Platform:いわゆるIDaaS。ユーザ管理・認証に利用。
- Cloud Functions:サーバレスクラウドコンピューティングサービス
- Firestore:ドキュメントベースのストレージサービス
- Cloud Scheduler:cronチックにjobを実行できるサービス
ユーザ管理・認証
ユーザ管理・認証にはGoogle CloudのIDaaS、Identity Platform を使っています。十分な機能が搭載されたIDaaSで、料金も5万MAUまで無料という太っ腹っぷりです。
Google OAuthは当然のこと、電話やメールなどの認証方法も自由に選択でき、またオミットすることもできます。
ジンフォでは現在Google OAuthでのサインアップ・サインインのみ可能にしていますが、Identity Platformを裏側に用いることで、今後のログイン手段の拡張も見据えています。
やはりウェブサービスを作るうえで一番怖いのはユーザ登録周りの処理なので、このクリティカルな部分を一部とはいえマネージドサービスに投げられるのは安心感もありますね。
セッション管理にはWorkers KVを用いています。
Firestore
Firestore はドキュメントベースのNoSQLデータベースです。Firebaseのサービスという印象がありますが、Google Cloudからも直接操作することができます。
D1との使い分けですが、こちらにはログデータや閲覧履歴データなど、非同期的に扱っても問題ないがデータ量が多くなりうるデータを蓄積させています。
バッチ処理
ジンフォには「注目されている同人誌」というエリアがありますが、この算出には過去の閲覧履歴データやいいねのデータなどを利用した独自の尺度を用いています。ここでその尺度の詳細は述べませんが、時間経過による重み付けを行っているため、計算には単純な集計情報だけではなく、時系列のデータが必要になってきます。
そこで、ジンフォではクラウドコンピューティングサービスである Cloud Functionsを用いてこの注目度の算出を行い、それを更にFirestoreに保存しています。
そこそこ重い処理ではあるので、一日に二回更新することにしており、そのスケジューリングを Cloud Schedulerで行っています。
RemixはFirestoreに保存されたこの情報を読みに行くことで、最新の注目同人誌を取得しています。
開発環境
次に、開発環境における技術選定についてご紹介します。
パッケージマネージャ
パッケージマネージャには pnpm を採用しました。これは特に深い理由はなく、単純にいつも使っていて使い慣れているというのが一番の理由です。
pnpmはユーザ単位でパッケージのキャッシュが保存されるので、使えば使うほどよく使うパッケージのキャッシュヒット率が高くなるのがいいですね。
リンタ・フォーマッタ
リンタ・フォーマッタにはbiomeを採用しました。これはパッケージマネージャとは逆に、今まで使ってなかったので使ってみたかったというのが大きな理由ですね。
これ以前はPrettier/ESLintを使っていたのですが、biomeに変えたことでの速度上での利点はそこまで感じられませんでした。ただ一方、プラグインでdevdepenciesが圧迫されたり、コンフィグファイルがわかりやすいのは良い点です。
微妙に使ったことないけど規模が大きくなると便利なのでは、と思えるような技術を選定できるのは個人開発の嬉しいところです。
コンポーネントカタログ
コンポーネントカタログとして、言わずとしれたStorybookを導入しました。あくまで一人で開発しているのでそこまでコンポーネントカタログを介したコミュニケーション用途としては重要視していないのですが、デザインカンプからコンポーネントのみを組み立てる作業をする際には個人開発とはいえ非常に強力なツールになります。
必要なコンポーネントがあらかた揃った開発終盤では、出番は減ってしまいましたが。
余談:StorybookでRemixのLinkコンポーネントを使う
リンクカードや各種ボタンなど、コンポーネントレベルでページ遷移の機能を持たせたいことはよくあります。しかし、Storybook内でRemixのLinkコンポーネントを使ったコンポーネントを使うと、Linkコンポーネントが親要素にRemixを見つけられず、エラーが出てしまいます。
これの解決には @remix/testing
に含まれる unstable_createRemixStub()
を用いると良さそうです。unstableがついていますが、まぁコンポーネント確認に用いるだけなので大丈夫でしょう。
テスト
テストにはVitestを用いました。テストライブラリも現時点ではVitestが一つの正解になっているような感じがするので、あまり他技術との比較は行っていません。基本的にはサーバーサイドのコアロジックについてのユニットテストを中心に書いています。
wrangler
wrangler はCloudflareが提供する開発用コマンドラインツールです。開発用ローカル環境の提供からリモート環境へのデプロイまでこなせるスグレモノです。基本的にCloudflare Workers周りの開発はこれなしでは行えないでしょう。
wranglerにはminiflareと呼ばれる、Cloudflare Workersとその周辺技術のシミュレーターが内蔵されています。ジンフォではD1やR2、KVといったCloudflare関連サービスを使っているためCloudflare依存が激しいですが、wranglerを使うことでモックコードを書かずにローカル環境でCloudflare環境の開発が完結します。
D1やR2のローカル実体は普通にファイルとしておいてあるため、リセットしたくなったらそれを消せばいいだけというのもお手軽です。
Drizzle Studio
後述するORM drizzle-orm
にくっついてきた開発用ツールDrizzle Studioが非常に便利だったのでこのセクションでご紹介します。
Drizzle Studioはdrizzle-ormの開発チームが手掛けるDB及びSQLの開発補助ツールです。DBをテーブル形式で閲覧できる機能や、簡単な編集機能、SQLやdrizzle-ormが用意する各種関数の簡易実行などを行うことができ、サンドボックスとして非常に便利に扱うことができます。
Drizzle Studioはnpm経由でいれるために、開発環境を汚さずに導入できるのも嬉しい点です。
D1やdrizzle-ormの挙動に惑わされる機会がかなりの頻度で起こったので、都度DBの様子を確認できるこのツールは開発速度に大きく貢献しました。
drizzleユーザは是非使ってみてください
Figma
開発環境というのにはちょっと違うかもしれませんが、デザインカンプの作成にはFigmaを用いました。
ジンフォは小規模なデザインシステムを作り、それに従ってデザインを作成したのですが、その運用にあたって活用したのがTokens Studio for Figma(旧Figma Tokens)というプラグインです。
これはFigmaのトークン機能を大幅に拡張してくれるプラグインで、絶妙にプログラマブルに管理しづらかったFigmaの各種トークンが抜群に活用しやすくなります。詳しい機能紹介は以下の記事が参考になります。
デザイントークンの表現について、2025年1月現在標準規格と呼べるものは正式には存在していません。しかし、W3Cのワーキンググループにより、デザイントークン標準規格のドラフトが提案され議論が続いています。Tokens StudioはこのW3C規格をベースとしたフォーマットでトークンを出力することができます。これによりデザインカンプとシステムへの実装をスムーズに接続することができます。(詳細はCSSの項で後述)
サーバーサイド
Remix
基本的なウェブフレームワークとして、今回は Remixを採用しました。現在、ある程度のウェブサービスを作るにあたって大きいフレームワークはNext.jsとRemixの二強であるというある種の偏見を持っています。コスト的な観点からCloudflareをインフラのベースにすることは事前に決まっていたため、それに乗せやすいフレームワークという観点から技術選定を行いました。
Cloudflare上でNextjsを動作させる方法もあるのですが、やはりVercel依存になっている技術が多すぎるため、CloudflareではNextjsの真価を十二分に発揮することは厳しいと言えます。その点、RemixはWeb標準を念頭に設計が行われているため、Cloudflareのほうが相性が良いと言えます。
他、細かいチューニングなども考えた結果、今回はRemixを採用することにしました。
さて、Remixといえばv2でRemixとしての開発が終了し、その次のバージョンからReact Routerに統合されることが決まっています。
その統合版であるReact Router v7ですが、先日リリースされましたね。
色々と破壊的変更も多いため、ジンフォは現時点ではRemix v2で運用しています。そのうちReact Router v7へ上げようと思っています。
ディレクトリ構造やドメインロジックの切り出し方など、Remixを用いた細かい開発方針などはいずれまた別記事でご紹介します。
ORM
ORMとして、drizzle-ormを採用しました。ORM選定において重要視した要素は「軽量」「シンプル」「Typescriptとの親和性」です。
Cloudflare Workersは無料枠でのビルドサイズ制限が1GBであるため、いかにこのサイズ以下に抑えるかというのが一つの目標になっています。ジンフォについてはWorkers Paidであるためサイズ制限は5GBですが、それでも起動にかかる時間のことを考えるとなるべく軽量にしておくに越したことはありません。
その点で、TypescriptにおけるORMの第一候補であるPrismaは選択せず、シンプル・軽量なORMであるdrizzle-ormを採用しました。
drizzle-ormはORMとしてはシンプルな部類で、むしろクエリビルダに近いとも言えます。しかし、スキーマ定義や型宣言周りは非常に強固で、Typescript前提として考えるのであれば必要十分な型機能を提供してくれるORMです。マイグレーションファイルを生成するdrizzle-kit
や、開発補助ツールであるdrizzle-studio
などの周辺ツールも揃っており、開発体験はなかなか悪くないです。
また、cloudflare D1用のアダプタもあり、絶妙に標準SQLiteとは異なるD1独自仕様をいい感じに吸収してくれています。
一方、v1に至っていないこともあってか、ところどころで怪しい挙動もあります。特にマイグレーション周りで感じることが多く、今のところは自動生成されたマイグレーションファイルを手放しに実行する不安はあります。これはD1の謎仕様によるところもあるのですが……
また、SQL標準な記法の他、ややActiveRecordチックな独自の記法であるRelational Queriesという記法でDBを叩くこともできます。
v1リリース前に改良版のQueries v2が提案され、組み込まれるとのことなのでそちらに期待です。
バリデーション
サーバ・クライアントともにバリデーションには Valibot を採用しました。これも前述の理由と同じく、なるべく軽量なライブラリを優先した結果の採用です。
バリデーションライブラリといえばZodが第一候補として上がる事が多いですが、現時点では特にZodに比べて使いづらいところはなく、快適に使えています。
uttkさんによる逆引きチートシートには非常に助けられました。
フロントエンド
CSSライブラリ
ある程度落ち着いてきたとはいえ、CSSライブラリは未だに群雄割拠の有り様です。パフォーマンスの観点からはゼロランタイムなライブラリが良さそうなのですが、この分野で第一候補としてよく上がるtailwindcssは信仰上の理由から使いたくないという状況にありました。
そこで採用したのが、Panda CSS というCSSライブラリです。
Panda CSSはZero-runtimeとtype-safeを売りにしたCSSライブラリです。Chakra UIのチームが主に開発しているライブラリであり、Chakra UIに慣れたユーザであれば親しみのある記法で記述することができます。私個人はChakra UIを使うことが多かったので、書き心地を損なわずゼロランタイムで動作するPanda CSSはかなり使いやすいライブラリであると感じました。
さて、Panda CSSですが、tailwindと同じようにデザインシステムとの相性が非常に良いです。コンフィグファイルで宣言的にデザイントークンを記述することで、セマンティックトークンベースでUIを記述することができます。
このデザイントークン宣言ですが、W3Cのデザイントークン標準とある程度互換性があるような形で記述することが可能です。そう、Figmaの項で紹介したTokens Studio for Figmaから出力したデザイントークンをそのまま持ってくることができるのです!
いくらデザイントークンが便利とはいえ、その接続が人力ではなかなかデザインと実装のPDCAを回すことができませんが、ここに互換性があることで非常に堅牢にデザインと実装を接続することができます。
余談ですが、Chakraの開発陣は結構しっかりXでエゴサをしているので、悩んでることを呟いていると向こうから話しかけてくれたりします。ありがたい。
UIライブラリ
ある程度実装がややこしいコンポーネントはUIライブラリに頼ってしまおうということで、UIライブラリも導入しました。とはいえデザインは自分で行いたかったので、デザイン抜きで機能だけを提供してくれるUIライブラリ、いわゆるヘッドレスUIライブラリを利用することにしました。
ヘッドレスUIライブラリといえば、Radix UIやHeadless UIあたりが有名ですが、今回はArk UIを採用しました。
理由は単純で、こちらもChakraの開発陣が作っているからですね。Panda CSSとの相性もよく、公式のリファレンスも、コードを覗きに行ったらデザインはPanda CSSで行われているので、実装の参考になるというのも大きいです。
また、Park UIというArk UIにPanda CSSでデザインをつけたUIライブラリもあるため、こちらの実装もかなり参考になります。
(ArkにPandaで色付したからParkって、ネーミングセンスありますよね)
ちなみに、少し前にChakra UI v3がリリースされました。ある種レガシーな遺産として残っていたemotionを切り捨てて、Zagjs+Ark UI+Panda CSSの構成になるのではという噂もありましたが、結局そうではなかったですね。
フォームライブラリ
RemixはWeb標準をベースとした思想で設計されており、クライアントサイドからのデータ送信もフォーム経由が推奨されています。そのため、フォームの登場回数はかなり多く、フォームライブラリを導入するメリットも多くなってきます。
Reactのフォームライブラリといえばreact-hook-formやFormikあたりがよく候補に上がりますが、Remixとの相性という点だと Conform に軍配が上がります。
詳細はchimameさんによる以下の記事が詳しいです。
上記の記事でも評価されていますが、サーバーサイドでのエラーハンドリングとそれをクライアントサイドへ返却するルーチンがRemixのLoader/Actionのルーチンと非常に相性がいいわけですね。
conform自体はバリデーションライブラリとの連携も想定した設計となっており、公式実装としてZodとYupへのアダプタが公開されています。ジンフォではValibotを採用しましたが、ValibotとConformのアダプタとしては、上記記事のchimameさんが公開なさっているconform-to-valibotがあります。
D&Dライブラリ
UXの観点からは導入したいけど、実装の観点からはかなりめんどくさいものの一つにドラッグ・アンド・ドロップ(D&D)があります。ジンフォにおいても最初は導入する予定がなかったのですが、同人誌編集のページにおいて、流石にD&Dを導入しないと冗長すぎるUIになる箇所があったため、D&Dを導入することにしました。
dnd kit を採用しました。
上掲の記事などを参考に技術を比較していましたが、ある程度モダンな構成かつ開発が活発という点から、今回は公式のドキュメントやデモがわかりやすく、D&Dとは思えないくらい素早く実装ができました。デモページがStorybookなのが面白いですね。
おわりに&宣伝
かなりダラダラと書き続けてしまいましたが、いかがでしたでしょうか。
技術選定と銘打ったものの、開発が一段落ついた今見直してみると結構ふわっと技術を選定しており、あまり後学のためにはならない記事になってしまっていました。。
まぁ、最悪うまくいかなかったら書き直せばいいや!という決断ができるのも個人開発の良いところですね(悪いところでもある)。
今回は技術選定を中心に紹介しましたが、もしかするとドメインロジックの戦略やRemixのディレクトリ構造等の詳細に踏み込んだ【実装編】、サービスを作るにあたって行った下準備や開発に直接かかわらない作業についてまとめた【企画編】などについての記事も執筆するかもしれません。
最後に宣伝ですが、情報系同人誌のためのプラットフォーム「ジンフォ」はいわゆる技術系同人誌の方々にも使っていただきたく設計しています。技術系同人誌には技術書典という大きなプラットフォームが存在していますが、それでもその同人誌情報は即売会に閉じており、実際のところは技術書典のページやCirclems、X、BOOTHなどに点在しています。
情報系同人誌をよく作る/読む方や、情報系同人誌の文化に興味がある方は、是非ジンフォに登録してみてください!
サービスの機能や理念についての詳細は以下の記事をご参照ください。
ここまで読んでいただき、ありがとうございます!
(おかしい点のご指摘やコメント等も大歓迎です!)
Discussion