🧀

【小規模サイト向け】Next.js ディレクトリ構成とコーディングルール

2023/01/24に公開

Next.jsで小規模サイト(ブログやコーポレートサイトなど)を複数人で開発する際のディレクトリ構成とコーディングルールをまとめました。

以下の点を意識した構成になります。

  • 小規模サイトの場合、コンポーネントの量は限られるのでディレクトリ構成は複雑になりすぎないようにする
    • → ファイルへのアクセスのしやすさを重視
  • 複数人での開発なので、できるだけ書き方を統一できるようにする
    • → ESLint, StyleLintを導入
  • CSS設計やベースコーディングに疎くても一定以上の品質を担保できるようにする
    • → jsxコンポーネントとCSSを同じ階層に配置して直感的に編集可能に
    • → Sassを採用するが、SASS記法ではなく扱いやすいSCSS記法を採用

はじめに

ディレクトリ構成はこちらの記事を参考にさせて頂きました。とても分かりやすく解説してくれているので是非読んでみてください。

https://zenn.dev/yodaka/articles/eca2d4bf552aeb

最近はFeature-Driven Folder Structureという構成が流行っているらしいですが、小規模サイトの場合こちらの構成は少しやりすぎ感があったので今回は見送ることにしました。

https://twitter.com/t__keshi/status/1609543086044024832?s=20&t=hosIhnLip2Ur4JHUpEGvXg

ディレクトリ構成

前提として、pages/styles/などのフォルダはsrc/にまとめています。このようにコンポーネントファイルをひとつのフォルダにまとめる方法はこちらの記事で解説しています。

https://zenn.dev/necscat/articles/435db2f1dbbb29

src/ 配下の構成は以下のようになっています。

src/
├── components
│   ├── base
│   │   └── Header
│   │       ├── Header.jsx
│   │       └── Header.module.scss
│   ├── page
│   │   ├── Index
│   │   │   ├── Index.jsx
│   │   │   └── Index.module.scss
│   │   └── News
│   │       ├── Archive.jsx
│   │       └── Archive.module.scss
│   └── ui
│       └── Button
│           ├── Button.jsx
│           └── Button.module.scss
├── const
│   └── site.js
├── features
│   └── news
│       ├── api
│       │   ├── getNewsCategory.js
│       │   └── getNewsPost.js
│       ├── components
│       │   ├── NewsCategoryList.jsx
│       │   ├── NewsCategoryList.module.scss
│       │   ├── NewsPostList.jsx
│       │   └── NewsPostList.module.scss
│       └── hooks
│           └── useXXX.js
├── lib
│   └── newt.js
├── pages/
└── styles/

これらをざっくり解説するとこのようになります。

src/
├── components/ ・・・ 様々なコンポーネントをまとめたフォルダ
│   ├── base/ ・・・ サイト全体を構成するコンポーネント(ヘッダー、レイアウト系、metaタグなど)
│   ├── page/ ・・・ pages/の中身の実態があるフォルダ
│   └── ui/ ・・・ ボタンなど最小単位のコンポーネント
├── const/ ・・・ 定数を定義するファイルを置くフォルダ
├── features/ ・・・ 特定の機能などをまとめたフォルダ。この中にも複数のコンポーネントが存在する
├── lib/ ・・・ ライブラリなど
├── pages/ ・・・ ページ(デフォルトのもの)
└── styles/ ・・・ サイト内全体で使う共通CSS

全体像が見えたところで、次は各ディレクトリの役割について解説していきます。


components/base/

components/base/には、サイトを構成するコンポーネントを置いています。

base/
├── EmbedTag/
│   └── GoogleTagManager.jsx
├── Footer/
│   ├── Footer.jsx
│   └── Footer.module.scss
├── Header/
│   ├── Header.jsx
│   └── Header.module.scss
├── Layout/
│   └── Layout.jsx
├── Meta/
│   └── Meta.jsx
└── Wrapper/
    ├── Inner.jsx
    └── Inner.module.scss

EmbedTag/には計測タグ(Googleアナリティクスやタグマネ)などの外部タグ系が入ります。

ヘッダー・フッターは、同階層にCSSも配置しています。これは最初に書いたこちらを意識しているからです。

CSS設計やベースコーディングに疎くても一定以上の品質を担保できるようにする
→ jsxコンポーネントとCSSを同じ階層に配置して直感的に編集可能に

他のCSSが絡むコンポーネントも全部この構成になります。

Layout.jsxは以下のような設計になっており、_app.jsxで呼び出しています。

Layout.jsx(一部省略)
export default function Layout({ children }) {
  return (
    <>
      <Header />
      <main>{children}</main>
      <Footer />
    </>
  );
}
_app.jsx(一部省略)
<Layout>
  <Component {...pageProps} />
</Layout>

meta/では<head>のtitleやogpなどのメタタグを定義して、全てのページコンポーネントで呼び出しています。

Wrapper/ではページ全体を囲うコンポーネントを配置しています。例えば、コンテンツ幅を定義するInner.jsxがそれに該当します。

https://moromi.net/artbord-size/


components/page/

components/page/pages/の中身の実態があるフォルダです。

つまり、pages/ではページを生成するために各ページコンポーネントファイルを置き、その中ではcomponents/page/のコンポーネント1つだけを呼び出し、components/page/のファイルで様々なコンポーネントを呼び出すような運用になります。

ニュース一覧ページのサンプルを用意しました。

ディレクトリ構成例
src/
├── components/
│   └── page/
│      └── News/
│         └── Archive.jsx
│         └── Archive.module.scss
└── pages/
    └── news/
          └── index.jsx // ニュース一覧のトップページ
          └── category/
          │     └── [slug]/
          │           └── index.jsx // カテゴリーごとのニュース一覧
          └── page/
                └── [slug]/
                      └── index.jsx // ニュース一覧のxページ目用
components/page/News/Archive.jsx
export default function News({ newsPostList, newsCategoryList }) {
  return (
    <>
        <div className={styles.category}>
	  // カテゴリーリンク一覧のコンポーネントを呼び出す
          <NewsCategoryList newsCategoryList={newsCategoryList} />
        </div>
        <div className={styles.post}>
	  // 投稿一覧のコンポーネントを呼び出す
          <NewsPostList newsPostList={newsPostList} />
        </div>
    </>
  );
}
pages/news/index.jsx
import Archive from '@/components/page/News/Archive';
export default function News({ newsPostList, newsCategoryList }) {
  return (
    <>
      // components/pageの中のコンポーネント1つを呼び出すだけ
      <Archive
        newsPostList={newsPostList}
        newsCategoryList={newsCategoryList}
      />
    </>
  );
}

上記サンプルのコードとディレクトリ構成では、components/page/News/Archive.jsxで記事一覧とカテゴリーリンク一覧のコンポーネントを呼び出しています。
そして、pages/news/には3つのindex.jsがあります。これらのページでは表示される記事が変わるだけで、コンポーネント構成は全く同じになります。なので、これらのファイルでcomponents/page/News/Archive.jsxを呼び出します。

ちなみに、記事一覧やカテゴリーリンク一覧のコンポーネントは、後ほど解説するfeatures/で定義しています。

ページが二重管理のようになってしまい少し手間ですが、その分共通化できたりと管理しやすい設計になるので採用しました。

この構成のメリットをまとめると以下のようになります。

  • pages/のコンポーネントの肥大化を防げる
    • components/page/に様々なコンポーネントをまとめられるので、pages/の中で再利用が可能になる
  • 理にかなったCSS設計を手軽に導入できる
    • 使い回すコンポーネントにはmarginを持たせないことで、再利用をしやすくする
    • それらのコンポーネントのmargincomponents/page/の中で定義する
      • 上記の例だとcomponents/page/News/Archive.jsxで配置しているコンポーネントに対してmarginを定義する
    • この部分に関しては、後の「コーディングルール」でもう少し詳しく解説します

components/ui/

components/ui/はサイト内で使い回すコンポーネントを配置します。ボタンや見出しなどが該当します。

ディレクトリ構成は以下のようになります。

.
└── Button
│   ├── Button.jsx
│   └── Button.module.scss
└── Component_1
│   ├── Component_1.jsx
│   └── Component_1.module.scss
└── Component_2
    ├── Component_2.jsx
    └── Component_2.module.scss

これらのコンポーネントは使い回す前提になるので、marginwidthなどのCSSプロパティは原則使わないようにします。

components/page/でページに対して直接呼び出すこともあれば、後ほど解説するfeatures/で呼び出す場合もあります。


const/

定数(共通の情報)を定義するディレクトリです。

以下はサイト全体で使用する情報を定義したファイルの例です。

site.js
export const siteData = {
  title: 'サイトタイトル',
  desc: '説明文',
  url: 'https://xxx.com',
  lang: 'ja',
  googleTagManagerId: 'xxx',
}

features/

特定の機能や処理をまとめたディレクトリです。components/ui/のコンポーネントをここで呼び出すこともあります。

構成の例を用意しました。

features/
├── _template
│   ├── api
│   │   └── getXXX.js
│   ├── components
│   │   ├── XXX.jsx
│   │   └── XXX.module.scss
│   └── hooks
│       └── useXXX.js
└── news // ニュースに関する処理やコンポーネントをまとめている
    ├── api
    │   ├── getNewsCategory.js // カテゴリーを取得するAPI
    │   └── getNewsPost.js // 投稿を取得するAPI
    └── components
        ├── NewsCategoryList.jsx // カテゴリー一覧のコンポーネント
        ├── NewsCategoryList.module.scss
        ├── NewsPostList.jsx // 投稿一覧のコンポーネント
        └── NewsPostList.module.scss

features/_template/は複製用のテンプレートです。このフォルダを複製して、新しくセットを作成します。

基本的に3つのフォルダから構成されます。

  • api
    • 関連するAPIを配置
    • getXXXremoveXXXなど、どのような処理かイメージできる命名にする
    • 1処理1ファイル(これはプロジェクトによって変えても良いかも)
    • 使わない場合はフォルダごと削除
  • components
    • 関連するコンポーネントとCSSを配置
    • ここで作ったコンポーネントを、components/page/で呼び出す
    • 関連するファイルが多くなりそうなら、この中でさらにフォルダ分けしても良い
  • hooks
    • 関連するカスタムフックを配置
    • useXXXのように、先頭にuseを付ける
    • 使わない場合はフォルダごと削除

https://zenn.dev/stripe/books/stripe-nextjs-use-shopping-cart/viewer/step1-2_nextjs_ssr

https://reffect.co.jp/react/react-custom-hook


lib/

外部ライブラリに関するファイルを配置するディレクトリです。

例えば、以下のファイルではヘッドレスCMSのNewtSDKにAPIトークンなどを与えてインスタンス化し、それをfeatures/XXX/api/で呼び出せるようにしています。

lib/newt.js
const { createClient } = require('newt-client-js');
export const newtCdnClient = createClient({
  spaceUid: process.env.NEXT_PUBLIC_NEWT_SPACE_UID,
  token: process.env.NEXT_PUBLIC_NEWT_API_TOKEN,
  apiType: process.env.NEXT_PUBLIC_NEWT_API_TYPE,
});

pages/

components/page/で解説した通り、pages/ではページを生成するために各ページコンポーネントを置くだけで、中身の実態はcomponents/page/で管理しています。


styles/

サイト全体に関するCSSを定義する場所です。

変数や関数、mixin、リセットCSS、<html><body>に対するスタイルなどを定義しています。

各コンポーネントのスタイルは前述の通り、各コンポーネントの.jsxと同階層に配置しています。


ディレクトリ構成の解説のまとめ

ディレクトリの解説は以上になります。これまでの内容をざっくりまとめると以下のようになります。

  • pages/はページを生成するためだけに使う
    • 中身の実態はcomponents/page/で管理
  • UIコンポーネントは、components/ui/features/XXX/components/のどちらかに作成する
    • 作成したコンポーネントファイル.jsxと同階層にCSSも配置する
    • components/ui/は汎用性を保たせたいので、marginwidthなどは指定しない
  • stylesにはサイト全体のCSSのみを定義して、各コンポーネントのCSSは上記のルールで管理する
  • 特定の機能やグループはfeatures/にフォルダを作り、その中に「コンポーネント」「CSS」「API」「カスタムフック」などをまとめる

また、必要に応じてこちらで解説されているconfigstoreのフォルダをsrc直下に作るのも良いと思います。

https://zenn.dev/yodaka/articles/eca2d4bf552aeb#構成





コーディングルール

次に、コーディングルールに関して解説します。

JSX, JavaScript全般

インデントや表記の統一はESLintPrettierで行っているので、JSを書くときはあまり細かく気にしないでいいと思います。

以下記事の環境構築を再現すれば、保存時に自動整形されるようになります。

https://zenn.dev/necscat/articles/435db2f1dbbb29

参考までに設定ファイルを載せておきます。

.eslintrc.js
module.exports = {
  // ルールをまとめて追加
  "extends": [
    "next/core-web-vitals",
    "plugin:import/recommended",
    "plugin:import/warnings",
    "prettier"
  ],
  // 個別ルール
  "rules": {
    // importするファイルをアルファベット順に自動で並び替える
    "import/order": [
      "error",
      {
        "alphabetize": {
          "order": "asc"
        }
      }
    ],
    // importのパスにエイリアス('@/components/xxxxx')を使うとエラーになるのでそれを防ぐ
    "import/no-unresolved": "off"
  }
}
.prettierrc.js
module.exports = {
  "trailingComma": "all", // 可能な限り末尾にカンマを付ける
  "tabWidth": 2, // インデントのスペースの数
  "semi": true, // ステートメントの最後にセミコロンを追加
  "singleQuote": true, // シングルクォートに統一
  "jsxSingleQuote": true, // JSXでダブルクォートの代わりにシングルクォートを使用する
  "printWidth": 100, // 折り返す行の長さ
  "singleAttributePerLine": true // ひとつの属性ごとに改行する
}

JS周りで唯一ルールがあるとすれば、jsx記法のファイル(コンポーネント)の拡張子は.jsxにすることくらいです。逆に、jsx記法以外のファイル(APIなど)は.jsにします。

こうすることで、ひと目でコンポーネントなのか、それ以外なのかが判断できるようになります。

SCSS

こちらも個人でバラツキがありそうな箇所はStylelintで管理しているので、そのあたりはあまり気にしなくて大丈夫です。

ちなみに、こちらの記事ではプロパティの自動並び替えなどを導入しています。とてもおすすめです!

https://zenn.dev/necscat/articles/435db2f1dbbb29

それ以外にはいくつかのルールを定めています。

remは使わない

remを使うことで、ブラウザの文字サイズを変更したらサイト上の文字サイズもそれに応じて変わるので、アクセシビリティ的に良いことは間違いありません。

しかし、remの理解度は人によって様々です。正しく理解している人もいれば、間違った理解をしている人もいます。

また、フォントサイズはremにするとして、他の余白や幅の単位はremにする?しない?など、ルールを統一するのも難しいです。初期開発時にはルールを守れていても、運用時に崩れることもあります。

今回は「複数人での開発」と「各々のスキルレベルがまばらでも問題ないようにする」を目指したかったので、remを使うのは禁止にしました。

https://qiita.com/NagayamaToshiaki/items/77e929d855d052863a85

https://webdou.net/font-rem/

クラス名でアンパサンド(&)は禁止(&:hoverなどはOK)

クラス名に&を使うのは禁止とします。例を用意しました。

.parent {
  &__child { ... } // NG
}

.parent {
  &:hover { ... } // 擬似クラスはOK
  &::before { ... } // 擬似要素もOK
}

このように、クラス名に対する&のみ禁止になります。

&を使うと、コンポーネントで定義したクラス名で、CSSファイルの中のクラス名を検索できないからです(&でクラス名が省略されるため)。

CSSファイルの管理方法

ディレクトリ構成でも解説しましたが、共通CSSはsrc/styles/にて管理し、各コンポーネントのスタイルは、そのコンポーネントの.jsxファイルと同階層で管理するようにします。

こうすることで、直感的にCSSを編集できるようになります。

コンポーネントの余白はpage/xxx/xx.scssで管理する

こちらもディレクトリ構成で解説済みですが、使い回すコンポーネントにはmarginwidthなど、使い回す際に障害になりそうなプロパティを指定するのは禁止とします。

それらのプロパティは、コンポーネントを呼び出すファイルのCSSで指定します。こうすることで、コンポーネントの再利用がしやすくなります。


最後に

この環境を作成するにあたり、こちらの記事はとても参考になったので是非皆さんにも読んでみてほしいです。

https://zenn.dev/yodaka/articles/eca2d4bf552aeb#構成

https://tech-blog.rakus.co.jp/entry/20230208/frontend

また、私の今の環境には合わなかったですが、これから流行る兆しがある?Feature-Driven Folder Structureの構成も目を通してみてはいかがでしょうか。

https://twitter.com/t__keshi/status/1609543086044024832?s=20&t=hosIhnLip2Ur4JHUpEGvXg

https://dev.to/profydev/screaming-architecture-evolution-of-a-react-folder-structure-4g25#discussion-of-the-featuredriven-folder-structure

ネクスキャット テックブログ

Discussion