こんにちは、普段 pixivcoban のフロントエンド開発を担当している nyamadan です。
今回は、pixivcoban でログイン必須ページの初期表示を改善するために、SSR で session cookie の有無を確認する仕組みを導入した話を紹介します。
pixivcoban では Next.js の Pages Router を利用しており、通信やローカル状態の管理には Vercel SWR を使っています。ログイン状態の確認も、クライアント側では SWR を利用した useLoginRequired() hook 経由で行っています。
ほとんどのページでログインが必要な構造になっているため、ログイン状態をどのタイミングで判定し、どのように UI に反映するかは、ユーザー体験に直結する課題でした。
この記事が、Next.js の Pages Router で認証まわりの初期表示を改善したい方の参考になれば幸いです。
pixivcoban が抱えていた課題
pixivcoban では、比較的短い時間でセッションが切れることがあります。そのため、ログイン切れのユーザーに対して再ログイン画面を表示する場面が少なくありません。
一方で、従来の実装ではログイン切れを判断するために、ブラウザから一度 API リクエストを送る必要がありました。
つまり、すでにログインしていないことが分かっているような状態でも、初期表示後にクライアント側で通信し、そのレスポンスを待ってからログイン UI を表示する流れになっていました。これは通信としても無駄があり、ユーザーにログイン UI を見せるまでの時間も長くなってしまいます。


改善方針
今回の改善では、SSR の時点で session cookie の有無を確認し、cookie が存在しない場合はログインが必要な状態として扱うことにしました。
ここで重要なのは、cookie があることをもって「認証成功」とはみなしていない点です。pixivcoban のセッションは cookie によって管理されていますが、最終的な認証・認可はバックエンド側で判断されます。
そのため、今回のチェックはあくまで初期表示のための軽量な判定です。
- cookie がない場合: 認証に成功しないことがほぼ確定しているため、SSR でログイン UI を表示する
- cookie がある場合: 認証成功とは確定せず、従来通りクライアント側のチェックと API 側の認証・認可に委ねる
たとえば、cookie 自体はブラウザに残っていても、バックエンド側のセッションが何らかの理由で無効化されているケースがあったとします。この場合、SSR の cookie チェックだけではログイン済みとは判断できません。そのため、cookie がある場合でも useLoginRequired() による認証チェックを行い、バックエンド側でセッションが無効になっていればログイン UI に遷移させます。
フローとしては次のようになります。

この方針により、「ログインしていない可能性が高いユーザーに、できるだけ早くログイン UI を見せる」ことを目指しました。
実装方法
Next.js のドキュメントでは、Proxy を使って軽量な認証チェックを行う方法が紹介されています。
https://nextjs.org/docs/pages/guides/authentication#optimistic-checks-with-proxy-optional
Proxy では、データベースアクセスのような重い処理ではなく、cookie の有無などの軽量なチェックに留めるべき、という考え方です。
pixivcoban でもやりたいことは近かったのですが、認証が必要なページと不要なページが混在しています。そのため、proxy で一括して処理するよりも、ページ単位で明示的に適用できる形の方が扱いやすいと判断しました。
今回の目的はリクエスト全体を横断的に制御することではなく、「ログイン必須ページとして実装されているページ」にだけ明示的に初期表示最適化を適用することでした。
この形にすると、ログイン必須であることが各ページの実装上に明示されます。Proxyで一括制御するよりも、ページごとの責務としてレビューしやすく、既存ページにも段階的に導入しやすいと考えました。
そこで、getServerSideProps をラップする withLoginRequiredGSSP を用意しました。
/** * ログインが必要なgetServerSidePropsを作成する */ export function withLoginRequiredGSSP<P extends { [key: string]: any }>( innerGSSP: GetServerSideProps<P> ): GetServerSideProps<P | { isLoginRequired: true }> { return async (ctx: GetServerSidePropsContext) => { const sessionId = ctx.req.cookies["session"]; if (!sessionId) { ctx.res.statusCode = 403; return { props: { isLoginRequired: true }, }; } return await innerGSSP(ctx); }; }
この関数では、リクエストに session cookie が存在しない場合、ログインが必要であることを示す props を返します。これにより、クライアント側でログイン状態確認 API の結果を待たずに、初期表示からログイン UI を出せるようになります。
実装上のハマりどころ
今回の実装で注意が必要だったのは、SSR 側のチェックとクライアント側のチェックを必ずセットで使う必要がある点です。
pixivcoban には、実際に API リクエストを行ってログイン状態を確認する useLoginRequired hook がすでに存在しています。SSR で cookie の有無を見るだけでは、cookie が残っているがサーバー側ではセッションが失効しているケースを検知できません。
そのため、ログイン必須ページでは次の 2 つを必ず一緒に使う必要があります。
withLoginRequiredGSSPuseLoginRequired()
片方だけを使ってしまうと、未ログイン時の初期表示が遅くなったり、逆に cookie が残っている失効セッションを正しく扱えなかったりします。
この組み合わせを人間の注意だけで守り続けるのは危険です。そこで、ESLint のカスタムルールを用意して、withLoginRequiredGSSP と useLoginRequired() がセットで使われていることを検出するようにしました。
export default { create(context) { let hasGssp = false; let hasHook = false; let gsspNode = null; let hookNode = null; return { 'CallExpression[callee.name="withLoginRequiredGSSP"]'(node) { hasGssp = true; gsspNode = node; }, 'CallExpression[callee.name="useLoginRequired"]'(node) { hasHook = true; hookNode = node; }, "Program:exit"() { if (hasGssp && !hasHook) { context.report({ node: gsspNode, messageId: "missingHook" }); } else if (!hasGssp && hasHook) { context.report({ node: hookNode, messageId: "missingGssp" }); } }, }; }, };
このルールにより、実装漏れをレビューで見つけるのではなく、機械的に検知できるようにしています。
この対応で改善できること
この対応により、session cookie が存在しないユーザーには、初期表示の時点でログイン UI を出せるようになります。
ログイン状態確認 API の結果を待たずにログイン UI を初期表示できるため、少なくとも未ログインユーザーに対しては不要な待ち時間を減らせます。
また、未ログイン状態のレスポンスを通常の 200 として扱わないことで、ログイン必須ページであることを HTTP ステータス上も明示できます。
まとめ
今回は、pixivcoban のログイン必須ページで、SSR による軽量な session cookie チェックを導入した話を紹介しました。
ポイントは、cookie の有無による判定を認証・認可の代替として使わないことです。あくまで初期表示の最適化として利用し、保護データの取得や操作は従来通り API 側で認証・認可します。
小さな改善ではありますが、ログインが必要なユーザーに素早く適切な UI を見せるという点では、ユーザー体験の改善につながる取り組みだったと感じています。
ログイン状態のように「最終判断はバックエンドに任せるべきもの」でも、初期表示のために安全に使える情報はあります。責務を分けて扱うことで、認証・認可の安全性を保ちながら、ユーザー体験を少しずつ改善できます。
