Open10

Next.js おぼえがき

よつよつ

sudo n が機能しない問題

手始めに Node.js のアップデートをしておこうと思ったら、sudo nが機能してなかった。

$ n latest
  installing : node-v21.6.0
       mkdir : /usr/local/n/versions/node/21.6.0
mkdir: cannot create directory ‘/usr/local/n’: Permission denied

  Error: sudo required (or change ownership, or define N_PREFIX)


$ sudo n latest
sudo: n: command not found

調べると以下のような記事がヒット。

https://www.codelab.jp/blog/?p=3637

どうやらnをインストールした際に現在使用しているユーザーのフォルダに対してインストールしていたのが問題で、sudoは別のフォルダを参照しているため、うまく機能していなかったっぽい。
これを解決するため、sudoの設定をちょっといじる。

sudo visudo

そしたら、以下の行をコメントアウト。

Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin"

かわりに以下の一行を追加。

Defaults env_keep +="PATH"

そしたら保存。
最後にもう一度 sudo n latest してみると...

$ sudo n latest
  installing : node-v21.6.0
       mkdir : /usr/local/n/versions/node/21.6.0
       fetch : https://nodejs.org/dist/v21.6.0/node-v21.6.0-linux-x64.tar.xz
     copying : node/21.6.0
   installed : v21.6.0 to /usr/local/bin/node
      active : v18.12.0 at /home/yotu/.nodenv/versions/18.12.0/bin/node

うむ。問題なく機能した。

よつよつ

プロジェクト作成~サーバーを立てる

https://www.tohoho-web.com/ex/nextjs.html#install

適当な場所にプロジェクトフォルダを作る。名前は何でもOK。

mkdir nextjs
cd nextjs

必要なライブラリのインストールをしておく。

npm install next@latest react@latest react-dom@latest typescript@latest @types/react @types/node

そしたら生成されたpackage.jsonにコマンドを追記。

{
+  "scripts": {
+    "dev": "next dev",
+   "build": "next build",
+    "start": "next start",
+    "lint": "next lint"
+  },
  "dependencies": {
    ...
  }
}

そしたら表示するページを作っていく。

mkdir app
cd app
touch layout.tsx
touch page.tsx

layout.tsxは以下の通り。

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="ja">
      <body>{children}</body>
    </html>
  )
}

page.tsxは以下の通り。

export default function Page() {
  return <h1>Top Page</h1>;
}

ここまでできたら、ファイル構造は以下の通り。

├── app
│   ├── layout.tsx
│   └── page.tsx
├── next-env.d.ts
├── node_modules
├── package-lock.json
├── package.json
└── tsconfig.json

そしたら起動する。

npm run dev

ブラウザを開いて、localhost:3000にアクセスする。
このような画面が表示されればOK。

よつよつ

page.tsx

最初に作成したpage.tsxだが、これは「/に対するページ」として自動的にルーティングされる。
これは Next.js の機能の一つで、たとえば/hogeにページを作りたかったら/app/hoge/page.tsxを作成してあげるとよい。

よつよつ

layout.tsx

すべてのページに適用されるレイアウトを記述する。
たとえば「/app/hoge以下のすべてのページ」など適用範囲を絞りたい場合は、app/hogeに別途layout.tsxを作ってあげるとよい。

よつよつ

template.tsx

layout.tsxとほぼ同等の機能だが、こちらはページを遷移するたびにDOMを再生成するという特徴を持つ。

よつよつ

ファイル名

指定のフォーマットでファイル名を付けることにより、ルーティングを調整したりできる。

プライベートフォルダ(_xxx

たとえば_hoge/page.tsxを作成したとして、/hogeもしくは_hogeにアクセスしたとき、ほかのファイルとは違いアクセスできない。これは_hogeプライベートフォルダだからであり、たとえroute.tsなどを配置したとしてもアクセスはできなくなる。

ルートグループ((xxx)

通常、app内に作成されたフォルダはすべてルートとして認識される。
しかし、「ルートには含めたくないけど、いくつかのファイルをグループとしてフォルダにまとめたい」ということがある。そういう時はフォルダ名を丸括弧()で囲むことで実現できる。
たとえばhoge/(huga)/page.tsxにアクセスするとき、URLはlocalhost:3000/hogeになる。

ちなみに、この機能を使ってルートが衝突した場合、より階層が浅いpage.tsxファイルが参照されてるみたい(要検証)。

ダイナミックルーティング([xxx]

urlにパラメータを含めたい場合は、ダイナミックルーティングを使うことができる。

たとえば/user/{id}のようなurlにアクセスして、ユーザーの詳細ページへ遷移したいとする。こういった場合はapp/user/[id]/page.tsxのように鍵括弧をつけたフォルダを作成することで表現できる。

ちなみに、コード内では以下のように引数としてパラメータを受け取ることで値を使用できる。

export default function Page({ params }: { params: { id: number } }) {
  return <h1>{ params.id }</h1>;
}
よつよつ

認証機能を作る

https://qiita.com/ke_sukesakuma/items/4b56b9e81c1788d38440
https://shirotamaki.hatenablog.com/entry/2023/03/26/155103#Google-APIの設定手順

よく躓くので作ってみる。
Basic認証は今後のサービスで使いたくないので、今回は適当にGoogleアカウント認証として作ってみる。

Next.jsの認証機能はnext-authを使って作成していく。
https://next-auth.js.org/

GCP上にプロジェクトを作成する

まずは下準備として、Google Cloud Platformにプロジェクトを作成する。
https://console.cloud.google.com/apis/dashboard

そしたら「OAuth 同意画面」に移動して、「User Type」に「外部」を選択して「作成」を押す。

フォームが表示されるので、必要な情報を埋めていく。

入力を終えたら「保存して次へ」を押す。
そしたらスコープの設定画面に飛ぶ。ここではメールアドレスを認証に使うので「Googleアカウントのメインのメールアドレスの参照」にチェックを入れ、下部「更新」を押す。

スコープ一覧に問題なく追加されていれば「保存して次へ」。

次に、テストユーザーを設定する。
デフォルトでは誰もアクセスできないようになっているので、テストユーザーとして自分を追加しておく。

ここまでできたら「保存して次へ」を押した後、「認証情報」をクリック。

Next.jsからGoogleの認証にアクセスするには、トークンが必要となる。
上部「認証情報を作成」から「OAuth クライアント ID」をクリック。

そしたら必要事項を入力していく。
種類は「ウェブアプリケーション」、リンクは以下の通りにする。

  • 承認済みのJavaScript 生成元: http://localhost:3000
  • 承認済みのリダイレクトURI: http://localhost:3000/api/auth/callback/google

書き終えたら「作成」をクリックする。
そしたら以下のような画面に遷移するので、「クライアントID」「クライアントシークレット」をそれぞれメモする。

最後に「ライブラリ」をクリックし、「Google+ API」を検索して「有効にする」をクリック。

よつよつ

環境変数の設定

そしたらNext.jsのプロジェクトフォルダへ戻る。

環境変数を扱うのでdotenvをインストールしておく。

npm i dotenv

そしたらルートフォルダに.envファイルを作成し、中に環境変数を書いておく。
NEXTAUTH_SECRETopenssl rand -base64 32で生成したものを書いておく。

GOOGLE_CLIENT_ID="xxx"
GOOGLE_CLIENT_SECRET="xxx"

NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET="xxx"
よつよつ

ルーティングの設定

まずはnext-authをインストールする。

npm i next-auth

そしたらsrc/app/api/auth/[...nextauth]/route.tsを作成し、以下のように記述する。

import NextAuth from 'next-auth';
import GoogleProvider from 'next-auth/providers/google';

const handler = NextAuth({
	secret: process.env.NEXTAUTH_SECRET,
	providers: [
		GoogleProvider({
			clientId: process.env.GOOGLE_CLIENT_ID ?? '',
			clientSecret: process.env.GOOGLE_CLIENT_SECRET ?? '',
		}),
	]
});
export { handler as GET, handler as POST };

次にsrc/providers/NextAuth.tsxを作成し、以下のように記述する。

'use client';  // SessionProviderがuseContextを使用するので必要

import { SessionProvider } from 'next-auth/react';
import { ReactNode } from 'react';

const NextAuthProvider = ({ children }: { children: ReactNode }) => {
	return <SessionProvider>{children}</SessionProvider>;
};

export default NextAuthProvider;

これでセッション情報をほかの画面で使用することができる。
そしたらこれをlayout.tsxに組み込んでおく。

import NextAuthProvider from "../src/providers/NextAuth";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="ja">
      <body>
        <NextAuthProvider>{children}</NextAuthProvider>
      </body>
    </html>
  );
}

ここまでで認証を組み込む下準備は完了。

よつよつ

コンポーネントを作成する

app/componentsLogin.tsxLogout.tsxを作成する。

Login.tsx

import React from "react";
import { useSession, signIn } from "next-auth/react";

export default function Login() {
  const { data: session, status } = useSession();

  if (status === "loading") {
    return <div>Loading...</div>;
  }

  if (status !== "authenticated") {
    return (
      <div>
        <p>あなたはログインしていません</p>
        <button onClick={() => signIn("google", {}, { prompt: "login" })}>
          Googleでログイン
        </button>
      </div>
    );
  }
  return null;
}

Logout.tsx

import React from "react";
import { useSession, signOut } from "next-auth/react";

export default function Logout() {
  const { data: session, status } = useSession();

  if (status === "authenticated") {
    return (
      <div>
        <button onClick={() => signOut()}>ログアウト</button>
      </div>
    );
  }
  return null;
}

そしたらこれをルートページに組み込む。

"use client";
import { useSession } from "next-auth/react";
import Login from "./components/Login";
import Logout from "./components/Logout";

export default function Page() {
  const { data: session, status } = useSession();
  return (
    <div>
      {status === "authenticated" ? (
        <div>
          <p>セッションの期限:{session.expires}</p>
          <p>ようこそ、{session.user?.name}さん</p>
          <img
            src={session.user?.image ?? ``}
            alt=""
            style={{ borderRadius: "50px" }}
          />
          <div>
            <Logout />
          </div>
        </div>
      ) : (
        <Login />
      )}
    </div>
  );
}

そしたらlocalhost:3000にアクセスする。

「Googleでログイン」をクリック。
アカウント一覧が表示されるので、ログインしたいアカウントでログインする。

以下のようにログインが通ったらOK。