Lesson 11

Next.jsとFirebaseでTODOアプリの開発 - ログの出力

Lesson 11 Chapter 1
サーバー側のログを出力

標準出力(stdout)とは

本チャプターではエラー処理の結果などをファイルに出力するロギングについて学んでいきます。 ログの出力は実装時のエラー解析や本番動作中のエラーの復旧にとても役立ちます。 Next.js はクライアントとサーバーの両方で動作するため、複数の形式のロギングがサポートされています。
ブラウザ上でのconsole.log
サーバ上でのstdout
例としてnpm run devはサーバ側での処理になりますので実行した際にCLIに出力されるメッセージはstdoutでのロギングになります。 それではstdoutでのログをJSON形式でフォーマットするnext-loggerモジュールをプロジェクトにインストールしてみましょう。

next-loggerをインストールとスクリプトの追加

それではプロジェクトにNext.jsデフォルトのロギングモジュール「next-logger」をインストールしていきます。 CLIでプロジェクトのルートパスに移動し、下記コマンドでインストールします。
npm install next-logger
次にnpm run dev起動時のオプションでロギングにnext-loggerを指定していきます。
オプションの指定はpackage.jsonのdevスクリプトに記述をします。
"scripts": "dev": "NODE_OPTIONS='-r next-logger' next dev",
上記のスクリプトを記述することでnpm run devコマンドを起動時にnext-loggerのJSON形式でサーバー側のロギングを設定することができます。
npm run devの起動で下記の様なログが表示されれば確認完了です。

~/work/next.js/next-firebase-todo-app % npm run dev

> next-firebase-todo-app@0.1.0 dev
> NODE_OPTIONS='-r next-logger' next dev

{"level":30,"time":1677309357406,"pid":69559,"hostname":"hogehoge.local","name":"next.js","prefix":"ready",
"msg":"started server on 0.0.0.0:3000, url: http://localhost:3000"}
{"level":30,"time":1677309357409,"pid":69559,"hostname":"hogehoge.local","name":"next.js","prefix":"info",
"msg":"Loaded env from /Users/yamato/work/next.js/next-firebase-todo-app/.env.local"}
{"level":30,"time":1677309357960,"pid":69559,"hostname":"hogehoge","name":"next.js","prefix":"event",
"msg":"compiled client and server successfully in 282 ms (187 modules)"}~>

エラー処理を実装

TODO一覧表示のデータ取得をサーバ側で実行

それではTODO一覧表示のエラー処理を実装していきます。
サーバ側でのログの出力を行う為にgetServerSidePropsでTODO一覧データの取得を実装していきます。 今まで使用してきたfirebaseSDKはクライアント側からFirebaseにアクセスするツールです。 サーバ側でデータの取得を行うには別途firebase-admin-sdkというツールを下記のコマンドでダウンロードします。
npm install firebase-admin
firebase-adminを使用するにはまずプロジェクトの設定のサービスアカウントから新しい秘密鍵の生成をクリックし、秘密鍵(json ファイル)をダウンロードします。

nextjs-11-1

ダウンロードした json ファイルの中にproject_id, client_email, private_keyがあるので、それを.env.localに追加します

env.local
# for client-side
NEXT_PUBLIC_FIREBASE_API_KEY="***************"
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN="***************"
NEXT_PUBLIC_FIREBASE_PROJECT_ID="***************"
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET="***************"
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID="***************"
NEXT_PUBLIC_FIREBASE_APP_ID="***************"

# for firebase-admin
FIREBASE_PROJECT_ID="***************"
FIREBASE_CLIENT_EMAIL="***************"
FIREBASE_PRIVATE_KEY="***************"

こちらもSDKの初期化が必要で、firebase.jsと同じ階層にFirebaseAdmin.jsを作成し、以下で初期化します。

FirebaseAdmin.js
import admin from "firebase-admin";
let adminApp;

if (!admin.apps.length) {
    adminApp = admin.initializeApp({
        credential: admin.credential.cert({
            projectId: process.env.FIREBASE_PROJECT_ID,
            clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
            privateKey: process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, "\n"),
        }),
    });
}

export const dbAdmin = admin.firestore(adminApp);

上記で初期化の設定が完了しました。 それではuseEffectでデータ取得していた部分をコメントアウトしてgetServerSidePropsを記述します。 index.jsxに下記の処理を追加します。

pages/index.jsx
export const getServerSideProps = async() => {
try {
    const col = dbAdmin.collection('todos');
    const snap = await col.get()
    const todos = snap.docs.map((todo) => { 
        return {
            id:todo.id,
            todo:todo.data().todo
        }
    })
    console.log(todos);
    return {
        props: { todos }
    }
} catch (error) {
    console.log(error);
    const todos = "";
    return {
        props:{ todos }
    }
}

上記の記述でSSRでfirebaseのtodoが取得できます。またtry catchで接続できなかった際のエラーをハンドリングしている為、エラーの出力ができます。 次にpageコンポーネントの引数でpropsを受け取ればサーバ側でのtodo取得とエラーハンドリングは完了です。

export default function Index(props) {処理内容}

動作確認

CLIで下記を実行することで標準出力を「app.log」というファイルに書き込むことができます。
npm run dev > app.log
上記を実行すると画面の出力が無くなり全てapp.logファイルに書き込まれることがわかります。 また、todoを正常に取得できた際は取得したデータがログファイルに表示され何らかのの理由でデータが取得できなかった場合はエラーが書き込まれます。 この実装でgetServerSidePropsは全てサーバ側で処理を行なっているということがさらに実感できましたね。