はじめに
みなさん、こんにちは torihaziです
5月が始ま、、、りすぎてもうすでに折り返し地点ですね。
月の最初にするべき振り返りも今月は忙しくてここまで先延ばしとなってしまいました。
4月はですね、機能一貫実装の日々でした。
何もかも0からというわけではなく、人に聞いたりとかいう感じでやりましたが、
にしても、まぁ本番落ちた落ちたで、メンタルやられまくりました。
ということで今月は懺悔とそこから得た反省が主になるかと思いますね。
ではいきましょう。
総括
いつも通りメンタル面から行きましょう。
心構えに関することですね。
負ける時は負ける、けど最後勝てばいい。
負けるというのは、ここでの意味は本番落とすとか失敗するという意味ですね。
先月は本当に負けまくりました。
月日は遡り、2月くらいから私がデプロイしたブランチはことごとく本番を落としました。
主にRailsで、Nil のものを参照しようとして発生するError、Not Found Methodのエラーですね。
ロジックはよく読み込んでいたつもりだったのですが、現実はそこまで甘くありませんでした。
社内リリースだったからまだダメージは少なかったものの、きつかったです。
いつの間にか社内の中でも「やらかす人」みたいなレッテルというか
そういう評判(もちろんそんなどぎついものではなくて、おっちょこちょいキャラ程度のものです)が
出回るようになっちゃいましたね。
まぁ悔しかったですけど、結果なので。
ただデプロイへの回を重ねるごとに、Railsに関する理解が深まっていったのも事実です。
どういう理解かというと基本的なメソッドであったり、トランザクション、そのほかモデルファイルに記載する
validateとvalidatesの違いとか名前空間の設定方法とか。。。
ざっと思いつく感じだとこのくらいですね。
次は失敗するわけにはいかない。
このマインドの元、毎日調べまくりました。
チェリー本を見返したり、公式ドキュメントを見返したりして発見が多かったですね。
でだ。
負けまくっていた私でしたが、そのような失敗を受けて知識を蓄えていった結果
昨日夜間に行った機能Update対応は、切り戻しレベルになるほどの大きなバグを生まずに無事リリースし切ることができました。
それまでにもBackendのデプロイで少しづつ小さな勝ちはありましたが、
今回のBackend、Frontendにわたるデプロイ(私の中では大規模Update😛)の成功でようやく自信がついてきました。
これまで5戦5敗でしたが、6戦目にしてようやく サッカーでいう2 - 1くらいで勝てました
よくある話ですが、最後勝ちゃあいいんです笑
周りが許す限り、勝つまでやり続けましょう。
ここまで長かったですが、これからも知識つけてスピードアップしていきたいと思います。
失敗からちゃんと学ぶこと
と前節でベラベラ述べましたが、本主題のことを肝に銘じているからこその結果だと思います。
失敗してもそこから学ぶこと、不貞腐れずに何かを吸収することです。
ベタな表現ですが、人間誰しも失敗することはあります。
ただそこから学習をすることが大切です。
でなければ1社会人として、仕事が雑な人間として扱われるだけです。
何度も失敗して現実見たくなくなる時もあるでしょう。
辛いですけど、決して目を背けてはダメです。
そこを踏ん張れるか否かで、成長するかが決まります。
けど命の危機があるレベルだったら流石に踏ん張らないでくださいね。
壊れそうならそれは逃げてください。
頑張っている人にさらに頑張れ、というようなつもりはないので
いい感じに受け取っていただければなと思います。
Frontend
いつも通りまずは箇条書きから。
今回は個人開発の部分もあってSupabaseよりのことが多いですかね。
- vercelのデプロイは秒で楽ちん。
- 基本デザインはv0にぶん投げ。
- supabaseのgithub認証は楽。
- supabaseのdb migrationをlocalとデプロイ時のcicdで対応
- supabaseのmiddleware認証は機能しない笑
- Suspenceの利用タイミング
- layout.tsxでdata-fetchをしない方がいい理由
- supabaseのfetch(dbへ直接クエリを叩くやつ)でdata-cacheを使う方法の模索
- EslintのFlat Configは小規模から始めること
- 私のpackage.jsonはまだまだだった。
- nvmの真価に気づいた
- huskyは使うべき
ほぼ実務ではpage routerで特に新しく習得した技術はなかったので個人開発で得た発見になりますね。
では1つずつ。
vercelデプロイ楽ちん
vercelのデプロイってやったことありますか?
今まで、「え、デプロイでしょ。なんかよくわかんないけど難しそう」
と敬遠していましたが、思っていた76倍楽でした。
個人事業の開業届と青色申告の提出と同じくらい警戒してましたけど同じくらい思ったより楽でした。
やることはGithubアカウント持ってたらシンプルです。
- Next.jsのプロジェクトのリポジトリを作成して
- そのGithubでVercelにログインして
- dashboardのAdd Newのところから 対象のGithubリポジトリをimportする
- buildのコマンドはpnpm buildとか指定のやつやっとく
であればokです。
repositoryとコネクトしてれば main mergeに伴って自動デプロイされるので最初のうちはいいのではないかと思います。
そこのデプロイをcicdで対応することもできるらしいですが、一度にやると終わるのでそこで一旦やめときましょう。
基本デザインはv0にぶん投げ。
個人開発やっていて思ったのが、デザインむずいなってこと。
デザインが いい感じ じゃないと作っていてアガらないですよね。
で、困ってたんですけど、そういう時は chatgptで prompt作って v0に投げた方がいいですね。
大枠作ってくれますし、「なんか良さげ」なデザインになります。
大事なのはデザインもそうですが、そのプロダクトのコンセプトの機能を実現することです。
見失わないように、挫折しないようにしましょう。
使えるものは使うべきです。最速で作って、機能は爆速随時追加のスタンスでいきましょう。
下手に盛り込みすぎると破綻します。
supabaseのgithub認証は楽。
これは思っていたより楽でした。同僚がsupabaseの認証を個人開発で使っていたので
試しにやってみたのですが、楽でしたね。
ただ文句言いたいのが全然ドキュメントに載ってへんやないかということがちらほらありましたね。
いや、載っていたのかもしれないけど上から読んでもできんやないかって感じですね。
これに関しては次の個人開発に合わせて記事を書こうかと思います。
本番とlocalの認証含めて、なんか機能一環で書いてみようかなと思います。
自分が一番詰まったのが、本番supabaseに認証成功後のredirect_urlとしてsite_urlを指定する必要があったことです。
localではうまくいっているのに、本番になると認証成功後localhost:3000に飛ばされて「は?」となりました。
確かこいつ公式に書いてなかったですね。許せんすね。笑
supabaseのdb migrationをlocalとデプロイ時のcicdで対応
これについては公式にしっかりと書いてありました。
cicdのサンプルもしっかり機能したし、これについては大感謝ですね。
初めてのまともなcicdもこれを機にできたし、よかったです。
公式のものから少し改良して、少し条件を狭めてます。
ちなみにsupabase linkコマンドは 本番dbと文字通りlinkするために必要なコマンドで
これしないと pushできません。
ただmigrationのrollbackがsupabaseのdocにも書いてなかったのでそこだけが不安ですね。
どうするのかいまだにわかりません。
rollforward?ていうのでやっていくんですかね。
name: Release (Production)
on:
pull_request:
branches:
- main
paths:
- "supabase/migrations/**"
types:
- closed
jobs:
migrate:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
env:
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
SUPABASE_DB_PASSWORD: ${{ secrets.PRODUCTION_DB_PASSWORD }}
SUPABASE_PROJECT_ID: ${{ secrets.PRODUCTION_PROJECT_ID }}
steps:
- uses: actions/checkout@v4
- uses: supabase/setup-cli@v1
with:
version: latest
- run: supabase link --project-ref $SUPABASE_PROJECT_ID
- run: supabase db push
supabaseのmiddleware認証は機能しない笑
これ私だけですかね。
以下のversionだと機能しないんですよね。
"next": "15.3.1",
"@supabase/ssr": "^0.6.1",
"@supabase/supabase-js": "^2.49.4",
公式通りにやってもmiddleware認証の中で読んでいるauthUserとかいうやつが
どうしてもnilで帰ってきてしまって認証に失敗しました。わかる人いたらお願いします。。。
Suspenceの利用タイミング
これについてはズバリ、datafetchをするコンポーネントでfallbackを設定したい時ですね。
page routerとかだと isLoadingの時は
やや読みづらいコードだったと思うんですけどSuspenceを使うことで
より宣言的に書けるようになりましたね。
ぱっと見であ、どういうページになるんだなというのがわかるようになりました。
使い方は PromiseをthrowするコンポーネントをSuspenceで囲めばいいだけですね。
<Suspence fallback=<fallback用のコンポーネント />> <Promiseをthrowする=>data-fetchをするようなコンポーネント /> </Suspence>
て感じですね。
エラー時の対象もするならErrorBoundaryとかでさらに囲む必要ありますが、
その時はそれらをラッピングしたもので囲んだら
見栄え良さそうになるんですけどどうなんですかね。
パッと思いついたのがこんな感じ。
export const ErrorBoundaryWithSuspence = ({
error,
fallback,
children,
}: {
error: React.ReactNode;
fallback: React.ReactNode;
children: React.ReactNode;
}) => {
return (
<ErrorBoundary fallback={error}>
<Suspense fallback={fallback}>
{children}
</Suspense>
</ErrorBoundary>
);
};
必要に応じて初期値与えて〜みたいな。間違ってたらすんません。
layout.tsxでdata-fetchをしない方がいい理由
これは公式にも明示してませんでしたけど、zennに良いことを書いている方がいました。
App Routerのlayout.jsはasyncにしない方が良いかも
結論、layout.tsxはasync / awaitで書くことはできます。
そのため技術的には中でdata-fetchを呼ぶことは可能です。(公式はその例を明示してませんが)
ただそうすると次のデメリットがあります。
それは予期せぬloading画面もしくは表示させたいページの描画がそのfetchが完了するまで終わらない事象が起きます。
これ公式の画像です。

仮にBのlayout.jsxで重いfecthをするとそれは上位のSuspence(Loading.tsx)が補足します。
そしてAで、Layout.tsxで同様のことをすると画面自体が読み込み中となり完全に描画されるまでに時間がかかります。
この挙動を理解した上でならlayout.tsxにfetchを書いてしまうのも良いかと思いますが、
それがuiux的によろしくないと考えるなら app routerの思想通り、ちゃんとleaf なcomponentとして
server componentとして作成し、それをsuspenceで囲み、その中でやりたかったdata-fecthをしてそれを
layout.tsxに組み込むようにした方がいいと思います。
supabaseのfetch(dbへ直接クエリを叩くやつ)でdata-cacheを使う方法の模索
これね、公式も書いてないんですけど、まだ模索中です。
app routerの長所の1つでもありデータキャッシュのやりかた、特にrequest-memolizationとdata-cacheのやり方について
supabseの公式で明示されてないんですよね。
createClientの宣言時にcross-fetchとかいうものしたらいけるよとあったのですが、それをするとlinterに
deprecatedと怒られましてですね。。。
てことで今はcacheとunstable_cacheを使う方針に切り替えてます。
request-memolizationについてはnext.jsの公式通りcacheで対応可能なんですけど問題はdata-cacheの方で。
unstable_cacheについては第一引数に設定するdata-fetch関数内でdynamicなことをしてはいけないそうで
それがsupabaseのdata-fetch時にお作法でするcreateClientの中で読んでるcookies = await cookies()が引っかかるんですよね。
今それの抜け道を模索中です。
調べてみてもあれだし。ただ唯一こんなのはあったから後で試す予定。
Accessing dynamic data sources such as headers or cookies inside a cache scope is not supported. If you need this data inside a cached function use headers outside of the cached function and pass the required dynamic data in as an argument.
実際のエラー
Error: Route (対象のパス) used "cookies" inside a function cached with "unstable_cache(...)". Accessing Dynamic data sources inside a cache scope is not supported. If you need this data inside a cached function use "cookies" outside of the cached function and pass the required dynamic data in as an argument.
EslintのFlat Configは小規模から始めること
やってみて思った。エディタ自体が最新のFlatConfigに対応してない。
なんか赤い波線でないし、自動保存でprettierしか治らないし。
どうなってることやら。
でことは本題に入りまして、慣れてないうちはEslintについては小規模から始めた方がいいです。
あれもこれも入れると挫折します。普通にFlat Configの書き方わかりづらいので。
これについては慣れるしかないと思います。
現状は最終的に出すものはオブジェクトの配列として適用したいものにはfilesで
無視したいのはignoreでruleとか設定すればいける、というのは理解しました。
これ、のちのhusky使えばさらに強くなるのでこれについては今後乞うご期待で感じです。
私のpackage.jsonはまだまだだった。
package.jsonに以下の項目を入れてますか?
- packageManager
- engines
それと.npmrcは作成していますか?
これがないと開発できないわけではないけどより厳しくルール化するなら入れとくべきです。
例えば、enginesを次のように設定して、
"engines": {
"node": "22.x",
"pnpm": "10.x"
},
.npmrcにおいて
engine-strict=true
とするとそのプロジェクトはその環境下でないと pnpm iとかできなくなります。
npm i とかやってもできません。怒られます。
こうしとかないとチーム開発の時に
誰かが追加packageを入れてそれをgit pullしてlocalに落として pnpm iでもnpm iしたときに
その人と環境が違えば pnpm.lock.ymlとかが違って毎度PR出すごとに大量差分が出るという
ちょっと怖いことが起きます。
そうしたことを未然に防ぐためにもできることは最初からしておきましょう。
あとは単純にドキュメント的な役割のためですね。
pacakgeManagerについては 昔はcorepackというものと合わせて使うことで
真価を発揮したそうですが最近非推奨になったそうです。
ただpnpm v10以降で仕様が変わったらしく、上のenginesも全部pnpm 側で設定できるようになったみたい。
まだまだ奥が深い。
huskyは使うべき
これについては記事書きました。
特に恩恵あるのはpre-pushの方ですね。
pre-commitについてはもっと上手いやり方がありそう。
個人的にはcommitした後に、そこで発生した差分を手作業でもう一度commitしなきゃいけないのが
なんかダサい。そこまで自動化したのならここも自動化したい欲が。。。
lint-stagedもあるそうですが、あれはstageしたものしか対象じゃなくて。
stageしたものは保存したらaddの対象としてなりますが、自分の場合エディタの自動保存で
linterとか走るようにしてるので重複すると思い、あえてしてません。
てな感じですね。frontendは。
Backend
backendはそうですね、基礎的なことをより深く知ったという感じですね。
- transactionブロック内では例外を発生させないとrollbackしない
- ActiveRecord::rollback例外はブロック外まで伝播しない
- ActiveRecordのsum, average, max, minはデータがない時の戻り値が違う。
- unlessはあまり使わない方がいいかも。
- validateとvalidatesメソッドの違い
- migrationのcheck制約の付け方
くらいですかねー。
基本crudとかは流石にできるし、それ以外はドメインロジックに関する発見とかだったりするし。
ということで。
transactionブロック内では例外を発生させないとrollbackしない
これは私が事故った件ですね。
transactionブロック書くところまではよかったのですが、
saveメソッド失敗時に内部でnilをreturnしていて、
結局rollbackしないで片方のデータだけ作成されたという事象です。
今となってはなんとお粗末なミスだと思っていますが、
その時には気づけなかったんですよね。
メンタルがメンタルだったので。量も量だったし。
ActiveRecord::rollback例外はブロック外まで伝播しない
これについては同じtransactionブロックでハマりうるポイントですね。
特にtransaction内でrescue とか使う時に起こりうるやつですね。
もしやるならtransaction外でrescue使う感じですね。
ただその時にtransaction内でrollback例外だとその外のrescueまで例外伝播しないので
注意してくださいね。
まぁそのtransactionがrollbackしてたら、最悪rescueに行かなくても
どうせrescueはログとか出すだけだと思うので大事故にはならないはず。
validateとvalidatesメソッドの違い
これについてモデルのカラムに関するvalidationはvalidatesとsがつきますが、
カスタムバリデーションを定義する際はvalidateになります。
migrationのcheck制約の付け方
これについてはmigrationファイルに設定しますね。
add_check_constraintという感じで描き始めます。
modelのvalidationをすり抜けた場合、DBレベルでバリデーションを行うために設定します。
これをやっておかないとバルクinsertとかものによってapplicationレベルのvalidationをすり抜けるやつがあるので
設定しておいた方が良いです。
DBのデータだけは死に物狂いで変なものが紛れ込まないように死守すべきです。
壊れたらマジでタチ悪いです。
手動で直せたら良いものの、心臓に悪いので。
と、backendはこんな感じですね
これっぽちですけど、あとは本当にドメインロジックなので
これ以上はないですね。
むしろこれをしっかりできればあとはドメインロジックかければ大丈夫ということです。
頑張らねば。
終わりに
ということで今月は前に比べたら少しはかけたのではないでしょうか。
潰れなくてよかったです。
個人開発もある程度はできたのであと少し手直しして出せたらいいっすね。
あと転職ドラフトもパスしていよいよ明日から始まるみたいなので、
どんなものか楽しみっすね。
では5月も頑張りましょう。