Webアプリを作ってみたいなと思って、Nodejsの勉強がてら東大生限定の掲示板を作ってみた。
作る過程は楽しかったし、同時に難しかったので記録を残しておく。
僕はガチ初心者だし完全なアマチュアなので、すごく基本的な内容だということは念頭に置いといてね。もしかしたらこの記事で間違いとか、セキュリティ上のリスクみたいな部分があるかもしれないから、その時はコメントで教えてくれるとありがたい。
あと言葉もかなり雰囲気・イメージで使っていると思うので、例えばパッケージ・ライブラリの細かい違いとかはお手柔らかに。文系なので。
ちなみにまだユーザーが皆無なので、もしこれを見ているあなたが東大生である、あるいは東大生の知り合いを知っているとかだったらぜひシェアしてみてね。
1.コンセプト・基本方針
きっかけ
やろうと思ったきっかけは、ザッカーバーグがFacebookをハーバード大学から始めたというのを知って、大学という枠で何か作るの面白そうだな〜と思ったから。しかもFacebookってそういうルーツがあるにも関わらず結局大きくなって一般公開したから、今現在そういう大学コミュニティサイトみたいなのって無いよなと。特に大学は学生全員に固有のメールアドレスが配られていて、大学メアドで限定したログイン機能を作れば簡単に限定コミュニティが作れるというイメージも持てた。
僕みたいな勉強中の初心者が自分で思いついたアイデアを実際に作れるかどうかを判断するのは結構難しいけど、掲示板くらいなら行けそうな感じがしたし、読んでいたNodejs入門本で簡単なコメントサイトを作れたからそれに機能を足していく感じで自然に作り上げることができた。
基本方針
掲示板の機能とかデザインはHacker Newsというサイトを基本的に丸パクリすることにした。
https://news.ycombinator.com/news
これはアメリカのYCombinatorというアクセラレータが運営してる掲示板で、テクノロジー分野を中心にリンク投稿・コメントするようなサイト。まあRedditの簡易版みたいな。
自分も日頃見ていて、デザイン・機能のシンプルさ的に丸パクリしやすいかなと思った。コーディングの勉強をする上で機能を自分で考えることはまだしも、デザインを自分で考えることは面倒臭いし時間が無限にかかるので、丸パクリ戦略はかなり効率的で良かった。
2.作るために使ったもの
技術構成
- 言語・フレームワーク
- Node.js、Express
- テンプレートエンジン
- EJS
- データベース
- 当初SQLiteだったけどデプロイで詰まってPostgreSQLに変えた
- Vercel Postgresを利用
- ORM
- Sequelize
- 公開する場所
- Azureでやろうとしたけど金かかりそうだからVercelにした
- ドメイン
- お名前.comの1円枠を使ってたから、XServerの1円/年にした
フロントエンドとバックエンドを上手く組み合わせる的なことがよくわからなかったし、学ぶことがさすがに多すぎるのでReactとかVueとかは使わなかった。
そういう場合なんて言うの?素のHTML/CSS?でもEJSを使っているからそれはフロントエンドっていうの?
CSSに関してはBootstrapを少し使ってる。
問題解決するため
ChatGPT
プログラミングをして初めてChatGPTの真髄を体感した。たぶんChatGPTが無くてGoogle検索頼みだったら10倍の時間がかかっていたし、完成を諦めてた可能性もある。エラーとか、自分がやりたいことを入力するだけでコードを教えてくれるから、コピペだけで物が作れていく。これの弊害は、コピペしたものが動いたら満足して次に進むので、実は理解できていないコードの箇所が出てくること。まあでも効率化の恩恵はすごいし、従来のGoogle検索だってコピペ中心だったことに変わりはないでしょ。ちなみにChatGPTには課金してないので3.5です。
Node.js入門本
めちゃ役に立った。すごくわかりやすいし、シンプルなサイトが作れるからそこに機能を追加する形で自分で色々実験できる。前までは動画チュートリアルとかも見てたけど、改めて本の良さも再確認した。ちなみに技術構成でEJSとかSequelizeを使ったのは、この本でそれを使っていたから。
僕みたいに、HTML/CSS/Javascriptの基本的な書き方とかは勉強したけど、データベースを使った動的なサイトを作ってみたいという人にはおすすめ。
https://www.amazon.co.jp/-/en/gp/product/B08HRMTXHB/ref=dbs_a_def_rwt_bibl_vppi_i46
Google検索
ChatGPTで解決できなかった、こみいった問題は個別にGoogle検索する感じ。特にデプロイするときの、AzureとかVercelについての情報やエラーはGoogle検索が中心だった。料金体系に関してHerokuが無料枠を廃止してそれ以降の情報とかは結構新しいからChatGPTは弱いし、デプロイ時の注意とかはかなり個別的な体験をした人の話が意味を持つからGoogle検索してStackOverflowを見たりした。ちなみに検索は英語でやった方が良いです。
3.機能・使い方
掲示板には基本的に以下の3つの機能だけ。
- 投稿(リンクとタイトルのセット)
- コメント(投稿に対してテキストコメント)
- いいね(投稿に対するいいね)
投稿
投稿されるのはリンク+タイトルで、ニュース記事やウェブサイトをシェアする。
コメント
投稿されたリンクそれぞれに対して、コメントすることができる。
いいね
投稿に対していいねができる。番号横の三角アイコンでいいね/いいね解除できる。コメントへのいいねはできない。
4.技術的なポイント
サイトの各機能の仕組みや自分が学んだ技術をまとめる。
いいね機能の部分
ツイッターのいいねみたいに、いいねボタンを押した時にわざわざページを更新する必要がないようにしたかったので、Ajaxという仕組みを使った。新しい用語が出てくると少し怖いし面倒くさいけど、意外とシンプルで、Expressで作られるPublicフォルダの中にJSファイルを作って少しコードを書くだけ。あとはRoutesの方でサーバーサイドのrouter.postみたいなのでデータベース変えるみたいな処理をする。
const upvotes = document.querySelectorAll(".upvote")
// いいねボタンのクリックハンドラ
upvotes.forEach((upvote)=>{
upvote.addEventListener('click', (event) => {
const id = event.target.id; // 投稿ID (適切なIDに置き換える)
const upDown = event.target.className.split(" ")[1]
// サーバーにAjaxリクエストを送信
fetch(`like/${id}/${upDown}`, {method: 'POST'})
.then(response => response.json())
.then(data => {
if (data.success) {
let newLikeCount = data.newLikeCount
document.getElementById(`${event.target.id}point`).innerText = newLikeCount
event.target.className = upDown == "up" ? "upvote down" : "upvote up"
event.target.src = upDown == "up" ? "https://news.ycombinator.com/triangle.svg" : "https://upload.wikimedia.org/wikipedia/commons/4/4f/Simple_triangle.svg"
}
}).catch(err=>console.error("エラー", err))
});
})
クライアント側にこのJSスクリプトを置いておいて、そこでPOST処理をするみたいな感じ?何やってるかよくわからないと思うけど、1.いいねの数更新、2.いいね画像を変えるの二つをやっているだけ。
データベースの仕組み
データベースの中にテーブルという表が何個かあるわけだけど、今回僕は4つのテーブルを作った。Session保存の関係で勝手に作られるテーブルとかもあるけど、それは後で解説するわ。
- Users
- Boards
- Comments
- Likes
こういうことを公開するのが危険かどうか分からんけど、まあ書いておく。
名前の通り、ユーザー・投稿・コメント・いいねをそれぞれ保存する。今回初めてで少し難しかったのが、Relationalデータベースという概念で、4つのテーブルの関係性をしっかり意識する必要がある。例えばUsersのそれぞれのユーザーにはIDが割り振られているけど、BoardsやCommentsテーブルの中で、誰が投稿したか、誰がコメントしたかをしっかり識別する必要があるから、「UsersのID」は「BoardsのUserID」「CommentsのUserID」と同じだよ〜っていう関連づけをAssociateする必要があった。
特に複雑なのが、Commentsのコメントは一つの投稿に紐づけられるだけでなく、一人のユーザーにも紐づけられるから、他のテーブルと関連する二つの項目がある。またBoardsの投稿は一つのユーザーに帰属するけど、Commentsのコメントは何個も所属する親の立場になるからそれも区別しなきゃいけない。こういうのはSequelizeというORMでbelongsToとかhadManyみたいな指示によって関係性を明示した。
static associate(models) {
Board.belongsTo(models.User, { foreignKey: 'userId' });
Board.hasMany(models.Comment, {foreignKey: "boardId"})
Board.hasMany(models.Like, {foreignKey: "boardId"})
}
Sequelizeのmodelsというフォルダ内でテーブルの設定をする。これはBoardsテーブルの他との関係性。
ユーザーのパスワード
開発の最初の頃はパスワードをそのままデータベースに保存していたけど、それがセキュリティ的にやばいことを知ってハッシュ化/ソルト化を学んで実装した。
気づいたきっかけはTom Scottが出てるYoutube動画で、めちゃくちゃ分かりやすくパスワード保存の仕方とそのリスクについて説明されていた。
色々アルゴリズムを通してハッシュ化して鬼のような長さにして、ソルトでさらに分かりにくくしてとなんだか難しそうな感じだけど、npmでbcryptというものをインストールして使えばかなり簡単にできた。
実際データベースのテーブルで確認しても、訳の分からない文字列になったから成功した。
その他セキュリティ
同じYoutubeチャンネルのTom Scott解説で、SQL InjectionとかCross Site Scriptingについても初めて知った。ユーザーが色々入力できる機能がある場合、そこにscriptタグやSQLクエリを書かれることによってそれが実行されてしまうリスクがあるらしい。
調べたり検証した結果、僕のサイトは基本的には大丈夫だろうと判断した。SequelizeみたいなORMを使用することでデータベースに悪さされるリスクは減るし、エスケープ処理とかでもリスクを低減できている。マジで高度な手法とかもあるのかもしれないけど、別にクレカ情報とか保存しないし、今のところはセキュリティの基本線を押さえているので良いとした。もし注意事項があればコメントでぜひ教えてください。
メール認証、パスワード忘れた機能
nodemailerというもので結構簡単にメールを送ることができる。とりあえず無料のoutlookのメールを使ってるけど、問題なく動作している。
アカウント登録段階での認証機能はverificationトークンを生成して、そのリンクを踏んだら初めて認証されるみたいな感じにした。認証されていないアカウントは一定時間過ぎたら消える設定。
パスワード忘れた機能は、同じ感じだけど、パスワード再設定リンクを送って書いてもらって、あとはデータベースを書き換える感じ。これもリンクを一定時間後に踏まなかったらリンクは使えなくなる設定。
ちなみにメールのアドレスやパスワードもソースコードに直書きするのではなく、しっかり.envファイルに環境変数として設定、.gitignoreでgitには保存せず、vercelのホスティングのところで環境変数を設定した。
セッション
ページを更新するごとのログインは面倒なので、一定時間はユーザーセッションという形でログインしたままにする機能。こういうのもNodejs入門本で教えてくれたからかなり助かった。Express Sessionというものでユーザーの情報とかを一定時間覚えておいて、getとかpostの処理をするときにreq.sessionで名前とかを簡単に取得できる。
一つ注意は、Express Sessionがデフォルトではログイン情報をインメモリで保存していて、リークしちゃうみたいな懸念があって本番環境用で使っちゃいけないこと。Express Sessionのnpmページの下の方に色々なデータベースで利用するためのパッケージがあるから、インストールしてどっかのデータベースに保存する必要がある。ちなみに僕はConnect Session Sequelizeという、SequelizeでPostgreSQLに保存できるやつを使った。
https://www.npmjs.com/package/express-session#user-content-compatible-session-stores
const session = require("express-session")
const SequelizeStore = require('connect-session-sequelize')(session.Store);
// SequelizeStoreの設定
const myStore = new SequelizeStore({
db: db.sequelize,
checkExpirationInterval: 15 * 60 * 1000,
expiration: 24 * 60 * 60 * 1000,
});
let session_opt = {
store: myStore,
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {maxAge: 60 * 60 * 1000}
}
myStore.sync();
app.use(session(session_opt))
5.つまずいて克服したところ
正直、全工程でつまずきまくっているから全部覚えてないんだけど、特に悩んだ部分・どう克服したかを書いておく。やっぱりデプロイに関することが記憶も新しいし多い。
.thenやらPromiseやらasync await
Nodejsを勉強する上で、非同期処理はなかなか難しいテーマである。言ってることはわかるんだけど、実際それがコード上でどういう風に実行されていくとか、どういうことに注意するかみたいなことが難しい。
.then()に関してはすごく理解しやすいし使いやすくて、文字通りその後にこれを実行するみたいな感じだけど、いっぱい.thenを重ねるとめちゃ見にくくなるから、prmoiseやらasync awaitを使って見やすくする必要もある。
僕は正直Promise、async await恐怖症なので、とりあえず.thenで書いたごちゃごちゃの長いやつをChatGPTにリファクタリングお願い!って言って書き直してもらった。ChatGPT君はすごく頭が良いからね。
SQLiteのデプロイ
これがおそらく一番悩んだ。Nodejs入門本でSQLiteが使われていたからそのままSQLiteで公開しようと思ったけど、全く上手くいかなかった。SQLiteは別でデータベースのサーバーがあるわけではなく、普通にプロジェクトのフォルダの中に.sqliteみたいなファイルがあって、それをいじる感じ。すごくシンプルで、本で学ぶ時に本当にわかりやすかったけど、どうやらデプロイには向いていないらしい。
まずVercelでやろうとしたらSQLiteが使えないことが発覚して、Mediumの記事でAzureではSQLiteが使えるという情報が書いてあったから挑戦したけどダメだった。
https://medium.com/@koistya/node-js-and-sqlite-for-rapid-prototyping-bc9cf1f26f10#7fec
マジでこの記事によってめちゃ時間を無駄にした
Azureは大学生アカウントみたいなのでクレカ登録しなくてよかったからこれは来たと思って、Web Appでデプロイしたけどダメ、フォーラムに書いてあったSQLiteだけ分けてStorage Mountみたいな仕組みも試してみたけどダメで、SQLiteの設定とかも色々やってみたけどどうにもならなかった。本当にあらゆる記事とかStackOverflowの回答を見まくって、なんとかやろうとした結果がダメだったから、やっぱりSQLiteはウェブアプリの公開には向いていないんだろうね。
結局、諦めて仕方なくPostgreSQLにすることにした。これで初めてMySQLかPostgreSQLの有名2大巨頭のうち一つに触れる体験をした。ただこれも結構検討が必要で、同じAzureでやろうと思ってたけどAzureのデータベースサーバーはどうやら金がかかりそうということで、無料でできるところとしてVercelに戻ってきて、ちょうどVercel Postgresというのがあったからゾウさんにした。
初めてゾウさんSQLを勉強したけど、そんなに難しいことはなかった。今までのSequelizeで同じように操作できるから、実質コードを書き換える必要はなくて、SequelizeのConfigとかをいじったりする必要があった。ただでもSQLiteの時に結構ゆるく書いてたModelのAssociateの部分をしっかり全部書く必要があって、そこで結構詰まった。
いかに安くするか
お金がないので無料で運用できるようにした。「Herokuの無料枠廃止 移行先」みたいな感じで検索することで効率的に無料枠のあるサービスを調べることができた。
ただ無料枠とは言ってもクレジットカードの登録が必要かどうかは結構大きな分水嶺で、請求される可能性が1%と0%では天と地ほどの差である。特に僕みたいな初心者アマチュア文系プログラマーにとって、クラウド設定ミスはマジで確率として高いから、1%どころか40%くらいの確率でクレカが使われる可能性がある。クレカを登録したばかりに1ヶ月の請求額が100万みたいな誤爆をしたくないので死守するべきラインである。
色々調べて最終的にVercelを選択した。Hobbyプランというのが無料で、それでPostgresも使えるから完璧である。クレカ登録の必要も全く無く、GitHubのアカウントとリポジトリをそのまま連携してGit Pushで更新できるのが夢のようである。
初心者ながら、VercelはなんかNextjsを使っているイケイケプログラマーが使っているイメージだったんだけど、僕もファンになってしまった。まあ結局は裏でAWSを使ってるからアマゾンさまさまなんだけど、Vercelの提供する利便性は素晴らしいし価値があるね。
他にも検討した公開先として、
サービス | 除外理由 |
---|---|
Heroku | 無料枠がなくなったので除外 |
Fly.io | 無料枠なのにクレカを求められて候補から除外 |
Render.com | なんかの理由で除外 |
Netlify | なんか静的サイトのイメージが強いから微妙かなと思って除外 |
AWS | 前使ったときの難しさを思い出して除外 |
Firebase | コードを結構大掛かりに改造する必要があるから除外 |
GCP | うーん。なんとなく嫌だ。 |
以上のようにホスティングはお金はかかってないから、かかったのはakamonnews.comというドメイン代だけ。しかもXServerドメインで初年度1円だから、合計1円だった。しかもXServerにも意地でもクレジットカードを登録したくなかったので、コンビニ支払いで現金1円玉で支払った。
Vercelへのデプロイ
ただVercelで実際サイトが動くまでも結構長い戦いではあった。
ルートのApp.jsをIndex.jsにしないとダメとか、Vercel.jsonのファイルを用意してうまく導いてあげないといけないとか細かな作業が結構あって少し難しかった。
特にPostgreSQLの設定は大変で、なんかSequelizeのMigrationがどうしても上手くいかなくてSQLを自分で書いたりして設定することでなんとか軌道に乗った。
このプロセスはひたすらVercelに表示されるログを見て、そこに吐き出される赤いエラーをコピペしてChatGPTとかGoogleに聞くというもの。マジでいくらこれを繰り返しても表示されずに大変だったけど、ようやくサイトが動いた時はかなり感動的だった。実際アカウント登録したり、投稿できたときはものすごく嬉しかった。
6:まとめというか感想
ものを作るというのはとても楽しいね。何か自分の考えたものが形になって動くのはとても良い気分になる。特にChatGPTによってコードを記述するスピードが爆速になってるから、僕みたいな初心者でもかなりゴールまで辿り着きやすくなってると思う。昔だったらコーディングを諦めてたような人でもChatGPTによって世界が結構変わってるんじゃないかな。ただChatGPTをコーディング以外にめちゃ使ってます!みたいな人はあまりいない気もするからそこは利用用途を拡大させていきたい。
今回よく理解できたのは、少しずつ機能を拡張していくというプロセスで、これは結構大きな学びだった。今までプログラマーがものすごい大きなプロジェクトとかを作る場面を想像して、なんで何千行ものコードを理解できるのかわからんし、それを作ろうと思ったらどこから始めれば良いのかわからんみたいな絶望感があったけど、そうではなくて、少しずつ機能を追加していくのが実態なんだなというのが分かった。たとえ一人だったとしても、本質的な機能の周りにできることを少しづつ加えていくような作業がやっぱり楽しいし、モチベーションも高まるんだなと。
なんかこういう「〇〇作ってみた」系の記事の書き方が全く分からないからこれで内容的に十分なのかわからないけど、もう終わろうと思う。あまりコードの詳細に入り込めなかったかな。
てかマークダウン方式もこの記事で初めて学んだから、訳わからんくらい時間かかったわ。HTMLよりは早いけどWordとかよりは確実に遅い気がする。