Edited at
Next.jsDay 23

Next.jsのプロジェクト構成を考える


この記事の概要

Next.jsを使ってBoilerplate用のプロジェクトを作りました。

GitHub上で公開してあるので、コードの解説を交えながら、設計方針を共有する為の記事です。

今回作成したプロジェクトは下記になります。

https://github.com/nekochans/redux-next-boilerplate


筆者のスペック

フリーランスエンジニアをやっています。

フロントエンド、バックエンド共にそこそこ開発経験があります。

Next.js、React共に実践経験がありますが、普段の仕事はAWSでDevOps周りの仕事をやる事が多いです。(どちらかと言うとサーバーサイド寄りのエンジニアです。)


想定読者


  • Next.js をチュートリアルレベルで触った事がある方

  • Reactに関して基礎的な知識をお持ちの方


サンプルコードに関するお詫び

一部上手く動作していない箇所があります。


  • Enzymeを使ったReactComponentのテストに失敗する

  • AWS上で一部機能が動作しない

これらの問題は順次、修正していきますので GitHub やこの記事に変更内容を適応していきます。


利用しているpackage一覧

現在利用しているpackageの一覧を記載します。

{

"name": "redux-next-boilerplate",
"version": "1.0.0",
"description": "Next.js + Redux boilerplate",
"scripts": {
"test": "jest --config=jest.config.json",
"test:coverage": "jest --config=jest.config.json --collectCoverage=true",
"dev": "yarn run build && concurrently \"tsc -p tsconfig.server.json -w\" \"node dist/server/index.js\"",
"build": "rm -rf src/.next && tsc -p tsconfig.server.json && next build src",
"start": "yarn run build && NODE_ENV=production node dist/server/index.js",
"tslint": "tslint --project tsconfig.json --config tslint.json 'src/**/*.{ts,tsx}' 'test/**/*.{ts,tsx}'",
"tslint:check": "tslint-config-prettier-check ./tslint.json",
"format:ts": "tslint --project tsconfig.json --config tslint.json --fix 'src/**/*.{ts,tsx}' 'test/**/*.{ts,tsx}'",
"format:css": "prettier-stylelint --write",
"format": "yarn run format:ts && yarn run format:css",
"aws:deploy:dev": "yarn run build && DEPLOY_STAGE=dev serverless deploy -v",
"aws:logs:dev": "serverless logs -s dev",
"aws:remove:dev": "DEPLOY_STAGE=dev DEPLOY_STAGE=dev serverless remove -v"
},
"repository": {
"type": "git",
"url": "git+https://github.com/nekochans/redux-next-boilerplate.git"
},
"author": "nekonomokochan <keita.koga.work@gmail.com> (https://github.com/nekonomokochan)",
"license": "MIT",
"bugs": {
"url": "https://github.com/nekochans/redux-next-boilerplate/issues"
},
"homepage": "https://github.com/nekochans/redux-next-boilerplate#readme",
"dependencies": {
"aws-serverless-express": "^3.3.5",
"axios": "^0.18.0",
"body-parser": "^1.18.3",
"bulma": "^0.7.2",
"compression": "^1.7.3",
"cookie-parser": "^1.4.3",
"cors": "^2.8.5",
"express": "^4.16.3",
"helmet": "^3.15.0",
"next": "^7.0.0",
"next-redux-saga": "^3.0.0-beta.1",
"next-redux-wrapper": "^2.1.0",
"react": "^16.7.0",
"react-dom": "^16.7.0",
"react-redux": "^6.0.0",
"recompose": "^0.30.0",
"redux": "^4.0.0",
"redux-devtools-extension": "^2.13.7",
"redux-logger": "^3.0.6",
"redux-saga": "^0.16.0",
"styled-components": "^4.1.3",
"typescript-fsa": "^3.0.0-beta-2",
"typescript-fsa-reducers": "^1.0.0",
"universal-cookie": "^3.0.7",
"uuid": "^3.3.2"
},
"devDependencies": {
"@types/aws-lambda": "^8.10.17",
"@types/aws-serverless-express": "^3.3.0",
"@types/body-parser": "^1.17.0",
"@types/compression": "^0.0.36",
"@types/cookie-parser": "^1.4.1",
"@types/cors": "^2.8.4",
"@types/enzyme": "^3.1.15",
"@types/enzyme-adapter-react-16": "^1.0.3",
"@types/express": "^4.16.0",
"@types/helmet": "^0.0.42",
"@types/jest": "^23.3.10",
"@types/next": "^7.0.5",
"@types/next-redux-wrapper": "^2.0.2",
"@types/react": "^16.7.17",
"@types/react-redux": "^6.0.11",
"@types/recompose": "^0.30.2",
"@types/redux-logger": "^3.0.6",
"@types/styled-components": "^4.1.4",
"@types/universal-cookie": "^2.2.0",
"@types/uuid": "^3.4.4",
"@zeit/next-sass": "^1.0.1",
"@zeit/next-typescript": "^1.1.0",
"babel-plugin-styled-components": "^1.10.0",
"concurrently": "^4.0.1",
"enzyme": "^3.8.0",
"enzyme-adapter-react-16": "^1.7.1",
"enzyme-to-json": "^3.3.5",
"jest": "^23.6.0",
"node-sass": "^4.11.0",
"prettier": "^1.15.3",
"prettier-stylelint": "^0.4.2",
"react-test-renderer": "^16.7.0",
"serverless": "^1.35.1",
"serverless-plugin-warmup": "^4.2.0-rc.1",
"serverless-prune-plugin": "^1.3.2",
"stylelint": "^9.9.0",
"stylelint-config-idiomatic-order": "^6.1.0",
"stylelint-config-prettier": "^4.0.0",
"stylelint-scss": "^3.4.1",
"ts-jest": "^23.10.5",
"tslint": "^5.12.0",
"tslint-config-airbnb": "^5.11.1",
"tslint-config-prettier": "^1.17.0",
"tslint-plugin-prettier": "^2.0.1",
"tslint-react": "^3.6.0",
"typescript": "^3.2.2"
}
}

どのpackageもこの記事を書いている 2018-12-23 時点での最新版となっています。

特に重要な部分をピックアップして解説させていただきます。


TypeScriptを利用

ある程度の規模のWebフロントエンド開発であればTypeScriptを利用するのが良いと思っています。

利用者も順調に増えていますし、各種ライブラリの対応状況もかなり進んできました。

私はこのような事情から2017年頃から、フロントエンドの開発には基本的にTypeScriptを採用しており、生のJavaScriptを書く機会は大幅に減っています。

(参考)世界最大のソフトウェア開発プラットフォームで最も人気なプログラミング言語は何なのか?

Next.jsでTypeScriptを扱う為の最も簡単な方法は @zeit/next-typescript を利用する事です。

プロジェクトルートに下記のファイルを用意します。


next.config.js

const withTypescript = require("@zeit/next-typescript");

module.exports = withTypescript();


.babelrc に以下の記述を追加します。


.babelrc

{

"presets": [
"next/babel",
"@zeit/next-typescript/babel"
]
}

tsconfig.json を用意します。


tsconfig.json

{

"compileOnSave": false,
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"jsx": "preserve",
"allowJs": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"noUnusedLocals": true,
"removeComments": false,
"preserveConstEnums": true,
"sourceMap": true,
"skipLibCheck": true,
"baseUrl": ".",
"esModuleInterop": true,
"lib": [
"dom",
"es2018"
]
}
}

基本的にはこれだけで扱う事が可能です。

ただし注意点があります。

@zeit/next-typescript はTypeScriptのトランスパイルにBabelを使っており、以下の制限があります。


  • TypeScript コンパイラによる型チェックが行われない

  • いくつかの制限がある

厳密な型チェックを行いたい場合、自前でWebpackをカスタマイズしてあげる必要があります。

この点に関しては @5t111111 さんがこのアドベントカレンダーに投稿してくれた記事が参考になりますので、ここに載せておきます。


React

Next.jsはReactがベースになっているので、Reactに関する補足をいくつか書かせて頂きます。


ReactComponent

少し前まではReactのComponentは HOC(Higher-order Components)形式で書くのが良いとされていて、recomposeを使ってComponentを書く事が多かったです。

実際今回私が作ったサンプルもrecomposeを使って書いています。

しかし2018年の10月終わり頃に Hooksという新機能が出た事により、関数型ComponentでライフサイクルやStateを扱う事が容易になった為、今後は Hooks を利用した書き方が浸透していくと思われます。

hooksはまだbeta版の機能ですが、 React Hooks 大喜利 Advent Calendar 2018 に利用例がたくさん載っているので今のうちから参考にしていければと思っています。


Redux

Hooksの影響でこちらもどうなるか分かりませんが、現時点ではReduxはまだまだ利用されていると思います。

非同期処理は redux-saga を利用しています。

ReduxActionを定義するライブラリとしては typescript-fsa, typescript-fsa-reducers を利用していますが、これらは現在開発が止まっています。

この点に関してはReactに非常に詳しいフロントエンドエンジニアの大岡さんが以下のTweetを行っています。

typescript-fsa の代替え案ですが今のところはこれという物が存在しないので、以下の記事等を参考に ReturnType を使って自前で用意するのが良いのではないかと思います。

(参考)TypeScriptで Redux の Reducer部分を型安全かつスッキリ書く


テストコード

ReactがFacebook製という事もあり、JESTを利用するのが良いと思います。

ReactComponentのテストは 事実上デファクトスタンダードになっていると思われる enzyme を利用するのが良いでしょう。


本番環境で動かす(AWS)

aws-serverless-express というAWS公式ライブラリを使っています。

aws-serverless-express は裏側でLambda関数を使ってNext.jsが乗ったExpressサーバーを稼働させる仕組みなのですが、エンドポイントが本来API用途で使うAmazon API Gatewayになるので、Next.js製のアプリケーションを動作させる為には少々工夫がいります。

このあたりは Firebase Hosting のほうがシンプルに動かせるかもしれません。


おわりに

今回作成したサンプル はベストプラクティスに沿っているとは言い難い内容なので今後も継続して改善をしていきたいと思っています。

以上になります。最後まで読んで頂き、ありがとうございました。