Next.jsを利用したプロジェクト構築手順について

# はじめに
お仕事で何個かNext.jsのプロジェクトを作っていると大体同じような手順を繰り返すことがわかったため手順書に起こしました。
もし良ければ参考・ご指摘頂けると嬉しいです。

# セットアップ内容
- 言語：TypeScript
- ライブラリ：React
- フレームワーク：Nextjs
- UIライブラリ：MUI
- パッケージマネージャー：npm
- コードフォーマッター：Prettier
- リント：ESLint
- テスト：Jest, React Testing Libraly

## Nextjsの初期設定
参考リンク：https://nextjs-ja-translation-docs.vercel.app/docs/getting-started

- 以下のコマンドを実行する。
npx create-next-app@latest --typescript
または 
yarn create next-app --typescript

```
$ npx create-next-app@latest --typescript
Need to install the following packages:
  create-next-app@12.3.1
Ok to proceed? (y) y
✔ What is your project named? … <Project-name>
Creating a new Next.js app in /home/<Project-name>.

Using yarn.

Installing dependencies:
- react
- react-dom
- next

yarn add v1.22.19
warning ../package.json: No license field
info No lockfile found.
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Saved lockfile.
success Saved 17 new dependencies.
info Direct dependencies
├─ next@12.3.1
├─ react-dom@18.2.0
└─ react@18.2.0
info All dependencies
├─ @next/env@12.3.1
├─ @next/swc-linux-x64-gnu@12.3.1
├─ @next/swc-linux-x64-musl@12.3.1
├─ @swc/helpers@0.4.11
├─ caniuse-lite@1.0.30001412
├─ js-tokens@4.0.0
├─ nanoid@3.3.4
├─ next@12.3.1
├─ picocolors@1.0.0
├─ postcss@8.4.14
├─ react-dom@18.2.0
├─ react@18.2.0
├─ scheduler@0.23.0
├─ source-map-js@1.0.2
├─ styled-jsx@5.0.7
├─ tslib@2.4.0
└─ use-sync-external-store@1.2.0
Done in 23.06s.

Installing devDependencies:
- eslint
- eslint-config-next
- typescript
- @types/react
- @types/node
- @types/react-dom

yarn add v1.22.19
warning ../package.json: No license field
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Saved lockfile.
success Saved 159 new dependencies.
info Direct dependencies
├─ @types/node@18.7.23
├─ @types/react-dom@18.0.6
├─ @types/react@18.0.21
├─ eslint-config-next@12.3.1
├─ eslint@8.24.0
└─ typescript@4.8.3
info All dependencies
├─ @babel/runtime-corejs3@7.19.1
├─ @babel/runtime@7.19.0
├─ @eslint/eslintrc@1.3.2
├─ @humanwhocodes/config-array@0.10.5
├─ @humanwhocodes/gitignore-to-minimatch@1.0.2
├─ @humanwhocodes/module-importer@1.0.1
├─ @humanwhocodes/object-schema@1.2.1
├─ @next/eslint-plugin-next@12.3.1
├─ @nodelib/fs.scandir@2.1.5
├─ @nodelib/fs.stat@2.0.5
├─ @nodelib/fs.walk@1.2.8
├─ @rushstack/eslint-patch@1.2.0
├─ @types/json5@0.0.29
├─ @types/node@18.7.23
├─ @types/prop-types@15.7.5
├─ @types/react-dom@18.0.6
├─ @types/react@18.0.21
├─ @types/scheduler@0.16.2
├─ @typescript-eslint/parser@5.38.1
├─ @typescript-eslint/scope-manager@5.38.1
├─ @typescript-eslint/typescript-estree@5.38.1
├─ acorn-jsx@5.3.2
├─ acorn@8.8.0
├─ ajv@6.12.6
├─ ansi-regex@5.0.1
├─ ansi-styles@4.3.0
├─ argparse@2.0.1
├─ aria-query@4.2.2
├─ array-union@2.1.0
├─ array.prototype.flat@1.3.0
├─ array.prototype.flatmap@1.3.0
├─ ast-types-flow@0.0.7
├─ axe-core@4.4.3
├─ axobject-query@2.2.0
├─ balanced-match@1.0.2
├─ brace-expansion@1.1.11
├─ braces@3.0.2
├─ callsites@3.1.0
├─ chalk@4.1.2
├─ color-convert@2.0.1
├─ color-name@1.1.4
├─ concat-map@0.0.1
├─ core-js-pure@3.25.3
├─ cross-spawn@7.0.3
├─ csstype@3.1.1
├─ damerau-levenshtein@1.0.8
├─ deep-is@0.1.4
├─ dir-glob@3.0.1
├─ emoji-regex@9.2.2
├─ es-to-primitive@1.2.1
├─ escape-string-regexp@4.0.0
├─ eslint-config-next@12.3.1
├─ eslint-import-resolver-typescript@2.7.1
├─ eslint-module-utils@2.7.4
├─ eslint-plugin-import@2.26.0
├─ eslint-plugin-jsx-a11y@6.6.1
├─ eslint-plugin-react-hooks@4.6.0
├─ eslint-plugin-react@7.31.8
├─ eslint-scope@7.1.1
├─ eslint-utils@3.0.0
├─ eslint@8.24.0
├─ esquery@1.4.0
├─ esrecurse@4.3.0
├─ estraverse@5.3.0
├─ fast-deep-equal@3.1.3
├─ fast-glob@3.2.12
├─ fast-json-stable-stringify@2.1.0
├─ fast-levenshtein@2.0.6
├─ fastq@1.13.0
├─ file-entry-cache@6.0.1
├─ fill-range@7.0.1
├─ find-up@5.0.0
├─ flat-cache@3.0.4
├─ flatted@3.2.7
├─ function.prototype.name@1.1.5
├─ get-symbol-description@1.0.0
├─ glob-parent@6.0.2
├─ glob@7.2.3
├─ grapheme-splitter@1.0.4
├─ has-bigints@1.0.2
├─ has-flag@4.0.0
├─ import-fresh@3.3.0
├─ imurmurhash@0.1.4
├─ is-bigint@1.0.4
├─ is-boolean-object@1.1.2
├─ is-callable@1.2.7
├─ is-date-object@1.0.5
├─ is-extglob@2.1.1
├─ is-negative-zero@2.0.2
├─ is-number-object@1.0.7
├─ is-number@7.0.0
├─ is-shared-array-buffer@1.0.2
├─ is-string@1.0.7
├─ is-symbol@1.0.4
├─ is-weakref@1.0.2
├─ isexe@2.0.0
├─ js-sdsl@4.1.4
├─ json-schema-traverse@0.4.1
├─ json-stable-stringify-without-jsonify@1.0.1
├─ json5@1.0.1
├─ jsx-ast-utils@3.3.3
├─ language-subtag-registry@0.3.22
├─ language-tags@1.0.5
├─ locate-path@6.0.0
├─ lodash.merge@4.6.2
├─ lru-cache@6.0.0
├─ merge2@1.4.1
├─ micromatch@4.0.5
├─ minimist@1.2.6
├─ ms@2.1.2
├─ natural-compare@1.4.0
├─ object-assign@4.1.1
├─ object-inspect@1.12.2
├─ object.assign@4.1.4
├─ object.entries@1.1.5
├─ object.fromentries@2.0.5
├─ object.hasown@1.1.1
├─ optionator@0.9.1
├─ p-limit@3.1.0
├─ p-locate@5.0.0
├─ parent-module@1.0.1
├─ path-exists@4.0.0
├─ path-key@3.1.1
├─ path-type@4.0.0
├─ picomatch@2.3.1
├─ prop-types@15.8.1
├─ punycode@2.1.1
├─ queue-microtask@1.2.3
├─ react-is@16.13.1
├─ regexp.prototype.flags@1.4.3
├─ regexpp@3.2.0
├─ resolve-from@4.0.0
├─ reusify@1.0.4
├─ rimraf@3.0.2
├─ run-parallel@1.2.0
├─ safe-regex-test@1.0.0
├─ shebang-command@2.0.0
├─ shebang-regex@3.0.0
├─ slash@3.0.0
├─ string.prototype.matchall@4.0.7
├─ string.prototype.trimend@1.0.5
├─ string.prototype.trimstart@1.0.5
├─ strip-ansi@6.0.1
├─ strip-bom@3.0.0
├─ strip-json-comments@3.1.1
├─ supports-color@7.2.0
├─ text-table@0.2.0
├─ to-regex-range@5.0.1
├─ tsutils@3.21.0
├─ type-check@0.4.0
├─ type-fest@0.20.2
├─ typescript@4.8.3
├─ unbox-primitive@1.0.2
├─ uri-js@4.4.1
├─ which-boxed-primitive@1.0.2
├─ which@2.0.2
├─ word-wrap@1.2.3
├─ yallist@4.0.0
└─ yocto-queue@0.1.0
Done in 15.86s.

Initialized a git repository.

Success! Created <Project-name> at /home/<Project-name>
```

### (srcディレクトリを作成する場合)
srcディレクトリを作成し、pages、stylesを配下に移動させる。そしてsrc配下にcomponents、utils、typesディレクトリを作成する。

```
cd <Project-name>
mkdir src && mv pages/ styles/ src/
mkdir {src/components,src/utils,src/types}
touch {src/components/.gitkeep,src/types/.gitkeep}
```

### tsconfigの修正
以下の設定値をcompilerOptionsに追加する。それによりcomponents等のディレクトリのファイルについて、@componentsで指定することが可能。

```
"baseUrl": ".",
"paths": {
    "@pages/*": ["./src/pages/*"],
    "@styles/*": ["./src/styles/*"],
    "@components/*": ["./src/components/*"],
    "@utils/*": ["./src/utils/*"],
    "@types/*": ["./src/types/*"],
}
```

### package.jsonの修正
デフォルトではscriptsにexportがないため追加が必要。
```
"export": "next export",
```

## Prettierの設定
- 以下のコマンドを実行する。
npm install -D prettier eslint-config-prettier
または
yarn add -D prettier eslint-config-prettier

```
$ npm install -D prettier eslint-config-prettier

added 2 packages, and audited 239 packages in 2s

79 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
```

コードフォーマットのルールを設定するために.prettierrc.jsonを作成する
設定値はPrettierの公式を参考に設定する
リンク：https://prettier.io/docs/en/options.html

```
{
  "arrowParens": "avoid",
  "printWidth": 120,
  "singleQuote": true,
  "jsxSingleQuote": true,
  "trailingComma": "all",
  "endOfLine": "auto"
}
```

### Prettierの整形対象を設定
package.jsonのscriptsに以下を追記し、Prettierの整形対象を設定する。
```
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx}\"",
```

## ESLintの設定ファイルを修正
.eslintrc.jsonにprettierを追記し、ESLintとPrettierが干渉しないようにする。
```
{
  "extends": ["next/core-web-vitals", "prettier"]
}
```

デフォルトではルールが設定されていないため、追加でルールを設定したい場合は.eslintrc.jsonに追記する。

```
{
  "extends": ["next/core-web-vitals", "plugin:@typescript-eslint/recommended","prettier"],
  "rules": {
    "react/display-name": "off",
    "@next/next/no-img-element": "off",
    "react/no-unescaped-entities": "off",
    "import/no-anonymous-default-export": "off",
    "@typescript-eslint/no-unused-vars": "error",
    "@typescript-eslint/ban-ts-comment": "off",
    "@typescript-eslint/no-explicit-any": "off",
    "@typescript-eslint/no-non-null-assertion": "off",
    // add new line above comment
    "lines-around-comment": [
      "error",
      {
        "beforeLineComment": true,
        "beforeBlockComment": true,
        "allowBlockStart": true,
        "allowClassStart": true,
        "allowObjectStart": true,
        "allowArrayStart": true
      }
    ],
    // add new line above return
    "newline-before-return": "error",
    // add new line below import
    "import/newline-after-import": [
      "error",
      {
        "count": 1
      }
    ],
    "@typescript-eslint/ban-types": [
      "error",
      {
        "extendDefaults": true,
        "types": {
          "{}": false
        }
      }
    ]
  },
  "plugins": [
    "import"
  ],
  "settings": {
    "import/parsers": {
      "@typescript-eslint/parser": [
        ".ts",
        ".tsx"
      ]
    },
    "import/resolver": {
      "typescript": {
        "alwaysTryTypes": true,
        "project": [
          "./tsconfig.json"
        ]
      }
    }
  }
}

```

ルールを追加したため、対応するパッケージをインストールする。

npm i -D @typescript-eslint/parser @typescript-eslint/eslint-plugin
または
yarn add -D @typescript-eslint/parser @typescript-eslint/eslint-plugin



## MUIを追加
リンク：
https://mui.com/material-ui/getting-started/installation/
https://zenn.dev/ttani/articles/next-materialui-setup

- 以下のコマンドを実行する。
```
npm install @mui/material @emotion/react @emotion/styled @mui/icons-material
または
yarn add @mui/material @emotion/react @emotion/styled @mui/icons-material

mkdir src/utils/cache
touch src/utils/cache/createEmotionCache.ts

# サイト全体のthemeを変更する場合
mkdir src/theme
touch src/theme/theme.ts
```

### キャッシュ関連の共通処理を追加
キャッシュ関連の共通処理を追加する。そのためにsrc/utils/cacheディレクトリを作成し、createEmotionCache.tsを作成する。
```
import createCache from '@emotion/cache';

export const createEmotionCache = () => {
  return createCache({ key: 'css', prepend: true })
}
```

#### サイト全体のthemeを変更する
サイト全体のthemeを変更する場合、src/themeディレクトリを作成し、theme.tsを作成する。
```
import { createTheme } from '@mui/material/styles';
// Create a theme instance.
const theme = createTheme({
palette: {
   primary: {
      main: '#556cd6',
   },
   secondary: {
     main: '#19857b',
   },
   error: {
   main: #d32f2f,
   },
  },
});
export default theme;
```

### _app.tsxを変更する
_app.tsxを変更し、createEmotnionCacheとthemeを読み込むように変更する。
また、stylesが不要となったため、削除する。
```
import type { AppProps } from 'next/app';
import Head from 'next/head';
import { ThemeProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import { CacheProvider, EmotionCache } from '@emotion/react';
import theme from '../theme/theme';
import { createEmotionCache } from '@utils/cache/createEmotionCache';

// Client-side cache, shared for the whole session of the user in the browser.
const clientSideEmotionCache = createEmotionCache();
interface MyAppProps extends AppProps {
  emotionCache?: EmotionCache;
}

function MyApp(props: MyAppProps) {
  const { Component, emotionCache = clientSideEmotionCache, pageProps } = props;

  return (
    <CacheProvider value={emotionCache}>
      <Head>
        <meta name='viewport' content='initial-scale=1, width=device-width' />
      </Head>
      <ThemeProvider theme={theme}>
        <CssBaseline />
        <Component {...pageProps} />
      </ThemeProvider>
    </CacheProvider>
  );
}

export default MyApp;
```

pages/index.tsxを書き換える

```
import type { NextPage } from 'next'
import { Button } from '@mui/material';

const Home: NextPage = () => {
  return (
    <>
      <Button variant="contained">Hello World</Button>
    </>
  )
}

export default Home
```


## Jestの設定
リンク：https://nextjs.org/docs/testing#jest-and-react-testing-library

- 以下のコマンドを実行する。
npm i -D jest jest-environment-jsdom @testing-library/react @testing-library/jest-dom
または
yarn add -D jest jest-environment-jsdom @testing-library/react @testing-library/jest-dom

- package.jsonのscriptへの追加
```
"test": "jest --watch"
```

- jest.config.jsを作成
```
// eslint-disable-next-line @typescript-eslint/no-var-requires
const nextJest = require('next/jest');

const createJestConfig = nextJest({
  dir: './',
});

// Add any custom config to be passed to Jest
/** @type {import('jest').Config} */
module.exports = createJestConfig({
  moduleDirectories: ['node_modules', '<rootDir>/'],
  moduleNameMapper: {
    '^@components/(.*)$': '<rootDir>/src/components/$1',
    '^@pages/(.*)$': '<rootDir>/src/pages/$1',
    '^@utils/(.*)$': '<rootDir>/src/utils/$1',
  },
  testEnvironment: 'jest-environment-jsdom',
});
```
src配下に__test__ディレクトリを作成し、テストファイルを用意すればテストが可能となる。