自分流 Next.js + TypeScript + ESLint + Prettier + VSCode (+Dockerfile) 初期設定

最近Next.jsを書くようになって、自分なりのコーディングスタイルが固まってきたので今後パパっとセットアップができるようにメモっておきます。

この記事では以下のことができるようになります。

- Next.jsをTypeScript / TSX で書く
- ESLintとPrettierとVSCodeを組み合わせ、保存時自動フォーマット
- importを相対パスではなく絶対パスで書く
- ServerをExpressで動かす
- 開発時は.envで、本番は環境変数によるアプリケーション設定値を扱う
- 【発展編】Kubernetesを見据えてDockerで動かす

# Next.jsをTypeScript / TSX で書く

**※執筆時Next.js version: 9.1.1**

`create-next-app` コマンドで雛形を作ってもいいのですが、余計なものもくっついてくるのでスクラッチでインストールします。

```bash
$ mkdir nextjs-starter
$ npm init -y
$ npm install next react react-dom
```

これでインストールできました。
ベースとなるディレクトリ構造は（自分の場合ですが）以下のようにしています。

```bash
$ mkdir -p public src/pages src/components
```
<img width="330" alt="スクリーンショット 2019-10-17 23.53.46.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495244/929d56e4-c25a-4e58-df6c-65dd96b4a14c.png">
Next.jsは[9.1から](https://nextjs.org/blog/next-9-1)、`pages`ディレクトリを`src`ディレクトリ以下に入れることが可能になりました。
また`static`ディレクトリはdeprecatedになり、代わりに`public`ディレクトリを使います。

次にTypeScriptと最低限必要な型定義をインストールします。

```bash
$ npm install -D typescript @types/node @types/react @types/react-dom
```

確認のため`src/pages/index.tsx`を作成します。

```jsx:src/pages/index.tsx
import { NextPage } from "next";

const Page: NextPage = () => <h1>Helloworld!</h1>;
export default Page;
```

`package.json`の`scripts`を以下のようにします。

```json:package.json
{
  "scripts": {
    "dev": "next",
    "build": "next build",
    "start": "next start"
  }
}
```

`npm run dev`で [http://localhost:3000](http://localhost:3000) に開発サーバが立ち上がるのを確認します。

このときNext.jsはTypeScriptを検知して`tsconfig.json`を勝手に作ってくれます。
厳し目に書きたいので

```json:tsconfig.json
"strict": true
```

としておきます。
それ以外のコンパイラオプションは
https://www.typescriptlang.org/docs/handbook/compiler-options.html
を参照してください。

# ESLintとPrettierとVSCodeを組み合わせ、保存時自動フォーマット

PrettierとESLint、ReactのLintルールをインストールします。

```bash
$ npm install -D eslint prettier eslint-plugin-react
```

ESLintとPrettierを併用する場合、双方でコンフリクトするルールを無効化してくれる [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier) が必要になります。
また、 [eslint-plugin-prettier](https://github.com/prettier/eslint-plugin-prettier) を利用することで、PrettierをESLintルールとして扱えるようになります。
詳しくは[Prettierのドキュメント](https://prettier.io/docs/en/integrating-with-linters.html#recommended-configuration)に記載があります。

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

TypeScriptのLintルールも利用したいので、[@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/parser) と [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin)もインストールします。

```bash
$ npm install -D @typescript-eslint/parser @typescript-eslint/eslint-plugin
```

詳しくは[typescript-eslint](https://github.com/typescript-eslint/typescript-eslint#how-do-i-configure-my-project-to-use-typescript-eslint) のドキュメントに記載があります。

`.eslintrc.json`を作成し、以下のようにします。

```.json:.eslintrc.json
{
	"extends": [
		"eslint:recommended",
		"plugin:react/recommended",
		"plugin:@typescript-eslint/recommended",
		"plugin:@typescript-eslint/eslint-recommended",
		"plugin:prettier/recommended",
		"prettier/@typescript-eslint"
	],
	"plugins": [
		"@typescript-eslint",
		"react"
	],
	"parser": "@typescript-eslint/parser",
	"env": {
		"browser": true,
		"node": true,
		"es6": true
	},
	"parserOptions": {
		"sourceType": "module",
		"ecmaFeatures": {
			"jsx": true
		}
	},
	"rules": {
		"react/prop-types": "off",
		"react/react-in-jsx-scope": "off",
		"@typescript-eslint/no-explicit-any": "off"
	}
}
```
rulesはお好みでどうぞ。

VSCodeで自動フォーマットするには、[VS Code ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)が必要です。
インストールしたらVSCodeの設定を以下のようにします。

```json:settings.json
{
  "eslint.autoFixOnSave": true,
  "eslint.validate": [
    "javascript",
    "javascriptreact",
    {"language": "typescript", "autoFix": true },
    {"language": "typescriptreact", "autoFix": true }
  ]
}
```

設定したら一度VSCodeを再起動して、自動フォーマットされるのを確認します。

# importを相対パスではなく絶対パスで書く

`src`ディレクトリ内にはコンポーネントやアプリ内モジュールを書いていくのですが、importするときに

```javascript
import Hello from "../../../components/Hello";
```
と書くのは面倒なので、

```javascript
import Hello from "~/components/Hello";
```
と書けるようにします。

TypeScriptコンパイラにモジュール解決のための設定を追記します。

```json:tsconfig.json
{
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "~/*": ["src/*"]
    },
    ...（略）...
  },
...（略）...
}
```

`next.config.js`を作成して、webpackにも同様にモジュール解決のための記述をします。

```javascript:next.config.js
const path = require("path");

module.exports = {
  webpack: config => {
    config.resolve.alias = {
      ...config.resolve.alias,
      "~": path.resolve(__dirname, "./src")
    };
    return config;
  },
};
```

試しにコンポーネントを作成し、importしてみます。

```tsx:src/components/Hello.tsx
const Hello: React.FC = () => <h1>HelloWorld!</h1>;

export default Hello;
```

```tsx:src/pages/index.tsx
import { NextPage } from "next";
import Hello from "~/components/Hello";

const Page: NextPage = () => <Hello />;
export default Page;
```

問題なく表示できればOKです。

# ServerをExpressで動かす

Next.jsのデフォルトサーバではなく、Expressで動作させようと思います。
理由としてはExpressのほうが使い慣れているからです。

```bash
$ npm install express
$ mkdir server
```

```javascript:server/indexjs
const express = require("express");
const next = require("next");
const http = require("http");

const port = parseInt(process.env.PORT, 10) || 3000;
const dev = process.env.NODE_ENV !== "production";
const nextApp = next({ dev });
const handle = nextApp.getRequestHandler();

nextApp.prepare().then(() => {
  const app = express();

  app.get("*", (req, res) => {
    return handle(req, res);
  });

  const server = http.createServer(app);

  server.listen(port, err => {
    if (err) throw err;
    console.log(`> Ready on http://localhost:${port}`);
  });
});
```

`package.json`の`scripts`を以下のよう書き換えます。

```json:package.json
{
  "scripts": {
    "dev": "node server/index.js",
    "build": "next build",
    "start": "NODE_ENV=production node server/index.js"
  }
}
```
`getInitialProps`以外のサーバサイドのコードは分離させたいので`server`というディレクトリを掘っていますが、そもそもサーバサイドのコードが肥大化している場合はマイクロサービスに切り出す、という気持ちを持っています。

# 開発時は.envで、本番は環境変数によるアプリケーション設定値を扱う

[Twelve-Factor App](https://12factor.net/ja/)によると、**[設定を環境変数に格納する](https://12factor.net/ja/config)** べきであるとあります。

とはいえ開発時は`.env`を利用するのが複数人で開発する上でも楽なので、開発時は.envを、その他の環境では環境変数が優先して利用されるようにします。

Next.jsの場合は[dotenv-webpack](https://github.com/mrsteele/dotenv-webpack)を利用するのが良いです。

```bash
$ npm install -D dotenv-webpack
```

`next.config.js`で以下のようにすることで、システムの環境変数も読み込まれ、優先的に利用されるようになります。

```javascript:next.config.js
const path = require("path");
const Dotenv = require("dotenv-webpack");

module.exports = {
  webpack: config => {
    config.resolve.alias = {
      ...config.resolve.alias,
      "~": path.resolve(__dirname, "./src")
    };
    config.plugins = [
      ...config.plugins,

      // 環境変数を優先して読み込む
      new Dotenv({
        systemvars: true
      })
    ];
    return config;
  }
};

```

`.env`ファイルは必ず`.gitignore`に追加するようにしてください。

# 【発展編】 Kubernetesを見据えてDockerで動かす

nodeアプリケーションをDockerで動かすことに関しては、DockerキャプテンであるBret Fisher氏の[記事](https://www.creationline.com/lab/29422)や、[DockerConでの発表資料](https://github.com/BretFisher/dockercon19)が参考になります。
ポイントは

- `node:alpine`より`node:slim`のほうがおすすめ
- `root`ユーザではなく`node`ユーザで実行する
- `npm`や`nodemon`でアプリケーションを起動してはいけない
- `SIGINT`や`SIGTERM`といったシグナルを受け付け、Graceful Shutdownが実現できるアプリケーションコードを書く

です。特に後半2つはnodeの[PID 1問題](https://www.google.com/search?q=docker+node+pid+1&oq=docker+node+pid+1)に起因するもので、Kubernetesの利用を考えている場合は要注意です。

これらのポイントを踏まえて、以下のようなDockerfileを作成しました。

```Dockerfile:Dockerfile
# Base Image
FROM node:10.16.3-slim as base
RUN apt-get update && apt-get install -y \
  git
ENV TINI_VERSION v0.18.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
RUN chmod +x /tini
EXPOSE 3000
RUN mkdir /app && chown -R node:node /app
WORKDIR	/app
USER node
COPY --chown=node:node package.json package-lock.json ./
RUN npm ci && npm cache clean --force

# Develop Image
FROM base as dev
ENV NODE_ENV=development
ENV PATH=/app/node_modules/.bin:$PATH
CMD ["npm", "run", "dev"]

# Source Image
FROM base as source
COPY --chown=node:node . .

# Production Image
FROM source as prod
ENV NODE_ENV=production
RUN npm run build
ENTRYPOINT ["/tini", "--"]
CMD	["node","server/index.js"]
```

余計なファイルが転送されないように`.dockerignore`ファイルを追加します。

```:.dockerignore
node_modules
npm-debug.log
.next
```

Graceful Shutdownを実現するために [@godaddy/terminus](https://github.com/godaddy/terminus) を利用します。

```bash
$ npm install @godaddy/terminus
```

`server/index.js` を以下のように書き換えます。

```javascript:server/index.js
const express = require("express");
const next = require("next");
const http = require("http");
const { createTerminus } = require("@godaddy/terminus"); // <-- 追加

const port = parseInt(process.env.PORT, 10) || 3000;
const dev = process.env.NODE_ENV !== "production";
const nextApp = next({ dev });
const handle = nextApp.getRequestHandler();

nextApp.prepare().then(() => {
  const app = express();

  app.get("*", (req, res) => {
    return handle(req, res);
  });

  const server = http.createServer(app);

  // 追加
  // Graceful shutdown
  function beforeShutdown() {
    // SIGTERMシグナルが送信された後でもリクエストが流れてくる可能性を考慮し5秒待つ
    return new Promise(resolve => {
      setTimeout(resolve, 5000);
    });
  }
  createTerminus(server, { beforeShutdown });
  // 追加ここまで

  server.listen(port, err => {
    if (err) throw err;
    console.log(`> Ready on http://localhost:${port}`);
  });
});
```

開発時は`docker-compose`を利用することで少し楽をします。

```yaml:docker-compse.yml
version: '3.7'

services:
  node:
    build:
      dockerfile: Dockerfile
      context: .
      target: dev
    volumes:
      - .:/app:delegated
    env_file: .env
    ports:
      - "3000:3000"
```
targetを`dev`にすることで、Dockerfileに記したマルチステージのdev用イメージを指定しています。

開発時はホスト側で`npm install`は行わず、

```bash
$ docker-compose run node npm install
```
でDocker内でnpmコマンドを叩くようにします。

```bash
$ docker-compose up
```
で開発サーバを起動します。

# まとめ

出来上がったものがこちらになります。
https://github.com/matkatsu/nextjs-starter
あくまで自分の場合はこうしてるというだけですが、参考になればと思います。
他の人がどんな工夫をしているかとかも知りたいので、なんでもコメント頂ければ幸いです。