Next.js13でアンケートフォーム作ってみた

ディップ [Advent Calendar 2023](https://qiita.com/advent-calendar/2023/dip-dev) の19日目の記事です!

# はじめに
はじめまして、私はディップ株式会社 R&D推進室に所属しております。

最近、業務でNext.jsを使用していたのですが、文献を調べても「あれ？記事とディレクトリ構成が違う...」となったり、記事通りに書いてもうまく表示されなかったりして、「なんかおかしいな」と悩んでいました。

理由を探ってみると、Next.jsのバージョンが`13`となっており、仕様が`12`からガラッと変わっており、ChatGPTに聞いてもなかなか解決できないことがあったりで開発に苦戦していました...

2022年10月26日にリリースされたばかりで、そもそもバージョン`13`の記事があまり出なかったので、学習・アウトプットを兼ねてNext.js 13でアンケートフォームを作成してみました。

今回はその備忘録になります。

# 成果物
「何系エンジニアか」「好きな言語」を質問し、回答結果を集計してグラフで表示するプロジェクトになります。
こちらで公開していますので、ぜひご覧になってください。
[アンケートフォーム](https://nextjs-survey.yutaka-create.com)
[集計結果表示](https://nextjs-survey.yutaka-create.com/SurveyResults)

[Github](https://github.com/yutaka520/nextjs_questionnaire)にも公開しています。立ち上げ方はreadme.mdをご参照ください。

# 環境構築
Next.js、MysqlをDockerの仮想環境に立てました。

```docker-compose.yml
version: '3.8'

services:
  nextjs-app:
    build: ./nextjs_questionnaire
    ports:
      - "3000:3000"
    volumes:
      - ./nextjs_questionnaire:/app  # ファイルを直接マウント
    depends_on:
      - db

  db:
    image: mysql:8.0.35
    environment:
      MYSQL_ROOT_PASSWORD: your_root_password
      MYSQL_DATABASE: your_database_name
      MYSQL_USER: your_user
      MYSQL_PASSWORD: your_password
    ports:
      - '3307:3306'
    volumes:
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
```

```Dockerfile:nextjs_questionnaire/Dockerfile
FROM node:18.17.0-alpine

WORKDIR /app

COPY package*.json ./

RUN npm install

COPY . .

EXPOSE 3000

CMD ["npm", "run", "dev"]

```

データを格納するテーブルはsurvey_responsesとし、engineer_typeに「何系エンジニアか」、favorite_languageに「好きな言語」を格納するようにしています。
```init.sql
CREATE TABLE survey_responses (
  id INT AUTO_INCREMENT PRIMARY KEY,
  engineer_type VARCHAR(255) NOT NULL,
  favorite_language VARCHAR(255) NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```

# フォームの実装
ユーザが選択したデータを保存するための変数を用意します。
```nextjs_questionnaire/src/app/SurveyForm.tsx
"use client"
// SurveyForm.tsx
import { useState } from 'react';

// use client

const SurveyForm: React.FC = () => {
  const [engineerType, setEngineerType] = useState<string>(''); //何系エンジニアか
  const [favoriteLanguage, setFavoriteLanguage] = useState<string>(''); //好きな言語
  const [otherEngineer, setOtherEngineer] = useState<string>(''); //何系エンジニアかの質問でその他が選ばれた時に入力されるテキスト
  const [otherLanguage, setOtherLanguage] = useState<string>(''); //好きな言語の質問でその他が選ばれた時に入力されるテキスト
  const [isCompleted, setIsCompleted] = useState<boolean>(false); //送信ボタンを押した時、リクエストが成功したかどうか
  ...
```
Reactでは、宣言した変数の値を直接変えても一度描画したビューには反映されないため、`useState`を利用して変数を宣言します。`engineerType`の値を変更したいときは、以下のようにします。
```tsx
setEngineerType("Webエンジニア");
```

また、`useState`のようなものを利用してクライアントサイドでコンポーネントの状態が変わることが期待される場合、`"use client"`を宣言しなければなりません。これを宣言したコンポーネント、及び派生のコンポーネントはクライアントサイドで`useState`のような状態を変更する関数が使えるようになります。

# 集計結果表示
フォームで回答いただいたデータを集計し、chart.jsを利用してグラフで表示します。

```nextjs_questionnaire/src/app/SurveyResults/page.tsx
"use client";
import { useEffect, useState } from "react";
import { SurveyResponse } from "../api/survey/route"
import { Pie } from 'react-chartjs-2'
import { Chart, ArcElement, Tooltip, Legend } from 'chart.js'
Chart.register(ArcElement, Tooltip, Legend);

const SurveyChart = () => {
  ...

  return (
    <div style={{ width: 300, margin: '50px auto', display: 'flex', flexDirection: 'column', gap: 40 }}>
      <div>
        <span>回答数: {surveyData.length}</span>
      </div>
      <div>
        <h2>エンジニア種別</h2>
        <Pie data={chartDataEngineer} />
      </div>
      <div>
        <h2>言語種別</h2>
        <Pie data={chartDataLanguage} />
      </div>
    </div>
  );
};

export default SurveyChart;
```
Next.js 13では、フロントで表示するコンポーネントをapp下に配置し、`[任意のフォルダ名]/page.tsx`のような構成でコンポーネントを定義します。
この時、`[任意のフォルダ名]`がURLのパスになり、`page.tsx`内の`export default`に定義されたコンポーネントがそのパスで呼ばれるコンポーネントになります。

# データ取得・保存API
フロントから送信されたフォームデータをMySQLに格納する処理、格納されたデータを取得する処理を定義しています。

```nextjs_questionnaire/src/app/api/survey/route.ts
import { NextResponse } from 'next/server';
import mysql from 'mysql2';

...

export async function POST(req: Request) {
  ...
}

export async function GET(req: Request) {
  ...
}
```
Next.js 13でAPIを定義したい場合、appディレクトリ配下に`[任意のフォルダ名]/route.ts`という形式でファイルを保存します。この時、`[任意のフォルダ名]`がURLのパスとなります。
このプロジェクトでは、`/api/survey`がパスになっており、以下のURLでAPIリクエストができます。
[https://nextjs-survey.yutaka-create.com/api/survey](https://nextjs-survey.yutaka-create.com/api/survey)

また、POST・GET関数を定義することで、それぞれのリクエスト時の挙動を定義することができます。

# 最後に
今年初めてReact系のフレームワークに触れましたが、vueのライフサイクルより動きが追いやすくて覚えやすいと感じました。
とはいえ、`use client`周りでまだ理解しきれていない部分もたくさんありますので、勉強してまた記事にしようと思います。

最後まで読んでいただき、ありがとうございました。