Next.jsとPythonの開発環境を爆速で構築する
はじめに
今回はフロントエンドにNext.js、バックエンドにPythonを採用して、Webサイトの環境構築を行いたいと思います。元々、Whisper APIとVOICEVOXによる合成音声を使用した会話アプリを開発したかったのですがですが、環境構築の段階でかなりのボリュームになった為、一旦記事として共有します。
モノレポについて
モノレポとはWebアプリの全てのコード(WebバックエンドやWebフロントエンド)を単一のリポジトリに保存するパターンを指します。 リポジトリを単一化することのメリットは、複数のチームで開発を行う際に、バックエンドとフロントエンドでコードを使い回すことができたり、コードの調査、変更の追跡が容易になる点です。
アプリケーションの概要
今回実装したコードは以下のリポジトリに格納しています。アプリの実装も入ってくるので少し見にくいかもしれませんが、ご容赦ください。
フロントエンドのfrontend/
ディレクトリではNext.jsのプロジェクトとそれに関連するeslint、prettier、stylelintrcの設定を行います。バックエンドのbackend/
ディレクトリにおいてはPythonのコードとPython用のlintツール(flake8 や black)の設定を配置します。huskyを、ルートディレクトリに配置して、フロントエンドとバックエンドの両方のlintが実行されるように設定します。GitHub Actionsの設定も行い、CI/CDタスクが自動的に実行されるように設定します。
フロントエンド(Next.js)の環境構築
フロントエンド(Next.js)の環境構築を行います。
styleLint
スタイルシートを整形してくれるstyleLintの設定を行います。
styleLintはチーム全体で使うCSSエラーを回避し、コードの一貫性を高めるために役立つlinterです。
{
"extends": ["stylelint-config-recess-order", "stylelint-config-standard"],
"customSyntax": "postcss-scss",
"overrides": [
{
"files": ["**/*.{ts,jsx,tsx}"],
"customSyntax": "@stylelint/postcss-css-in-js"
}
],
"rules": {
"string-no-newline": true,
"property-no-unknown": true,
"declaration-block-no-duplicate-properties": true,
"block-no-empty": true,
"no-duplicate-selectors": true,
"declaration-empty-line-before": "never"
}
}
- string-no-newline: contentなどの文字列を途中で改行を禁止します。
- property-no-unknown: 使用できないプロパティを禁止します。
- declaration-block-no-duplicate-properties: プロパティが重複していたらエラーとなります。
- block-no-empty: 空ブロックを禁止します。
- no-duplicate-selectors: セレクタが重複していた場合エラーとなります。
- declaration-empty-line-before: 宣言前の空行を禁止します。
stylelintのorderモジュール選定については以下の理由からstylelint-config-recess-order
を採用します。
- 視覚的な関連性に基づいた順序を設定してくれる(CSSプロパティの書き順についてアルファベット順よりも視覚順の方が好みのため)
- 定義しているpropertiesが多い
保存時の自動整形
保存時に自動で視覚順に整形してくれるようにVisual Studio Codeの拡張機能の「stylelint-plus」をインストールしておきます。
Visual Studio CodeのSettingにおける、「Stylelint: Auto Fix On Save」の項目にチェックを入れます。
setting.json
のeditor.codeActionsOnSave
で保存時に実行されるコードアクションを指定することができます。source.fixAll.stylelint
をtrueにして、 stylelintによる問題を保存時に自動修正するようにしておきます。
"editor.codeActionsOnSave": {
+ "source.fixAll.stylelint": true,
}
ESLint
次に構文チェックツールである、ESLintの設定を行います。
{
"env": {
"browser": true,
"es2021": true
},
"plugins": ["unused-imports"],
"extends": ["next/core-web-vitals", "airbnb-typescript", "prettier"],
"parserOptions": {
"project": "./tsconfig.json"
},
"rules": {
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-throw-literal": "off",
"@typescript-eslint/explicit-module-boundary-types": "error",
"no-unused-vars": "error",
"unused-imports/no-unused-imports": "error",
"unused-imports/no-unused-vars": [
"warn",
{
"vars": "all",
"varsIgnorePattern": "^_",
"args": "after-used",
"argsIgnorePattern": "^_"
}
],
・
・
・
}
}
- @typescript-eslint/no-non-null-assertion": 非nullアサーション演算子を許可しない。
- @typescript-eslint/no-throw-literal: Errorオブジェクトの代わりとしてリテラルを投げることを禁止。
- @typescript-eslint/explicit-module-boundary-types": 戻り値に明示的に型をつけない関数の禁止。
- no-unused-vars: 実際にコード内で使用されていない変数の禁止。
LintルールはAirbnbのairbnb-typescript
を適用します。本来であれば、eslint-config-airbnb
を適用したかったのですが、eslint-config-next
が要求するeslint-plugin-react-hooks
のバージョンと、eslint-config-airbnb
が要求するeslint-plugin-react-hooks
のバージョンが競合してエラーが検出される為、今回はairbnb-typescript
のみ適用します。
eslint-config-airbnb
については以下Zennの記事で詳しく記載されています。
eslint --fix
を実施した際に、未使用のimportを自動で削除してくれるプラグインno-unused-imports
をインストールしてeslintに適用してます。
"import/order": [
"error",
{
"groups": ["builtin", "external", "internal", ["parent", "sibling"], "object", "type", "index"],
"newlines-between": "never",
"pathGroupsExcludedImportTypes": ["builtin"],
"alphabetize": { "order": "asc", "caseInsensitive": true },
"pathGroups": [
/** importの順序を設定 **/
{
"pattern": "app/types/**",
"group": "internal",
"position": "before"
},
/** 最後尾にStyleを設定 **/
{
"pattern": "./**.module.css",
"group": "index",
"position": "before"
}
]
}
]
importの自動整列に関する設定も追加しておきます。
stylelintと同様にeslintでもsetting.json
に自動修正するように設定を追加します。
"editor.codeActionsOnSave": {
"source.fixAll.stylelint": true,
+ "source.fixAll.eslint": true,
}
Prettier
コードフォーマッターで、ソースコードを整形してくれるPrettierの設定を行います。
{
"plugins": ["strict-dependencies", "unused-imports", "unused-imports"],
"extends": [
"next/core-web-vitals",
"plugin:import/recommended",
"plugin:import/warnings",
+ "prettier"
],
"rules": {
.
.
.
ESLintとprettierを併用する為に、.eslintrc.json
に設定を追加します。
その後、prettierrc.json
を作成して任意のルールを追加します。
{
"tabWidth": 2,
"printWidth": 110,
"semi": false,
"singleQuote": true,
"jsxSingleQuote": true
}
- tabWidth: インデントのスペースの数を2行で指定。
- printWidth: 1行のコードは最大幅を110文字にまで設定。
- semi: 文の終わりにセミコロン(;)をつけない。
- singleQuote: 文字列を囲むときにシングルクォート(')を使用する。
- jsxSingleQuote: JSXの属性を囲む際もシングルクォート(')を使用する。
settings.json
に、保存時に自動でフォーマットしてくれるように以下の項目を設定しておきます。
"editor.formatOnSave": true, // ファイル保存時の自動フォーマット有効
"editor.formatOnPaste": true, // ペーストした文字の自動フォーマット有効
"editor.formatOnType": true, // 文字入力行の自動フォーマット有効
"editor.defaultFormatter": "esbenp.prettier-vscode", // デフォルトフォーマッターをPrettierに指定
バックエンド(Python)の環境構築
次にバックエンド(Python)の環境構築を行います。
仮想環境の設定
Python の仮想環境を構築するために、venvを使用します。仮想環境を構築せず、pipパッケージを利用する場合は、グローバルインストールされてしまい、パッケージのコンフリクトが発生してしまう可能性があります。そのため、venvを使用して、プロジェクト毎にPythonの環境を分けるように設定します。
python3 -m venv venv # 仮想環境を作成
source venv/bin/activate # 仮想環境を有効化
deactivate # 仮想環境を終了
パッケージ管理
パッケージ管理については、公式が提供するpipを使用します。
ただ使用し始めて分かったのですが、pipを使用する場合、pip install <パッケージ>
でインストール後、pip freeze > requirements.txt
を実行してrequirements.txt に書き込まない限り、一覧化されず、どのバージョンをインストールすれば良いか分かりにくくなってしまう問題が発生します。
Pipenv・Poetry・Ryeのどれかを使えばこの問題は解決できるようですが、ひとまずrequirements.txtに自動更新されるようにシェルスクリプトファイルを作成します。
touch pip_install.sh
#!/bin/bash
# パッケージインストール時にrequirements.txtを自動更新する
pip install "$@"
pip freeze > requirements.txt
作成したスクリプトを実行可能にします。
chmod +x pip_install.sh
これでパッケージをインストールするたびに、requirements.txt が自動的に更新されるようになります。
./pip_install.sh <package_name>
Flake8
Flake8は、Pythonコードの論理エラーやスタイルをチェックするリンターです。
Flake8自体は以下ライブラリのラッパーであり、Flake8を入れることで簡単にそれぞれのライブラリを適用することができます。
ライブラリ | 概要 |
---|---|
PyFlakes | コードのエラーチェック |
pycodestyle | PEP8に準拠しているかチェック |
mccabe | 循環的複雑度のチェック |
PEP8はPythonのコード規約の一種で、空白に関する内容や行の長さ、インデントなどコーディング規約に則ってチェックを行ってくれます。
flake8の設定を.flake8
に追加します。
[flake8]
exclude = venv/*
max-line-length = 95
extend-ignore = E203
max-complexity = 10
- 仮想環境venvはFlake8のチェックから外しておきます。
- max-line-length:行の長さを設定します。PEP8の標準では最大行長が79文字なので、少し長めに拡張してます。
- extend-ignore: E203のエラーコードを無視します。後述するBlackなどのコードフォーマッタと競合するために設定します。
- max-complexity: コードの複雑さがどこまで許容できるかを指定します。
Black
Flake8はPEP8の規約違反をチェックしてくれますが、修正まで行ってくれません。これを自動的に修正するように行ってくれるツールがBlackです。
Blackのインストール後、Black Formatterのインストールしてsetting.jsonに項目を追加します。
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter",
"editor.formatOnSave": true
}
[python]を記載することで、Pythonファイルの場合、フォーマッタとして "ms-python.black-formatter"(Black)が使用され、それ以外のファイルでは、デフォルトとして "esbenp.prettier-vscode"(Prettier)が使用されます。
mypy
最後にmypyをインストールします。mypyは型ヒントに基づいた型のチェックを行うことが可能です。
mypyをインストール後コマンドを実行してエラーが出力されてた場合、正常に動作しています。
(base) ➜ backend git:(main) ✗ mypy app
app/api/config.py:2: error: Cannot find implementation or library stub for module named "dotenv" [import]
app/api/config.py:2: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports
app/api/whisper.py:3: error: Cannot find implementation or library stub for module named "config" [import]
Found 2 errors in 2 files (checked 7 source files)
VScodeのMypyの拡張機能もインストールしておきましょう。
Github Actionsの設定
CI/CDタスクを自動的に実行させる為に、Github Actionsの設定を行います。
name: ci
on:
push:
branches:
- "main"
jobs:
ci:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest]
node: [16]
steps:
- name: Checkout 🛎
uses: actions/checkout@v3
- name: Setup node env 🏗
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
check-latest: true
- name: Install dependencies 💾
run: npm ci
- name: Cache node_modules 📦
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-build-${{ github.sha }}
restore-keys: |
${{ runner.os }}-build-
-
matrix
はGitHub Actions のワークフローで実行されるjobsにおいて、複数のOSやNode.jsのバージョンでのテストやビルドを行いたい場合に使用される機能です。matrix.os
やmatrix.node
に複数のバージョンをリストとして指定すれば、それぞれのバージョンでジョブが並行して実行されます。 -
actions/checkout@v3
でリポジトリのコードをGitHub Actionsランナー上にチェックアウトして、actions/setup-node@v3
でNode.jsのセットアップを行い、その後の依存関係のインストール(npm ciコマンドによる、node_modulesの再構築)が実行される準備を整えます。 -
path: ~/.npm
でャッシュするディレクトリのパスを.npmに指定しています。.npmはnpmが利用するキャッシュディレクトリとして利用されており、外部から取得したパッケージのバージョン情報や実際のパッケージコンテンツがキャッシュとして保存されています。
Husky
最後のHuskyの設定を行います。
以下のコマンドを実行してHuskyをインストールします。
npx husky-init && npm install
.git と package.json が同一ディレクトリでない場合は以下のようなエラーが発生するため、package.json
にscriptを追加して、ルートディレクトリから、必要ファイルは frontend/.huskyに追加されるように追記します。
husky - .git can't be found (see https://typicode.github.io/husky/#/?id=custom-directory)
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
+ "prepare": "cd .. && husky install frontend/.husky"
},
Huskyを設定して、pre-commit や pre-push などのGitフックを使用して、commitやpushの前にESLintを実行することができるので、不適切なコードがリポジトリにpushされるのを防ぐことが可能です。
pre-commit
でコードをコミットする前に、ESLintやPrettier、flake8などのツールを使ってコードを整形・検証することが可能となります。
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
# frontend
cd frontend
echo "\n 🚧🏗️ frontend: Checking format, lint, stylelint in your project before committing"
# Check Prettier ESLint stylelint standards
npm run lint-fix ||
(
echo "\n ❌🟨 Check Failed$";
false;
)
# backend
cd ../
cd backend
echo "\n 🚧🏗️ backend: Checking format, lint, stylelint in your project before committing"
flake8 # リンター
black app # コードフォーマッター
mypy app # 型チェック
lint-staged/husky
の組み合わせでも良かったのですが、今回は導入しませんでした。
HuskyとGithub Actionsの両方を組み合わせるメリット
GitHub Actionsでもlintの設定が可能ですが、今回はHuskyで実施してます。
Huskyはローカル環境でのコードの品質を確保するためのツールであり、開発者が記述したコードをcommitやpushする前に、自動的にlintを行い、問題点を早期に発見することができます。一方、Github Actionsはリモートリポジトリでのコードの品質と互換性を確保するための役割を果たしていて、プルリクエスト作成時や特定のブランチがプッシュされた際に、自動的にテスト、Lint、ビルドなどのタスクを実行します。HuskyとGitHub Actionsは排他的ではなく、Huskyは開発者がpushした際に、問題点の早期解決を行うことができ、GitHub Actionsはチーム全体のコード品質と互換性を保証し、デプロイプロセスの自動化などのより広範囲なタスクを担当することになります。
さいごに
今回は、linterやFormatterの設定を中心に記事にしました。本来であれば、Next.jsとPythonといった異なる環境下においては、Dockerを導入した方が良いかと思いますので、アプリの作成もしつつその点も踏まえて次回紹介できればと思います。
参考文献
Discussion