最近のWebアプリ開発環境の紹介
最近のWebアプリ開発環境の紹介

最近、個人開発などでWebアプリを開発するとなると、Next.js+Vercelで開発することが多いです。

その中でも、TypeScriptやESLintなどの設定が固まりつつあるので、自分の開発しやすい設定を紹介します。

Create a project

create-next-app でNext.jsを作成する際の設定

$ npx create-next-app@latest
What is your project named? my-app
Would you like to use TypeScript with this project? Yes
Would you like to use ESLint with this project? Yes
Would you like to use Tailwind CSS with this project? No
Would you like to use `src/` directory with this project? Yes
Use App Router (recommended)? Yes
Would you like to customize the default import alias? Yes

TailwindだけNoとしています。
TypeScript、ESLintは当然入れています。

src/ディレクトリを使うのは、ルートディレクトリが.eslintrc, .prettierrc, tsconfig.json, codegen.config.js, pathpida.const.js, ... と設定ファイルで溢れ返るので開発中に触るファイルはsrc/以下にまとまっていた方が見やすくなるので、src/を使うようにしています。

App Routerは、今後のスタンダードになっていくと思うので、Yesにしています。

the default import aliasはimport ... from '@/...' のようにエイリアスで参照できる設定を入れてくれます。自分はエイリアス参照が好みなので入れています。

Tailwindを入れていない理由としては、例えばMUIなど外部コンポーネントライブラリを使う場合などに、ここのスタイルはTailwind、こっちのスタイルはMUIなど、ごちゃごちゃになるので使っていません。
あと、クラス名でコードが埋まってしまい、可読性も落ちるのが好きじゃないので使っていません。

TypeScriptの設定

TypeScriptの設定は、create-next-appを使って生成されるファイルで満足しているのでほぼいじっていません。

ひとつだけ設定を追加していて、それはnoUncheckedIndexedAccessです

{
  "compilerOptions": {
    "noUncheckedIndexedAccess": true,
  }
}

この設定を追加すると、オブジェクトや配列のプロパティにアクセスする際に、undefinedを考慮した書き方にしないとTypeErrorにしてくれる機能です。

例えば、以下のコードでstrの型が何になるかというと、str: string | undefinedとなります。

const arr: string[] = ['1', '2']
  
const str = arr[0]

しかし、noUncheckedIndexedAccessを設定しないと、str: stringとなります。

これの何がいけないかというと、arr[3] = undefinedなのに、string型と認識してしまい。
arr[3].lengthなんて書いてもエラーも出ずにコンパイルが通ってしまいます。

こういったバグの温床を防ぐために、、noUncheckedIndexedAccessを設定しています。

ESLint, Prettier

Linterには、ESLint,Prettierを入れています。

Prettier

フォーマットの設定は、以下のようにしています。

module.exports = {
  trailingComma: 'all',
  semi: false,
  singleQuote: true,
}

trailingComma: 'all'

常にカンマを入れる設定をしています。
理由は、gitの差分を見やすくするためです。

例えば以下のある型にプロパティを追加したいとき、設定によって差分が変わります。

デフォルトのes5の場合

trailingComma: 'all'の場合

画像のようにカンマが追加される行も差分として表示され、若干見づらくなるので、trailingComma: 'all'を好んで使っています。

semi: false

行末につける;を省略しています。

セミコロンつける必要なくない?てことがほとんどなので、省略しています。

singleQuote: true

文字列を書くときは、'hoge'とシングルクォートで囲います。

これは好みかもしれません。昔からシングルクォートだったので入れています。

ESLint

初期設定では、'next/core-web-vitals'しか入っておらず、ほぼLintが効いていない状態なのでカスタマイズしています。

以下の設定がどのプロジェクトでも使っている設定です。

/**
 * @type {import('eslint').ESLint.ConfigData}
 */
module.exports = {
  extends: ['eslint:recommended', 'next/core-web-vitals'],
  overrides: [
    {
      files: ['**/*.ts?(x)'],
      extends: ['plugin:@typescript-eslint/recommended'],
      rules: {
        '@typescript-eslint/no-unused-vars': [
          'warn',
          {
            argsIgnorePattern: '^_',
            caughtErrorsIgnorePattern: '^_',
            destructuredArrayIgnorePattern: '^_',
            // Note { ref, ...others } = propsのように不要なrefの取り除くときに、unused-vars警告を無視できるが、refを使っているのかがわからなくなってしまうので使わない
            // 代わりに、varsIgnorePatternで、{ ref: _, ...others } = propsのように回避できるようにする
            // ignoreRestSiblings: true,
            varsIgnorePattern: '^_',
          },
        ],
      },
    },
    { files: ['*'], extends: ['prettier'] },
  ],
  rules: {
    'no-duplicate-imports': 'error',
    curly: 'error',
    eqeqeq: 'error',
    'no-nested-ternary': 'error',
    'no-param-reassign': 'error',
    'no-restricted-imports': [
      'error',
      {
        patterns: ['../*'],
      },
    ],
    'no-return-assign': 'error',
    'no-return-await': 'error',
    'object-shorthand': 'error',
    'prefer-const': 'error',
    yoda: 'error',
    'import/order': [
      'error',
      {
        alphabetize: {
          order: 'asc',
        },
        distinctGroup: false,
        pathGroups: [
          {
            pattern: '@/**',
            group: 'parent',
            position: 'before',
          },
        ],
      },
    ],
  },
}

'eslint:recommended', 'plugin:@typescript-eslint/recommended'で、ESLintとTypeScriptのおすすめのLintを入れています。

rulesには、開発者によって書き方の揺れが発生するのを防いだり、他の人にとって理解しづらいコードにならないようにするためのルールを追加しています。

その中でも工夫しているルールをピックアップして紹介します。

@typescript-eslint/no-unused-vars

no-unused-varsは、使ってない変数をかくなよというルールですが、少しルールに手を加えています。

'@typescript-eslint/no-unused-vars': [
  'warn',
  {
    argsIgnorePattern: '^_',
    caughtErrorsIgnorePattern: '^_',
    destructuredArrayIgnorePattern: '^_',
    varsIgnorePattern: '^_',
  },
],

例えば以下のように使いたいときに、@typescript-eslint/no-unused-varsが効いているとアラートが出てしまい困ることがありました。

const { ref, ...others } = props

return (
  <div>
    <Component {...others} />
  </div>
)

propsから変数refを取り除いて、Componentにpropsを流したときに、refは使っていないのでアラートが出てしまいます。

それを解消するために上記の設定をすると以下のように書くことができます。

const { ref: _, ...others } = props

return (
  <div>
    <Component {...others} />
  </div>
)

使っていないref_という変数にしています。

このように書くことで、refは使っていないということを他の開発者に伝えることができ、わざわざこの変数を使っていないか確認する必要がありません。
アンダーバーから始まっている変数は使っていないということが明示できているので。

GitHub Actions

GitHub Actionsを使って、LintやTypeCheckの確認を行うようにしています。

ワークフローのコードは、少し長いので以下のリンク先でご覧ください。

https://codesandbox.io/p/sandbox/lingering-hill-9w2p9v?file=%2F.github%2Fworkflows%2Fci.yaml%3A122%2C1

やっていることは、setup-nodeジョブで環境セットアップを行い、並列でESLint,Prettier,TypeScriptビルドのジョブを走らせています。

node_modulesをキャッシュすることで実行時間を削減

actions/setup-node@v3にキャッシュの機能がありますが、それではnode_modulesをキャッシュすることができず、毎回yarn installが走ってしまい実行時間がかかってしまいます。

ですので、node_modulesをキャッシュする設定を加えます。それが以下になります。

jobs:
  setup-node:
    steps:
      - uses: actions/cache@v3
        id: node_modules_cache_id
        with:
          path: ${{ github.workspace }}/node_modules
          key: ${{ runner.os }}-node${{ env.NODE_BUILD_VERSION }}-yarn-${{ hashFiles('**/yarn.lock') }}

      - name: Package install
        if: steps.node_modules_cache_id.outputs.cache-hit != 'true'
        run: yarn install --frozen-lock

actions/cache@v3でnode_modulesディレクトリをキャッシュするように設定して、キャッシュが見つかった場合はyarn installを省略しています。

キャッシュキーに各パラメータを持たせることで、nodeバージョンやyarn.lockに変更があったらキャッシュを更新するようにしています。

キャッシュを使う側のジョブでは、このキャッシュをリストアする設定を追加します。

jobs:
  setup-node:
    steps:
      - name: Restore cache node_modules
        id: restore_cache_node_modules
        uses: actions/cache/restore@v3
        with:
          path: ${{ github.workspace }}/node_modules
          key: ${{ runner.os }}-node${{ env.NODE_BUILD_VERSION }}-yarn-${{ hashFiles('**/yarn.lock') }}

actions/cache/restore@v3を使って、先程のキーからキャッシュを取得します。

まとめ

Next.jsで開発する際の設定を、なぜその設定なのか、どう工夫しているかを紹介しました。

プロジェクトによってzustand, zod, storybook, jestなどのツールを導入するので、それに合わせた設定を追加していますが、今回紹介したものはどのプロジェクトにも共通して入れています。

説明したコードは以下においていますので参考にしてみてください。
https://codesandbox.io/p/sandbox/lingering-hill-9w2p9v?f