LoginSignup
2
3

React のサイトを Next.js(v14)にしたら個人的に学びが多かったのでまとめてみる

Last updated at Posted at 2024-02-22

はじめに

以前筆者が制作した2つのReact, TypeScript制サイトを今回Next.js, TypeScriptに作り直したのですが、情報収集に地味に時間がかかるところもあったので今回のリプレース作業を通じて得た学びとともにまとめていきたいと思います。

ちなみに、リプレースした理由は単純にNext.jsの経験を深めたい(Next.jsを使って何か作りたかった)のと「Next.jsはSEOに強い」という情報を見聞きしたためです。
こんな動機で自由にさせてくれる会社には感謝です。

  • リプレース前(React, TypeScript
    ied-20240221.png

  • リプレース後(Next.js, TypeScript
    ied-next.png

実際リプレースしてからSEOのスコアは向上しました。
ありがとう、Next.js

対象読者

  • ReactNext.jsに興味のある方
  • Next.js初心者
  • Next.js × 国内ホスティング先を考えている方

概要

サイトは2つともSSGした静的サイトで、ホスティング先はXサーバーです。そのため、今回以下のような内容はあまり出てきませんので悪しからず。

  • SSR, ISRを用いた内容や情報
  • MUI,shadcn/ui, Mantine UI, Tailwind CSSといったUIライブラリの話
  • Vercel, AWS, Cloudflare, Firebaseといったデプロイ先の話

Xサーバーを選定した理由は端的に言えば「社内都合」です。筆者の所属企業が管理するサイトは大半がXサーバーにホスティングしていることもあり、中長期的な運用 + 今回は静的サイトという性質も考慮して上記に決めました。

以下より国内と海外のサービスを区別するため、国内はホスティング先という表記、海外はデプロイ先という表記で進めさせていただきます。

あと、筆者の所属企業のようにXサーバーやロリポップ、さくらインターネットなど国内の有名なホスティング先を利用されているところも少なくないと思います。
そういった方々にNext.jsで作ったサイトのデプロイ時の注意事項などを共有できれば良いなと考え、記事を書いていきます。

お察しの通り筆者はNext.jsに対して深い経験があるわけではありません。間違っている部分なども散見されるかもしれませんので、そのような場合はお手数ですがコメント欄などでご指摘・ご教示いただけますと嬉しく存じます。

まずは結論

デプロイ先は極力Vercelなど周辺環境が整っているところにしましょう。
誤解を招かないために申すと、Xサーバーなど国内ホスティング先がダメということでは一切ないです。

今回のサイトでは環境変数の使用やデータベースとの連携といった作業はありませんでしたが、そのような要件があるとVercelなど他のデプロイ先を利用したほうが賢明だと思います。
筆者はまだVercelでしか個人サイト(Next.js)をデプロイしていないので限定された意見になりますが、VercelだとGitHubでリポジトリを作って連携しておけばビルドしただけで(簡易なチェックも交えて)デプロイしてくれます。
環境変数の設定もGUIで簡単に行えます。すごく楽ですし、<Image>コンポーネントを使うことで画像最適化などNext.jsの恩恵も受けられます。蛇足ですが、<Image>コンポーネントはpriorityという属性を追加しない場合、すべてlazyローディングにしてくれるなどかゆいところに手が届く感じです。

それでも「抜き差しならない事情で国内ホスティング先を使いたい」という方に向けて、自身の備忘録も兼ねて書いていきます。

SSG, SSR, ISRNext.jsについて少し復習しておきたい方は、簡易的な説明を置いておきますので必要に応じてご参照ください。今回は本記事で(ISR)は一切出てきませんが。

SSG, SSR, ISR について
  • SSG, SSR, ISRはコンポーネント単位で使い分けられ、ビルド時にNext.jsが記述に応じた仕様で各種ページを生成してくれる

    • SSG(Static Site Generators)
      静的サイト生成。build時に各ページ(サイト)のhtmlデータを生成する。既に完成度100% の状態なので、ユーザーからのリクエストに迅速にレスポンスできる。更新頻度が低い箇所に使用する(例:コーポレートサイトのアバウトページやFAQページなど静的アセットで済む処理

    • SSR(Server Side Rendering)
      サーバー側で各ページ(サイト)を生成する。完成度50%くらいの状態で生成し、ユーザーからリクエストがあると100%の状態にしてレスポンスする。更新頻度が高い箇所に使用する(例:SNSのタイムラインやユーザープロフィールなど動的な処理

    • ISR(Incremental Static Regeneration)
      SSGSSRのハイブリッド版のようなもの。生成した各ページ(サイト)をCDN(自身の最寄り倉庫のようなもの)にキャッシュしており、指定した時間(Revalidate)に応じて更新(SSRのような動き)したものをユーザーにレスポンスする。
      ※指定した時間の経過後にアクセスすると更新データが表示されるという仕組みではなく、アクセス時は一旦更新前のものが表示され、その後にアクセスすると更新後のものが表示される。
      よりリアクティブなレスポンスにしたい場合はSWRを使用する

    • fetch API通じて各種生成方法(SSG, SSR, ISR)を利用できる

    /* SSG */
    // This request should be cached until manually invalidated.
    // Similar to `getStaticProps` --- to Next 12.
    // `force-cache` is the default and can be omitted.
    fetch(URL, { cache: 'force-cache' }); // --- from Next 13 against SSG
    
    /* SSR */
    // This request should be refetched on every request.
    // Similar to `getServerSideProps` --- to Next 12.
    fetch(URL, { cache: 'no-store' }); // --- from Next 13 against SSR
    
    /* ISR */
    // This request should be cached with a lifetime of 10 seconds.
    // Similar to `getStaticProps` with the `revalidate` option --- to Next 12.
    fetch(URL, { next: { revalidate: 10 } }); // --- from Next 13 against ISR
    

参照情報:https://nextjs.org/blog/next-13#data-fetching

Next.js のファイル構成について
  • ファイル構成(srcディレクトリ配下)
    • appディレクトリ(配下)は原則サーバー側でレンダリングされる(サーバー側でデータ取得を行うほうがパフォーマンス的に早いため)。デフォルトではuse server(サーバーコンポーネントの)状態。ファイルの先頭にuse clientを記述するとクライアントコンポーネントとなる(クライアントコンポーネントでしかState, EffectなどのHooksが扱えない)

    • use client(クライアントコンポーネント)は苗字みたいなもので、親で宣言していればそのディレクティブ(配下の子コンポーネント)も全てクライアントコンポーネントになる(各自子コンポーネントでuse clientを宣言するとエラーになるので注意)
      参照記事:【Next.js】RSCとクライアントコンポーネントを改めて理解する

    • app/page.tsx
      Next 12でいうところのindex.tsxの役割。各ディレクトリごとに用意することでファイルシステムベースルーティングの恩恵を得られる

    • app/layout.tsx
      Next 12でいうところの_documet.tsx_app.tsxの役割。Next 13で新たに設けられたファイルで、各ページ(ディレクトリごとのpage.tsx)のレイアウト情報(meta情報など)の管理を担う。layout.tsxは入れ子も可能
      参考記事:Next.js 13 Template と Layout の使い分け

SSGに必要な設定・記述

先にSSGの方法やその他関連情報をお求めの方に向けて、next.config.js or next.config.mjsへの設定・記述を箇条書きの形式で紹介していきます。

  • SSGを行うために必要な記述
    output: 'export'を追記

    /** @type {import('next').NextConfig} */
    const nextConfig = {
        output: 'export',
    };
    
    export default nextConfig;
    

  • サブページの再読み込みまたは直リンクへの対策(各種サブページディレクトリとindex.htmlを生成)
    trailingSlash: trueを設定しない場合、Linkhref属性やrouter.push()の引数に指定した 文字列の静的ファイルが生成される(例:about.html ため、サブページを再読み込みまたは直リンクしようとするとabout/index.htmlは存在しないので意図した挙動になりません(ホスティング先の設定によって404リダイレクトまたはTOPページへリダイレクトされたりする)

    /** @type {import('next').NextConfig} */
    const nextConfig = {
        trailingSlash: true,
    };
    
    export default nextConfig;
    

    trailingSlash: trueを設定することで期待する挙動になってくれます(各種サブページディレクトリとindex.htmlが生成される)

以下からは必要に応じて設定してください。


  • 外部(ドメイン)サイトから(画像などの素材)データを引っ張ってくる場合に必要な記述

    /** @type {import('next').NextConfig} */
    const nextConfig = {
        images: {
            remotePatterns: [
            {
                protocol: 'https',
                hostname: 'domain.co.jp' // ドメイン
            },
            ],
        },
    };
    
    export default nextConfig;
    

  • サブディレクトリの指定
    https://exapmle.com/sub/hoge ← 左記URLにおけるsub/hogeの部分にデプロイしたいケースです。ディレクトリパスの先頭には/が必要で、末尾に/を付けるとエラーでビルドできないため指定時は注意してください

    /** @type {import('next').NextConfig} */
    const nextConfig = {
        assetPrefix:  '/サブディレクトリ',
        basePath:  '/サブディレクトリ'
    };
    
    export default nextConfig;
    

  • ビルド時の出力先フォルダの設定
    /** @type {import('next').NextConfig} */
    const nextConfig = {
        distDir: 'dist', // 出力先を'dist'フォルダに設定
    };
    
    export default nextConfig;
    

デプロイ時に起きたトラブル

  • 静的エクスポートしたページの画像(jpgなど)が表示されない
    images: { unoptimized: true, }を追記することで解決しましたが、これだと画像最適化されていないのでNext.jsを使うメリットを失うことになります。

    /** @type {import('next').NextConfig} */
    const nextConfig = {
        images: {
            unoptimized: true,
        },
    };
    
    export default nextConfig;
    

筆者の勝手な推論ですが、ホスティング先(今回はXサーバー)がNextImageコンポーネントの画像最適化(Image Optimization)の処理に対応していないので「画像パスを読み込めず表示されない?」ということなのかなぁと思っています。

  • ホスティング先によってはルーティングの設定(.htaccessの調整)が必要
    筆者が遭遇した現象は「存在しないパスを打っても404ページは表示されずTOPページが表示される」というものでした。
    以下のように、当該ドメイン(FTPサーバールート)の.htaccessにリダイレクト処理を記述することで期待した挙動になりました。

    ErrorDocument 404 /subdir/hoge/404.html
    

    ※404リダイレクト処理の設定は必ず相対パスで指定してください。サブディレクトリの場合はサブディレクトリからのパスを指定する形です。

Google Analytics 4OGPの設定

一般的なwebサイトでは、Google Analytics 4OGPを設定するのはよくあることだと思うので必要な情報を下記に貼っておきます。

  • Google Analytics 4の設定

  • OGPの設定


本記事の主題となる情報まとめは一旦済んだので、以下より筆者が今回リプレースした2つのサイトについての体験録を書いていきます。関心のある方はぜひ読んでやってください。

毎月更新するサイトA

まずは1つ目の月次更新のサイトAから説明していきます。
冒頭でも説明した通り、静的サイトですが将来的にデータベースを用いて更新する可能性を意識してSSR仕様で制作しました。……そのためビルド時には余計な調整作業(SSR関連の不要なファイルの削除)が入ってくるという生産性悪しな仕様です。

技術スタックは以下になります。

jotaiでは状態管理を、styled-componentsで要素のスタイルを、swiperでスライダーを実装しています。
json-serverは先述のSSRする可能性を意識してローカル環境に簡易サーバーを置くために、html-react-parserは定期更新する際のjsonデータに記述したhtml要素の解析に利用しています。

「定期更新する際のjsonデータに記述したhtml要素の解析に利用」について

Reactでサイト制作した折に、毎月更新する度にファイルを弄ったり、都度ビルドしたりするのが億劫だったので「更新データをfetchさせればいいんじゃね?」と(短絡的な思考で)実装した内容です。

const [hogeData, setHogeData] = useState<hogeAryType[]>([]);
useEffect(() => {
      const fecthHogeData = async () => {
      // キャッシュ対策として`cache: 'no-store'`でデータを保存しないようにしています。
      const response = await fetch('https://example.com/data.json', { cache: 'no-store' });
      const resObj: hogeAryType[] = await response.json();
      setHogeData((_prevHogeData) => resObj);
    }
    fecthHogeData();
}, []);

jsonデータ(data.json)は以下の感じです。

[
    {
        "yearMonths": "2024/2",
        "imgPath": "images/content2402",
        "title": "Lorem ipsum dolor sit amet, <br> consectetur adipiscing elit",
        "column001": "mollit anim id est laborum",
        "column002": "dolor in reprehenderit in voluptate <br> velit esse cillum dolore eu fugiat",
    },
    ...
    ..
    .
]

この方法に変えてから今まで10分程度かかっていた更新作業が長くて3分程度に短縮されたので個人的にはハッピーです。

では、以下より筆者が制作時に詰まったポイントを書いていきます。

内部データはフェッチできない

例えば、開発時に同Next.jsプロジェクトのpublic内にjsonデータを置いてフェッチしようとするとできませんでした。
Reactでは普通にできていたので初めに最も戸惑ったところです。

外部のjsonデータでもCSR仕様(useEffectを使用した非同期のフェッチ処理など)ではCORSでエラーが出てフェッチできませんでしたが、SSR仕様(サーバーコンポーネントとしてfetch apiを使用)だとフェッチできて驚きました。
クライアント側での処理ではなくサーバー側で処理しておくからフェッチできる、という漠然とした理解?もNext.jsを触らないと知らないままだったので良い学びになりました。

ちなみに、CSR仕様でもフェッチできないのは開発時のみの話でビルドして当該ドメインサイトのサーバーにデプロイすれば当然CORSには引っかかりませんでした。

styled-componentsはクライアントコンポーネントでしか使えない

公式にも書いてありますが、サーバーコンポーネントではstyled-componentsは使えません。
あと、こちらも同じく公式に書いていますが、next.config.jsに所定の記述が無いとスタイルがうまく反映されませんのでご注意ください。

/** @type {import('next').NextConfig} */
const nextConfig = {
    compiler: {
        styledComponents: true,
    },
};

export default nextConfig;

コンポーネントの命名の仕方

export default コンポーネント名;で行いましょう。あと、パスカルケースで命名しないと、build時にエラーが出ます。

また、以下の関数宣言の書き方でないとESLintに怒られます。

type hogeType = {
    urlStr: string;
    urlPathName: string;
}

/* コンポーネントはパスカルケースで命名 */
function HogeComponent({ props }: { props: hogeType }) {
    // ...コンポーネントの中身
}
export default HogeComponent;

or 

/* コンポーネントはパスカルケースで命名 */
export default function HogeComponent({ props }: { props: hogeType }) {
    // ...コンポーネントの中身
}

/* --------------------- memo を使う場合 --------------------- */
import { memo } from "react";

type hogeType = {
    urlStr: string;
    urlPathName: string;
}

/* コンポーネントはパスカルケースで命名 */
function HogeComponent({ props }: { props: hogeType }) {
    // ...コンポーネントの中身
}
export default memo(HogeComponent);

いつもReactでは以下のように書いていたので少しだけ戸惑いました。

export const HogeComponent = memo(({ props }: { props: hogeType }) => {
    // ...コンポーネントの中身
});

or

export const HogeComponent:FC<hogeType> = memo((props) => {
    // ...コンポーネントの中身
});

さらに、この記述に変えるとimportの記述及び使用方法も変わるので、そこも戸惑いポイントでした。

- import { HeadingComponent } from '@/app/utils/HeadingComponent';
+ import HeadingComponent from '@/app/utils/HeadingComponent';
.
.
- <HeadingComponent title="よくあるご質問" subTitle="F & Q" />
+ <HeadingComponent props={{
+   title: "よくあるご質問",
+   subTitle: "F & Q"
+ }} />

型によって記述が独特になります。

<ArticlesContentDetail props={{
    articles: { ...articles }, // 配列
    detailCheck: detailCheck, // state(bool)
    setDetailCheck: setDetailCheck, // 上記 state のセッター関数
}} />

useEffectの依存関係で警告

ESLint関連で驚いたのがもう一つ、useEffectの依存関係で警告でした。

useEffect(() => {
    ScrollObserver('section h2', 'OnView', {
        rootMargin: '-300px 0px'
    });
    // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

このあたりも実際にNext.jsで作成し、ビルドまでしないと知らないことだったので良かったです。

リプレースして

routes-next.png

Reactの時点で数値はそこまで悪くなかった(※リプレース前のを記録し忘れていて申し訳ない)のですが、リプレースしてSEOとか100になったので、これはすごくびっくりしました。

画像最適化の恩恵を無効化したのでパフォーマンスが少し落ちましたが、もし有効だったら数値はさらに向上しそうですよね。このあたりがデプロイ先にVercelとかを選んだほうが賢明という部分につながってくるかと思います。

年に数回程度な更新のサイトB

こちらはサイトAと違って完全にSSGによる静的サイトです。

技術スタックは以下になります。

  • next@14.1.0
  • styled-components
  • swiper

先ほどのサイトAでは状態管理にjotaiを用いていましたが、React Contextでの実装を試してみたかったので、こちらではReact Contextを使って状態管理しています。

参照情報:

React Context

先程の公式の参照情報にある通りなのですが以下のような記述で簡単に実装できました。

src/app/layout.tsx
export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="ja">
      {/* head 関連:GA4 / OGP */}
      <Script
        strategy="afterInteractive"
        src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXX"
      />
      <Script
        id="gtag-config"
        strategy="afterInteractive"
      >
        {`
         window.dataLayer = window.dataLayer || [];
         function gtag(){dataLayer.push(arguments);}
         gtag('js', new Date());
         gtag('config', 'G-XXXXXXXXX'); 
        `}
      </Script>
      <link rel="icon" href="/favicon.ico" sizes="any" />
      <meta property="og:image" content="<generated>" />
      <meta property="og:image:type" content="<generated>" />
      <meta property="og:image:width" content="<generated>" />
      <meta property="og:image:height" content="<generated>" />
      <meta name="twitter:image" content="<generated>" />
      <meta name="twitter:image:type" content="<generated>" />
      <meta name="twitter:image:width" content="<generated>" />
      <meta name="twitter:image:height" content="<generated>" />
      {/* head 関連:GA4 / OGP */}
      <body>
        <TopPageContextFragment>
          <NotFoundContextFragment>
            <Header />
            {children}
            <Footer />
          </NotFoundContextFragment>
        </TopPageContextFragment>
      </body>
    </html >
  );
}

以下は制作時に詰まったポイントです。

CSS の 背景画像の設定

Next.jsではpublicディレクトリ(フォルダ)で すべての静的データを管理する 形のため、CSSの値でURLにパス指定する再はpublic直下からの指定となるようです。

.something_elements {
    /* (public)/img/something.jpg */
    background: url('/img/something.jpg') no-repeat center/cover;
}

pdf の別タブ表示

pdfファイルなどはLinkhrefに直接(public直下から)指定することで表示できます。

/* (public)/pdf/documents/abouthogefoo.pdf */
<Link href="/pdf/documents/abouthogefoo.pdf" target="_blank">hogefooに関するpdfドキュメント</Link>

ただ、この書き方だとNext.jsのプリレンダリングの影響?でコンソールに404の警告が出るのです(クリックするとpdfファイルはしっかり表示できます)。<a>タグを使っても同じだったので何かご存じの方は教えていただけますと嬉しいです。

詰まったのはここらあたりで、(サイトAでの経験を経ていたこともあり)概ね滞ったような部分は少なく、シンプルな作りであったこともあって3日ほどでリプレースは済みました。

実は、サイトBは納期2週間で制作という状況(要望)だったことから当時は「とりあえず形に(焦)」的な部分を重視したスピード制作でした。そのため、いつか手を加えたいと思っていたのです。

Next.jsのファイルシステムベースルーティングが便利だったので、おかげさまでサイトのページを拡充したり、それに伴ったTOPページの大幅なデザイン修正を行ったりできました。TOPページの情報整理をはじめ、提供サービスの明確な住み分けの実現、ページ拡充によるサイト流入経路への期待などサイト改善ができたので良かったと感じています。

あと、TOPページの情報整理をする上で各種提供サービスのリード文をChat-GPTにまとめてもらいました。以前は提供サービス内の細目ごとの紹介文を掲載していたのですが、それら紹介文をChat-GPTに投げて150〜300文字程度のリード文に編集してもらったのです。社内のライターに依頼したり、やり取りしたりなどの労力もなく、ものの数分で生成してもらって超時短になりました。ありがとう、Chat-GPT

リプレースして

  • リプレース前
    ied-20240221.png

  • リプレース後
    ied-next.png

こちらもサイトA同様、もともとそこまで悪くありませんでしたが、Next.jsにしてからSEOのスコアは向上しました。
ありがとう、Next.js

筆者がReactNext.js双方での制作を通じて得た所感

  • ルーティング
    圧倒的にNext.jsが楽です。ReactだとReact Routerで実装していましたが色々記述が必要です。これは正直、React Routerが不便とかいう話では全くなくて(実際使いやすいですし)、ファイルシステムベースルーティングが便利すぎる。という筆者の感想です。

  • ビルド後のデプロイ
    個人的にビルドしてdist一つを出してくれるReactのほうが分かりやすいです。

    Next.jsではSSGするのに記述が必要だったり、記述せずビルドすると.nextに大量のファイルが出力されたりして、特にmacだと不可視ファイルだったので当初『Reactでいうdistはどこだ?』となりました。
    しかも出力された.nextの中にはすごい量のファイルが入っていて『これどうやって使うの?』とか混乱した記憶があります。

    今でも.nextの中にあるファイルたちの具体的な役割を理解していませんが、『おそらくこの中のファイルたちは、例えばVercelなどNext.jsがフルパワーを発揮できるようなデプロイ先だけが扱えて、それらファイルをもとに良い感じの仕様でデプロイまでしてくれるのに必要なものなのだろう』と漠然と理解したつもりになっています。

  • 名もなき家事
    <Image>コンポーネントのpriorityという属性の話を冒頭にしましたが、その他の404ページやローディング、エラーページ、サイトマップの作成などサイト制作に必要とされる多様な部分への気配りがNext.jsにはあると感じました。

    • エラーページ(※ファイル名はerror.tsxで固定です)
      app直下にerror.tsxというファイルを置くだけで利用できます。
      参照情報:Error Handling

    • not-found.tsx,loading.tsx(※ファイル名固定)
      error.tsx同様、これらのファイルを用意するだけで404ページやローディングが利用できます。
      参照情報:not-found.js
      参照情報:loading.js
      参照情報:Loading UI and Streaming

    • サイトマップ
      参照情報:Generating a sitemap using code (.js, .ts)

あとNext.jsはドキュメントが充実していて良いなと感じました。

さいごに

ここまで読んでいただきありがとうございました。
基礎的な内容も多いでしょうし、『そもそもNext.js使う必要ある?』と思われた方も少なくないと思います。
しかし冒頭にあるように試してみたかった好奇心を抑えられず突っ走った感じです。
改めて会社には感謝したいと思います。

そして、筆者が実体験を通じて学んだのは「やはりアウトプットすることで知識は身に付く」という感覚と「Next.jsを使用するだけでなく活用したいのならばデプロイ先は大切」という印象でした。
とはいえ、それでもスコアが高いのでNext.jsは素晴らしいと感じます。

この記事がどなたかの役に少しでも立てれば幸いです。

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3